@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 @@
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../../src/auth/jwt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Role-enforcement middleware: `protectedRoute({...}, handler)`.
3
+ *
4
+ * Every protected endpoint runs through this exact §8.6 order:
5
+ *
6
+ * 1. Pull the bearer token from `Authorization: Bearer <token>` (401 on miss).
7
+ * 2. Load the signing key from `admin_setting.service_token_signing_key`
8
+ * (500 if missing — should not happen after the first-boot seed).
9
+ * 3. `verifyServiceToken(key, token)` (401 on invalid signature, expiry,
10
+ * issuer, or claim shape).
11
+ * 4. Reject password-change-only tokens unless the route opts into them
12
+ * (401 — same envelope as 1/3 so no scope-leakage).
13
+ * 5. Load the user row by `sub` (401 if the user was deleted after the
14
+ * token was minted — defensive).
15
+ * 6. For session tokens, if `requiredRole` is set and the token's role
16
+ * claim doesn't satisfy it, return 403 with `forbidden`. The role check
17
+ * uses the **JWT claim**, not the row — by spec design (§8.6 step 3):
18
+ * rotating someone's role does not change their existing token (24 h max
19
+ * impact), and the system invalidates everything in one cut via the
20
+ * signing-key rotation.
21
+ * 7. Invoke the wrapped handler with the loaded user + the discriminator
22
+ * so it knows what kind of token authenticated the call.
23
+ *
24
+ * Shield + min-count invariants are NOT enforced here — that's the job of
25
+ * the per-handler `apply*InTx` helpers (`auth/roleRules.ts`), each of which
26
+ * re-reads target state inside a SQLite transaction so two simultaneous
27
+ * mutations cannot race.
28
+ *
29
+ * The middleware lives at one boundary so a future external-IdP feature
30
+ * (§6.7 "Future direction", §17 question 5) can slot a parallel
31
+ * `verifyExternalToken` in alongside without touching every handler.
32
+ *
33
+ * @see SPECIFICATIONS.md §6.7 — auth model
34
+ * @see SPECIFICATIONS.md §8.6 — endpoint enforcement order
35
+ * @see SPECIFICATIONS.md §6.7 — single-function-boundary seam
36
+ */
37
+ import type { AIHubRole } from "@data-club/ai-hub";
38
+ import type { UserRow } from "../db/schema.js";
39
+ import type { AIHubServerContext, RouteHandler } from "../handlers/types.js";
40
+ import type { PasswordChangeTokenClaims, SessionTokenClaims } from "./jwt.js";
41
+ /**
42
+ * Auth context handed to a protected handler. Carries both the loaded user
43
+ * row (so the handler doesn't re-query) and a discriminated kind so the
44
+ * handler can branch when it needs to (the `/auth/change-password` handler is
45
+ * the only one in v1 that cares — both token kinds reach it).
46
+ */
47
+ export interface AuthContext {
48
+ /**
49
+ * The loaded user row. `auth.role` mirrors `claims.role` for session tokens
50
+ * but is read from the row for password-change-only tokens (which carry no
51
+ * role claim). Handlers should use the JWT-derived `role` for matrix checks
52
+ * and the row for anything else.
53
+ */
54
+ user: UserRow;
55
+ /**
56
+ * Role to use for matrix-style checks. For session tokens this is the JWT
57
+ * claim (per §8.6 step 3 — frozen at mint time). For password-change-only
58
+ * tokens this is `user.role` from the row; the change-only token only
59
+ * reaches `/auth/change-password`, which doesn't read this field.
60
+ */
61
+ role: AIHubRole;
62
+ /** Which kind of token authenticated the request. */
63
+ tokenKind: "session" | "password_change";
64
+ /**
65
+ * The raw verified claims, in case a handler wants to log them or branch
66
+ * on something the role/user/tokenKind don't expose.
67
+ */
68
+ claims: SessionTokenClaims | PasswordChangeTokenClaims;
69
+ }
70
+ /**
71
+ * Options accepted by `protectedRoute`. Both fields default to "session
72
+ * token required, any role." Endpoints that need more (e.g. role gate or
73
+ * password-change-only-token acceptance) set the appropriate field.
74
+ */
75
+ export interface ProtectedRouteOptions {
76
+ /**
77
+ * Minimum role the caller must have. Default: any authenticated session
78
+ * (no role check). The matrix check uses the JWT's `role` claim, not the
79
+ * current row state — see file-level comment.
80
+ */
81
+ requiredRole?: AIHubRole;
82
+ /**
83
+ * `true` if the endpoint also accepts password-change-only tokens. Only
84
+ * `/auth/change-password` sets this in v1. Default: false (every other
85
+ * endpoint rejects change-only tokens with 401).
86
+ */
87
+ allowPasswordChangeToken?: boolean;
88
+ }
89
+ /** A handler that has been authenticated by `protectedRoute`. */
90
+ export type AuthenticatedHandler = (req: Request, ctx: AIHubServerContext, auth: AuthContext, params: Readonly<Record<string, string>>) => Promise<Response>;
91
+ /**
92
+ * Wraps a handler with the §8.6 enforcement order. Returns a plain
93
+ * `RouteHandler` that the route table mounts as if the auth check didn't
94
+ * exist.
95
+ *
96
+ * @param opts - per-route auth options.
97
+ * @param handler - the wrapped handler. Called only when auth succeeds.
98
+ * @returns a `RouteHandler` that runs auth, then delegates to `handler`.
99
+ */
100
+ export declare function protectedRoute(opts: ProtectedRouteOptions, handler: AuthenticatedHandler): RouteHandler;
101
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAG7E,OAAO,KAAK,EACV,yBAAyB,EACzB,kBAAkB,EACnB,MAAM,UAAU,CAAC;AAGlB;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,IAAI,EAAE,OAAO,CAAC;IACd;;;;;OAKG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB,qDAAqD;IACrD,SAAS,EAAE,SAAS,GAAG,iBAAiB,CAAC;IACzC;;;OAGG;IACH,MAAM,EAAE,kBAAkB,GAAG,yBAAyB,CAAC;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,iEAAiE;AACjE,MAAM,MAAM,oBAAoB,GAAG,CACjC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,kBAAkB,EACvB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KACrC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,qBAAqB,EAC3B,OAAO,EAAE,oBAAoB,GAC5B,YAAY,CA+Gd"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Role-enforcement middleware: `protectedRoute({...}, handler)`.
3
+ *
4
+ * Every protected endpoint runs through this exact §8.6 order:
5
+ *
6
+ * 1. Pull the bearer token from `Authorization: Bearer <token>` (401 on miss).
7
+ * 2. Load the signing key from `admin_setting.service_token_signing_key`
8
+ * (500 if missing — should not happen after the first-boot seed).
9
+ * 3. `verifyServiceToken(key, token)` (401 on invalid signature, expiry,
10
+ * issuer, or claim shape).
11
+ * 4. Reject password-change-only tokens unless the route opts into them
12
+ * (401 — same envelope as 1/3 so no scope-leakage).
13
+ * 5. Load the user row by `sub` (401 if the user was deleted after the
14
+ * token was minted — defensive).
15
+ * 6. For session tokens, if `requiredRole` is set and the token's role
16
+ * claim doesn't satisfy it, return 403 with `forbidden`. The role check
17
+ * uses the **JWT claim**, not the row — by spec design (§8.6 step 3):
18
+ * rotating someone's role does not change their existing token (24 h max
19
+ * impact), and the system invalidates everything in one cut via the
20
+ * signing-key rotation.
21
+ * 7. Invoke the wrapped handler with the loaded user + the discriminator
22
+ * so it knows what kind of token authenticated the call.
23
+ *
24
+ * Shield + min-count invariants are NOT enforced here — that's the job of
25
+ * the per-handler `apply*InTx` helpers (`auth/roleRules.ts`), each of which
26
+ * re-reads target state inside a SQLite transaction so two simultaneous
27
+ * mutations cannot race.
28
+ *
29
+ * The middleware lives at one boundary so a future external-IdP feature
30
+ * (§6.7 "Future direction", §17 question 5) can slot a parallel
31
+ * `verifyExternalToken` in alongside without touching every handler.
32
+ *
33
+ * @see SPECIFICATIONS.md §6.7 — auth model
34
+ * @see SPECIFICATIONS.md §8.6 — endpoint enforcement order
35
+ * @see SPECIFICATIONS.md §6.7 — single-function-boundary seam
36
+ */
37
+ import { eq } from "drizzle-orm";
38
+ import { adminSetting, user } from "../db/schema.js";
39
+ import { forbidden, internalError, unauthorized } from "../handlers/responses.js";
40
+ import { hasRoleAtLeast } from "./roleRules.js";
41
+ import { verifyServiceToken } from "./jwt.js";
42
+ /**
43
+ * Wraps a handler with the §8.6 enforcement order. Returns a plain
44
+ * `RouteHandler` that the route table mounts as if the auth check didn't
45
+ * exist.
46
+ *
47
+ * @param opts - per-route auth options.
48
+ * @param handler - the wrapped handler. Called only when auth succeeds.
49
+ * @returns a `RouteHandler` that runs auth, then delegates to `handler`.
50
+ */
51
+ export function protectedRoute(opts, handler) {
52
+ return async (req, ctx, params) => {
53
+ // 1. Extract bearer token.
54
+ const token = extractBearer(req.headers.get("authorization"));
55
+ if (!token)
56
+ return unauthorized();
57
+ // 2. Load signing key from the singleton settings row.
58
+ const settings = ctx.db
59
+ .select()
60
+ .from(adminSetting)
61
+ .where(eq(adminSetting.id, 1))
62
+ .get();
63
+ if (!settings?.serviceTokenSigningKey) {
64
+ ctx.logger.error({ event: "missing_signing_key" }, "admin_setting.service_token_signing_key is empty; cannot verify token.");
65
+ return internalError("Server misconfigured");
66
+ }
67
+ // 3. Verify token signature + issuer + expiry + claim shape.
68
+ const verified = await verifyServiceToken(settings.serviceTokenSigningKey, token);
69
+ if (verified.kind === "invalid")
70
+ return unauthorized();
71
+ // 4. Reject password-change-only tokens unless this route opts in.
72
+ if (verified.kind === "password_change" && !opts.allowPasswordChangeToken) {
73
+ return unauthorized();
74
+ }
75
+ // 5. Load user row by sub. The row may be missing if the user was deleted
76
+ // between mint and verify; a 401 (rather than 404) keeps the response
77
+ // indistinguishable from an invalid-token response — no enumeration.
78
+ const subject = verified.claims.sub;
79
+ if (!subject)
80
+ return unauthorized();
81
+ const row = ctx.db.select().from(user).where(eq(user.id, subject)).get();
82
+ if (!row)
83
+ return unauthorized();
84
+ // 5b. Session-invalidation gate — for **session tokens only**, reject
85
+ // JWTs issued before the user's last password mutation. This is
86
+ // what makes admin-driven password resets (and voluntary changes)
87
+ // sign out the affected user's other devices immediately rather
88
+ // than waiting for the 24h JWT expiry. Change-only tokens are
89
+ // allowed through; the change-password handler itself bumps
90
+ // `passwordChangedAt` AND mints a fresh session token in one go,
91
+ // so the user can still complete the forced-change flow.
92
+ //
93
+ // Comparison runs in **second precision** because jose's `iat`
94
+ // truncates to whole seconds — comparing higher precision would
95
+ // occasionally reject a JWT minted in the same second as the
96
+ // password mutation.
97
+ if (verified.kind === "session" && row.passwordChangedAt) {
98
+ const iatSeconds = verified.claims.iat;
99
+ const pwdChangedSeconds = Math.floor(Date.parse(row.passwordChangedAt) / 1000);
100
+ if (typeof iatSeconds === "number" &&
101
+ Number.isFinite(pwdChangedSeconds) &&
102
+ iatSeconds < pwdChangedSeconds) {
103
+ ctx.logger.info({
104
+ user_id: row.id,
105
+ jwt_iat: iatSeconds,
106
+ password_changed_at: row.passwordChangedAt,
107
+ event: "session_invalidated_by_password_change",
108
+ }, "Rejecting session token issued before last password change.");
109
+ return unauthorized();
110
+ }
111
+ }
112
+ // 6. Role check — drives off the **DB row's current role** rather than
113
+ // the JWT claim's role. The JWT signature still proves identity
114
+ // (we trust `sub`), but ACL decisions read the freshest source so:
115
+ //
116
+ // - **Promotions take effect immediately** — a user just promoted
117
+ // to admin can use admin endpoints without logging out + back in.
118
+ // - **Demotions revoke access immediately** — a demoted admin
119
+ // loses admin endpoints right away rather than retaining them
120
+ // until their 24 h JWT expires.
121
+ //
122
+ // The spec's §8.6 step 3 ("Extract the role claim") describes the
123
+ // procedural shape but the security property it cares about
124
+ // ("the user can't escalate privileges via a tampered token") is
125
+ // preserved here — DB role can't be tampered with by the client.
126
+ //
127
+ // Change-only tokens fall through with `row.role` too, so the same
128
+ // code path handles both kinds.
129
+ const effectiveRole = row.role;
130
+ if (verified.kind === "session" && opts.requiredRole) {
131
+ if (!hasRoleAtLeast(effectiveRole, opts.requiredRole)) {
132
+ return forbidden(`Requires role at least ${opts.requiredRole}.`);
133
+ }
134
+ }
135
+ // 7. Invoke the wrapped handler.
136
+ const auth = {
137
+ user: row,
138
+ role: effectiveRole,
139
+ tokenKind: verified.kind,
140
+ claims: verified.claims,
141
+ };
142
+ return handler(req, ctx, auth, params ?? {});
143
+ };
144
+ }
145
+ /**
146
+ * Parses the `Authorization` header value, returning the bearer token or
147
+ * `null` when the header is missing or malformed.
148
+ *
149
+ * @param value - the raw header value (or `null` if absent).
150
+ * @returns the bearer token, or `null`.
151
+ */
152
+ function extractBearer(value) {
153
+ if (!value)
154
+ return null;
155
+ const m = value.match(/^Bearer\s+(.+)$/i);
156
+ if (!m)
157
+ return null;
158
+ const token = m[1]?.trim() ?? "";
159
+ return token.length > 0 ? token : null;
160
+ }
161
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/auth/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Password complexity policy (§8.5).
3
+ *
4
+ * Every new password supplied to `/auth/change-password` must satisfy two
5
+ * checks:
6
+ *
7
+ * 1. **Minimum length** — `PASSWORD_MIN_LENGTH` (8) characters.
8
+ * 2. **Complexity** — at least **2 of these 3 categories** must appear in
9
+ * the password:
10
+ * - letters (any Unicode letter, `\p{L}`)
11
+ * - digits (any Unicode number, `\p{N}`)
12
+ * - special characters (anything that's neither letter nor digit nor
13
+ * whitespace — symbols, punctuation, etc.)
14
+ *
15
+ * Examples:
16
+ *
17
+ * - `"12345678"` — only digits → **rejected**.
18
+ * - `"abcdefgh"` — only letters → **rejected**.
19
+ * - `"!!!!!!!!"` — only special → **rejected**.
20
+ * - `"abcd1234"` — letters + digits → **accepted**.
21
+ * - `"password!"` — letters + special → **accepted**.
22
+ * - `"1234!@#$"` — digits + special → **accepted**.
23
+ * - `"Aa1!Aa1!"` — all three → **accepted**.
24
+ *
25
+ * Whitespace does not count as "special" because allowing whitespace to
26
+ * tick the box would let `"password "` pass; we want at least one
27
+ * meaningfully non-letter character.
28
+ *
29
+ * This module is the single source of truth — the spec-mandated rule is
30
+ * defined here and consumed by handlers. Any future relaxation or
31
+ * tightening happens in one place.
32
+ *
33
+ * @see SPECIFICATIONS.md §8.5 — password requirements
34
+ */
35
+ /** Minimum length for a new password (§8.5). */
36
+ export declare const PASSWORD_MIN_LENGTH = 8;
37
+ /** Minimum distinct character categories required (§8.5). */
38
+ export declare const PASSWORD_REQUIRED_CATEGORIES = 2;
39
+ /** Result of `validatePasswordPolicy`. */
40
+ export type PasswordPolicyResult = {
41
+ ok: true;
42
+ } | {
43
+ ok: false;
44
+ code: PasswordPolicyErrorCode;
45
+ message: string;
46
+ };
47
+ /** Machine-readable codes the validator emits. */
48
+ export type PasswordPolicyErrorCode = "password_too_short" | "password_too_simple";
49
+ /**
50
+ * Validates `password` against the §8.5 policy. Returns a discriminated
51
+ * result the handler maps directly into a 400 response.
52
+ *
53
+ * @param password - the candidate plaintext password.
54
+ * @returns `{ ok: true }` on pass, `{ ok: false, code, message }` on fail.
55
+ */
56
+ export declare function validatePasswordPolicy(password: string): PasswordPolicyResult;
57
+ /**
58
+ * Counts how many of the three categories the password matches.
59
+ *
60
+ * Categories are mutually exclusive:
61
+ *
62
+ * - letter: any Unicode letter (`\p{L}`)
63
+ * - digit: any Unicode number (`\p{N}`)
64
+ * - special: everything else that is **not** whitespace (`\s`)
65
+ *
66
+ * Whitespace contributes to neither.
67
+ *
68
+ * @param password - the candidate plaintext password.
69
+ * @returns the count in `[0, 3]`.
70
+ */
71
+ export declare function countCharacterCategories(password: string): number;
72
+ //# sourceMappingURL=passwordPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordPolicy.d.ts","sourceRoot":"","sources":["../../src/auth/passwordPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,gDAAgD;AAChD,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,6DAA6D;AAC7D,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,0CAA0C;AAC1C,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,uBAAuB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,kDAAkD;AAClD,MAAM,MAAM,uBAAuB,GAC/B,oBAAoB,GACpB,qBAAqB,CAAC;AAE1B;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,CAkB7E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMjE"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Password complexity policy (§8.5).
3
+ *
4
+ * Every new password supplied to `/auth/change-password` must satisfy two
5
+ * checks:
6
+ *
7
+ * 1. **Minimum length** — `PASSWORD_MIN_LENGTH` (8) characters.
8
+ * 2. **Complexity** — at least **2 of these 3 categories** must appear in
9
+ * the password:
10
+ * - letters (any Unicode letter, `\p{L}`)
11
+ * - digits (any Unicode number, `\p{N}`)
12
+ * - special characters (anything that's neither letter nor digit nor
13
+ * whitespace — symbols, punctuation, etc.)
14
+ *
15
+ * Examples:
16
+ *
17
+ * - `"12345678"` — only digits → **rejected**.
18
+ * - `"abcdefgh"` — only letters → **rejected**.
19
+ * - `"!!!!!!!!"` — only special → **rejected**.
20
+ * - `"abcd1234"` — letters + digits → **accepted**.
21
+ * - `"password!"` — letters + special → **accepted**.
22
+ * - `"1234!@#$"` — digits + special → **accepted**.
23
+ * - `"Aa1!Aa1!"` — all three → **accepted**.
24
+ *
25
+ * Whitespace does not count as "special" because allowing whitespace to
26
+ * tick the box would let `"password "` pass; we want at least one
27
+ * meaningfully non-letter character.
28
+ *
29
+ * This module is the single source of truth — the spec-mandated rule is
30
+ * defined here and consumed by handlers. Any future relaxation or
31
+ * tightening happens in one place.
32
+ *
33
+ * @see SPECIFICATIONS.md §8.5 — password requirements
34
+ */
35
+ /** Minimum length for a new password (§8.5). */
36
+ export const PASSWORD_MIN_LENGTH = 8;
37
+ /** Minimum distinct character categories required (§8.5). */
38
+ export const PASSWORD_REQUIRED_CATEGORIES = 2;
39
+ /**
40
+ * Validates `password` against the §8.5 policy. Returns a discriminated
41
+ * result the handler maps directly into a 400 response.
42
+ *
43
+ * @param password - the candidate plaintext password.
44
+ * @returns `{ ok: true }` on pass, `{ ok: false, code, message }` on fail.
45
+ */
46
+ export function validatePasswordPolicy(password) {
47
+ if (password.length < PASSWORD_MIN_LENGTH) {
48
+ return {
49
+ ok: false,
50
+ code: "password_too_short",
51
+ message: `New password must be at least ${PASSWORD_MIN_LENGTH} characters.`,
52
+ };
53
+ }
54
+ const categories = countCharacterCategories(password);
55
+ if (categories < PASSWORD_REQUIRED_CATEGORIES) {
56
+ return {
57
+ ok: false,
58
+ code: "password_too_simple",
59
+ message: "New password must contain at least two of these three: letters, digits, special characters.",
60
+ };
61
+ }
62
+ return { ok: true };
63
+ }
64
+ /**
65
+ * Counts how many of the three categories the password matches.
66
+ *
67
+ * Categories are mutually exclusive:
68
+ *
69
+ * - letter: any Unicode letter (`\p{L}`)
70
+ * - digit: any Unicode number (`\p{N}`)
71
+ * - special: everything else that is **not** whitespace (`\s`)
72
+ *
73
+ * Whitespace contributes to neither.
74
+ *
75
+ * @param password - the candidate plaintext password.
76
+ * @returns the count in `[0, 3]`.
77
+ */
78
+ export function countCharacterCategories(password) {
79
+ let count = 0;
80
+ if (/\p{L}/u.test(password))
81
+ count++;
82
+ if (/\p{N}/u.test(password))
83
+ count++;
84
+ if (/[^\p{L}\p{N}\s]/u.test(password))
85
+ count++;
86
+ return count;
87
+ }
88
+ //# sourceMappingURL=passwordPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordPolicy.js","sourceRoot":"","sources":["../../src/auth/passwordPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,gDAAgD;AAChD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAY9C;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,IAAI,QAAQ,CAAC,MAAM,GAAG,mBAAmB,EAAE,CAAC;QAC1C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,iCAAiC,mBAAmB,cAAc;SAC5E,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IACtD,IAAI,UAAU,GAAG,4BAA4B,EAAE,CAAC;QAC9C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EACL,6FAA6F;SAChG,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,wBAAwB,CAAC,QAAgB;IACvD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,KAAK,EAAE,CAAC;IACrC,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,KAAK,EAAE,CAAC;IACrC,IAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,KAAK,EAAE,CAAC;IAC/C,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Shield + min-count rules + the transactional helpers that compose them.
3
+ *
4
+ * Three concerns live here:
5
+ *
6
+ * 1. **Role hierarchy** — a tiny `ROLE_RANK` map + `hasRoleAtLeast` used by the
7
+ * middleware to drive §8.2 matrix checks.
8
+ * 2. **Pure assertion helpers** — `assertCanChangeUserRole`,
9
+ * `assertCanResetPassword`, `assertCanDeleteUser`. Each throws
10
+ * `RoleRuleError` on a §8.1 shield or super-admin-min-count violation.
11
+ * Pure functions of `(caller, target, newState, totalSuperAdmins)` so they
12
+ * are trivial to unit-test against every cell.
13
+ * 3. **Transactional appliers** — `applyUserRoleChangeInTx`,
14
+ * `applyPasswordResetInTx` (signature only; Step 4 wires the password
15
+ * write), `applyUserDeletionInTx`. Each opens one SQLite transaction,
16
+ * re-reads the target row + super-admin count inside the transaction so a
17
+ * concurrent demote cannot bypass the invariant, calls the matching
18
+ * assertion helper, then applies the write. Returns a discriminated
19
+ * `{ ok: true, ... } | { ok: false, status, code, message }` so the
20
+ * Step-4 handler maps it directly into a `Response`.
21
+ *
22
+ * Step 3 ships the helpers + extensive tests; Step 4 is the first endpoint
23
+ * that consumes them.
24
+ *
25
+ * @see SPECIFICATIONS.md §8.1 — role model + shield + min-count rules
26
+ * @see SPECIFICATIONS.md §8.5 — password reset
27
+ * @see SPECIFICATIONS.md §8.6 — endpoint enforcement order
28
+ */
29
+ import type { AIHubRole } from "@data-club/ai-hub";
30
+ import type { DrizzleDB } from "../db/connection.js";
31
+ import type { UserRow } from "../db/schema.js";
32
+ /**
33
+ * Role ordering — least → most privileged. Authoritative for any
34
+ * "is this role at least as privileged as X" comparison.
35
+ *
36
+ * @see SPECIFICATIONS.md §8.1
37
+ */
38
+ export declare const ROLE_HIERARCHY: ReadonlyArray<AIHubRole>;
39
+ /**
40
+ * Rank within the hierarchy — higher = more privileged. Driven by
41
+ * `ROLE_HIERARCHY` so a future role addition is one constant change.
42
+ *
43
+ * @see SPECIFICATIONS.md §8.1
44
+ */
45
+ export declare const ROLE_RANK: Readonly<Record<AIHubRole, number>>;
46
+ /**
47
+ * Returns `true` if `actual` is at least as privileged as `required`. Used by
48
+ * the role-enforcement middleware to gate every protected endpoint per the
49
+ * §8.2 matrix.
50
+ *
51
+ * @param actual - the role the caller's JWT carries.
52
+ * @param required - the minimum role the endpoint requires.
53
+ * @returns `true` if `actual >= required` in the hierarchy.
54
+ */
55
+ export declare function hasRoleAtLeast(actual: AIHubRole, required: AIHubRole): boolean;
56
+ /**
57
+ * Thrown by the assertion helpers below. The transactional helpers catch
58
+ * `RoleRuleError`s and translate them into the `{ ok: false, ... }` return
59
+ * shape; raw throws are reserved for unexpected programmer errors.
60
+ */
61
+ export declare class RoleRuleError extends Error {
62
+ /** HTTP status the caller should return (typically 403, sometimes 404). */
63
+ readonly status: number;
64
+ /** Machine-readable code matching the §7.5 `AIHubError` codes. */
65
+ readonly code: string;
66
+ /**
67
+ * @param status - HTTP status code to surface.
68
+ * @param code - machine-readable code.
69
+ * @param message - human-readable description.
70
+ */
71
+ constructor(status: number, code: string, message: string);
72
+ }
73
+ /** A caller's identity bits — id + current role. */
74
+ export interface RoleRuleCaller {
75
+ /** The caller's user id (from `auth.user.id`). */
76
+ id: string;
77
+ /** The caller's current role (from `auth.role`). */
78
+ role: AIHubRole;
79
+ }
80
+ /**
81
+ * Asserts a role-change is allowed under the §8.1 shield + min-count rules.
82
+ *
83
+ * The rules in plain English:
84
+ *
85
+ * - **Only `super_admin` can demote the last `super_admin`** — min-count rule.
86
+ * Applies regardless of who the caller is (including the super_admin's own
87
+ * self-demote when they are the only one).
88
+ * - **Only `super_admin` can promote anyone to `super_admin`**.
89
+ * - **`admin` callers can only mutate `user` / `power_user` targets** — they
90
+ * cannot change the role of another `admin` or a `super_admin` (including
91
+ * themselves — we block all admin-touches-admin, by an explicit decision
92
+ * recorded in the Step-3 prompt).
93
+ * - **`power_user` and `user` cannot change any role** — the matrix gates
94
+ * that at the endpoint level, but we double-check here so the helper is
95
+ * safe to call from anywhere.
96
+ *
97
+ * @param caller - the caller's id + current role.
98
+ * @param target - the target user row (loaded inside the same transaction).
99
+ * @param newRole - the requested new role for `target`.
100
+ * @param totalSuperAdmins - count of super-admin rows in the DB, read inside
101
+ * the same transaction so a concurrent demote can't bypass the rule.
102
+ * @throws {RoleRuleError} when the requested change violates any rule.
103
+ */
104
+ export declare function assertCanChangeUserRole(caller: RoleRuleCaller, target: UserRow, newRole: AIHubRole, totalSuperAdmins: number): void;
105
+ /**
106
+ * Asserts a password reset is allowed under the §8.1 shield + min-count rules.
107
+ *
108
+ * - `admin` cannot reset another `admin`'s or any `super_admin`'s password
109
+ * (shield rule).
110
+ * - Cannot reset the only `super_admin`'s password — would amount to
111
+ * demotion (min-count rule, §8.5).
112
+ *
113
+ * @param caller - the caller's id + current role.
114
+ * @param target - the target user row.
115
+ * @param totalSuperAdmins - count of super-admin rows read inside the same
116
+ * transaction.
117
+ * @throws {RoleRuleError} on shield or min-count violation.
118
+ */
119
+ export declare function assertCanResetPassword(caller: RoleRuleCaller, target: UserRow, totalSuperAdmins: number): void;
120
+ /**
121
+ * Asserts a user deletion is allowed under the §8.1 shield + min-count rules.
122
+ *
123
+ * Same shape as `assertCanResetPassword`:
124
+ *
125
+ * - `admin` cannot delete another `admin` or any `super_admin`.
126
+ * - Cannot delete the only `super_admin`.
127
+ *
128
+ * @param caller - the caller's id + current role.
129
+ * @param target - the target user row.
130
+ * @param totalSuperAdmins - count of super-admin rows read inside the same
131
+ * transaction.
132
+ * @throws {RoleRuleError} on shield or min-count violation.
133
+ */
134
+ export declare function assertCanDeleteUser(caller: RoleRuleCaller, target: UserRow, totalSuperAdmins: number): void;
135
+ /**
136
+ * Result shape returned by every `apply*InTx` helper. Step-4 endpoints branch
137
+ * on `ok` to produce the response.
138
+ */
139
+ export type TxResult<T> = {
140
+ ok: true;
141
+ data: T;
142
+ } | {
143
+ ok: false;
144
+ status: number;
145
+ code: string;
146
+ message: string;
147
+ };
148
+ /**
149
+ * Applies a role change inside one SQLite transaction.
150
+ *
151
+ * Reads the target row + super-admin count inside the transaction so that a
152
+ * racing demote cannot win against the min-count rule. The Drizzle
153
+ * `db.transaction(fn)` API runs `fn` synchronously inside `BEGIN` / `COMMIT`;
154
+ * any thrown error inside `fn` triggers a `ROLLBACK` automatically.
155
+ *
156
+ * @param db - Drizzle client (the caller passes `ctx.db`; the helper opens
157
+ * its own transaction internally).
158
+ * @param caller - caller's id + role.
159
+ * @param targetId - the user whose role is being changed.
160
+ * @param newRole - the new role.
161
+ * @returns the updated row on success, or an error result.
162
+ */
163
+ export declare function applyUserRoleChangeInTx(db: DrizzleDB, caller: RoleRuleCaller, targetId: string, newRole: AIHubRole): TxResult<{
164
+ updated: UserRow;
165
+ }>;
166
+ /**
167
+ * Applies a user deletion inside one SQLite transaction. Same shape as
168
+ * `applyUserRoleChangeInTx` but DELETEs rather than UPDATEs.
169
+ *
170
+ * @param db - Drizzle client.
171
+ * @param caller - caller's id + role.
172
+ * @param targetId - the user to delete.
173
+ * @returns `{ ok: true, data: { deleted: <row before delete> } }` on success.
174
+ */
175
+ export declare function applyUserDeletionInTx(db: DrizzleDB, caller: RoleRuleCaller, targetId: string): TxResult<{
176
+ deleted: UserRow;
177
+ }>;
178
+ /**
179
+ * Runs the password-reset assertion inside a transaction and yields the
180
+ * fresh target row. The caller (Step 4's `POST /admin/users/{id}/reset-password`
181
+ * handler) generates the new password, hashes it, and writes inside the
182
+ * same transaction — this helper exposes the read-and-assert plumbing so
183
+ * the write can happen alongside.
184
+ *
185
+ * The shape is slightly different from the role-change / delete helpers
186
+ * because the password generation + hashing is async (Argon2) and cannot
187
+ * happen inside better-sqlite3's synchronous transaction. The Step-4 handler
188
+ * will run this helper first (to validate + lock-read), then write outside
189
+ * the transaction. The risk of a concurrent role-change is small but real;
190
+ * Step 4 can re-verify after the write or accept the tiny window.
191
+ *
192
+ * @param db - Drizzle client.
193
+ * @param caller - caller's id + role.
194
+ * @param targetId - the user whose password is being reset.
195
+ * @returns the target row on success, or an error result.
196
+ */
197
+ export declare function assertPasswordResetInTx(db: DrizzleDB, caller: RoleRuleCaller, targetId: string): TxResult<{
198
+ target: UserRow;
199
+ }>;
200
+ //# sourceMappingURL=roleRules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleRules.d.ts","sourceRoot":"","sources":["../../src/auth/roleRules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAM/C;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,SAAS,CAK1C,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAKxD,CAAC;AAEH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,SAAS,GAClB,OAAO,CAET;AAMD;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,2EAA2E;IAC3E,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,kEAAkE;IAClE,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;;;OAIG;gBACS,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D;AAMD,oDAAoD;AACpD,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,IAAI,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,SAAS,EAClB,gBAAgB,EAAE,MAAM,GACvB,IAAI,CA8CN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,OAAO,EACf,gBAAgB,EAAE,MAAM,GACvB,IAAI,CA6BN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,OAAO,EACf,gBAAgB,EAAE,MAAM,GACvB,IAAI,CA4BN;AAMD;;;GAGG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACrB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AA2BjE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,SAAS,GACjB,QAAQ,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA4ChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,GACf,QAAQ,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA2BhC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,GACf,QAAQ,CAAC;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CA0B/B"}