diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e2470d04..25f009577 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/services/mana-ai/src/config.ts b/services/mana-ai/src/config.ts index 3b400697b..0a4c1102d 100644 --- a/services/mana-ai/src/config.ts +++ b/services/mana-ai/src/config.ts @@ -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, }; } diff --git a/services/mana-ai/src/crypto/unwrap-grant.test.ts b/services/mana-ai/src/crypto/unwrap-grant.test.ts new file mode 100644 index 000000000..b5a772b84 --- /dev/null +++ b/services/mana-ai/src/crypto/unwrap-grant.test.ts @@ -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 { + 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; +} diff --git a/services/mana-ai/src/crypto/unwrap-grant.ts b/services/mana-ai/src/crypto/unwrap-grant.ts new file mode 100644 index 000000000..c880c4b71 --- /dev/null +++ b/services/mana-ai/src/crypto/unwrap-grant.ts @@ -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 | 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 { + 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 { + 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; +} diff --git a/services/mana-ai/src/db/migrate.ts b/services/mana-ai/src/db/migrate.ts index 1d6e1aa47..3287424f5 100644 --- a/services/mana-ai/src/db/migrate.ts +++ b/services/mana-ai/src/db/migrate.ts @@ -39,4 +39,54 @@ export async function migrate(sql: Sql): Promise { 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 $$ + `; } diff --git a/services/mana-ai/src/index.ts b/services/mana-ai/src/index.ts index 2c819e22a..9211de898 100644 --- a/services/mana-ai/src/index.ts +++ b/services/mana-ai/src/index.ts @@ -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. diff --git a/services/mana-auth/package.json b/services/mana-auth/package.json index 6da294242..cc56a94db 100644 --- a/services/mana-auth/package.json +++ b/services/mana-auth/package.json @@ -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", diff --git a/services/mana-auth/src/config.ts b/services/mana-auth/src/config.ts index d773215e3..d0bf518ff 100644 --- a/services/mana-auth/src/config.ts +++ b/services/mana-auth/src/config.ts @@ -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, }; } diff --git a/services/mana-auth/src/index.ts b/services/mana-auth/src/index.ts index 5d7296ee2..156499b74 100644 --- a/services/mana-auth/src/index.ts +++ b/services/mana-auth/src/index.ts @@ -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)); diff --git a/services/mana-auth/src/routes/ai-mission-grant.ts b/services/mana-auth/src/routes/ai-mission-grant.ts new file mode 100644 index 000000000..f6b45c27c --- /dev/null +++ b/services/mana-auth/src/routes/ai-mission-grant.ts @@ -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, + }; +} diff --git a/services/mana-auth/src/services/encryption-vault/mission-grant.test.ts b/services/mana-auth/src/services/encryption-vault/mission-grant.test.ts new file mode 100644 index 000000000..5559cfc12 --- /dev/null +++ b/services/mana-auth/src/services/encryption-vault/mission-grant.test.ts @@ -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; +} diff --git a/services/mana-auth/src/services/encryption-vault/mission-grant.ts b/services/mana-auth/src/services/encryption-vault/mission-grant.ts new file mode 100644 index 000000000..e6f278641 --- /dev/null +++ b/services/mana-auth/src/services/encryption-vault/mission-grant.ts @@ -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 | 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 { + 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 { + 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 { + 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 };