@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,422 @@
1
+ /**
2
+ * `/auth/*` route handlers.
3
+ *
4
+ * Step 3 ships the full surface:
5
+ *
6
+ * - `POST /auth/login` (Step 2) — public.
7
+ * - `POST /auth/change-password` — accepts session OR password-change-only
8
+ * tokens. Voluntary path requires `current_password`; forced path doesn't
9
+ * (the change-only token IS the auth). Enforces the §8.5 password policy
10
+ * (length + complexity).
11
+ * - `POST /auth/logout` — server-side no-op in v1 (the React client just
12
+ * discards the token).
13
+ * - `GET /auth/me` — returns the `{ user, deployment }` envelope per §6.3.4.
14
+ * - `PATCH /auth/me/preferences` — updates `user.theme_preference` (rejects
15
+ * with 409 when the deployment-wide switcher is off).
16
+ *
17
+ * @see SPECIFICATIONS.md §6.3.4 — auth endpoints
18
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
19
+ * @see SPECIFICATIONS.md §8.5 — Argon2id password verification + complexity
20
+ */
21
+ import { eq } from "drizzle-orm";
22
+ import { z } from "zod";
23
+ import { hashPassword, needsRehash, verifyPassword, } from "../auth/argon2.js";
24
+ import { mintPasswordChangeToken, mintSessionToken, } from "../auth/jwt.js";
25
+ import { protectedRoute } from "../auth/middleware.js";
26
+ import { validatePasswordPolicy } from "../auth/passwordPolicy.js";
27
+ import { buildAuthContext, buildDeploymentView, buildUserView, } from "../auth/userView.js";
28
+ import { adminSetting, user } from "../db/schema.js";
29
+ import { conflictError, internalError, jsonResponse, unauthorized, validationError, } from "../handlers/responses.js";
30
+ /* ────────────────────────────────────────────────────────────────────────── */
31
+ /* POST /auth/login */
32
+ /* ────────────────────────────────────────────────────────────────────────── */
33
+ /** Login request body shape. Wire is snake_case (§6.3.4). */
34
+ const loginSchema = z
35
+ .object({
36
+ username: z.string().min(1).max(255),
37
+ password: z.string().min(1).max(1024),
38
+ })
39
+ .strict();
40
+ /**
41
+ * `POST /auth/login` — Argon2id-verifies username + password, mints either
42
+ * a session JWT or a 5-minute password-change-only JWT depending on the
43
+ * user's `is_initial_password` flag.
44
+ *
45
+ * **Generic 401 on every failure path** — wrong username, wrong password,
46
+ * NULL `password_hash` (the future-IdP seam test), or any malformed body.
47
+ * No enumeration leakage (§13.1, §8.6).
48
+ *
49
+ * On success returns `{ token, must_change_password, user, deployment }`.
50
+ * When `must_change_password = true`, the `deployment` block is omitted
51
+ * entirely (the change-only token doesn't grant deployment access) and
52
+ * `user.role` is omitted (the change-only token carries no role claim) —
53
+ * the client re-fetches `/auth/me` after the change-password handshake.
54
+ *
55
+ * Hash-upgrade-on-login (§8.5): if the stored hash was produced with weaker
56
+ * params than the current `ARGON2_PARAMS` baseline, we rehash and write
57
+ * back. Best-effort — a write failure does not fail the login.
58
+ *
59
+ * @see SPECIFICATIONS.md §6.3.4 — `POST /auth/login`
60
+ * @see SPECIFICATIONS.md §8.5 — auth + hash upgrade
61
+ */
62
+ export const handleLogin = async (req, ctx) => {
63
+ let body;
64
+ try {
65
+ body = await req.json();
66
+ }
67
+ catch {
68
+ // Malformed JSON — return a generic 401, not a 400. The endpoint should
69
+ // not distinguish "bad shape" from "wrong credentials" to clients.
70
+ return unauthorized();
71
+ }
72
+ const parsed = loginSchema.safeParse(body);
73
+ if (!parsed.success)
74
+ return unauthorized();
75
+ const { username, password } = parsed.data;
76
+ const row = ctx.db.select().from(user).where(eq(user.username, username)).get();
77
+ if (!row || !row.passwordHash) {
78
+ // Either the user doesn't exist or it's an externally-authenticated row
79
+ // (NULL `password_hash` — §6.7 future-IdP seam). Same generic 401.
80
+ return unauthorized();
81
+ }
82
+ const ok = await verifyPassword(row.passwordHash, password);
83
+ if (!ok) {
84
+ ctx.logger.info({ username, event: "login_failed" }, "Login attempt failed: invalid credentials.");
85
+ return unauthorized();
86
+ }
87
+ // Hash-upgrade-on-login (§8.5). Best-effort — never fail a login because
88
+ // the rehash itself failed.
89
+ if (needsRehash(row.passwordHash)) {
90
+ try {
91
+ const upgraded = await hashPassword(password);
92
+ ctx.db
93
+ .update(user)
94
+ .set({
95
+ passwordHash: upgraded,
96
+ updatedAt: nowIso(),
97
+ })
98
+ .where(eq(user.id, row.id))
99
+ .run();
100
+ ctx.logger.info({ user_id: row.id, event: "password_hash_upgraded" }, "Upgraded stored password hash to current parameters.");
101
+ }
102
+ catch (err) {
103
+ ctx.logger.warn({ user_id: row.id, err: errMessage(err) }, "Hash-upgrade write failed; login still proceeds.");
104
+ }
105
+ }
106
+ const settings = ctx.db
107
+ .select()
108
+ .from(adminSetting)
109
+ .where(eq(adminSetting.id, 1))
110
+ .get();
111
+ const signingKey = settings?.serviceTokenSigningKey;
112
+ if (!signingKey) {
113
+ // Should not happen — the seed creates the row on first boot.
114
+ ctx.logger.error({ event: "missing_signing_key" }, "admin_setting.service_token_signing_key is empty; cannot mint tokens.");
115
+ return internalError("Server misconfigured");
116
+ }
117
+ const must = row.isInitialPassword;
118
+ const token = must
119
+ ? await mintPasswordChangeToken(signingKey, { id: row.id })
120
+ : await mintSessionToken(signingKey, {
121
+ id: row.id,
122
+ username: row.username,
123
+ role: row.role,
124
+ });
125
+ ctx.logger.info({
126
+ user_id: row.id,
127
+ username: row.username,
128
+ role: row.role,
129
+ must_change_password: must,
130
+ event: "login_success",
131
+ }, "Login succeeded.");
132
+ // Per §6.3.4: on `must_change_password = true`:
133
+ // - `user.role` is omitted (the change-only token carries no role claim).
134
+ // - The `deployment` block is omitted entirely (no deployment access via
135
+ // the change-only token).
136
+ // The client re-fetches /auth/me after the change-password handshake.
137
+ if (must) {
138
+ const minimalUser = buildUserView(row);
139
+ const userWithoutRole = { ...minimalUser };
140
+ delete userWithoutRole.role;
141
+ return jsonResponse(200, {
142
+ token,
143
+ must_change_password: true,
144
+ user: userWithoutRole,
145
+ });
146
+ }
147
+ const context = buildAuthContext(row, settings ?? null);
148
+ return jsonResponse(200, {
149
+ token,
150
+ must_change_password: false,
151
+ user: context.user,
152
+ deployment: context.deployment,
153
+ });
154
+ };
155
+ /* ────────────────────────────────────────────────────────────────────────── */
156
+ /* POST /auth/change-password */
157
+ /* ────────────────────────────────────────────────────────────────────────── */
158
+ /** Body for change-password. `current_password` required only on voluntary path. */
159
+ const changePasswordSchema = z
160
+ .object({
161
+ current_password: z.string().min(1).max(1024).optional(),
162
+ new_password: z.string().min(1).max(1024),
163
+ })
164
+ .strict();
165
+ /**
166
+ * `POST /auth/change-password` — two distinct flows on one endpoint:
167
+ *
168
+ * - **Voluntary (session token):** caller supplies `current_password`; the
169
+ * server Argon2id-verifies it, then hashes + writes the new password. Does
170
+ * not touch `is_initial_password`. Returns a fresh session token (with the
171
+ * caller's current role).
172
+ * - **Forced (password-change-only token):** caller does not supply
173
+ * `current_password` (the change-only token already proves a successful
174
+ * prior login). The server hashes + writes the new password, clears
175
+ * `is_initial_password`, and returns a fresh session token. The client
176
+ * transitions to the normal chat at this point.
177
+ *
178
+ * Server-side validation: the new password must satisfy the §8.5 policy —
179
+ * at least 8 characters AND at least two of {letters, digits, special
180
+ * characters}. Length-only passwords (e.g. `"12345678"`) are rejected with
181
+ * 400 `password_too_simple`.
182
+ *
183
+ * Mismatched current password → 401 `unauthorized` with a "current
184
+ * password"-mentioning message. Voluntary path with no `current_password`
185
+ * is rejected as a validation error (the field IS required when the caller
186
+ * authenticated with a session token). Forced path with a `current_password`
187
+ * is rejected too — that signals client confusion.
188
+ *
189
+ * @see SPECIFICATIONS.md §6.3.4 — `POST /auth/change-password`
190
+ * @see SPECIFICATIONS.md §8.5 — change flows + password policy
191
+ */
192
+ export const handleChangePassword = protectedRoute({ allowPasswordChangeToken: true }, async (req, ctx, auth) => {
193
+ let raw;
194
+ try {
195
+ raw = await req.json();
196
+ }
197
+ catch {
198
+ return validationError("Request body must be valid JSON.");
199
+ }
200
+ const parsed = changePasswordSchema.safeParse(raw);
201
+ if (!parsed.success) {
202
+ return validationError("Invalid request body.");
203
+ }
204
+ const body = parsed.data;
205
+ const policy = validatePasswordPolicy(body.new_password);
206
+ if (!policy.ok) {
207
+ return validationError(policy.message, policy.code);
208
+ }
209
+ if (auth.tokenKind === "session") {
210
+ // Voluntary path — current_password required.
211
+ const current = body.current_password;
212
+ if (!current) {
213
+ return validationError("current_password is required for the voluntary change-password flow.", "current_password_required");
214
+ }
215
+ if (!auth.user.passwordHash) {
216
+ // The auth middleware already rejected NULL-password-hash users, but
217
+ // belt-and-braces here — if we somehow reached this line, log + 401.
218
+ ctx.logger.error({ user_id: auth.user.id, event: "change_password_no_hash" }, "Reached change-password with a NULL password_hash on the user row.");
219
+ return unauthorized();
220
+ }
221
+ const ok = await verifyPassword(auth.user.passwordHash, current);
222
+ if (!ok) {
223
+ ctx.logger.info({ user_id: auth.user.id, event: "change_password_wrong_current" }, "Change-password rejected: current password incorrect.");
224
+ return unauthorized("Current password is incorrect.");
225
+ }
226
+ }
227
+ else {
228
+ // Forced path — current_password not accepted (would confuse callers).
229
+ if (body.current_password !== undefined) {
230
+ return validationError("current_password must not be supplied with a password-change-only token.", "current_password_not_allowed");
231
+ }
232
+ }
233
+ // Hash + write the new password. Forced path also clears the flag.
234
+ // Bumping `passwordChangedAt` invalidates other sessions the user
235
+ // may have open (multi-device sign-out on password change) — see
236
+ // the middleware's iat-vs-passwordChangedAt gate.
237
+ const newHash = await hashPassword(body.new_password);
238
+ const clearInitialFlag = auth.tokenKind === "password_change";
239
+ const now = nowIso();
240
+ ctx.db
241
+ .update(user)
242
+ .set({
243
+ passwordHash: newHash,
244
+ isInitialPassword: clearInitialFlag ? false : auth.user.isInitialPassword,
245
+ passwordChangedAt: now,
246
+ updatedAt: now,
247
+ })
248
+ .where(eq(user.id, auth.user.id))
249
+ .run();
250
+ // Re-read for the role + theme bits (and to mint a session token with
251
+ // the freshest row state — Step 4's role-change endpoint can demote a
252
+ // user between login and change-password, in theory).
253
+ const freshRow = ctx.db.select().from(user).where(eq(user.id, auth.user.id)).get();
254
+ if (!freshRow) {
255
+ // Should not happen.
256
+ return internalError();
257
+ }
258
+ const settings = ctx.db
259
+ .select()
260
+ .from(adminSetting)
261
+ .where(eq(adminSetting.id, 1))
262
+ .get();
263
+ const signingKey = settings?.serviceTokenSigningKey;
264
+ if (!signingKey) {
265
+ ctx.logger.error({ event: "missing_signing_key" }, "Cannot mint session token after password change — signing key empty.");
266
+ return internalError("Server misconfigured");
267
+ }
268
+ const sessionToken = await mintSessionToken(signingKey, {
269
+ id: freshRow.id,
270
+ username: freshRow.username,
271
+ role: freshRow.role,
272
+ });
273
+ ctx.logger.info({
274
+ user_id: freshRow.id,
275
+ username: freshRow.username,
276
+ role: freshRow.role,
277
+ token_kind: auth.tokenKind,
278
+ cleared_initial: clearInitialFlag,
279
+ event: "password_changed",
280
+ }, "Password changed successfully.");
281
+ return jsonResponse(200, { token: sessionToken });
282
+ });
283
+ /* ────────────────────────────────────────────────────────────────────────── */
284
+ /* POST /auth/logout */
285
+ /* ────────────────────────────────────────────────────────────────────────── */
286
+ /**
287
+ * `POST /auth/logout` — server-side no-op in v1.
288
+ *
289
+ * Per §6.3.4 and SPECIFICATIONS.md's "Implementer's call" note: v1's
290
+ * simplest implementation is for the client to discard its token; we don't
291
+ * maintain a blacklist. A future minor may add token revocation if a
292
+ * concrete threat model demands it. Returns 200 with a minimal body so the
293
+ * client has something to act on.
294
+ *
295
+ * Still routes through the protected middleware so an anonymous caller gets
296
+ * a 401 — that signals to the client that its token was already invalid (in
297
+ * which case it should already be on the login screen).
298
+ *
299
+ * @see SPECIFICATIONS.md §6.3.4
300
+ */
301
+ export const handleLogout = protectedRoute({}, async () => {
302
+ return jsonResponse(200, { status: "ok" });
303
+ });
304
+ /* ────────────────────────────────────────────────────────────────────────── */
305
+ /* GET /auth/me */
306
+ /* ────────────────────────────────────────────────────────────────────────── */
307
+ /**
308
+ * `GET /auth/me` — returns the `{ user, deployment }` envelope.
309
+ *
310
+ * `deployment.letta_ade_url` is included only for callers with role >=
311
+ * `power_user` (handled inside `buildDeploymentView`); `user`-role callers
312
+ * don't even see the field exists — by design (§6.3.4).
313
+ *
314
+ * The resolved theme is computed client-side from
315
+ * `user.theme_preference + deployment.{theme_default, theme_switcher_enabled}`
316
+ * per §7.6 — the server does not pre-compute it.
317
+ *
318
+ * @see SPECIFICATIONS.md §6.3.4 — `GET /auth/me`
319
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
320
+ */
321
+ export const handleMe = protectedRoute({}, async (_req, ctx, auth) => {
322
+ const settings = ctx.db
323
+ .select()
324
+ .from(adminSetting)
325
+ .where(eq(adminSetting.id, 1))
326
+ .get();
327
+ const context = buildAuthContext(auth.user, settings ?? null);
328
+ return jsonResponse(200, {
329
+ user: context.user,
330
+ deployment: context.deployment,
331
+ });
332
+ });
333
+ /* ────────────────────────────────────────────────────────────────────────── */
334
+ /* PATCH /auth/me/preferences */
335
+ /* ────────────────────────────────────────────────────────────────────────── */
336
+ /** Preferences body. Only `theme` is supported in v1. Strict — extras rejected. */
337
+ const preferencesSchema = z
338
+ .object({
339
+ theme: z.enum(["light", "dark"]).optional(),
340
+ })
341
+ .strict();
342
+ /**
343
+ * `PATCH /auth/me/preferences` — updates per-user preferences.
344
+ *
345
+ * Body: `{ theme?: "light" | "dark" }`. Empty body is a 200 no-op (returns
346
+ * the current `{ user, deployment }` envelope unchanged) — by an explicit
347
+ * decision recorded in the Step-3 prompt. Unknown keys → 400 (§13.4).
348
+ *
349
+ * Returns 409 `theme_switcher_disabled` when the deployment-wide
350
+ * `admin_setting.theme_switcher_enabled = false`: per §7.6 the user's
351
+ * stored preference is ignored while the switcher is off, and accepting a
352
+ * write in that state would be silently swallowed. We surface the
353
+ * configuration mismatch rather than pretend the write applied.
354
+ *
355
+ * @see SPECIFICATIONS.md §6.3.4 — `PATCH /auth/me/preferences`
356
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
357
+ */
358
+ export const handleMePreferences = protectedRoute({}, async (req, ctx, auth) => {
359
+ let raw;
360
+ try {
361
+ raw = await req.json();
362
+ }
363
+ catch {
364
+ return validationError("Request body must be valid JSON.");
365
+ }
366
+ const parsed = preferencesSchema.safeParse(raw);
367
+ if (!parsed.success) {
368
+ return validationError("Invalid request body.");
369
+ }
370
+ const body = parsed.data;
371
+ if (body.theme !== undefined) {
372
+ const settings = ctx.db
373
+ .select()
374
+ .from(adminSetting)
375
+ .where(eq(adminSetting.id, 1))
376
+ .get();
377
+ if (settings && !settings.themeSwitcherEnabled) {
378
+ return conflictError("theme_switcher_disabled", "Theme switcher is disabled by the deployment administrator.");
379
+ }
380
+ ctx.db
381
+ .update(user)
382
+ .set({
383
+ themePreference: body.theme,
384
+ updatedAt: nowIso(),
385
+ })
386
+ .where(eq(user.id, auth.user.id))
387
+ .run();
388
+ ctx.logger.info({
389
+ user_id: auth.user.id,
390
+ theme: body.theme,
391
+ event: "theme_preference_updated",
392
+ }, "User theme preference updated.");
393
+ }
394
+ const freshRow = ctx.db
395
+ .select()
396
+ .from(user)
397
+ .where(eq(user.id, auth.user.id))
398
+ .get();
399
+ if (!freshRow)
400
+ return internalError();
401
+ const settings = ctx.db
402
+ .select()
403
+ .from(adminSetting)
404
+ .where(eq(adminSetting.id, 1))
405
+ .get();
406
+ return jsonResponse(200, {
407
+ user: buildUserView(freshRow),
408
+ deployment: buildDeploymentView(settings ?? null, freshRow.role),
409
+ });
410
+ });
411
+ /* ────────────────────────────────────────────────────────────────────────── */
412
+ /* Helpers */
413
+ /* ────────────────────────────────────────────────────────────────────────── */
414
+ /** Returns the current ISO-8601 timestamp with millisecond precision. */
415
+ function nowIso() {
416
+ return new Date().toISOString();
417
+ }
418
+ /** Extracts a string for logging from any thrown value. */
419
+ function errMessage(err) {
420
+ return err instanceof Error ? err.message : String(err);
421
+ }
422
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `/chat/*` — SSE chat, resumption, cancel (§6.3.2).
3
+ *
4
+ * Three endpoints:
5
+ *
6
+ * - `POST /chat` — body `{ session_id, user_message, effort?, idempotency_key? }`.
7
+ * Streams events per §9. Owner-scoped: the caller must own the
8
+ * referenced session.
9
+ * - `GET /chat/:turn_id/events?from_seq=N` — resumption: replays
10
+ * persisted events with `seq > N`, then live-attaches if the turn is
11
+ * still active.
12
+ * - `POST /chat/:turn_id/cancel` — aborts an in-flight turn. Idempotent
13
+ * when the turn already terminated.
14
+ *
15
+ * The runner (`state/turnRunner.ts`) owns the actual state machine; this
16
+ * file is the HTTP/SSE shell. The runner publishes events to a live
17
+ * channel + persists them; the `POST /chat` writer subscribes to the
18
+ * same channel and ships bytes to the client. The runner's `onEvent`
19
+ * callback can write directly; this file uses
20
+ * `subscribeToTurn` because it shares the same plumbing the resumption
21
+ * endpoint uses.
22
+ *
23
+ * @see SPECIFICATIONS.md §6.3.2 — chat (SSE)
24
+ * @see SPECIFICATIONS.md §6.6 — server-authoritative state
25
+ * @see SPECIFICATIONS.md §6.9 — cancellation
26
+ */
27
+ import { turnEvent } from "../db/schema.js";
28
+ import type { RouteHandler } from "../handlers/types.js";
29
+ import { replayPersistedEvents } from "../state/turnRunner.js";
30
+ /**
31
+ * `POST /chat` — opens an SSE stream for the new turn.
32
+ *
33
+ * Owner-scoped: returns 404 for sessions not owned by the caller. The
34
+ * `power_user`+ "all-sessions overview" is a different surface.
35
+ *
36
+ * The handler returns a `Response` whose body is a `ReadableStream`. The
37
+ * runner runs in the background, posts events to the registry's live
38
+ * channel, and the stream subscriber writes them out as `event:`/`data:`
39
+ * pairs. The runner returns after the stream's terminal event is
40
+ * persisted; the writer closes the stream shortly after.
41
+ *
42
+ * @see SPECIFICATIONS.md §6.3.2
43
+ * @see SPECIFICATIONS.md §9 — wire protocol
44
+ */
45
+ export declare const handlePostChat: RouteHandler;
46
+ /**
47
+ * `GET /chat/:turn_id/events?from_seq=N` — resumption.
48
+ *
49
+ * Two-phase:
50
+ *
51
+ * 1. Replay persisted `turn_event` rows with `seq > N`, in order.
52
+ * 2. If the turn is still live, attach to the in-memory broadcaster so
53
+ * new events stream through. If the turn is terminal, the response
54
+ * closes after the replay.
55
+ *
56
+ * Owner-scoped: the caller must own the parent session (the admin
57
+ * "sessions overview" doesn't go through this endpoint — that's a
58
+ * separate JSON read).
59
+ *
60
+ * @see SPECIFICATIONS.md §6.3.2 — resumption
61
+ */
62
+ export declare const handleGetChatEvents: RouteHandler;
63
+ /**
64
+ * `POST /chat/:turn_id/cancel` — aborts an in-flight turn authoritatively.
65
+ *
66
+ * Owner-scoped via the parent session. Idempotent — calling it on a
67
+ * terminal turn returns the same envelope as the first call (no error,
68
+ * the existing status is reported as `already_terminal`).
69
+ *
70
+ * Two cancellation paths fire here, in order:
71
+ *
72
+ * 1. **Letta-side cancel** — `client.agents.messages.cancel({ run_ids })`.
73
+ * This is the load-bearing one: it tells Letta to stop the run
74
+ * server-side and roll back the in-progress step from the
75
+ * conversation history (completed steps survive). **Without this,
76
+ * aborting only our SDK call's HTTP connection does nothing** —
77
+ * Letta keeps generating, persists the full output, and the user's
78
+ * refresh reveals it. Empirically verified.
79
+ *
80
+ * 2. **Local registry cancel** — `requestTurnCancellation(turnId)`. Fires
81
+ * our runner's AbortSignal so the in-process for-await loop exits
82
+ * cleanly + emits `stream_end{reason:"cancelled"}`. Also serves as
83
+ * the fallback when (1) fails (network blip, Letta-side error, run
84
+ * already terminal): the client still gets a clean terminal event.
85
+ *
86
+ * Race-free ordering: we fire (1) first so Letta has begun winding the
87
+ * run down by the time (2) fires; the few final events Letta delivers
88
+ * during that window flow through the runner's stream naturally before
89
+ * the abort lands.
90
+ *
91
+ * @see SPECIFICATIONS.md §6.3.2
92
+ * @see SPECIFICATIONS.md §6.9 — cancellation
93
+ */
94
+ export declare const handlePostChatCancel: RouteHandler;
95
+ export { replayPersistedEvents };
96
+ export { turnEvent };
97
+ //# sourceMappingURL=chat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/routes/chat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAQH,OAAO,EAAkC,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAQ5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAEL,qBAAqB,EAGtB,MAAM,wBAAwB,CAAC;AAmChC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,EAAE,YAgN5B,CAAC;AAMF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,YA0GjC,CAAC;AAMF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,oBAAoB,EAAE,YAqDlC,CAAC;AAQF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,OAAO,EAAE,SAAS,EAAE,CAAC"}