@data-club/ai-hub-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/auth/argon2.d.ts +72 -0
  2. package/dist/auth/argon2.d.ts.map +1 -0
  3. package/dist/auth/argon2.js +145 -0
  4. package/dist/auth/argon2.js.map +1 -0
  5. package/dist/auth/encryption.d.ts +52 -0
  6. package/dist/auth/encryption.d.ts.map +1 -0
  7. package/dist/auth/encryption.js +98 -0
  8. package/dist/auth/encryption.js.map +1 -0
  9. package/dist/auth/jwt.d.ts +133 -0
  10. package/dist/auth/jwt.d.ts.map +1 -0
  11. package/dist/auth/jwt.js +147 -0
  12. package/dist/auth/jwt.js.map +1 -0
  13. package/dist/auth/middleware.d.ts +101 -0
  14. package/dist/auth/middleware.d.ts.map +1 -0
  15. package/dist/auth/middleware.js +161 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/passwordPolicy.d.ts +72 -0
  18. package/dist/auth/passwordPolicy.d.ts.map +1 -0
  19. package/dist/auth/passwordPolicy.js +88 -0
  20. package/dist/auth/passwordPolicy.js.map +1 -0
  21. package/dist/auth/roleRules.d.ts +200 -0
  22. package/dist/auth/roleRules.d.ts.map +1 -0
  23. package/dist/auth/roleRules.js +365 -0
  24. package/dist/auth/roleRules.js.map +1 -0
  25. package/dist/auth/userView.d.ts +55 -0
  26. package/dist/auth/userView.d.ts.map +1 -0
  27. package/dist/auth/userView.js +77 -0
  28. package/dist/auth/userView.js.map +1 -0
  29. package/dist/bootstrap.d.ts +104 -0
  30. package/dist/bootstrap.d.ts.map +1 -0
  31. package/dist/bootstrap.js +160 -0
  32. package/dist/bootstrap.js.map +1 -0
  33. package/dist/db/connection.d.ts +39 -0
  34. package/dist/db/connection.d.ts.map +1 -0
  35. package/dist/db/connection.js +62 -0
  36. package/dist/db/connection.js.map +1 -0
  37. package/dist/db/migrate.d.ts +31 -0
  38. package/dist/db/migrate.d.ts.map +1 -0
  39. package/dist/db/migrate.js +197 -0
  40. package/dist/db/migrate.js.map +1 -0
  41. package/dist/db/schema.d.ts +1902 -0
  42. package/dist/db/schema.d.ts.map +1 -0
  43. package/dist/db/schema.js +662 -0
  44. package/dist/db/schema.js.map +1 -0
  45. package/dist/db/seed.d.ts +92 -0
  46. package/dist/db/seed.d.ts.map +1 -0
  47. package/dist/db/seed.js +160 -0
  48. package/dist/db/seed.js.map +1 -0
  49. package/dist/handlers/fetch.d.ts +89 -0
  50. package/dist/handlers/fetch.d.ts.map +1 -0
  51. package/dist/handlers/fetch.js +173 -0
  52. package/dist/handlers/fetch.js.map +1 -0
  53. package/dist/handlers/nextjs.d.ts +40 -0
  54. package/dist/handlers/nextjs.d.ts.map +1 -0
  55. package/dist/handlers/nextjs.js +42 -0
  56. package/dist/handlers/nextjs.js.map +1 -0
  57. package/dist/handlers/responses.d.ts +46 -0
  58. package/dist/handlers/responses.d.ts.map +1 -0
  59. package/dist/handlers/responses.js +74 -0
  60. package/dist/handlers/responses.js.map +1 -0
  61. package/dist/handlers/routeTable.d.ts +21 -0
  62. package/dist/handlers/routeTable.d.ts.map +1 -0
  63. package/dist/handlers/routeTable.js +133 -0
  64. package/dist/handlers/routeTable.js.map +1 -0
  65. package/dist/handlers/router.d.ts +37 -0
  66. package/dist/handlers/router.d.ts.map +1 -0
  67. package/dist/handlers/router.js +82 -0
  68. package/dist/handlers/router.js.map +1 -0
  69. package/dist/handlers/sse.d.ts +38 -0
  70. package/dist/handlers/sse.d.ts.map +1 -0
  71. package/dist/handlers/sse.js +52 -0
  72. package/dist/handlers/sse.js.map +1 -0
  73. package/dist/handlers/types.d.ts +96 -0
  74. package/dist/handlers/types.d.ts.map +1 -0
  75. package/dist/handlers/types.js +14 -0
  76. package/dist/handlers/types.js.map +1 -0
  77. package/dist/index.d.ts +25 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +24 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/letta/client.d.ts +355 -0
  82. package/dist/letta/client.d.ts.map +1 -0
  83. package/dist/letta/client.js +529 -0
  84. package/dist/letta/client.js.map +1 -0
  85. package/dist/letta/eventTranslator.d.ts +166 -0
  86. package/dist/letta/eventTranslator.d.ts.map +1 -0
  87. package/dist/letta/eventTranslator.js +532 -0
  88. package/dist/letta/eventTranslator.js.map +1 -0
  89. package/dist/letta/index.d.ts +13 -0
  90. package/dist/letta/index.d.ts.map +1 -0
  91. package/dist/letta/index.js +12 -0
  92. package/dist/letta/index.js.map +1 -0
  93. package/dist/letta/modelSettings.d.ts +176 -0
  94. package/dist/letta/modelSettings.d.ts.map +1 -0
  95. package/dist/letta/modelSettings.js +314 -0
  96. package/dist/letta/modelSettings.js.map +1 -0
  97. package/dist/letta/promptWrapping.d.ts +141 -0
  98. package/dist/letta/promptWrapping.d.ts.map +1 -0
  99. package/dist/letta/promptWrapping.js +213 -0
  100. package/dist/letta/promptWrapping.js.map +1 -0
  101. package/dist/letta/sandbox.d.ts +100 -0
  102. package/dist/letta/sandbox.d.ts.map +1 -0
  103. package/dist/letta/sandbox.js +151 -0
  104. package/dist/letta/sandbox.js.map +1 -0
  105. package/dist/letta/settingsFileSeed.d.ts +146 -0
  106. package/dist/letta/settingsFileSeed.d.ts.map +1 -0
  107. package/dist/letta/settingsFileSeed.js +177 -0
  108. package/dist/letta/settingsFileSeed.js.map +1 -0
  109. package/dist/letta/subprocessThrottle.d.ts +151 -0
  110. package/dist/letta/subprocessThrottle.d.ts.map +1 -0
  111. package/dist/letta/subprocessThrottle.js +195 -0
  112. package/dist/letta/subprocessThrottle.js.map +1 -0
  113. package/dist/letta/tpmRecovery.d.ts +202 -0
  114. package/dist/letta/tpmRecovery.d.ts.map +1 -0
  115. package/dist/letta/tpmRecovery.js +314 -0
  116. package/dist/letta/tpmRecovery.js.map +1 -0
  117. package/dist/letta/types.d.ts +54 -0
  118. package/dist/letta/types.d.ts.map +1 -0
  119. package/dist/letta/types.js +16 -0
  120. package/dist/letta/types.js.map +1 -0
  121. package/dist/observability/logger.d.ts +48 -0
  122. package/dist/observability/logger.d.ts.map +1 -0
  123. package/dist/observability/logger.js +81 -0
  124. package/dist/observability/logger.js.map +1 -0
  125. package/dist/observability/metrics.d.ts +53 -0
  126. package/dist/observability/metrics.d.ts.map +1 -0
  127. package/dist/observability/metrics.js +92 -0
  128. package/dist/observability/metrics.js.map +1 -0
  129. package/dist/routes/admin/agents.d.ts +63 -0
  130. package/dist/routes/admin/agents.d.ts.map +1 -0
  131. package/dist/routes/admin/agents.js +461 -0
  132. package/dist/routes/admin/agents.js.map +1 -0
  133. package/dist/routes/admin/rotatingMessages.d.ts +51 -0
  134. package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
  135. package/dist/routes/admin/rotatingMessages.js +292 -0
  136. package/dist/routes/admin/rotatingMessages.js.map +1 -0
  137. package/dist/routes/admin/sessions.d.ts +61 -0
  138. package/dist/routes/admin/sessions.d.ts.map +1 -0
  139. package/dist/routes/admin/sessions.js +232 -0
  140. package/dist/routes/admin/sessions.js.map +1 -0
  141. package/dist/routes/admin/settings.d.ts +110 -0
  142. package/dist/routes/admin/settings.d.ts.map +1 -0
  143. package/dist/routes/admin/settings.js +401 -0
  144. package/dist/routes/admin/settings.js.map +1 -0
  145. package/dist/routes/admin/users.d.ts +115 -0
  146. package/dist/routes/admin/users.d.ts.map +1 -0
  147. package/dist/routes/admin/users.js +374 -0
  148. package/dist/routes/admin/users.js.map +1 -0
  149. package/dist/routes/agents.d.ts +30 -0
  150. package/dist/routes/agents.d.ts.map +1 -0
  151. package/dist/routes/agents.js +55 -0
  152. package/dist/routes/agents.js.map +1 -0
  153. package/dist/routes/auth.d.ts +121 -0
  154. package/dist/routes/auth.d.ts.map +1 -0
  155. package/dist/routes/auth.js +422 -0
  156. package/dist/routes/auth.js.map +1 -0
  157. package/dist/routes/chat.d.ts +97 -0
  158. package/dist/routes/chat.d.ts.map +1 -0
  159. package/dist/routes/chat.js +484 -0
  160. package/dist/routes/chat.js.map +1 -0
  161. package/dist/routes/featureToggles.d.ts +34 -0
  162. package/dist/routes/featureToggles.d.ts.map +1 -0
  163. package/dist/routes/featureToggles.js +47 -0
  164. package/dist/routes/featureToggles.js.map +1 -0
  165. package/dist/routes/health.d.ts +24 -0
  166. package/dist/routes/health.d.ts.map +1 -0
  167. package/dist/routes/health.js +37 -0
  168. package/dist/routes/health.js.map +1 -0
  169. package/dist/routes/metrics.d.ts +35 -0
  170. package/dist/routes/metrics.d.ts.map +1 -0
  171. package/dist/routes/metrics.js +106 -0
  172. package/dist/routes/metrics.js.map +1 -0
  173. package/dist/routes/sessions.d.ts +161 -0
  174. package/dist/routes/sessions.d.ts.map +1 -0
  175. package/dist/routes/sessions.js +1501 -0
  176. package/dist/routes/sessions.js.map +1 -0
  177. package/dist/routes/stubs.d.ts +25 -0
  178. package/dist/routes/stubs.d.ts.map +1 -0
  179. package/dist/routes/stubs.js +25 -0
  180. package/dist/routes/stubs.js.map +1 -0
  181. package/dist/state/cron.d.ts +58 -0
  182. package/dist/state/cron.d.ts.map +1 -0
  183. package/dist/state/cron.js +160 -0
  184. package/dist/state/cron.js.map +1 -0
  185. package/dist/state/rotationRunner.d.ts +96 -0
  186. package/dist/state/rotationRunner.d.ts.map +1 -0
  187. package/dist/state/rotationRunner.js +246 -0
  188. package/dist/state/rotationRunner.js.map +1 -0
  189. package/dist/state/rotationScheduler.d.ts +76 -0
  190. package/dist/state/rotationScheduler.d.ts.map +1 -0
  191. package/dist/state/rotationScheduler.js +124 -0
  192. package/dist/state/rotationScheduler.js.map +1 -0
  193. package/dist/state/turnRegistry.d.ts +95 -0
  194. package/dist/state/turnRegistry.d.ts.map +1 -0
  195. package/dist/state/turnRegistry.js +149 -0
  196. package/dist/state/turnRegistry.js.map +1 -0
  197. package/dist/state/turnRunner.d.ts +134 -0
  198. package/dist/state/turnRunner.d.ts.map +1 -0
  199. package/dist/state/turnRunner.js +476 -0
  200. package/dist/state/turnRunner.js.map +1 -0
  201. package/package.json +47 -0
  202. package/src/auth/argon2.ts +152 -0
  203. package/src/auth/encryption.ts +110 -0
  204. package/src/auth/jwt.ts +217 -0
  205. package/src/auth/middleware.ts +250 -0
  206. package/src/auth/passwordPolicy.ts +99 -0
  207. package/src/auth/roleRules.ts +497 -0
  208. package/src/auth/userView.ts +88 -0
  209. package/src/bootstrap.ts +254 -0
  210. package/src/db/connection.ts +75 -0
  211. package/src/db/migrate.ts +212 -0
  212. package/src/db/schema.ts +750 -0
  213. package/src/db/seed.ts +236 -0
  214. package/src/handlers/fetch.ts +251 -0
  215. package/src/handlers/nextjs.ts +49 -0
  216. package/src/handlers/responses.ts +95 -0
  217. package/src/handlers/routeTable.ts +188 -0
  218. package/src/handlers/router.ts +96 -0
  219. package/src/handlers/sse.ts +55 -0
  220. package/src/handlers/types.ts +104 -0
  221. package/src/index.ts +60 -0
  222. package/src/letta/client.ts +860 -0
  223. package/src/letta/eventTranslator.ts +667 -0
  224. package/src/letta/index.ts +46 -0
  225. package/src/letta/lettaCliShim.mjs +40 -0
  226. package/src/letta/sandbox.ts +167 -0
  227. package/src/letta/settingsFileSeed.ts +254 -0
  228. package/src/letta/subprocessThrottle.ts +251 -0
  229. package/src/letta/types.ts +65 -0
  230. package/src/observability/logger.ts +103 -0
  231. package/src/observability/metrics.ts +114 -0
  232. package/src/routes/admin/agents.ts +574 -0
  233. package/src/routes/admin/rotatingMessages.ts +351 -0
  234. package/src/routes/admin/sessions.ts +283 -0
  235. package/src/routes/admin/settings.ts +463 -0
  236. package/src/routes/admin/users.ts +455 -0
  237. package/src/routes/agents.ts +66 -0
  238. package/src/routes/auth.ts +513 -0
  239. package/src/routes/chat.ts +534 -0
  240. package/src/routes/featureToggles.ts +52 -0
  241. package/src/routes/health.ts +44 -0
  242. package/src/routes/metrics.ts +111 -0
  243. package/src/routes/sessions.ts +1878 -0
  244. package/src/state/cron.ts +179 -0
  245. package/src/state/rotationRunner.ts +326 -0
  246. package/src/state/rotationScheduler.ts +164 -0
  247. package/src/state/turnRegistry.ts +210 -0
  248. package/src/state/turnRunner.ts +588 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Argon2id helpers.
