mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(ai,auth): Mission Grant endpoint + unwrap helper + audit table
Phase 1 of the Mission Key-Grant rollout. Webapp can now request a
wrapped per-mission data key; mana-ai can unwrap and (Phase 2) use it.
mana-auth:
- POST /api/v1/me/ai-mission-grant — HKDF-derives MDK from the user
master key, RSA-OAEP-2048-wraps with the mana-ai public key, returns
{ wrappedKey, derivation, issuedAt, expiresAt }
- MissionGrantService refuses zero-knowledge users (409 ZK_ACTIVE) and
returns 503 GRANT_NOT_CONFIGURED when MANA_AI_PUBLIC_KEY_PEM is unset
- TTL clamped to [1h, 30d]
mana-ai:
- configureMissionGrantKey + unwrapMissionGrant with structured failure
reasons (not-configured / expired / malformed / wrap-rejected)
- mana_ai.decrypt_audit table + RLS policy scoped to
app.current_user_id — append-only row per server-side decrypt attempt
- MANA_AI_PRIVATE_KEY_PEM env slot; absent = grants silently disabled
No existing behaviour changes: missions without a grant run exactly as
before. Grant flow is wired end-to-end but unused until Phase 2 lands
the encrypted resolver.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6882ffb626
commit
9a3025fed8
12 changed files with 1258 additions and 207 deletions
532
pnpm-lock.yaml
generated
532
pnpm-lock.yaml
generated
|
|
@ -138,14 +138,14 @@ importers:
|
|||
version: link:../../../../packages/shared-landing-ui
|
||||
astro:
|
||||
specifier: ^5.16.0
|
||||
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
|
||||
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
devDependencies:
|
||||
'@astrojs/tailwind':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.18
|
||||
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
|
@ -154,13 +154,13 @@ importers:
|
|||
version: 20.19.39
|
||||
eslint:
|
||||
specifier: ^9.0.0
|
||||
version: 9.39.4(jiti@2.6.1)
|
||||
version: 9.39.4(jiti@1.21.7)
|
||||
eslint-config-prettier:
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.2(eslint@9.39.4(jiti@2.6.1))
|
||||
version: 9.1.2(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-astro:
|
||||
specifier: ^1.0.0
|
||||
version: 1.6.0(eslint@9.39.4(jiti@2.6.1))
|
||||
version: 1.6.0(eslint@9.39.4(jiti@1.21.7))
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.8.1
|
||||
|
|
@ -484,19 +484,19 @@ importers:
|
|||
version: 19.1.17
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^7.7.0
|
||||
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.7.0
|
||||
version: 7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
version: 7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
eslint:
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.4(jiti@1.21.7)
|
||||
version: 9.39.4(jiti@2.6.1)
|
||||
eslint-config-universe:
|
||||
specifier: ^12.0.1
|
||||
version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.4(jiti@1.21.7))(prettier@3.8.1)(typescript@5.3.3)
|
||||
version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.1)(typescript@5.3.3)
|
||||
prettier:
|
||||
specifier: ^3.2.5
|
||||
version: 3.8.1
|
||||
|
|
@ -3340,6 +3340,9 @@ importers:
|
|||
|
||||
services/mana-auth:
|
||||
dependencies:
|
||||
'@mana/shared-ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared-ai
|
||||
'@mana/shared-hono':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared-hono
|
||||
|
|
@ -18032,6 +18035,16 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
|
||||
autoprefixer: 10.4.27(postcss@8.5.8)
|
||||
postcss: 8.5.8
|
||||
postcss-load-config: 4.0.2(postcss@8.5.8)
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
|
||||
|
|
@ -20380,6 +20393,82 @@ snapshots:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@expo/cli@55.0.22(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0))(expo-router@55.0.11)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@expo/code-signing-certificates': 0.0.6
|
||||
'@expo/config': 55.0.13(typescript@5.9.3)
|
||||
'@expo/config-plugins': 55.0.8
|
||||
'@expo/devcert': 1.2.1
|
||||
'@expo/env': 2.1.1
|
||||
'@expo/image-utils': 0.8.12
|
||||
'@expo/json-file': 10.0.13
|
||||
'@expo/log-box': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
|
||||
'@expo/metro': 55.0.0
|
||||
'@expo/metro-config': 55.0.14(expo@55.0.12)(typescript@5.9.3)
|
||||
'@expo/osascript': 2.4.2
|
||||
'@expo/package-manager': 1.10.4
|
||||
'@expo/plist': 0.5.2
|
||||
'@expo/prebuild-config': 55.0.13(expo@55.0.12)(typescript@5.9.3)
|
||||
'@expo/require-utils': 55.0.3(typescript@5.9.3)
|
||||
'@expo/router-server': 55.0.13(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0))(expo-router@55.0.11)(expo-server@55.0.7)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@expo/schema-utils': 55.0.3
|
||||
'@expo/spawn-async': 1.7.2
|
||||
'@expo/ws-tunnel': 1.0.6
|
||||
'@expo/xcpretty': 4.4.1
|
||||
'@react-native/dev-middleware': 0.83.4
|
||||
accepts: 1.3.8
|
||||
arg: 5.0.2
|
||||
better-opn: 3.0.2
|
||||
bplist-creator: 0.1.0
|
||||
bplist-parser: 0.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
compression: 1.8.1
|
||||
connect: 3.7.0
|
||||
debug: 4.4.3
|
||||
dnssd-advertise: 1.1.4
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-server: 55.0.7
|
||||
fetch-nodeshim: 0.4.10
|
||||
getenv: 2.0.0
|
||||
glob: 13.0.6
|
||||
lan-network: 0.2.1
|
||||
multitars: 0.2.4
|
||||
node-forge: 1.4.0
|
||||
npm-package-arg: 11.0.3
|
||||
ora: 3.4.0
|
||||
picomatch: 4.0.4
|
||||
pretty-format: 29.7.0
|
||||
progress: 2.0.3
|
||||
prompts: 2.4.2
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.4
|
||||
send: 0.19.2
|
||||
slugify: 1.6.9
|
||||
source-map-support: 0.5.21
|
||||
stacktrace-parser: 0.1.11
|
||||
structured-headers: 0.4.1
|
||||
terminal-link: 2.1.1
|
||||
toqr: 0.1.1
|
||||
wrap-ansi: 7.0.0
|
||||
ws: 8.20.0
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
expo-router: 55.0.11(pdj77jcnw7u2taeri5ruj5t4mi)
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@expo/dom-webview'
|
||||
- '@expo/metro-runtime'
|
||||
- bufferutil
|
||||
- expo-constants
|
||||
- expo-font
|
||||
- react
|
||||
- react-dom
|
||||
- react-server-dom-webpack
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
'@expo/cli@55.0.22(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6)(expo-router@55.0.11)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@expo/code-signing-certificates': 0.0.6
|
||||
|
|
@ -20532,82 +20621,6 @@ snapshots:
|
|||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
'@expo/cli@55.0.22(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6)(expo-router@55.0.11)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@expo/code-signing-certificates': 0.0.6
|
||||
'@expo/config': 55.0.13(typescript@5.9.3)
|
||||
'@expo/config-plugins': 55.0.8
|
||||
'@expo/devcert': 1.2.1
|
||||
'@expo/env': 2.1.1
|
||||
'@expo/image-utils': 0.8.12
|
||||
'@expo/json-file': 10.0.13
|
||||
'@expo/log-box': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
|
||||
'@expo/metro': 55.0.0
|
||||
'@expo/metro-config': 55.0.14(expo@55.0.12)(typescript@5.9.3)
|
||||
'@expo/osascript': 2.4.2
|
||||
'@expo/package-manager': 1.10.4
|
||||
'@expo/plist': 0.5.2
|
||||
'@expo/prebuild-config': 55.0.13(expo@55.0.12)(typescript@5.9.3)
|
||||
'@expo/require-utils': 55.0.3(typescript@5.9.3)
|
||||
'@expo/router-server': 55.0.13(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6)(expo-router@55.0.11)(expo-server@55.0.7)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@expo/schema-utils': 55.0.3
|
||||
'@expo/spawn-async': 1.7.2
|
||||
'@expo/ws-tunnel': 1.0.6
|
||||
'@expo/xcpretty': 4.4.1
|
||||
'@react-native/dev-middleware': 0.83.4
|
||||
accepts: 1.3.8
|
||||
arg: 5.0.2
|
||||
better-opn: 3.0.2
|
||||
bplist-creator: 0.1.0
|
||||
bplist-parser: 0.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
compression: 1.8.1
|
||||
connect: 3.7.0
|
||||
debug: 4.4.3
|
||||
dnssd-advertise: 1.1.4
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-server: 55.0.7
|
||||
fetch-nodeshim: 0.4.10
|
||||
getenv: 2.0.0
|
||||
glob: 13.0.6
|
||||
lan-network: 0.2.1
|
||||
multitars: 0.2.4
|
||||
node-forge: 1.4.0
|
||||
npm-package-arg: 11.0.3
|
||||
ora: 3.4.0
|
||||
picomatch: 4.0.4
|
||||
pretty-format: 29.7.0
|
||||
progress: 2.0.3
|
||||
prompts: 2.4.2
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.4
|
||||
send: 0.19.2
|
||||
slugify: 1.6.9
|
||||
source-map-support: 0.5.21
|
||||
stacktrace-parser: 0.1.11
|
||||
structured-headers: 0.4.1
|
||||
terminal-link: 2.1.1
|
||||
toqr: 0.1.1
|
||||
wrap-ansi: 7.0.0
|
||||
ws: 8.20.0
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
expo-router: 55.0.11(eq6waxwbjtmotercssqwfwfcim)
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@expo/dom-webview'
|
||||
- '@expo/metro-runtime'
|
||||
- bufferutil
|
||||
- expo-constants
|
||||
- expo-font
|
||||
- react
|
||||
- react-dom
|
||||
- react-server-dom-webpack
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
'@expo/cli@55.0.22(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6)(expo-router@55.0.11)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@expo/code-signing-certificates': 0.0.6
|
||||
|
|
@ -20835,7 +20848,7 @@ snapshots:
|
|||
|
||||
'@expo/dom-webview@55.0.5(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
|
||||
|
|
@ -20948,7 +20961,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@expo/dom-webview': 55.0.5(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
|
||||
anser: 1.4.10
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
stacktrace-parser: 0.1.11
|
||||
|
|
@ -21072,7 +21085,7 @@ snapshots:
|
|||
postcss: 8.4.49
|
||||
resolve-from: 5.0.0
|
||||
optionalDependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
|
|
@ -21099,7 +21112,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@expo/log-box': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
|
||||
anser: 1.4.10
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
pretty-format: 29.7.0
|
||||
react: 19.2.0
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
|
|
@ -21251,7 +21264,7 @@ snapshots:
|
|||
'@expo/json-file': 10.0.13
|
||||
'@react-native/normalize-colors': 0.83.4
|
||||
debug: 4.4.3
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.4
|
||||
xml2js: 0.6.0
|
||||
|
|
@ -21297,6 +21310,21 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@expo/router-server@55.0.13(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0))(expo-router@55.0.11)(expo-server@55.0.7)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-constants: 55.0.12(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(typescript@5.9.3)
|
||||
expo-font: 55.0.6(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
|
||||
expo-server: 55.0.7
|
||||
react: 19.2.0
|
||||
optionalDependencies:
|
||||
'@expo/metro-runtime': 55.0.9(@expo/dom-webview@55.0.5)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
|
||||
expo-router: 55.0.11(pdj77jcnw7u2taeri5ruj5t4mi)
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@expo/router-server@55.0.13(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6)(expo-router@55.0.11)(expo-server@55.0.7)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
|
|
@ -25131,16 +25159,16 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/scope-manager': 6.21.0
|
||||
'@typescript-eslint/type-utils': 6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/type-utils': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/visitor-keys': 6.21.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.2
|
||||
natural-compare: 1.4.0
|
||||
|
|
@ -25189,15 +25217,15 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
'@typescript-eslint/type-utils': 7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/type-utils': 7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.2
|
||||
natural-compare: 1.4.0
|
||||
|
|
@ -25270,14 +25298,14 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 6.21.0
|
||||
'@typescript-eslint/types': 6.21.0
|
||||
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
|
||||
'@typescript-eslint/visitor-keys': 6.21.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -25309,14 +25337,14 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3)
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -25412,12 +25440,12 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/type-utils@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/type-utils@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
ts-api-utils: 1.4.3(typescript@5.3.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.3.3
|
||||
|
|
@ -25448,12 +25476,12 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/type-utils@7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/type-utils@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
ts-api-utils: 1.4.3(typescript@5.3.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.3.3
|
||||
|
|
@ -25606,15 +25634,15 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/utils@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/utils@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.7.1
|
||||
'@typescript-eslint/scope-manager': 6.21.0
|
||||
'@typescript-eslint/types': 6.21.0
|
||||
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
semver: 7.7.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -25645,13 +25673,13 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/utils@7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)':
|
||||
'@typescript-eslint/utils@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3)
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
|
@ -26335,6 +26363,108 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.13.1
|
||||
'@astrojs/internal-helpers': 0.7.6
|
||||
'@astrojs/markdown-remark': 6.3.11
|
||||
'@astrojs/telemetry': 3.3.0
|
||||
'@capsizecss/unpack': 4.0.0
|
||||
'@oslojs/encoding': 1.1.0
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
acorn: 8.16.0
|
||||
aria-query: 5.3.2
|
||||
axobject-query: 4.1.0
|
||||
boxen: 8.0.1
|
||||
ci-info: 4.4.0
|
||||
clsx: 2.1.1
|
||||
common-ancestor-path: 1.0.1
|
||||
cookie: 1.1.1
|
||||
cssesc: 3.0.0
|
||||
debug: 4.4.3
|
||||
deterministic-object-hash: 2.0.2
|
||||
devalue: 5.7.0
|
||||
diff: 8.0.4
|
||||
dlv: 1.1.3
|
||||
dset: 3.1.4
|
||||
es-module-lexer: 1.7.0
|
||||
esbuild: 0.27.7
|
||||
estree-walker: 3.0.3
|
||||
flattie: 1.1.1
|
||||
fontace: 0.4.1
|
||||
github-slugger: 2.0.0
|
||||
html-escaper: 3.0.3
|
||||
http-cache-semantics: 4.2.0
|
||||
import-meta-resolve: 4.2.0
|
||||
js-yaml: 4.1.1
|
||||
magic-string: 0.30.21
|
||||
magicast: 0.5.2
|
||||
mrmime: 2.0.1
|
||||
neotraverse: 0.6.18
|
||||
p-limit: 6.2.0
|
||||
p-queue: 8.1.1
|
||||
package-manager-detector: 1.6.0
|
||||
piccolore: 0.1.3
|
||||
picomatch: 4.0.4
|
||||
prompts: 2.4.2
|
||||
rehype: 13.0.2
|
||||
semver: 7.7.4
|
||||
shiki: 3.23.0
|
||||
smol-toml: 1.6.1
|
||||
svgo: 4.0.1
|
||||
tinyexec: 1.0.4
|
||||
tinyglobby: 0.2.15
|
||||
tsconfck: 3.1.6(typescript@5.9.3)
|
||||
ultrahtml: 1.6.0
|
||||
unifont: 0.7.4
|
||||
unist-util-visit: 5.1.0
|
||||
unstorage: 1.17.5(@azure/storage-blob@12.31.0)(ioredis@5.10.1)
|
||||
vfile: 6.0.3
|
||||
vite: 6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitefu: 1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
yocto-spinner: 0.2.3
|
||||
zod: 3.25.76
|
||||
zod-to-json-schema: 3.25.2(zod@3.25.76)
|
||||
zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76)
|
||||
optionalDependencies:
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
- '@azure/data-tables'
|
||||
- '@azure/identity'
|
||||
- '@azure/keyvault-secrets'
|
||||
- '@azure/storage-blob'
|
||||
- '@capacitor/preferences'
|
||||
- '@deno/kv'
|
||||
- '@netlify/blobs'
|
||||
- '@planetscale/database'
|
||||
- '@types/node'
|
||||
- '@upstash/redis'
|
||||
- '@vercel/blob'
|
||||
- '@vercel/functions'
|
||||
- '@vercel/kv'
|
||||
- aws4fetch
|
||||
- db0
|
||||
- idb-keyval
|
||||
- ioredis
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- rollup
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- typescript
|
||||
- uploadthing
|
||||
- yaml
|
||||
|
||||
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.13.1
|
||||
|
|
@ -26848,7 +26978,7 @@ snapshots:
|
|||
resolve-from: 5.0.0
|
||||
optionalDependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- supports-color
|
||||
|
|
@ -28350,6 +28480,11 @@ snapshots:
|
|||
eslint: 9.39.4(jiti@2.6.1)
|
||||
semver: 7.7.4
|
||||
|
||||
eslint-compat-utils@0.6.5(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
semver: 7.7.4
|
||||
|
||||
eslint-compat-utils@0.6.5(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -28397,14 +28532,14 @@ snapshots:
|
|||
dependencies:
|
||||
eslint: 8.57.1
|
||||
|
||||
eslint-config-prettier@8.10.2(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
|
||||
eslint-config-prettier@8.10.2(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
||||
eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
|
||||
eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -28429,17 +28564,17 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.4(jiti@1.21.7))(prettier@3.8.1)(typescript@5.3.3):
|
||||
eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.1)(typescript@5.3.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-config-prettier: 8.10.2(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-node: 11.1.0(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))(prettier@3.8.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-react-hooks: 4.6.2(eslint@9.39.4(jiti@1.21.7))
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-config-prettier: 8.10.2(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-node: 11.1.0(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 4.6.2(eslint@9.39.4(jiti@2.6.1))
|
||||
optionalDependencies:
|
||||
prettier: 3.8.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -28546,12 +28681,12 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@1.21.7)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -28588,6 +28723,20 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-astro@1.6.0(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
astro-eslint-parser: 1.4.0
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-compat-utils: 0.6.5(eslint@9.39.4(jiti@1.21.7))
|
||||
globals: 16.5.0
|
||||
postcss: 8.5.8
|
||||
postcss-selector-parser: 7.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-astro@1.6.0(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
||||
|
|
@ -28615,12 +28764,6 @@ snapshots:
|
|||
eslint-utils: 2.1.0
|
||||
regexpp: 3.2.0
|
||||
|
||||
eslint-plugin-es@3.0.1(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-utils: 2.1.0
|
||||
regexpp: 3.2.0
|
||||
|
||||
eslint-plugin-es@3.0.1(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -28674,7 +28817,7 @@ snapshots:
|
|||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.4(jiti@1.21.7)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -28683,9 +28826,9 @@ snapshots:
|
|||
array.prototype.flatmap: 1.3.3
|
||||
debug: 3.2.7
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -28697,7 +28840,7 @@ snapshots:
|
|||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.3.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
|
|
@ -28815,16 +28958,6 @@ snapshots:
|
|||
resolve: 1.22.11
|
||||
semver: 6.3.1
|
||||
|
||||
eslint-plugin-node@11.1.0(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-plugin-es: 3.0.1(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-utils: 2.1.0
|
||||
ignore: 5.3.2
|
||||
minimatch: 3.1.5
|
||||
resolve: 1.22.11
|
||||
semver: 6.3.1
|
||||
|
||||
eslint-plugin-node@11.1.0(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -28855,16 +28988,6 @@ snapshots:
|
|||
'@types/eslint': 9.6.1
|
||||
eslint-config-prettier: 8.10.2(eslint@8.57.1)
|
||||
|
||||
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))(prettier@3.8.1):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
prettier: 3.8.1
|
||||
prettier-linter-helpers: 1.0.1
|
||||
synckit: 0.11.12
|
||||
optionalDependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
eslint-config-prettier: 8.10.2(eslint@9.39.4(jiti@1.21.7))
|
||||
|
||||
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.1):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -28889,10 +29012,6 @@ snapshots:
|
|||
dependencies:
|
||||
eslint: 8.57.1
|
||||
|
||||
eslint-plugin-react-hooks@4.6.2(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
|
||||
eslint-plugin-react-hooks@4.6.2(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -28923,28 +29042,6 @@ snapshots:
|
|||
string.prototype.matchall: 4.0.12
|
||||
string.prototype.repeat: 1.0.0
|
||||
|
||||
eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
array-includes: 3.1.9
|
||||
array.prototype.findlast: 1.2.5
|
||||
array.prototype.flatmap: 1.3.3
|
||||
array.prototype.tosorted: 1.1.4
|
||||
doctrine: 2.1.0
|
||||
es-iterator-helpers: 1.3.1
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
estraverse: 5.3.0
|
||||
hasown: 2.0.2
|
||||
jsx-ast-utils: 3.3.5
|
||||
minimatch: 3.1.5
|
||||
object.entries: 1.1.9
|
||||
object.fromentries: 2.0.8
|
||||
object.values: 1.2.1
|
||||
prop-types: 15.8.1
|
||||
resolve: 2.0.0-next.6
|
||||
semver: 6.3.1
|
||||
string.prototype.matchall: 4.0.12
|
||||
string.prototype.repeat: 1.0.0
|
||||
|
||||
eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -29448,7 +29545,7 @@ snapshots:
|
|||
|
||||
expo-dev-client@6.0.20(expo@55.0.12):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-dev-launcher: 6.0.20(expo@55.0.12)
|
||||
expo-dev-menu: 7.0.18(expo@55.0.12)
|
||||
expo-dev-menu-interface: 2.0.0(expo@55.0.12)
|
||||
|
|
@ -29470,7 +29567,7 @@ snapshots:
|
|||
expo-dev-launcher@6.0.20(expo@55.0.12):
|
||||
dependencies:
|
||||
ajv: 8.18.0
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-dev-menu: 7.0.18(expo@55.0.12)
|
||||
expo-manifests: 1.0.10(expo@55.0.12)
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -29478,7 +29575,7 @@ snapshots:
|
|||
|
||||
expo-dev-menu-interface@2.0.0(expo@55.0.12):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
|
||||
expo-dev-menu-interface@55.0.2(expo@55.0.12):
|
||||
dependencies:
|
||||
|
|
@ -29491,7 +29588,7 @@ snapshots:
|
|||
|
||||
expo-dev-menu@7.0.18(expo@55.0.12):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-dev-menu-interface: 2.0.0(expo@55.0.12)
|
||||
|
||||
expo-device@55.0.13(expo@55.0.12):
|
||||
|
|
@ -29517,7 +29614,7 @@ snapshots:
|
|||
|
||||
expo-file-system@55.0.15(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
|
||||
expo-file-system@55.0.15(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)):
|
||||
|
|
@ -29541,7 +29638,7 @@ snapshots:
|
|||
|
||||
expo-font@55.0.6(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
fontfaceobserver: 2.3.0
|
||||
react: 19.2.0
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
|
|
@ -29562,7 +29659,7 @@ snapshots:
|
|||
|
||||
expo-glass-effect@55.0.10(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
|
||||
|
|
@ -29578,11 +29675,11 @@ snapshots:
|
|||
|
||||
expo-image-loader@55.0.0(expo@55.0.12):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
|
||||
expo-image-picker@55.0.17(expo@55.0.12):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-image-loader: 55.0.0(expo@55.0.12)
|
||||
|
||||
expo-image@55.0.8(expo@54.0.33)(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
|
||||
|
|
@ -29597,7 +29694,7 @@ snapshots:
|
|||
|
||||
expo-image@55.0.8(expo@55.0.12)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
sf-symbols-typescript: 2.2.0
|
||||
|
|
@ -29624,7 +29721,7 @@ snapshots:
|
|||
|
||||
expo-keep-awake@55.0.6(expo@55.0.12)(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
|
||||
expo-linear-gradient@15.0.8(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0):
|
||||
|
|
@ -29720,7 +29817,7 @@ snapshots:
|
|||
expo-manifests@1.0.10(expo@55.0.12):
|
||||
dependencies:
|
||||
'@expo/config': 12.0.13
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-json-utils: 0.15.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -30092,7 +30189,7 @@ snapshots:
|
|||
|
||||
expo-secure-store@55.0.12(expo@55.0.12):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
|
||||
expo-server@1.0.5: {}
|
||||
|
||||
|
|
@ -30187,7 +30284,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@react-native/normalize-colors': 0.83.4
|
||||
debug: 4.4.3
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
optionalDependencies:
|
||||
react-native-web: 0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
|
|
@ -30213,7 +30310,7 @@ snapshots:
|
|||
|
||||
expo-updates-interface@2.0.0(expo@55.0.12):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
|
||||
expo-updates-interface@55.1.5(expo@55.0.12):
|
||||
dependencies:
|
||||
|
|
@ -30243,7 +30340,7 @@ snapshots:
|
|||
|
||||
expo-web-browser@55.0.13(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)):
|
||||
dependencies:
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
|
||||
expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0)
|
||||
|
||||
expo@54.0.33(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
|
||||
|
|
@ -30367,7 +30464,7 @@ snapshots:
|
|||
expo@55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@expo/cli': 55.0.22(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6)(expo-router@55.0.11)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
'@expo/cli': 55.0.22(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-constants@55.0.12)(expo-font@55.0.6(expo@55.0.12)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0))(expo-router@55.0.11)(expo@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
'@expo/config': 55.0.13(typescript@5.9.3)
|
||||
'@expo/config-plugins': 55.0.8
|
||||
'@expo/devtools': 55.0.2(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)
|
||||
|
|
@ -37159,6 +37256,23 @@ snapshots:
|
|||
lightningcss: 1.32.0
|
||||
terser: 5.46.1
|
||||
|
||||
vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.8
|
||||
rollup: 4.60.1
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.39
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
lightningcss: 1.32.0
|
||||
terser: 5.46.1
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.3
|
||||
|
||||
vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
|
|
@ -37210,6 +37324,10 @@ snapshots:
|
|||
tsx: 4.21.0
|
||||
yaml: 2.8.3
|
||||
|
||||
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
vite: 6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,20 @@ export interface Config {
|
|||
/** Flip to false to boot the HTTP surface without the background tick
|
||||
* — useful for local smoke-tests + Docker image build verification. */
|
||||
tickEnabled: boolean;
|
||||
/**
|
||||
* PEM-encoded RSA-OAEP-2048 private key for unwrapping Mission Grants.
|
||||
* Paired with the public key pinned in mana-auth's config. Provision
|
||||
* via Docker secret / out-of-band env; never commit.
|
||||
*
|
||||
* Optional at boot so the service can start without grant support
|
||||
* (development, legacy deployments). When absent, Missions that
|
||||
* carry a Grant are skipped with state='grant-missing'.
|
||||
*
|
||||
* Generate with:
|
||||
* openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out priv.pem
|
||||
* openssl pkey -in priv.pem -pubout -out pub.pem
|
||||
*/
|
||||
missionGrantPrivateKeyPem?: string;
|
||||
}
|
||||
|
||||
function requireEnv(key: string, fallback?: string): string {
|
||||
|
|
@ -38,5 +52,6 @@ export function loadConfig(): Config {
|
|||
serviceKey: requireEnv('MANA_SERVICE_KEY', 'dev-service-key'),
|
||||
tickIntervalMs: parseInt(process.env.TICK_INTERVAL_MS ?? '60000', 10),
|
||||
tickEnabled: process.env.TICK_ENABLED !== 'false',
|
||||
missionGrantPrivateKeyPem: process.env.MANA_AI_PRIVATE_KEY_PEM || undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
155
services/mana-ai/src/crypto/unwrap-grant.test.ts
Normal file
155
services/mana-ai/src/crypto/unwrap-grant.test.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* Unwrap-grant round-trip test. Generates a fresh RSA keypair, uses the
|
||||
* public half to wrap an MDK via the same shape mana-auth uses, then
|
||||
* unwraps with the private half and verifies the MDK decrypts what the
|
||||
* MDK-derived AES-GCM key encrypted.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
import {
|
||||
deriveMissionDataKeyRaw,
|
||||
GRANT_DERIVATION_VERSION,
|
||||
type GrantDerivation,
|
||||
type MissionGrant,
|
||||
} from '@mana/shared-ai';
|
||||
import { configureMissionGrantKey, unwrapMissionGrant, _resetForTesting } from './unwrap-grant';
|
||||
|
||||
const fixedMasterKey = new Uint8Array(32).map((_, i) => i + 1);
|
||||
|
||||
async function genKeypair() {
|
||||
const kp = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
const spki = new Uint8Array(await crypto.subtle.exportKey('spki', kp.publicKey));
|
||||
const pkcs8 = new Uint8Array(await crypto.subtle.exportKey('pkcs8', kp.privateKey));
|
||||
return {
|
||||
publicKey: kp.publicKey,
|
||||
privatePem: toPem(pkcs8, 'PRIVATE KEY'),
|
||||
publicPem: toPem(spki, 'PUBLIC KEY'),
|
||||
};
|
||||
}
|
||||
|
||||
function toPem(der: Uint8Array, label: string): string {
|
||||
const b64 = bytesToBase64(der);
|
||||
const body: string[] = [];
|
||||
for (let i = 0; i < b64.length; i += 64) body.push(b64.slice(i, i + 64));
|
||||
return `-----BEGIN ${label}-----\n${body.join('\n')}\n-----END ${label}-----`;
|
||||
}
|
||||
|
||||
function derivation(): GrantDerivation {
|
||||
return {
|
||||
version: GRANT_DERIVATION_VERSION,
|
||||
missionId: 'mission-abc',
|
||||
tables: ['notes'],
|
||||
recordIds: ['notes:n1'],
|
||||
};
|
||||
}
|
||||
|
||||
async function mintGrant(publicKey: CryptoKey, ttlMs = 60_000): Promise<MissionGrant> {
|
||||
const mdk = await deriveMissionDataKeyRaw(new Uint8Array(fixedMasterKey), derivation());
|
||||
const wrapped = new Uint8Array(
|
||||
await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, toBufferSource(mdk))
|
||||
);
|
||||
const now = Date.now();
|
||||
return {
|
||||
wrappedKey: bytesToBase64(wrapped),
|
||||
derivation: derivation(),
|
||||
issuedAt: new Date(now).toISOString(),
|
||||
expiresAt: new Date(now + ttlMs).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
_resetForTesting();
|
||||
});
|
||||
|
||||
describe('unwrapMissionGrant', () => {
|
||||
it('round-trips a freshly minted grant into a usable AES-GCM key', async () => {
|
||||
const { publicKey, privatePem } = await genKeypair();
|
||||
configureMissionGrantKey(privatePem);
|
||||
const grant = await mintGrant(publicKey);
|
||||
|
||||
const result = await unwrapMissionGrant(grant);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
// The unwrapped CryptoKey must decrypt what the same-derivation
|
||||
// MDK encrypted. Derive MDK separately, import as AES-GCM for
|
||||
// encrypt, encrypt a known plaintext, then decrypt with the
|
||||
// unwrapped key.
|
||||
const mdkRaw = await deriveMissionDataKeyRaw(new Uint8Array(fixedMasterKey), grant.derivation);
|
||||
const encKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
toBufferSource(mdkRaw),
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const plaintext = new TextEncoder().encode('hello mission');
|
||||
const ct = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: toBufferSource(iv) },
|
||||
encKey,
|
||||
toBufferSource(plaintext)
|
||||
);
|
||||
|
||||
const dec = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: toBufferSource(iv) },
|
||||
result.mdk,
|
||||
ct
|
||||
);
|
||||
expect(new TextDecoder().decode(dec)).toBe('hello mission');
|
||||
});
|
||||
|
||||
it('returns not-configured when key was never set', async () => {
|
||||
// no configureMissionGrantKey call
|
||||
const { publicKey } = await genKeypair();
|
||||
const grant = await mintGrant(publicKey);
|
||||
const result = await unwrapMissionGrant(grant);
|
||||
expect(result).toEqual({ ok: false, reason: 'not-configured' });
|
||||
});
|
||||
|
||||
it('returns not-configured when key is explicitly unset', async () => {
|
||||
const { publicKey } = await genKeypair();
|
||||
configureMissionGrantKey(undefined);
|
||||
const grant = await mintGrant(publicKey);
|
||||
const result = await unwrapMissionGrant(grant);
|
||||
expect(result).toEqual({ ok: false, reason: 'not-configured' });
|
||||
});
|
||||
|
||||
it('returns expired for past expiresAt', async () => {
|
||||
const { publicKey, privatePem } = await genKeypair();
|
||||
configureMissionGrantKey(privatePem);
|
||||
const grant = await mintGrant(publicKey, -1000);
|
||||
const result = await unwrapMissionGrant(grant);
|
||||
expect(result).toEqual({ ok: false, reason: 'expired' });
|
||||
});
|
||||
|
||||
it('returns wrap-rejected for a wrapped key encrypted with the wrong pubkey', async () => {
|
||||
const { privatePem } = await genKeypair();
|
||||
const { publicKey: otherPub } = await genKeypair();
|
||||
configureMissionGrantKey(privatePem);
|
||||
const grant = await mintGrant(otherPub);
|
||||
const result = await unwrapMissionGrant(grant);
|
||||
expect(result).toEqual({ ok: false, reason: 'wrap-rejected' });
|
||||
});
|
||||
});
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
let bin = '';
|
||||
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
function toBufferSource(bytes: Uint8Array): ArrayBuffer {
|
||||
const buf = new ArrayBuffer(bytes.length);
|
||||
new Uint8Array(buf).set(bytes);
|
||||
return buf;
|
||||
}
|
||||
155
services/mana-ai/src/crypto/unwrap-grant.ts
Normal file
155
services/mana-ai/src/crypto/unwrap-grant.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* Unwrap side of the Mission Key-Grant.
|
||||
*
|
||||
* The `mana-auth` service wrapped the per-mission Data Key (MDK) with
|
||||
* our RSA-OAEP-2048 public key before writing it into `Mission.grant`.
|
||||
* At tick time, this module unwraps the MDK back into a CryptoKey that
|
||||
* the encrypted resolver (Phase 2) can use to decrypt `sync_changes`
|
||||
* rows for the mission's allowlisted records.
|
||||
*
|
||||
* Security notes:
|
||||
* - The private key is loaded once per process from the PEM in
|
||||
* `config.missionGrantPrivateKeyPem`. If missing, unwrap returns
|
||||
* null so the tick loop can skip the mission without crashing.
|
||||
* - Callers MUST discard the returned CryptoKey as soon as the tick
|
||||
* finishes. The key itself is non-extractable, but the reference
|
||||
* staying alive keeps the underlying bytes pinned in WebCrypto's
|
||||
* internal tables. A per-tick scope map + finally-clear is the
|
||||
* agreed pattern; see `docs/plans/ai-mission-key-grant.md`.
|
||||
* - Grant expiry is checked here, not in the caller. A stale grant
|
||||
* returns null + a structured `reason` the caller logs but never
|
||||
* surfaces as an error (foreground runner picks up the slack).
|
||||
*/
|
||||
|
||||
import type { MissionGrant } from '@mana/shared-ai';
|
||||
|
||||
export type UnwrapResult =
|
||||
| { ok: true; mdk: CryptoKey }
|
||||
| { ok: false; reason: UnwrapFailureReason };
|
||||
|
||||
export type UnwrapFailureReason = 'not-configured' | 'expired' | 'malformed' | 'wrap-rejected';
|
||||
|
||||
let _privateKeyPromise: Promise<CryptoKey | null> | null = null;
|
||||
|
||||
/** Install (or re-install) the PEM-encoded RSA private key. Call at
|
||||
* service boot after `loadConfig`. Passing undefined disables grant
|
||||
* unwrapping for the lifetime of the process — tick loop skips any
|
||||
* mission with a grant, same as for legacy deployments. */
|
||||
export function configureMissionGrantKey(pem: string | undefined): void {
|
||||
if (!pem) {
|
||||
_privateKeyPromise = Promise.resolve(null);
|
||||
return;
|
||||
}
|
||||
_privateKeyPromise = importRsaPrivateKey(pem);
|
||||
}
|
||||
|
||||
/** Test-only reset so bun test can swap in different keys across
|
||||
* describe blocks without holding stale state. Not exported through
|
||||
* any barrel. */
|
||||
export function _resetForTesting(): void {
|
||||
_privateKeyPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps a grant into the Mission Data Key. Never throws on the
|
||||
* expected failure modes — returns a structured reason instead so
|
||||
* the caller can bump the right metric and skip cleanly.
|
||||
*/
|
||||
export async function unwrapMissionGrant(grant: MissionGrant): Promise<UnwrapResult> {
|
||||
if (!_privateKeyPromise) {
|
||||
// configureMissionGrantKey was never called. Treat as not
|
||||
// configured rather than crashing — lets tests/local dev boot
|
||||
// without setting the env var.
|
||||
return { ok: false, reason: 'not-configured' };
|
||||
}
|
||||
const pk = await _privateKeyPromise;
|
||||
if (!pk) return { ok: false, reason: 'not-configured' };
|
||||
|
||||
if (isExpired(grant.expiresAt)) {
|
||||
return { ok: false, reason: 'expired' };
|
||||
}
|
||||
|
||||
let wrappedBytes: Uint8Array;
|
||||
try {
|
||||
wrappedBytes = base64ToBytes(grant.wrappedKey);
|
||||
} catch {
|
||||
return { ok: false, reason: 'malformed' };
|
||||
}
|
||||
|
||||
let mdkBytes: Uint8Array;
|
||||
try {
|
||||
const plain = await crypto.subtle.decrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
pk,
|
||||
toBufferSource(wrappedBytes)
|
||||
);
|
||||
mdkBytes = new Uint8Array(plain);
|
||||
} catch {
|
||||
// Auth-tag mismatch, wrong key, corrupted ciphertext — the
|
||||
// caller can't meaningfully distinguish, and we don't want to
|
||||
// leak timing details by branching here.
|
||||
return { ok: false, reason: 'wrap-rejected' };
|
||||
}
|
||||
|
||||
if (mdkBytes.length !== 32) {
|
||||
mdkBytes.fill(0);
|
||||
return { ok: false, reason: 'malformed' };
|
||||
}
|
||||
|
||||
try {
|
||||
const mdk = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
toBufferSource(mdkBytes),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
/* extractable */ false,
|
||||
['decrypt']
|
||||
);
|
||||
return { ok: true, mdk };
|
||||
} finally {
|
||||
mdkBytes.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────
|
||||
|
||||
function isExpired(expiresAt: string): boolean {
|
||||
const ts = Date.parse(expiresAt);
|
||||
if (Number.isNaN(ts)) return true; // treat unparseable as expired
|
||||
return ts < Date.now();
|
||||
}
|
||||
|
||||
async function importRsaPrivateKey(pem: string): Promise<CryptoKey | null> {
|
||||
try {
|
||||
const body = pem
|
||||
.replace(/-----BEGIN [^-]+-----/g, '')
|
||||
.replace(/-----END [^-]+-----/g, '')
|
||||
.replace(/\s+/g, '');
|
||||
const der = base64ToBytes(body);
|
||||
return await crypto.subtle.importKey(
|
||||
'pkcs8',
|
||||
toBufferSource(der),
|
||||
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'[mana-ai:grant] failed to import MANA_AI_PRIVATE_KEY_PEM — grants disabled:',
|
||||
(err as Error).message
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function base64ToBytes(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function toBufferSource(bytes: Uint8Array): ArrayBuffer {
|
||||
const buf = new ArrayBuffer(bytes.length);
|
||||
new Uint8Array(buf).set(bytes);
|
||||
return buf;
|
||||
}
|
||||
|
|
@ -39,4 +39,54 @@ export async function migrate(sql: Sql): Promise<void> {
|
|||
CREATE INDEX IF NOT EXISTS idx_mission_snapshots_user
|
||||
ON mana_ai.mission_snapshots (user_id, last_applied_at)
|
||||
`;
|
||||
|
||||
// ─── Mission Grant decrypt audit ─────────────────────────────
|
||||
// Every server-side decrypt of an encrypted record (triggered by a
|
||||
// Mission with a valid Grant) writes one row here. Surfaces in the
|
||||
// webapp under "Mission → Datenzugriff" so the user can see exactly
|
||||
// what the runner has read. Keep the row shape flat + append-only;
|
||||
// never mutate after insert.
|
||||
//
|
||||
// Why in mana_ai and not mana-auth? The write originates here and
|
||||
// the read is operator-scoped to a specific mission — keeping it
|
||||
// adjacent to `mission_snapshots` means `withUser` transactions
|
||||
// already have the right RLS set up.
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS mana_ai.decrypt_audit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
mission_id TEXT NOT NULL,
|
||||
iteration_id TEXT,
|
||||
table_name TEXT NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('ok', 'failed', 'scope-violation')),
|
||||
reason TEXT,
|
||||
ts TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_decrypt_audit_mission
|
||||
ON mana_ai.decrypt_audit (user_id, mission_id, ts DESC)
|
||||
`;
|
||||
|
||||
// Mirror the RLS pattern used on sync_changes / mission_snapshots:
|
||||
// every read goes through a `withUser` transaction that sets
|
||||
// `app.user_id`; the policy gates row visibility to that user.
|
||||
await sql`ALTER TABLE mana_ai.decrypt_audit ENABLE ROW LEVEL SECURITY`;
|
||||
|
||||
await sql`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'mana_ai'
|
||||
AND tablename = 'decrypt_audit'
|
||||
AND policyname = 'decrypt_audit_user_scope'
|
||||
) THEN
|
||||
CREATE POLICY decrypt_audit_user_scope ON mana_ai.decrypt_audit
|
||||
USING (user_id = current_setting('app.current_user_id', true));
|
||||
END IF;
|
||||
END $$
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { migrate } from './db/migrate';
|
|||
import { runTickOnce, startTick, stopTick, isTickRunning } from './cron/tick';
|
||||
import { serviceAuth } from './middleware/service-auth';
|
||||
import { register, httpRequestsTotal, httpRequestDuration } from './metrics';
|
||||
import { configureMissionGrantKey } from './crypto/unwrap-grant';
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
|
|
@ -24,6 +25,11 @@ const config = loadConfig();
|
|||
// every restart and after rolling deploys.
|
||||
await migrate(getSql(config.syncDatabaseUrl));
|
||||
|
||||
// Install the RSA private key used to unwrap Mission Key-Grants. Absent
|
||||
// env var → grants stay disabled (tick loop skips any mission carrying
|
||||
// one). See docs/plans/ai-mission-key-grant.md.
|
||||
configureMissionGrantKey(config.missionGrantPrivateKeyPem);
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// HTTP instrumentation — labels by method/path/status, surfaced on /metrics.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mana/shared-ai": "workspace:*",
|
||||
"@mana/shared-hono": "workspace:*",
|
||||
"hono": "^4.7.0",
|
||||
"better-auth": "^1.4.3",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ export interface Config {
|
|||
* — in development a deterministic dev KEK is auto-generated so the
|
||||
* service still boots, with a loud warning. */
|
||||
encryptionKek: string;
|
||||
/**
|
||||
* PEM-encoded RSA-OAEP-2048 public key for the mana-ai Mission
|
||||
* Grant runner. The `/me/ai-mission-grant` endpoint wraps per-
|
||||
* mission data keys with this public key so only mana-ai (holder
|
||||
* of the paired private key) can unwrap them. Optional at boot:
|
||||
* when absent, the endpoint returns 503 so the UI can degrade
|
||||
* to foreground-only execution.
|
||||
*/
|
||||
missionGrantPublicKeyPem?: string;
|
||||
}
|
||||
|
||||
export function loadConfig(): Config {
|
||||
|
|
@ -57,5 +66,6 @@ export function loadConfig(): Config {
|
|||
manaSubscriptionsUrl: env('MANA_SUBSCRIPTIONS_URL', 'http://localhost:3063'),
|
||||
manaMailUrl: env('MANA_MAIL_URL', 'http://localhost:3042'),
|
||||
encryptionKek,
|
||||
missionGrantPublicKeyPem: env('MANA_AI_PUBLIC_KEY_PEM') || undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ import { SignupLimitService } from './services/signup-limit';
|
|||
import { ApiKeysService } from './services/api-keys';
|
||||
import { UserDataService } from './services/user-data';
|
||||
import { EncryptionVaultService } from './services/encryption-vault';
|
||||
import { MissionGrantService } from './services/encryption-vault/mission-grant';
|
||||
import { loadKek } from './services/encryption-vault/kek';
|
||||
import { createAuthRoutes } from './routes/auth';
|
||||
import { createGuildRoutes } from './routes/guilds';
|
||||
import { createApiKeyRoutes, createApiKeyValidationRoute } from './routes/api-keys';
|
||||
import { createMeRoutes } from './routes/me';
|
||||
import { createEncryptionVaultRoutes } from './routes/encryption-vault';
|
||||
import { createAiMissionGrantRoutes } from './routes/ai-mission-grant';
|
||||
import { createSettingsRoutes } from './routes/settings';
|
||||
import { createAdminRoutes } from './routes/admin';
|
||||
|
||||
|
|
@ -45,6 +47,10 @@ const signupLimit = new SignupLimitService(db);
|
|||
const apiKeysService = new ApiKeysService(db);
|
||||
const userDataService = new UserDataService(db, config);
|
||||
const encryptionVaultService = new EncryptionVaultService(db);
|
||||
const missionGrantService = new MissionGrantService(
|
||||
encryptionVaultService,
|
||||
config.missionGrantPublicKeyPem
|
||||
);
|
||||
|
||||
// ─── App ────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -97,6 +103,12 @@ app.route('/api/v1/me', createMeRoutes(userDataService));
|
|||
// up in the same self-service surface as the GDPR endpoints.
|
||||
app.route('/api/v1/me/encryption-vault', createEncryptionVaultRoutes(encryptionVaultService));
|
||||
|
||||
// ─── AI Mission Grant ──────────────────────────────────────
|
||||
// Mints per-mission Key-Grants so the mana-ai background runner can
|
||||
// decrypt scoped encrypted records. Under /me so it inherits the JWT
|
||||
// middleware above. See docs/plans/ai-mission-key-grant.md.
|
||||
app.route('/api/v1/me/ai-mission-grant', createAiMissionGrantRoutes(missionGrantService));
|
||||
|
||||
// ─── Settings ──────────────────────────────────────────────
|
||||
|
||||
app.use('/api/v1/settings/*', jwtAuth(config.baseUrl));
|
||||
|
|
|
|||
115
services/mana-auth/src/routes/ai-mission-grant.ts
Normal file
115
services/mana-auth/src/routes/ai-mission-grant.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* Mission Grant route — `POST /api/v1/me/ai-mission-grant`.
|
||||
*
|
||||
* Mints a grant that lets the mana-ai background runner decrypt the
|
||||
* allowlisted records for a specific mission without needing the user's
|
||||
* browser to be open. See `docs/plans/ai-mission-key-grant.md` for the
|
||||
* full flow; crypto details in `services/encryption-vault/mission-grant.ts`.
|
||||
*
|
||||
* The client posts `{ missionId, tables, recordIds, ttlMs? }`; the server
|
||||
* derives + RSA-wraps a Mission Data Key and returns the grant blob.
|
||||
* The webapp attaches this to `Mission.grant` via the normal sync path.
|
||||
* The recovery / revocation side lives on the webapp — revoking is just
|
||||
* setting `Mission.grant = null` on the Dexie record; the server has
|
||||
* nothing to remember.
|
||||
*/
|
||||
|
||||
import { Hono, type Context } from 'hono';
|
||||
import type { AuthUser } from '../middleware/jwt-auth';
|
||||
import {
|
||||
MissionGrantService,
|
||||
MissionGrantNotConfigured,
|
||||
ZeroKnowledgeGrantForbidden,
|
||||
VaultNotFoundError,
|
||||
} from '../services/encryption-vault/mission-grant';
|
||||
import type { AuditContext } from '../services/encryption-vault';
|
||||
|
||||
type AppContext = Context<{ Variables: { user: AuthUser } }>;
|
||||
|
||||
export function createAiMissionGrantRoutes(service: MissionGrantService) {
|
||||
const app = new Hono<{ Variables: { user: AuthUser } }>();
|
||||
|
||||
app.post('/', async (c) => {
|
||||
const user = c.get('user');
|
||||
const ctx = readAuditContext(c);
|
||||
|
||||
const body = (await c.req.json().catch(() => null)) as {
|
||||
missionId?: unknown;
|
||||
tables?: unknown;
|
||||
recordIds?: unknown;
|
||||
ttlMs?: unknown;
|
||||
} | null;
|
||||
|
||||
if (
|
||||
!body ||
|
||||
typeof body.missionId !== 'string' ||
|
||||
!body.missionId ||
|
||||
!Array.isArray(body.tables) ||
|
||||
!body.tables.every((t) => typeof t === 'string') ||
|
||||
!Array.isArray(body.recordIds) ||
|
||||
!body.recordIds.every((r) => typeof r === 'string')
|
||||
) {
|
||||
return c.json(
|
||||
{
|
||||
error: 'missionId (string), tables (string[]), recordIds (string[]) are required',
|
||||
code: 'BAD_REQUEST',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
const ttlMs = typeof body.ttlMs === 'number' ? body.ttlMs : undefined;
|
||||
|
||||
try {
|
||||
const grant = await service.createGrant(
|
||||
user.userId,
|
||||
{
|
||||
missionId: body.missionId,
|
||||
tables: body.tables as string[],
|
||||
recordIds: body.recordIds as string[],
|
||||
ttlMs,
|
||||
},
|
||||
ctx
|
||||
);
|
||||
return c.json(grant);
|
||||
} catch (err) {
|
||||
if (err instanceof MissionGrantNotConfigured) {
|
||||
return c.json(
|
||||
{
|
||||
error: 'mission grants are not configured on this server',
|
||||
code: 'GRANT_NOT_CONFIGURED',
|
||||
},
|
||||
503
|
||||
);
|
||||
}
|
||||
if (err instanceof ZeroKnowledgeGrantForbidden) {
|
||||
return c.json(
|
||||
{
|
||||
error:
|
||||
'mission grants are unavailable in zero-knowledge mode — disable ZK or use the foreground runner',
|
||||
code: 'ZK_ACTIVE',
|
||||
},
|
||||
409
|
||||
);
|
||||
}
|
||||
if (err instanceof VaultNotFoundError) {
|
||||
return c.json({ error: 'vault not initialised', code: 'VAULT_NOT_INITIALISED' }, 404);
|
||||
}
|
||||
if (err instanceof Error && /required|must/.test(err.message)) {
|
||||
return c.json({ error: err.message, code: 'BAD_REQUEST' }, 400);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function readAuditContext(c: AppContext): AuditContext {
|
||||
return {
|
||||
ipAddress:
|
||||
c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
|
||||
c.req.header('x-real-ip') ||
|
||||
undefined,
|
||||
userAgent: c.req.header('user-agent') || undefined,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* MissionGrantService unit tests.
|
||||
*
|
||||
* Crypto-only — stubs the EncryptionVaultService so we don't need a
|
||||
* real Postgres. Generates a fresh RSA-OAEP-2048 keypair per-test,
|
||||
* exports the public key as SPKI PEM, feeds it into the service, then
|
||||
* unwraps the returned grant with the private key and checks it matches
|
||||
* the expected HKDF output.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { deriveMissionDataKeyRaw, GRANT_DERIVATION_VERSION } from '@mana/shared-ai';
|
||||
import {
|
||||
MissionGrantService,
|
||||
MissionGrantNotConfigured,
|
||||
ZeroKnowledgeGrantForbidden,
|
||||
} from './mission-grant';
|
||||
import type { EncryptionVaultService, VaultFetchResult } from './index';
|
||||
|
||||
const fixedMasterKey = new Uint8Array(32).map((_, i) => i + 1);
|
||||
|
||||
/** The service zero-fills the returned masterKey after use, so each
|
||||
* getMasterKey() call must return a fresh copy — otherwise a second
|
||||
* call in the same test would derive from all-zero bytes. */
|
||||
function stubVault(result: VaultFetchResult): EncryptionVaultService {
|
||||
return {
|
||||
getMasterKey: async () => ({
|
||||
...result,
|
||||
masterKey: result.masterKey ? new Uint8Array(result.masterKey) : null,
|
||||
}),
|
||||
} as unknown as EncryptionVaultService;
|
||||
}
|
||||
|
||||
async function genKeypair() {
|
||||
const kp = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
const spki = new Uint8Array(await crypto.subtle.exportKey('spki', kp.publicKey));
|
||||
const pem =
|
||||
'-----BEGIN PUBLIC KEY-----\n' +
|
||||
chunk(bytesToBase64(spki), 64).join('\n') +
|
||||
'\n-----END PUBLIC KEY-----';
|
||||
return { pem, privateKey: kp.privateKey };
|
||||
}
|
||||
|
||||
describe('MissionGrantService', () => {
|
||||
it('mints a grant whose wrappedKey unwraps to the derived MDK', async () => {
|
||||
const { pem, privateKey } = await genKeypair();
|
||||
const service = new MissionGrantService(
|
||||
stubVault({ masterKey: new Uint8Array(fixedMasterKey), formatVersion: 1, kekId: 'env-v1' }),
|
||||
pem
|
||||
);
|
||||
|
||||
const grant = await service.createGrant('user-1', {
|
||||
missionId: 'mission-abc',
|
||||
tables: ['notes', 'tasks'],
|
||||
recordIds: ['notes:n1', 'tasks:t1'],
|
||||
});
|
||||
|
||||
expect(grant.derivation.version).toBe(GRANT_DERIVATION_VERSION);
|
||||
expect(grant.derivation.missionId).toBe('mission-abc');
|
||||
expect(grant.derivation.tables).toEqual(['notes', 'tasks']);
|
||||
expect(grant.derivation.recordIds).toEqual(['notes:n1', 'tasks:t1']);
|
||||
|
||||
const wrappedBytes = base64ToBytes(grant.wrappedKey);
|
||||
const plain = new Uint8Array(
|
||||
await crypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, toBufferSource(wrappedBytes))
|
||||
);
|
||||
|
||||
const expectedMdk = await deriveMissionDataKeyRaw(
|
||||
new Uint8Array(fixedMasterKey),
|
||||
grant.derivation
|
||||
);
|
||||
expect(Array.from(plain)).toEqual(Array.from(expectedMdk));
|
||||
});
|
||||
|
||||
it('sorts tables and recordIds before binding into the key', async () => {
|
||||
const { pem, privateKey } = await genKeypair();
|
||||
const service = new MissionGrantService(
|
||||
stubVault({ masterKey: new Uint8Array(fixedMasterKey), formatVersion: 1, kekId: 'env-v1' }),
|
||||
pem
|
||||
);
|
||||
|
||||
const a = await service.createGrant('u', {
|
||||
missionId: 'm',
|
||||
tables: ['tasks', 'notes'],
|
||||
recordIds: ['tasks:t1', 'notes:n1'],
|
||||
});
|
||||
const b = await service.createGrant('u', {
|
||||
missionId: 'm',
|
||||
tables: ['notes', 'tasks'],
|
||||
recordIds: ['notes:n1', 'tasks:t1'],
|
||||
});
|
||||
|
||||
const keyA = new Uint8Array(
|
||||
await crypto.subtle.decrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
privateKey,
|
||||
toBufferSource(base64ToBytes(a.wrappedKey))
|
||||
)
|
||||
);
|
||||
const keyB = new Uint8Array(
|
||||
await crypto.subtle.decrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
privateKey,
|
||||
toBufferSource(base64ToBytes(b.wrappedKey))
|
||||
)
|
||||
);
|
||||
expect(Array.from(keyA)).toEqual(Array.from(keyB));
|
||||
});
|
||||
|
||||
it('rejects zero-knowledge users', async () => {
|
||||
const { pem } = await genKeypair();
|
||||
const service = new MissionGrantService(
|
||||
stubVault({
|
||||
masterKey: null,
|
||||
formatVersion: 1,
|
||||
kekId: '',
|
||||
requiresRecoveryCode: true,
|
||||
recoveryWrappedMk: 'x',
|
||||
recoveryIv: 'y',
|
||||
}),
|
||||
pem
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.createGrant('u', { missionId: 'm', tables: ['notes'], recordIds: ['notes:n1'] })
|
||||
).rejects.toBeInstanceOf(ZeroKnowledgeGrantForbidden);
|
||||
});
|
||||
|
||||
it('throws MissionGrantNotConfigured when no public key is set', async () => {
|
||||
const service = new MissionGrantService(
|
||||
stubVault({ masterKey: new Uint8Array(fixedMasterKey), formatVersion: 1, kekId: 'env-v1' }),
|
||||
undefined
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.createGrant('u', { missionId: 'm', tables: ['notes'], recordIds: ['notes:n1'] })
|
||||
).rejects.toBeInstanceOf(MissionGrantNotConfigured);
|
||||
});
|
||||
|
||||
it('rejects missing tables / recordIds', async () => {
|
||||
const { pem } = await genKeypair();
|
||||
const service = new MissionGrantService(
|
||||
stubVault({ masterKey: new Uint8Array(fixedMasterKey), formatVersion: 1, kekId: 'env-v1' }),
|
||||
pem
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.createGrant('u', { missionId: 'm', tables: [], recordIds: ['a'] })
|
||||
).rejects.toThrow(/tables/);
|
||||
await expect(
|
||||
service.createGrant('u', { missionId: 'm', tables: ['notes'], recordIds: [] })
|
||||
).rejects.toThrow(/recordIds/);
|
||||
});
|
||||
|
||||
it('clamps ttl to the upper bound', async () => {
|
||||
const { pem } = await genKeypair();
|
||||
const service = new MissionGrantService(
|
||||
stubVault({ masterKey: new Uint8Array(fixedMasterKey), formatVersion: 1, kekId: 'env-v1' }),
|
||||
pem
|
||||
);
|
||||
|
||||
const grant = await service.createGrant('u', {
|
||||
missionId: 'm',
|
||||
tables: ['notes'],
|
||||
recordIds: ['notes:n1'],
|
||||
ttlMs: 365 * 24 * 60 * 60 * 1000, // 1 year → clamped to 30d
|
||||
});
|
||||
const ttlMs = new Date(grant.expiresAt).getTime() - new Date(grant.issuedAt).getTime();
|
||||
expect(ttlMs).toBe(30 * 24 * 60 * 60 * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
let bin = '';
|
||||
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
function base64ToBytes(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function toBufferSource(bytes: Uint8Array): ArrayBuffer {
|
||||
const buf = new ArrayBuffer(bytes.length);
|
||||
new Uint8Array(buf).set(bytes);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function chunk(s: string, n: number): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < s.length; i += n) out.push(s.slice(i, i + n));
|
||||
return out;
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* MissionGrantService — issues Key-Grants that let the `mana-ai`
|
||||
* background runner decrypt scoped encrypted records without the
|
||||
* user's browser being open.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Fetch the user's master key via the existing vault service.
|
||||
* Zero-knowledge users return null → grant is refused.
|
||||
* 2. Derive a Mission Data Key (MDK) with the canonical HKDF from
|
||||
* `@mana/shared-ai`. Scope (tables + recordIds) is cryptographically
|
||||
* bound, so any scope change invalidates the grant automatically.
|
||||
* 3. RSA-OAEP-2048-wrap the raw MDK bytes with the mana-ai public
|
||||
* key. Only the paired private key (held in mana-ai's process
|
||||
* memory) can unwrap.
|
||||
* 4. Return the grant blob `{ wrappedKey, derivation, issuedAt,
|
||||
* expiresAt }`. The route attaches it to `Mission.grant` via the
|
||||
* webapp's normal sync write path.
|
||||
*
|
||||
* Why here and not in mana-ai?
|
||||
* Only mana-auth has the KEK, the vault rows, and therefore the
|
||||
* unwrapped master key. Everyone else either doesn't get the key
|
||||
* at all (services) or gets it transiently on first login (webapp).
|
||||
* Centralising the grant mint means one audit-logged path, not two.
|
||||
*/
|
||||
|
||||
import {
|
||||
deriveMissionDataKeyRaw,
|
||||
GRANT_DERIVATION_VERSION,
|
||||
type GrantDerivation,
|
||||
type MissionGrant,
|
||||
} from '@mana/shared-ai';
|
||||
import { EncryptionVaultService, VaultNotFoundError, type AuditContext } from './index';
|
||||
|
||||
/** Default lifetime of a freshly-minted grant. User keeps a mission
|
||||
* editing / ticking within this window → grant stays fresh; long
|
||||
* idle → grant expires and the runner falls back to foreground. */
|
||||
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
|
||||
export interface CreateGrantInput {
|
||||
missionId: string;
|
||||
tables: string[];
|
||||
recordIds: string[];
|
||||
/** Override the default 7-day TTL. Upper-bounded by the service to
|
||||
* stay below the rotation horizon. */
|
||||
ttlMs?: number;
|
||||
}
|
||||
|
||||
/** Thrown when the user is in zero-knowledge mode. The server has no
|
||||
* usable master key → cannot derive an MDK → grant is impossible.
|
||||
* Routes convert to 409 ZK_ACTIVE so the UI can fall back to the
|
||||
* foreground runner without treating this as an error. */
|
||||
export class ZeroKnowledgeGrantForbidden extends Error {
|
||||
constructor(public userId: string) {
|
||||
super(`cannot issue mission grant: user ${userId} is in zero-knowledge mode`);
|
||||
this.name = 'ZeroKnowledgeGrantForbidden';
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when the service boots without a configured mission-grant
|
||||
* public key. Routes convert to 503 so the UI degrades cleanly to
|
||||
* foreground-only execution. */
|
||||
export class MissionGrantNotConfigured extends Error {
|
||||
constructor() {
|
||||
super('mana-auth: MANA_AI_PUBLIC_KEY_PEM is not set — grants are disabled');
|
||||
this.name = 'MissionGrantNotConfigured';
|
||||
}
|
||||
}
|
||||
|
||||
export class MissionGrantService {
|
||||
private pubKeyPromise: Promise<CryptoKey> | null = null;
|
||||
|
||||
constructor(
|
||||
private vaultService: EncryptionVaultService,
|
||||
private publicKeyPem: string | undefined
|
||||
) {}
|
||||
|
||||
/** Mints a fresh grant for the given mission + scope. Idempotent in
|
||||
* the sense that callers can invoke repeatedly to refresh the TTL —
|
||||
* each call produces a new `wrappedKey` with the same MDK (HKDF is
|
||||
* deterministic) but fresh `issuedAt`/`expiresAt`. */
|
||||
async createGrant(
|
||||
userId: string,
|
||||
input: CreateGrantInput,
|
||||
ctx: AuditContext = {}
|
||||
): Promise<MissionGrant> {
|
||||
if (!this.publicKeyPem) {
|
||||
throw new MissionGrantNotConfigured();
|
||||
}
|
||||
|
||||
validateInput(input);
|
||||
|
||||
// VaultFetchResult with null masterKey means the user is in
|
||||
// zero-knowledge mode. The server simply has no way to help — the
|
||||
// user has to disable ZK first or stick to the foreground runner.
|
||||
const vault = await this.vaultService.getMasterKey(userId, ctx);
|
||||
if (!vault.masterKey) {
|
||||
throw new ZeroKnowledgeGrantForbidden(userId);
|
||||
}
|
||||
|
||||
const derivation: GrantDerivation = {
|
||||
version: GRANT_DERIVATION_VERSION,
|
||||
missionId: input.missionId,
|
||||
tables: [...input.tables].sort(),
|
||||
recordIds: [...input.recordIds].sort(),
|
||||
};
|
||||
|
||||
let mdkBytes: Uint8Array | null = null;
|
||||
try {
|
||||
mdkBytes = await deriveMissionDataKeyRaw(vault.masterKey, derivation);
|
||||
|
||||
const pubKey = await this.loadPublicKey();
|
||||
const ct = await crypto.subtle.encrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
pubKey,
|
||||
toBufferSource(mdkBytes)
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
const ttl = clampTtl(input.ttlMs ?? DEFAULT_TTL_MS);
|
||||
|
||||
return {
|
||||
wrappedKey: bytesToBase64(new Uint8Array(ct)),
|
||||
derivation,
|
||||
issuedAt: new Date(now).toISOString(),
|
||||
expiresAt: new Date(now + ttl).toISOString(),
|
||||
};
|
||||
} finally {
|
||||
if (mdkBytes) mdkBytes.fill(0);
|
||||
vault.masterKey.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Lazily parse the PEM once per process. Web Crypto doesn't speak PEM
|
||||
* directly — we strip the header/footer and decode the base64 DER. */
|
||||
private loadPublicKey(): Promise<CryptoKey> {
|
||||
if (!this.pubKeyPromise) {
|
||||
this.pubKeyPromise = importRsaPublicKey(this.publicKeyPem!);
|
||||
}
|
||||
return this.pubKeyPromise;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────
|
||||
|
||||
function validateInput(input: CreateGrantInput): void {
|
||||
if (!input.missionId) throw new Error('missionId is required');
|
||||
if (!Array.isArray(input.tables) || input.tables.length === 0) {
|
||||
throw new Error('tables must be a non-empty array');
|
||||
}
|
||||
if (!Array.isArray(input.recordIds) || input.recordIds.length === 0) {
|
||||
throw new Error('recordIds must be a non-empty array');
|
||||
}
|
||||
if (input.recordIds.length > 1000) {
|
||||
// Hard cap so a pathological client can't blow up the HKDF info
|
||||
// string. 1000 is ~50KB of info bytes which Web Crypto handles
|
||||
// fine but we don't need more than that for any real mission.
|
||||
throw new Error('recordIds must not exceed 1000 entries');
|
||||
}
|
||||
}
|
||||
|
||||
/** Clamp the requested TTL to [1h, 30d]. Below 1h is probably a bug;
|
||||
* above 30d forces a re-consent eventually even for long-running
|
||||
* missions. */
|
||||
function clampTtl(ms: number): number {
|
||||
const MIN = 60 * 60 * 1000;
|
||||
const MAX = 30 * 24 * 60 * 60 * 1000;
|
||||
if (ms < MIN) return MIN;
|
||||
if (ms > MAX) return MAX;
|
||||
return ms;
|
||||
}
|
||||
|
||||
async function importRsaPublicKey(pem: string): Promise<CryptoKey> {
|
||||
const body = pem
|
||||
.replace(/-----BEGIN [^-]+-----/g, '')
|
||||
.replace(/-----END [^-]+-----/g, '')
|
||||
.replace(/\s+/g, '');
|
||||
const der = base64ToBytes(body);
|
||||
return crypto.subtle.importKey(
|
||||
'spki',
|
||||
toBufferSource(der),
|
||||
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
let bin = '';
|
||||
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
function base64ToBytes(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function toBufferSource(bytes: Uint8Array): ArrayBuffer {
|
||||
const buf = new ArrayBuffer(bytes.length);
|
||||
new Uint8Array(buf).set(bytes);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Re-export VaultNotFoundError so the route can catch it from one import.
|
||||
export { VaultNotFoundError };
|
||||
Loading…
Add table
Add a link
Reference in a new issue