3
+ *
4
+ * One canonical place to declare the OWASP-baseline parameters (`m=19456` KiB
5
+ * = 19 MiB, `t=2`, `p=1`, 16-byte salt, 32-byte digest) so a future bump is
6
+ * one constant change rather than hunting through call sites (§8.5).
7
+ *
8
+ * The encoded hash is PHC-string format, which embeds m/t/p/salt/digest
9
+ * inline — `verify` reads them straight from the stored string, and
10
+ * `needsRehash` compares the embedded params against the current baseline so
11
+ * we can re-hash on next successful login (the spec calls this "hash
12
+ * upgrade").
13
+ *
14
+ * @see SPECIFICATIONS.md §8.5 — Argon2id parameters + hash upgrade
15
+ */
16
+ /**
17
+ * Current Argon2id parameter baseline. Bumping any of these:
18
+ *
19
+ * 1. Update this object.
20
+ * 2. Existing hashes still verify (the encoded hash carries its own params).
21
+ * 3. On the next successful login `needsRehash` returns true and `hashPassword`
22
+ * rewrites the stored hash with the new params.
23
+ *
24
+ * @see SPECIFICATIONS.md §8.5
25
+ */
26
+ export declare const ARGON2_PARAMS: Readonly<{
27
+ algorithm: 2;
28
+ /** OWASP baseline as of 2024 — 19 MiB. */
29
+ memoryCost: 19456;
30
+ /** Time cost — 2 iterations. */
31
+ timeCost: 2;
32
+ /** Parallelism — single lane is fine for password hashing. */
33
+ parallelism: 1;
34
+ /** 32-byte digest. */
35
+ outputLen: 32;
36
+ }>;
37
+ /**
38
+ * Hashes `password` with the current `ARGON2_PARAMS`. Returns a PHC-string-
39
+ * format encoded hash safe to store in `user.password_hash`.
40
+ *
41
+ * @param password - the plaintext password.
42
+ * @returns the encoded hash.
43
+ */
44
+ export declare function hashPassword(password: string): Promise<string>;
45
+ /**
46
+ * Verifies `password` against `encoded` (a PHC-string-format Argon2id hash
47
+ * previously written by `hashPassword`).
48
+ *
49
+ * Returns `false` for any reason — bad password, malformed hash, wrong
50
+ * algorithm — without throwing. Callers should treat false as "credentials
51
+ * do not match" and respond with a generic 401.
52
+ *
53
+ * @param encoded - the encoded hash from `user.password_hash`.
54
+ * @param password - the candidate plaintext password.
55
+ * @returns `true` if the password is correct.
56
+ */
57
+ export declare function verifyPassword(encoded: string, password: string): Promise<boolean>;
58
+ /**
59
+ * Returns `true` if `encoded` was produced with weaker params than the
60
+ * current `ARGON2_PARAMS` baseline. The spec's hash-upgrade-on-login policy
61
+ * uses this — on a successful login with `needsRehash(stored) === true`, we
62
+ * rehash the supplied plaintext and write the new hash back (§8.5).
63
+ *
64
+ * Parses the PHC string by hand because `@node-rs/argon2` doesn't expose a
65
+ * params-only inspector. Format:
66
+ * `$argon2id$v=19$m=19456,t=2,p=1$<salt-b64>$<hash-b64>`.
67
+ *
68
+ * @param encoded - the encoded hash to inspect.
69
+ * @returns `true` if the hash should be replaced on next successful login.
70
+ */
71
+ export declare function needsRehash(encoded: string): boolean;
72
+ //# sourceMappingURL=argon2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"argon2.d.ts","sourceRoot":"","sources":["../../src/auth/argon2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAcH;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa;;IAExB,0CAA0C;;IAE1C,gCAAgC;;IAEhC,8DAA8D;;IAE9D,sBAAsB;;EAEtB,CAAC;AAEH;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQpD"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Argon2id helpers.
3
+ *
4
+ * One canonical place to declare the OWASP-baseline parameters (`m=19456` KiB
5
+ * = 19 MiB, `t=2`, `p=1`, 16-byte salt, 32-byte digest) so a future bump is
6
+ * one constant change rather than hunting through call sites (§8.5).
7
+ *
8
+ * The encoded hash is PHC-string format, which embeds m/t/p/salt/digest
9
+ * inline — `verify` reads them straight from the stored string, and
10
+ * `needsRehash` compares the embedded params against the current baseline so
11
+ * we can re-hash on next successful login (the spec calls this "hash
12
+ * upgrade").
13
+ *
14
+ * @see SPECIFICATIONS.md §8.5 — Argon2id parameters + hash upgrade
15
+ */
16
+ import { hash, verify } from "@node-rs/argon2";
17
+ /**
18
+ * `@node-rs/argon2` exports `Algorithm` as a `const enum` whose values are
19
+ * compiled away — `verbatimModuleSyntax` (our tsconfig baseline) forbids
20
+ * referencing those at runtime. We hardcode the numeric tag instead. The
21
+ * upstream values are stable: `Argon2d = 0, Argon2i = 1, Argon2id = 2`.
22
+ *
23
+ * @see https://github.com/napi-rs/node-rs/blob/main/packages/argon2/index.d.ts
24
+ */
25
+ const ARGON2_ID_ALGORITHM = 2;
26
+ /**
27
+ * Current Argon2id parameter baseline. Bumping any of these:
28
+ *
29
+ * 1. Update this object.
30
+ * 2. Existing hashes still verify (the encoded hash carries its own params).
31
+ * 3. On the next successful login `needsRehash` returns true and `hashPassword`
32
+ * rewrites the stored hash with the new params.
33
+ *
34
+ * @see SPECIFICATIONS.md §8.5
35
+ */
36
+ export const ARGON2_PARAMS = Object.freeze({
37
+ algorithm: ARGON2_ID_ALGORITHM,
38
+ /** OWASP baseline as of 2024 — 19 MiB. */
39
+ memoryCost: 19456,
40
+ /** Time cost — 2 iterations. */
41
+ timeCost: 2,
42
+ /** Parallelism — single lane is fine for password hashing. */
43
+ parallelism: 1,
44
+ /** 32-byte digest. */
45
+ outputLen: 32,
46
+ });
47
+ /**
48
+ * Hashes `password` with the current `ARGON2_PARAMS`. Returns a PHC-string-
49
+ * format encoded hash safe to store in `user.password_hash`.
50
+ *
51
+ * @param password - the plaintext password.
52
+ * @returns the encoded hash.
53
+ */
54
+ export async function hashPassword(password) {
55
+ return hash(password, ARGON2_PARAMS);
56
+ }
57
+ /**
58
+ * Verifies `password` against `encoded` (a PHC-string-format Argon2id hash
59
+ * previously written by `hashPassword`).
60
+ *
61
+ * Returns `false` for any reason — bad password, malformed hash, wrong
62
+ * algorithm — without throwing. Callers should treat false as "credentials
63
+ * do not match" and respond with a generic 401.
64
+ *
65
+ * @param encoded - the encoded hash from `user.password_hash`.
66
+ * @param password - the candidate plaintext password.
67
+ * @returns `true` if the password is correct.
68
+ */
69
+ export async function verifyPassword(encoded, password) {
70
+ try {
71
+ return await verify(encoded, password);
72
+ }
73
+ catch {
74
+ // Malformed hash, wrong algorithm, etc. — don't crash a login attempt
75
+ // because of stored data integrity issues; just deny.
76
+ return false;
77
+ }
78
+ }
79
+ /**
80
+ * Returns `true` if `encoded` was produced with weaker params than the
81
+ * current `ARGON2_PARAMS` baseline. The spec's hash-upgrade-on-login policy
82
+ * uses this — on a successful login with `needsRehash(stored) === true`, we
83
+ * rehash the supplied plaintext and write the new hash back (§8.5).
84
+ *
85
+ * Parses the PHC string by hand because `@node-rs/argon2` doesn't expose a
86
+ * params-only inspector. Format:
87
+ * `$argon2id$v=19$m=19456,t=2,p=1$<salt-b64>$<hash-b64>`.
88
+ *
89
+ * @param encoded - the encoded hash to inspect.
90
+ * @returns `true` if the hash should be replaced on next successful login.
91
+ */
92
+ export function needsRehash(encoded) {
93
+ const parsed = parseArgon2Hash(encoded);
94
+ if (!parsed)
95
+ return true;
96
+ if (parsed.algorithm !== "argon2id")
97
+ return true;
98
+ if (parsed.memoryCost < ARGON2_PARAMS.memoryCost)
99
+ return true;
100
+ if (parsed.timeCost < ARGON2_PARAMS.timeCost)
101
+ return true;
102
+ if (parsed.parallelism < ARGON2_PARAMS.parallelism)
103
+ return true;
104
+ return false;
105
+ }
106
+ /**
107
+ * Cracks a PHC-formatted Argon2 hash into its component parameters. Returns
108
+ * `null` on any malformed input.
109
+ *
110
+ * @param encoded - the PHC-string-format encoded hash.
111
+ * @returns the parsed parameters, or `null` if the string isn't a valid hash.
112
+ */
113
+ function parseArgon2Hash(encoded) {
114
+ // Format: $argon2id$v=19$m=19456,t=2,p=1$salt$hash
115
+ const parts = encoded.split("$");
116
+ if (parts.length < 6)
117
+ return null;
118
+ const [, algorithm, , params] = parts;
119
+ if (!algorithm || !params)
120
+ return null;
121
+ const out = {
122
+ algorithm,
123
+ memoryCost: 0,
124
+ timeCost: 0,
125
+ parallelism: 0,
126
+ };
127
+ for (const seg of params.split(",")) {
128
+ const [k, v] = seg.split("=");
129
+ if (!k || !v)
130
+ continue;
131
+ const n = Number(v);
132
+ if (!Number.isFinite(n))
133
+ return null;
134
+ if (k === "m")
135
+ out.memoryCost = n;
136
+ else if (k === "t")
137
+ out.timeCost = n;
138
+ else if (k === "p")
139
+ out.parallelism = n;
140
+ }
141
+ if (!out.memoryCost || !out.timeCost || !out.parallelism)
142
+ return null;
143
+ return out;
144
+ }
145
+ //# sourceMappingURL=argon2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"argon2.js","sourceRoot":"","sources":["../../src/auth/argon2.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * AES-256-GCM helpers for encrypting `admin_setting.letta_api_key_encrypted`
3
+ * (§6.8). The key (`AI_HUB_ENCRYPTION_KEY`) lives in env only — separating
4
+ * the key-encrypting key from the encrypted blob is the entire point.
5
+ *
6
+ * Output layout (a single Buffer): `<12-byte IV><16-byte auth tag><ciphertext>`.
7
+ * Reading code slices the same offsets back out before calling `decipher`.
8
+ *
9
+ * **Key format:** the env value is parsed as a hex string (64 chars = 32
10
+ * bytes) or a base64 string (44 chars including `=` padding). Operators with
11
+ * a stricter preference can pre-decode and pass the bytes through their own
12
+ * config layer — `parseEncryptionKey` is the single boundary that interprets
13
+ * the env string.
14
+ *
15
+ * @see SPECIFICATIONS.md §6.8 — persistence
16
+ * @see SPECIFICATIONS.md §12.1 — `AI_HUB_ENCRYPTION_KEY`
17
+ */
18
+ /**
19
+ * Encrypts `plaintext` with AES-256-GCM under `key`. Returns
20
+ * `IV(12) || tag(16) || ciphertext` as a single Buffer.
21
+ *
22
+ * @param key - 32-byte encryption key.
23
+ * @param plaintext - utf-8 string (typically the Letta API key).
24
+ * @returns the binary blob to store in `admin_setting.letta_api_key_encrypted`.
25
+ */
26
+ export declare function encryptString(key: Buffer, plaintext: string): Buffer;
27
+ /**
28
+ * Decrypts a blob produced by `encryptString` back to its utf-8 plaintext.
29
+ *
30
+ * Throws on tampering / wrong key — the GCM tag check is the trust gate. The
31
+ * thrown error is intentionally generic; callers should not surface details
32
+ * to clients (avoid distinguishing "wrong key" from "corrupted ciphertext"
33
+ * in user-facing messages).
34
+ *
35
+ * @param key - 32-byte encryption key.
36
+ * @param blob - the binary blob from the DB.
37
+ * @returns the original utf-8 plaintext.
38
+ */
39
+ export declare function decryptString(key: Buffer, blob: Buffer): string;
40
+ /**
41
+ * Parses `AI_HUB_ENCRYPTION_KEY` into a 32-byte Buffer.
42
+ *
43
+ * Accepts either a 64-char hex string (`"0a1b…"`, lower or upper case) or a
44
+ * base64 / base64url string that decodes to exactly 32 bytes. Throws a
45
+ * clear error otherwise — the operator hits this on first boot and the
46
+ * message tells them what shape we want.
47
+ *
48
+ * @param raw - the raw env value.
49
+ * @returns the parsed 32-byte key.
50
+ */
51
+ export declare function parseEncryptionKey(raw: string): Buffer;
52
+ //# sourceMappingURL=encryption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../src/auth/encryption.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAaH;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAWpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAgB/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkBtD"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * AES-256-GCM helpers for encrypting `admin_setting.letta_api_key_encrypted`
3
+ * (§6.8). The key (`AI_HUB_ENCRYPTION_KEY`) lives in env only — separating
4
+ * the key-encrypting key from the encrypted blob is the entire point.
5
+ *
6
+ * Output layout (a single Buffer): `<12-byte IV><16-byte auth tag><ciphertext>`.
7
+ * Reading code slices the same offsets back out before calling `decipher`.
8
+ *
9
+ * **Key format:** the env value is parsed as a hex string (64 chars = 32
10
+ * bytes) or a base64 string (44 chars including `=` padding). Operators with
11
+ * a stricter preference can pre-decode and pass the bytes through their own
12
+ * config layer — `parseEncryptionKey` is the single boundary that interprets
13
+ * the env string.
14
+ *
15
+ * @see SPECIFICATIONS.md §6.8 — persistence
16
+ * @see SPECIFICATIONS.md §12.1 — `AI_HUB_ENCRYPTION_KEY`
17
+ */
18
+ import { createCipheriv, createDecipheriv, randomBytes, } from "node:crypto";
19
+ /** AES-256-GCM uses a 256-bit key (32 bytes) and a 96-bit IV (12 bytes). */
20
+ const KEY_BYTES = 32;
21
+ const IV_BYTES = 12;
22
+ const TAG_BYTES = 16;
23
+ /**
24
+ * Encrypts `plaintext` with AES-256-GCM under `key`. Returns
25
+ * `IV(12) || tag(16) || ciphertext` as a single Buffer.
26
+ *
27
+ * @param key - 32-byte encryption key.
28
+ * @param plaintext - utf-8 string (typically the Letta API key).
29
+ * @returns the binary blob to store in `admin_setting.letta_api_key_encrypted`.
30
+ */
31
+ export function encryptString(key, plaintext) {
32
+ if (key.length !== KEY_BYTES) {
33
+ throw new Error(`encryption key must be ${KEY_BYTES} bytes; got ${key.length}`);
34
+ }
35
+ const iv = randomBytes(IV_BYTES);
36
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
37
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
38
+ const tag = cipher.getAuthTag();
39
+ return Buffer.concat([iv, tag, ct]);
40
+ }
41
+ /**
42
+ * Decrypts a blob produced by `encryptString` back to its utf-8 plaintext.
43
+ *
44
+ * Throws on tampering / wrong key — the GCM tag check is the trust gate. The
45
+ * thrown error is intentionally generic; callers should not surface details
46
+ * to clients (avoid distinguishing "wrong key" from "corrupted ciphertext"
47
+ * in user-facing messages).
48
+ *
49
+ * @param key - 32-byte encryption key.
50
+ * @param blob - the binary blob from the DB.
51
+ * @returns the original utf-8 plaintext.
52
+ */
53
+ export function decryptString(key, blob) {
54
+ if (key.length !== KEY_BYTES) {
55
+ throw new Error(`encryption key must be ${KEY_BYTES} bytes; got ${key.length}`);
56
+ }
57
+ if (blob.length < IV_BYTES + TAG_BYTES + 1) {
58
+ throw new Error("ciphertext too short");
59
+ }
60
+ const iv = blob.subarray(0, IV_BYTES);
61
+ const tag = blob.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
62
+ const ct = blob.subarray(IV_BYTES + TAG_BYTES);
63
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
64
+ decipher.setAuthTag(tag);
65
+ const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
66
+ return pt.toString("utf8");
67
+ }
68
+ /**
69
+ * Parses `AI_HUB_ENCRYPTION_KEY` into a 32-byte Buffer.
70
+ *
71
+ * Accepts either a 64-char hex string (`"0a1b…"`, lower or upper case) or a
72
+ * base64 / base64url string that decodes to exactly 32 bytes. Throws a
73
+ * clear error otherwise — the operator hits this on first boot and the
74
+ * message tells them what shape we want.
75
+ *
76
+ * @param raw - the raw env value.
77
+ * @returns the parsed 32-byte key.
78
+ */
79
+ export function parseEncryptionKey(raw) {
80
+ const trimmed = raw.trim();
81
+ if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
82
+ return Buffer.from(trimmed, "hex");
83
+ }
84
+ // base64 / base64url — Node tolerates both via the "base64" decoder if
85
+ // the input is base64; we normalize url-safe variants first.
86
+ const normalized = trimmed.replace(/-/g, "+").replace(/_/g, "/");
87
+ const padded = normalized + "===".slice((normalized.length + 3) % 4);
88
+ try {
89
+ const buf = Buffer.from(padded, "base64");
90
+ if (buf.length === KEY_BYTES)
91
+ return buf;
92
+ }
93
+ catch {
94
+ /* fall through to throw below */
95
+ }
96
+ throw new Error(`AI_HUB_ENCRYPTION_KEY must be 32 bytes (64 hex chars or base64-encoded); got ${trimmed.length} chars`);
97
+ }
98
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../../src/auth/encryption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * JWT mint + verify for both token shapes.
3
+ *
4
+ * Two shapes share one signing key (§6.7) — the discriminator is the `scope`
5
+ * claim:
6
+ *
7
+ * - **Session token** — TTL 24 h, claims `{ iss, sub, username, role, exp }`.
8
+ * Required on every protected endpoint.
9
+ * - **Password-change-only token** — TTL 5 min, claims
10
+ * `{ iss, sub, scope: "password_change_only", exp }`. Accepted only by
11
+ * `/auth/change-password`; the `scope` claim is the gate.
12
+ *
13
+ * Both are HS256 (symmetric HMAC) — appropriate because ai-hub is the only
14
+ * identity provider; there is no second party that needs to verify without
15
+ * possessing the secret.
16
+ *
17
+ * The signing key lives in `admin_setting.service_token_signing_key` (DB) —
18
+ * env `SERVICE_TOKEN_SIGNING_KEY` is **bootstrap-only**. Rotation via
19
+ * `/admin/settings/rotate-signing-key` is sufficient and does not require a
20
+ * server restart (Step 4 wires that endpoint; Step 2 just defines the
21
+ * function boundary).
22
+ *
23
+ * @see SPECIFICATIONS.md §6.7 — auth model
24
+ * @see SPECIFICATIONS.md §8.6 — endpoint enforcement
25
+ */
26
+ import { type JWTPayload } from "jose";
27
+ import type { AIHubRole } from "@data-club/ai-hub";
28
+ /** Issuer string baked into every token's `iss` claim. */
29
+ export declare const JWT_ISSUER = "ai-hub";
30
+ /** Session token lifetime (§6.7). */
31
+ export declare const SESSION_TOKEN_TTL_SECONDS: number;
32
+ /** Password-change-only token lifetime (§6.7). */
33
+ export declare const PASSWORD_CHANGE_TOKEN_TTL_SECONDS: number;
34
+ /** Scope claim that gates the password-change-only token (§6.7). */
35
+ export declare const PASSWORD_CHANGE_SCOPE = "password_change_only";
36
+ /**
37
+ * Decoded session-token claims. The `role` claim is what the role-enforcement
38
+ * middleware (Step 3) reads for the §8.2 matrix.
39
+ */
40
+ export interface SessionTokenClaims extends JWTPayload {
41
+ /** Issuer — always "ai-hub". */
42
+ iss: string;
43
+ /** Subject — the user's id. */
44
+ sub: string;
45
+ /** Username, mirrored into the token for log correlation. */
46
+ username: string;
47
+ /** Role at mint-time. */
48
+ role: AIHubRole;
49
+ }
50
+ /** Decoded password-change-only token claims. Carries no `role` (§6.3.4). */
51
+ export interface PasswordChangeTokenClaims extends JWTPayload {
52
+ /** Issuer — always "ai-hub". */
53
+ iss: string;
54
+ /** Subject — the user's id. */
55
+ sub: string;
56
+ /** Always `"password_change_only"`. */
57
+ scope: typeof PASSWORD_CHANGE_SCOPE;
58
+ }
59
+ /**
60
+ * Discriminated outcome returned by `verifyServiceToken`.
61
+ *
62
+ * The role-enforcement middleware (Step 3) calls this exactly once per
63
+ * request and branches on `kind`:
64
+ *
65
+ * - `"session"` → run the §8.2 matrix check against `claims.role`.
66
+ * - `"password_change"` → reject everywhere except `/auth/change-password`.
67
+ * - `"invalid"` → 401.
68
+ *
69
+ * Keeping the verification at a single function boundary is a §6.7
70
+ * forward-compat seam — when external IdPs land, a parallel
71
+ * `verifyExternalToken` slots in alongside without touching every handler.
72
+ */
73
+ export type VerifyTokenResult = {
74
+ kind: "session";
75
+ claims: SessionTokenClaims;
76
+ } | {
77
+ kind: "password_change";
78
+ claims: PasswordChangeTokenClaims;
79
+ } | {
80
+ kind: "invalid";
81
+ };
82
+ /**
83
+ * Mints a 24-hour session JWT for `user`. `role` is read from the user row
84
+ * at mint time, not at verify time — rotating someone's role does not change
85
+ * their existing token (the role embedded in the JWT is what the matrix
86
+ * sees). Tokens expire in 24 h regardless.
87
+ *
88
+ * @param key - signing key from `admin_setting.service_token_signing_key`.
89
+ * @param user - identity bits to embed in the claims.
90
+ * @param user.id - user UUID; goes into the `sub` claim.
91
+ * @param user.username - login identifier; goes into the `username` claim.
92
+ * @param user.role - access-control role; goes into the `role` claim.
93
+ * @returns the signed compact JWT string.
94
+ */
95
+ export declare function mintSessionToken(key: Buffer, user: {
96
+ id: string;
97
+ username: string;
98
+ role: AIHubRole;
99
+ }): Promise<string>;
100
+ /**
101
+ * Mints a 5-minute password-change-only JWT for `user`. Used on login when
102
+ * `is_initial_password = true` (§8.5).
103
+ *
104
+ * No `role` claim — by design (§6.3.4): the change-only token doesn't grant
105
+ * any access beyond `/auth/change-password`, and adding role would tempt
106
+ * future code to use it for other things.
107
+ *
108
+ * @param key - signing key.
109
+ * @param user - identity bits to embed.
110
+ * @param user.id - user UUID; goes into the `sub` claim.
111
+ * @returns the signed compact JWT string.
112
+ */
113
+ export declare function mintPasswordChangeToken(key: Buffer, user: {
114
+ id: string;
115
+ }): Promise<string>;
116
+ /**
117
+ * Verifies a token's signature + issuer + expiry, then narrows it to the
118
+ * session or password-change-only shape based on the `scope` claim.
119
+ *
120
+ * Returns `{ kind: "invalid" }` for any failure (bad signature, wrong
121
+ * issuer, expired, missing required claim, unknown scope) — callers must
122
+ * treat that as a 401 with a generic message; we do not surface "why".
123
+ *
124
+ * **This is the single function boundary** the §6.7 / §8.6 enforcement order
125
+ * demands — every protected handler in Step 3 calls through here, and a
126
+ * future `verifyExternalToken` will sit beside it.
127
+ *
128
+ * @param key - signing key from `admin_setting.service_token_signing_key`.
129
+ * @param token - compact JWT string from the `Authorization: Bearer …` header.
130
+ * @returns a discriminated result the middleware branches on.
131
+ */
132
+ export declare function verifyServiceToken(key: Buffer, token: string): Promise<VerifyTokenResult>;
133
+ //# sourceMappingURL=jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/auth/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,0DAA0D;AAC1D,eAAO,MAAM,UAAU,WAAW,CAAC;AAKnC,qCAAqC;AACrC,eAAO,MAAM,yBAAyB,QAAe,CAAC;AAEtD,kDAAkD;AAClD,eAAO,MAAM,iCAAiC,QAAS,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,qBAAqB,yBAAyB,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,yBAA0B,SAAQ,UAAU;IAC3D,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,KAAK,EAAE,OAAO,qBAAqB,CAAC;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACtD,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GACnB,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAAC,CAkC5B"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * JWT mint + verify for both token shapes.
3
+ *
4
+ * Two shapes share one signing key (§6.7) — the discriminator is the `scope`
5
+ * claim:
6
+ *
7
+ * - **Session token** — TTL 24 h, claims `{ iss, sub, username, role, exp }`.
8
+ * Required on every protected endpoint.
9
+ * - **Password-change-only token** — TTL 5 min, claims
10
+ * `{ iss, sub, scope: "password_change_only", exp }`. Accepted only by
11
+ * `/auth/change-password`; the `scope` claim is the gate.
12
+ *
13
+ * Both are HS256 (symmetric HMAC) — appropriate because ai-hub is the only
14
+ * identity provider; there is no second party that needs to verify without
15
+ * possessing the secret.
16
+ *
17
+ * The signing key lives in `admin_setting.service_token_signing_key` (DB) —
18
+ * env `SERVICE_TOKEN_SIGNING_KEY` is **bootstrap-only**. Rotation via
19
+ * `/admin/settings/rotate-signing-key` is sufficient and does not require a
20
+ * server restart (Step 4 wires that endpoint; Step 2 just defines the
21
+ * function boundary).
22
+ *
23
+ * @see SPECIFICATIONS.md §6.7 — auth model
24
+ * @see SPECIFICATIONS.md §8.6 — endpoint enforcement
25
+ */
26
+ import { jwtVerify, SignJWT } from "jose";
27
+ /** Issuer string baked into every token's `iss` claim. */
28
+ export const JWT_ISSUER = "ai-hub";
29
+ /** HS256 — symmetric HMAC. Matches the §6.7 "single signing key" stance. */
30
+ const JWT_ALG = "HS256";
31
+ /** Session token lifetime (§6.7). */
32
+ export const SESSION_TOKEN_TTL_SECONDS = 24 * 60 * 60;
33
+ /** Password-change-only token lifetime (§6.7). */
34
+ export const PASSWORD_CHANGE_TOKEN_TTL_SECONDS = 5 * 60;
35
+ /** Scope claim that gates the password-change-only token (§6.7). */
36
+ export const PASSWORD_CHANGE_SCOPE = "password_change_only";
37
+ /**
38
+ * Mints a 24-hour session JWT for `user`. `role` is read from the user row
39
+ * at mint time, not at verify time — rotating someone's role does not change
40
+ * their existing token (the role embedded in the JWT is what the matrix
41
+ * sees). Tokens expire in 24 h regardless.
42
+ *
43
+ * @param key - signing key from `admin_setting.service_token_signing_key`.
44
+ * @param user - identity bits to embed in the claims.
45
+ * @param user.id - user UUID; goes into the `sub` claim.
46
+ * @param user.username - login identifier; goes into the `username` claim.
47
+ * @param user.role - access-control role; goes into the `role` claim.
48
+ * @returns the signed compact JWT string.
49
+ */
50
+ export async function mintSessionToken(key, user) {
51
+ return new SignJWT({
52
+ username: user.username,
53
+ role: user.role,
54
+ })
55
+ .setProtectedHeader({ alg: JWT_ALG })
56
+ .setIssuer(JWT_ISSUER)
57
+ .setSubject(user.id)
58
+ .setIssuedAt()
59
+ .setExpirationTime(`${SESSION_TOKEN_TTL_SECONDS}s`)
60
+ .sign(key);
61
+ }
62
+ /**
63
+ * Mints a 5-minute password-change-only JWT for `user`. Used on login when
64
+ * `is_initial_password = true` (§8.5).
65
+ *
66
+ * No `role` claim — by design (§6.3.4): the change-only token doesn't grant
67
+ * any access beyond `/auth/change-password`, and adding role would tempt
68
+ * future code to use it for other things.
69
+ *
70
+ * @param key - signing key.
71
+ * @param user - identity bits to embed.
72
+ * @param user.id - user UUID; goes into the `sub` claim.
73
+ * @returns the signed compact JWT string.
74
+ */
75
+ export async function mintPasswordChangeToken(key, user) {
76
+ return new SignJWT({
77
+ scope: PASSWORD_CHANGE_SCOPE,
78
+ })
79
+ .setProtectedHeader({ alg: JWT_ALG })
80
+ .setIssuer(JWT_ISSUER)
81
+ .setSubject(user.id)
82
+ .setIssuedAt()
83
+ .setExpirationTime(`${PASSWORD_CHANGE_TOKEN_TTL_SECONDS}s`)
84
+ .sign(key);
85
+ }
86
+ /**
87
+ * Verifies a token's signature + issuer + expiry, then narrows it to the
88
+ * session or password-change-only shape based on the `scope` claim.
89
+ *
90
+ * Returns `{ kind: "invalid" }` for any failure (bad signature, wrong
91
+ * issuer, expired, missing required claim, unknown scope) — callers must
92
+ * treat that as a 401 with a generic message; we do not surface "why".
93
+ *
94
+ * **This is the single function boundary** the §6.7 / §8.6 enforcement order
95
+ * demands — every protected handler in Step 3 calls through here, and a
96
+ * future `verifyExternalToken` will sit beside it.
97
+ *
98
+ * @param key - signing key from `admin_setting.service_token_signing_key`.
99
+ * @param token - compact JWT string from the `Authorization: Bearer …` header.
100
+ * @returns a discriminated result the middleware branches on.
101
+ */
102
+ export async function verifyServiceToken(key, token) {
103
+ let payload;
104
+ try {
105
+ const verified = await jwtVerify(token, key, {
106
+ issuer: JWT_ISSUER,
107
+ algorithms: [JWT_ALG],
108
+ });
109
+ payload = verified.payload;
110
+ }
111
+ catch {
112
+ return { kind: "invalid" };
113
+ }
114
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
115
+ return { kind: "invalid" };
116
+ }
117
+ const scope = payload.scope;
118
+ if (scope === PASSWORD_CHANGE_SCOPE) {
119
+ return {
120
+ kind: "password_change",
121
+ claims: payload,
122
+ };
123
+ }
124
+ const role = payload.role;
125
+ const username = payload.username;
126
+ if (typeof role !== "string" ||
127
+ !isAIHubRole(role) ||
128
+ typeof username !== "string") {
129
+ return { kind: "invalid" };
130
+ }
131
+ return { kind: "session", claims: payload };
132
+ }
133
+ /**
134
+ * Type-guard for the `AIHubRole` enum — keeps `verifyServiceToken` honest
135
+ * against tokens whose `role` claim was tampered with into something not in
136
+ * the spec's set.
137
+ *
138
+ * @param value - candidate role string.
139
+ * @returns `true` if `value` is one of the four spec roles.
140
+ */
141
+ function isAIHubRole(value) {
142
+ return (value === "user" ||
143
+ value === "power_user" ||
144
+ value === "admin" ||
145
+ value === "super_admin");
146
+ }
147
+ //# sourceMappingURL=jwt.js.map