@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,374 @@
1
+ /**
2
+ * `/admin/users/*` — admin + super_admin user management.
3
+ *
4
+ * Five endpoints:
5
+ *
6
+ * - `GET /admin/users` — list every user in the deployment.
7
+ * - `POST /admin/users` — create a user; the server generates a 16-char
8
+ * alphanumeric password, hashes it, and returns the **plaintext exactly
9
+ * once** in the response. The caller delivers it to the user out-of-band.
10
+ * Sets `is_initial_password = true` so the user is forced to change on
11
+ * first login (§8.5).
12
+ * - `PATCH /admin/users/:id/role` — change a user's role. Uses
13
+ * `applyUserRoleChangeInTx` so the shield + min-count rules are enforced
14
+ * inside the transaction (no race possible).
15
+ * - `POST /admin/users/:id/reset-password` — regenerate a 16-char password
16
+ * for an existing user. Same return-once semantics; sets the initial-
17
+ * password flag back to true. Uses `assertPasswordResetInTx` to validate
18
+ * the shield + min-count rules, then performs the async Argon2 hash + write
19
+ * outside the transaction (better-sqlite3 transactions are synchronous so
20
+ * an Argon2 hash cannot live inside one). The narrow window between assert
21
+ * and write is documented in the helper's JSDoc and is acceptable in v1.
22
+ * - `DELETE /admin/users/:id` — hard-delete the user. Uses
23
+ * `applyUserDeletionInTx`; cascades to `session` via the FK.
24
+ *
25
+ * **Admin shield + min-count rules** are enforced by the helpers in
26
+ * `auth/roleRules.ts`; the handlers here just translate the helper's
27
+ * `TxResult` into a Response.
28
+ *
29
+ * **Admin cannot create / promote to `super_admin`.** The role enum on the
30
+ * POST body is filtered by caller role (admins see user / power_user /
31
+ * admin; super_admin sees all four). This mirrors the [05-admin-users.md]
32
+ * sketch's "Create User" modal.
33
+ *
34
+ * @see SPECIFICATIONS.md §6.3.4 — admin users endpoints
35
+ * @see SPECIFICATIONS.md §8.1 — shield + min-count rules
36
+ * @see SPECIFICATIONS.md §8.5 — password reset semantics
37
+ * @see ui-sketches/05-admin-users.md
38
+ */
39
+ import { randomBytes, randomUUID } from "node:crypto";
40
+ import { eq } from "drizzle-orm";
41
+ import { z } from "zod";
42
+ import { hashPassword } from "../../auth/argon2.js";
43
+ import { protectedRoute } from "../../auth/middleware.js";
44
+ import { applyUserDeletionInTx, applyUserRoleChangeInTx, assertPasswordResetInTx, } from "../../auth/roleRules.js";
45
+ import { user } from "../../db/schema.js";
46
+ import { conflictError, errorResponse, forbidden, jsonResponse, notFound, validationError, } from "../../handlers/responses.js";
47
+ /* ────────────────────────────────────────────────────────────────────────── */
48
+ /* Helpers */
49
+ /* ────────────────────────────────────────────────────────────────────────── */
50
+ /**
51
+ * Wire-format view of a `user` row for admin listings. Excludes
52
+ * `password_hash` and `identity_source` (server-internal — §6.3.4).
53
+ *
54
+ * @param row - the DB row.
55
+ * @returns the JSON-safe response shape.
56
+ */
57
+ function adminUserView(row) {
58
+ return {
59
+ id: row.id,
60
+ username: row.username,
61
+ display_name: row.displayName ?? null,
62
+ role: row.role,
63
+ is_initial_password: row.isInitialPassword,
64
+ theme_preference: row.themePreference ?? null,
65
+ created_at: row.createdAt,
66
+ updated_at: row.updatedAt,
67
+ };
68
+ }
69
+ /** ISO-8601 timestamp with millisecond precision. */
70
+ function nowIso() {
71
+ return new Date().toISOString();
72
+ }
73
+ /**
74
+ * Generates a 16-character alphanumeric password ([A-Za-z0-9]).
75
+ *
76
+ * Uses rejection sampling against the 62-char alphabet — every char is drawn
77
+ * uniformly via `randomBytes`, masked to 6 bits, then discarded if the value
78
+ * lands in the unused `[62, 64)` range. Average overhead is ~3%.
79
+ *
80
+ * The generated password trivially satisfies the §8.5 complexity policy
81
+ * (letters + digits = 2 categories), so no extra validation is needed before
82
+ * hashing. The 16-char length is also above the §8.5 8-char minimum.
83
+ *
84
+ * @returns a fresh 16-char alphanumeric password.
85
+ */
86
+ function generatePassword() {
87
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
88
+ const out = [];
89
+ while (out.length < 16) {
90
+ const bytes = randomBytes(32);
91
+ for (const b of bytes) {
92
+ const v = b & 0b00111111;
93
+ if (v < alphabet.length) {
94
+ out.push(alphabet[v]);
95
+ if (out.length === 16)
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ return out.join("");
101
+ }
102
+ /**
103
+ * Returns `true` if the caller's role is allowed to create / promote a user
104
+ * to `targetRole`. Admins cannot touch `super_admin`; super-admins can do
105
+ * anything.
106
+ *
107
+ * @param caller - caller role.
108
+ * @param targetRole - the requested role for the new (or promoted) user.
109
+ * @returns `true` if the operation is permitted.
110
+ */
111
+ function callerCanGrantRole(caller, targetRole) {
112
+ if (caller === "super_admin")
113
+ return true;
114
+ if (caller === "admin")
115
+ return targetRole !== "super_admin";
116
+ return false;
117
+ }
118
+ /* ────────────────────────────────────────────────────────────────────────── */
119
+ /* GET /admin/users */
120
+ /* ────────────────────────────────────────────────────────────────────────── */
121
+ /**
122
+ * `GET /admin/users` — list every user in the deployment.
123
+ *
124
+ * `admin` and `super_admin` only; gated by `protectedRoute({ requiredRole: "admin" })`.
125
+ * Sorted alphabetically by username for stable rendering in the admin table.
126
+ *
127
+ * @see SPECIFICATIONS.md §6.3.4
128
+ * @see ui-sketches/05-admin-users.md
129
+ */
130
+ export const handleListUsers = protectedRoute({ requiredRole: "admin" }, async (_req, ctx) => {
131
+ const rows = ctx.db.select().from(user).all();
132
+ // Sort client-side for a stable order regardless of insert order.
133
+ rows.sort((a, b) => a.username.localeCompare(b.username));
134
+ return jsonResponse(200, { users: rows.map(adminUserView) });
135
+ });
136
+ /* ────────────────────────────────────────────────────────────────────────── */
137
+ /* POST /admin/users */
138
+ /* ────────────────────────────────────────────────────────────────────────── */
139
+ /** Body for `POST /admin/users`. `role` defaults to `user` per §6.3.4. */
140
+ const createUserSchema = z
141
+ .object({
142
+ username: z.string().min(1).max(255),
143
+ display_name: z.string().min(1).max(255).nullish(),
144
+ role: z
145
+ .enum(["user", "power_user", "admin", "super_admin"])
146
+ .optional(),
147
+ })
148
+ .strict();
149
+ /**
150
+ * `POST /admin/users` — create a new user with a server-generated password.
151
+ *
152
+ * The 16-char alphanumeric plaintext is returned **exactly once** in the
153
+ * response (`{ user, generated_password }`); the caller (admin) is expected
154
+ * to deliver it out-of-band per §8.5. The plaintext never appears in logs
155
+ * (pino's redaction policy plus we never log the field).
156
+ *
157
+ * Caller-role gates:
158
+ *
159
+ * - `admin` can create role `user` / `power_user` / `admin`.
160
+ * - `super_admin` can additionally create `super_admin`.
161
+ *
162
+ * On username collision returns 409 `username_taken` (not 400) — the request
163
+ * itself was well-formed; the conflict is with existing data.
164
+ *
165
+ * @see SPECIFICATIONS.md §6.3.4 / §8.5
166
+ * @see ui-sketches/05-admin-users.md
167
+ */
168
+ export const handleCreateUser = protectedRoute({ requiredRole: "admin" }, async (req, ctx, auth) => {
169
+ let raw;
170
+ try {
171
+ raw = await req.json();
172
+ }
173
+ catch {
174
+ return validationError("Request body must be valid JSON.");
175
+ }
176
+ const parsed = createUserSchema.safeParse(raw);
177
+ if (!parsed.success) {
178
+ return validationError(parsed.error.issues[0]?.message ?? "Invalid request body.");
179
+ }
180
+ const body = parsed.data;
181
+ const role = body.role ?? "user";
182
+ if (!callerCanGrantRole(auth.role, role)) {
183
+ return forbidden(`Caller role ${auth.role} cannot create a user with role ${role}.`);
184
+ }
185
+ const existing = ctx.db
186
+ .select({ id: user.id })
187
+ .from(user)
188
+ .where(eq(user.username, body.username))
189
+ .get();
190
+ if (existing) {
191
+ return conflictError("username_taken", "Username is already in use.");
192
+ }
193
+ const plaintext = generatePassword();
194
+ const passwordHash = await hashPassword(plaintext);
195
+ const id = randomUUID();
196
+ try {
197
+ ctx.db
198
+ .insert(user)
199
+ .values({
200
+ id,
201
+ username: body.username,
202
+ displayName: body.display_name ?? null,
203
+ passwordHash,
204
+ identitySource: "local",
205
+ isInitialPassword: true,
206
+ role,
207
+ })
208
+ .run();
209
+ }
210
+ catch (err) {
211
+ // Race against another simultaneous create with the same username.
212
+ const message = err instanceof Error ? err.message : String(err);
213
+ if (message.includes("UNIQUE")) {
214
+ return conflictError("username_taken", "Username is already in use.");
215
+ }
216
+ throw err;
217
+ }
218
+ const row = ctx.db.select().from(user).where(eq(user.id, id)).get();
219
+ if (!row)
220
+ throw new Error("Just-inserted user vanished.");
221
+ ctx.logger.info({
222
+ actor_id: auth.user.id,
223
+ new_user_id: row.id,
224
+ new_role: row.role,
225
+ event: "admin_user_created",
226
+ }, "Admin created a new user.");
227
+ return jsonResponse(201, {
228
+ user: adminUserView(row),
229
+ generated_password: plaintext,
230
+ });
231
+ });
232
+ /* ────────────────────────────────────────────────────────────────────────── */
233
+ /* PATCH /admin/users/:id/role */
234
+ /* ────────────────────────────────────────────────────────────────────────── */
235
+ /** Body for `PATCH /admin/users/:id/role`. */
236
+ const patchRoleSchema = z
237
+ .object({
238
+ role: z.enum(["user", "power_user", "admin", "super_admin"]),
239
+ })
240
+ .strict();
241
+ /**
242
+ * `PATCH /admin/users/:id/role` — change a user's role.
243
+ *
244
+ * Delegates the shield + min-count check to `applyUserRoleChangeInTx`, which
245
+ * re-reads the target row + super-admin count inside one transaction so a
246
+ * concurrent demote cannot bypass the invariants.
247
+ *
248
+ * Additional caller-role gate on top of the helper: an `admin` cannot grant
249
+ * the `super_admin` role on a target they could otherwise mutate (e.g.
250
+ * promoting a `power_user → super_admin`) — only super-admins can.
251
+ *
252
+ * @see SPECIFICATIONS.md §6.3.4 / §8.1
253
+ */
254
+ export const handlePatchUserRole = protectedRoute({ requiredRole: "admin" }, async (req, ctx, auth, params) => {
255
+ const id = params.id;
256
+ if (!id)
257
+ return notFound();
258
+ let raw;
259
+ try {
260
+ raw = await req.json();
261
+ }
262
+ catch {
263
+ return validationError("Request body must be valid JSON.");
264
+ }
265
+ const parsed = patchRoleSchema.safeParse(raw);
266
+ if (!parsed.success) {
267
+ return validationError(parsed.error.issues[0]?.message ?? "Invalid request body.");
268
+ }
269
+ const newRole = parsed.data.role;
270
+ if (!callerCanGrantRole(auth.role, newRole)) {
271
+ return forbidden(`Caller role ${auth.role} cannot promote a user to ${newRole}.`);
272
+ }
273
+ const result = applyUserRoleChangeInTx(ctx.db, { id: auth.user.id, role: auth.role }, id, newRole);
274
+ if (!result.ok) {
275
+ return errorResponse(result.status, result.code, result.message);
276
+ }
277
+ ctx.logger.info({
278
+ actor_id: auth.user.id,
279
+ target_id: id,
280
+ new_role: newRole,
281
+ event: "admin_role_changed",
282
+ }, "Admin changed a user's role.");
283
+ return jsonResponse(200, { user: adminUserView(result.data.updated) });
284
+ });
285
+ /* ────────────────────────────────────────────────────────────────────────── */
286
+ /* POST /admin/users/:id/reset-password */
287
+ /* ────────────────────────────────────────────────────────────────────────── */
288
+ /**
289
+ * `POST /admin/users/:id/reset-password` — regenerate the user's password.
290
+ *
291
+ * Uses `assertPasswordResetInTx` to validate shield + min-count inside a
292
+ * transaction, then performs the async Argon2 hash + UPDATE *outside* the
293
+ * transaction (better-sqlite3 transactions are synchronous). The narrow
294
+ * window between assertion and write is acceptable in v1 — a concurrent
295
+ * role-mutation could in theory bypass the rule, but the worst-case is a
296
+ * single password reset on a user the admin shouldn't have been able to
297
+ * touch; the admin already had to authenticate and the new password is
298
+ * displayed only to them. Mitigation if this ever bites: re-verify the
299
+ * caller's role hasn't dropped between assertion and write.
300
+ *
301
+ * Returns `{ generated_password }` exactly once. Sets `is_initial_password =
302
+ * true` so the user is forced to change on their next login.
303
+ *
304
+ * @see SPECIFICATIONS.md §6.3.4 / §8.5
305
+ * @see ui-sketches/05-admin-users.md
306
+ */
307
+ export const handleResetUserPassword = protectedRoute({ requiredRole: "admin" }, async (_req, ctx, auth, params) => {
308
+ const id = params.id;
309
+ if (!id)
310
+ return notFound();
311
+ const result = assertPasswordResetInTx(ctx.db, { id: auth.user.id, role: auth.role }, id);
312
+ if (!result.ok) {
313
+ return errorResponse(result.status, result.code, result.message);
314
+ }
315
+ const plaintext = generatePassword();
316
+ const newHash = await hashPassword(plaintext);
317
+ const now = nowIso();
318
+ ctx.db
319
+ .update(user)
320
+ .set({
321
+ passwordHash: newHash,
322
+ isInitialPassword: true,
323
+ // Bumping `passwordChangedAt` invalidates the target's existing
324
+ // session JWTs (the middleware compares `iat` against this column)
325
+ // — the user is signed out from every device the next time their
326
+ // browser fires a protected request. Step 11 bugfix per the
327
+ // SPECIFICATIONS.md §8.5 password-reset semantics.
328
+ passwordChangedAt: now,
329
+ updatedAt: now,
330
+ })
331
+ .where(eq(user.id, id))
332
+ .run();
333
+ ctx.logger.info({
334
+ actor_id: auth.user.id,
335
+ target_id: id,
336
+ event: "admin_password_reset",
337
+ }, "Admin reset a user's password.");
338
+ return jsonResponse(200, { generated_password: plaintext });
339
+ });
340
+ /* ────────────────────────────────────────────────────────────────────────── */
341
+ /* DELETE /admin/users/:id */
342
+ /* ────────────────────────────────────────────────────────────────────────── */
343
+ /**
344
+ * `DELETE /admin/users/:id` — hard-delete the user.
345
+ *
346
+ * Uses `applyUserDeletionInTx`; the FK on `session.user_id` cascades, so
347
+ * deleting a user removes all their sessions atomically. The shield + min-
348
+ * count invariants apply (an admin cannot delete another admin or super
349
+ * admin; nobody can delete the only super admin).
350
+ *
351
+ * @see SPECIFICATIONS.md §6.3.4 / §8.1
352
+ */
353
+ export const handleDeleteUser = protectedRoute({ requiredRole: "admin" }, async (_req, ctx, auth, params) => {
354
+ const id = params.id;
355
+ if (!id)
356
+ return notFound();
357
+ if (id === auth.user.id) {
358
+ // Defensive: even though min-count would catch the only-super_admin
359
+ // self-delete, we block all self-deletes outright as a usability
360
+ // safeguard (deleting yourself logs you out and could brick the UI).
361
+ return forbidden("Cannot delete your own account.");
362
+ }
363
+ const result = applyUserDeletionInTx(ctx.db, { id: auth.user.id, role: auth.role }, id);
364
+ if (!result.ok) {
365
+ return errorResponse(result.status, result.code, result.message);
366
+ }
367
+ ctx.logger.info({
368
+ actor_id: auth.user.id,
369
+ target_id: id,
370
+ event: "admin_user_deleted",
371
+ }, "Admin deleted a user.");
372
+ return jsonResponse(200, { deleted: { id } });
373
+ });
374
+ //# sourceMappingURL=users.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users.js","sourceRoot":"","sources":["../../../src/routes/admin/users.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE1C,OAAO,EACL,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,eAAe,GAChB,MAAM,6BAA6B,CAAC;AAGrC,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,GAAY;IACjC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,YAAY,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;QACrC,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,mBAAmB,EAAE,GAAG,CAAC,iBAAiB;QAC1C,gBAAgB,EAAE,GAAG,CAAC,eAAe,IAAI,IAAI;QAC7C,UAAU,EAAE,GAAG,CAAC,SAAS;QACzB,UAAU,EAAE,GAAG,CAAC,SAAS;KAC1B,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,SAAS,MAAM;IACb,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,gBAAgB;IACvB,MAAM,QAAQ,GACZ,gEAAgE,CAAC;IACnE,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,OAAO,GAAG,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC;YACzB,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACxB,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAW,CAAC,CAAC;gBAChC,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE;oBAAE,MAAM;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,kBAAkB,CAAC,MAAiB,EAAE,UAAqB;IAClE,IAAI,MAAM,KAAK,aAAa;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,UAAU,KAAK,aAAa,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiB,cAAc,CACzD,EAAE,YAAY,EAAE,OAAO,EAAE,EACzB,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;IAClB,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;IAC9C,kEAAkE;IAClE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1D,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;AAC/D,CAAC,CACF,CAAC;AAEF,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,0EAA0E;AAC1E,MAAM,gBAAgB,GAAG,CAAC;KACvB,MAAM,CAAC;IACN,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE;IAClD,IAAI,EAAE,CAAC;SACJ,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;SACpD,QAAQ,EAAE;CACd,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAiB,cAAc,CAC1D,EAAE,YAAY,EAAE,OAAO,EAAE,EACzB,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACvB,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,eAAe,CAAC,kCAAkC,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,uBAAuB,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,MAAM,IAAI,GAAc,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC;IAC5C,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QACzC,OAAO,SAAS,CACd,eAAe,IAAI,CAAC,IAAI,mCAAmC,IAAI,GAAG,CACnE,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,CAAC,EAAE;SACpB,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;SACvB,IAAI,CAAC,IAAI,CAAC;SACV,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;SACvC,GAAG,EAAE,CAAC;IACT,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,aAAa,CAAC,gBAAgB,EAAE,6BAA6B,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,SAAS,GAAG,gBAAgB,EAAE,CAAC;IACrC,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;IACnD,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;IACxB,IAAI,CAAC;QACH,GAAG,CAAC,EAAE;aACH,MAAM,CAAC,IAAI,CAAC;aACZ,MAAM,CAAC;YACN,EAAE;YACF,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,WAAW,EAAE,IAAI,CAAC,YAAY,IAAI,IAAI;YACtC,YAAY;YACZ,cAAc,EAAE,OAAO;YACvB,iBAAiB,EAAE,IAAI;YACvB,IAAI;SACL,CAAC;aACD,GAAG,EAAE,CAAC;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,mEAAmE;QACnE,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,OAAO,aAAa,CAAC,gBAAgB,EAAE,6BAA6B,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IACpE,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAE1D,GAAG,CAAC,MAAM,CAAC,IAAI,CACb;QACE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QACtB,WAAW,EAAE,GAAG,CAAC,EAAE;QACnB,QAAQ,EAAE,GAAG,CAAC,IAAI;QAClB,KAAK,EAAE,oBAAoB;KAC5B,EACD,2BAA2B,CAC5B,CAAC;IAEF,OAAO,YAAY,CAAC,GAAG,EAAE;QACvB,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC;QACxB,kBAAkB,EAAE,SAAS;KAC9B,CAAC,CAAC;AACL,CAAC,CACF,CAAC;AAEF,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAEhF,8CAA8C;AAC9C,MAAM,eAAe,GAAG,CAAC;KACtB,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;CAC7D,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAiB,cAAc,CAC7D,EAAE,YAAY,EAAE,OAAO,EAAE,EACzB,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAC/B,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE;QAAE,OAAO,QAAQ,EAAE,CAAC;IAC3B,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,eAAe,CAAC,kCAAkC,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,uBAAuB,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,OAAO,GAAc,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IAC5C,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;QAC5C,OAAO,SAAS,CACd,eAAe,IAAI,CAAC,IAAI,6BAA6B,OAAO,GAAG,CAChE,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,uBAAuB,CACpC,GAAG,CAAC,EAAE,EACN,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EACrC,EAAE,EACF,OAAO,CACR,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACnE,CAAC;IAED,GAAG,CAAC,MAAM,CAAC,IAAI,CACb;QACE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QACtB,SAAS,EAAE,EAAE;QACb,QAAQ,EAAE,OAAO;QACjB,KAAK,EAAE,oBAAoB;KAC5B,EACD,8BAA8B,CAC/B,CAAC;IAEF,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AACzE,CAAC,CACF,CAAC;AAEF,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAiB,cAAc,CACjE,EAAE,YAAY,EAAE,OAAO,EAAE,EACzB,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAChC,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE;QAAE,OAAO,QAAQ,EAAE,CAAC;IAE3B,MAAM,MAAM,GAAG,uBAAuB,CACpC,GAAG,CAAC,EAAE,EACN,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EACrC,EAAE,CACH,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,SAAS,GAAG,gBAAgB,EAAE,CAAC;IACrC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE;SACH,MAAM,CAAC,IAAI,CAAC;SACZ,GAAG,CAAC;QACH,YAAY,EAAE,OAAO;QACrB,iBAAiB,EAAE,IAAI;QACvB,gEAAgE;QAChE,mEAAmE;QACnE,iEAAiE;QACjE,4DAA4D;QAC5D,mDAAmD;QACnD,iBAAiB,EAAE,GAAG;QACtB,SAAS,EAAE,GAAG;KACf,CAAC;SACD,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;SACtB,GAAG,EAAE,CAAC;IAET,GAAG,CAAC,MAAM,CAAC,IAAI,CACb;QACE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QACtB,SAAS,EAAE,EAAE;QACb,KAAK,EAAE,sBAAsB;KAC9B,EACD,gCAAgC,CACjC,CAAC;IAEF,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,CAAC,CAAC;AAC9D,CAAC,CACF,CAAC;AAEF,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAiB,cAAc,CAC1D,EAAE,YAAY,EAAE,OAAO,EAAE,EACzB,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAChC,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE;QAAE,OAAO,QAAQ,EAAE,CAAC;IAC3B,IAAI,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACxB,oEAAoE;QACpE,iEAAiE;QACjE,qEAAqE;QACrE,OAAO,SAAS,CAAC,iCAAiC,CAAC,CAAC;IACtD,CAAC;IACD,MAAM,MAAM,GAAG,qBAAqB,CAClC,GAAG,CAAC,EAAE,EACN,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EACrC,EAAE,CACH,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACnE,CAAC;IACD,GAAG,CAAC,MAAM,CAAC,IAAI,CACb;QACE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QACtB,SAAS,EAAE,EAAE;QACb,KAAK,EAAE,oBAAoB;KAC5B,EACD,uBAAuB,CACxB,CAAC;IACF,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAChD,CAAC,CACF,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * User-facing `/agents` — the catalog the agents picker reads.
3
+ *
4
+ * Returns **non-archived** ai-hub agents only; each entry carries a
5
+ * `session_count` computed against the *caller's* non-archived sessions for
6
+ * that agent (one round-trip populates the "(N of your sessions)" subtitles
7
+ * on the picker — see [03-chat.md](../../../../docs/ui-sketches/03-chat.md)).
8
+ *
9
+ * The underlying Letta `agent_id` is **not** exposed here — it's an admin-only
10
+ * concern (`/admin/agents` returns it for editors).
11
+ *
12
+ * @see SPECIFICATIONS.md §6.3.3 — Agents (catalog)
13
+ */
14
+ import type { RouteHandler } from "../handlers/types.js";
15
+ /**
16
+ * `GET /agents` — every authenticated session.
17
+ *
18
+ * Returns `{ agents: [{ id, display_name, session_count }, ...] }`. Sorted by
19
+ * `display_name` for stable picker ordering. Excludes archived agents
20
+ * server-side so the user-facing picker stays focused on the working set.
21
+ *
22
+ * The `session_count` is **caller-scoped** — counted via a left-join filtered
23
+ * to `(session.user_id = caller.id AND session.is_archived = false)` so each
24
+ * user sees their own count, never another user's.
25
+ *
26
+ * @see SPECIFICATIONS.md §6.3.3 — Agents (catalog)
27
+ * @see ui-sketches/03-chat.md — agents picker
28
+ */
29
+ export declare const handleListAgents: RouteHandler;
30
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/routes/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,EAAE,YA8B7B,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * User-facing `/agents` — the catalog the agents picker reads.
3
+ *
4
+ * Returns **non-archived** ai-hub agents only; each entry carries a
5
+ * `session_count` computed against the *caller's* non-archived sessions for
6
+ * that agent (one round-trip populates the "(N of your sessions)" subtitles
7
+ * on the picker — see [03-chat.md](../../../../docs/ui-sketches/03-chat.md)).
8
+ *
9
+ * The underlying Letta `agent_id` is **not** exposed here — it's an admin-only
10
+ * concern (`/admin/agents` returns it for editors).
11
+ *
12
+ * @see SPECIFICATIONS.md §6.3.3 — Agents (catalog)
13
+ */
14
+ import { and, asc, eq, sql } from "drizzle-orm";
15
+ import { protectedRoute } from "../auth/middleware.js";
16
+ import { agent, session } from "../db/schema.js";
17
+ import { jsonResponse } from "../handlers/responses.js";
18
+ /**
19
+ * `GET /agents` — every authenticated session.
20
+ *
21
+ * Returns `{ agents: [{ id, display_name, session_count }, ...] }`. Sorted by
22
+ * `display_name` for stable picker ordering. Excludes archived agents
23
+ * server-side so the user-facing picker stays focused on the working set.
24
+ *
25
+ * The `session_count` is **caller-scoped** — counted via a left-join filtered
26
+ * to `(session.user_id = caller.id AND session.is_archived = false)` so each
27
+ * user sees their own count, never another user's.
28
+ *
29
+ * @see SPECIFICATIONS.md §6.3.3 — Agents (catalog)
30
+ * @see ui-sketches/03-chat.md — agents picker
31
+ */
32
+ export const handleListAgents = protectedRoute({}, async (_req, ctx, auth) => {
33
+ const rows = ctx.db
34
+ .select({
35
+ id: agent.id,
36
+ displayName: agent.displayName,
37
+ // Count caller's non-archived sessions against this agent. The
38
+ // double-condition LEFT JOIN keeps agents with 0 sessions in the result.
39
+ sessionCount: sql `COUNT(${session.id})`.as("session_count"),
40
+ })
41
+ .from(agent)
42
+ .leftJoin(session, and(eq(session.agentId, agent.id), eq(session.userId, auth.user.id), eq(session.isArchived, false)))
43
+ .where(eq(agent.isArchived, false))
44
+ .groupBy(agent.id)
45
+ .orderBy(asc(agent.displayName))
46
+ .all();
47
+ return jsonResponse(200, {
48
+ agents: rows.map((r) => ({
49
+ id: r.id,
50
+ display_name: r.displayName,
51
+ session_count: Number(r.sessionCount),
52
+ })),
53
+ });
54
+ });
55
+ //# sourceMappingURL=agents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../src/routes/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAiB,cAAc,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACzF,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE;SAChB,MAAM,CAAC;QACN,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,+DAA+D;QAC/D,yEAAyE;QACzE,YAAY,EAAE,GAAG,CAAQ,SAAS,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,eAAe,CAAC;KACpE,CAAC;SACD,IAAI,CAAC,KAAK,CAAC;SACX,QAAQ,CACP,OAAO,EACP,GAAG,CACD,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,EAC7B,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAChC,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAC9B,CACF;SACA,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;SAClC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;SACjB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;SAC/B,GAAG,EAAE,CAAC;IAET,OAAO,YAAY,CAAC,GAAG,EAAE;QACvB,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACvB,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,YAAY,EAAE,CAAC,CAAC,WAAW;YAC3B,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC;SACtC,CAAC,CAAC;KACJ,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,121 @@
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 type { RouteHandler } from "../handlers/types.js";
22
+ /**
23
+ * `POST /auth/login` — Argon2id-verifies username + password, mints either
24
+ * a session JWT or a 5-minute password-change-only JWT depending on the
25
+ * user's `is_initial_password` flag.
26
+ *
27
+ * **Generic 401 on every failure path** — wrong username, wrong password,
28
+ * NULL `password_hash` (the future-IdP seam test), or any malformed body.
29
+ * No enumeration leakage (§13.1, §8.6).
30
+ *
31
+ * On success returns `{ token, must_change_password, user, deployment }`.
32
+ * When `must_change_password = true`, the `deployment` block is omitted
33
+ * entirely (the change-only token doesn't grant deployment access) and
34
+ * `user.role` is omitted (the change-only token carries no role claim) —
35
+ * the client re-fetches `/auth/me` after the change-password handshake.
36
+ *
37
+ * Hash-upgrade-on-login (§8.5): if the stored hash was produced with weaker
38
+ * params than the current `ARGON2_PARAMS` baseline, we rehash and write
39
+ * back. Best-effort — a write failure does not fail the login.
40
+ *
41
+ * @see SPECIFICATIONS.md §6.3.4 — `POST /auth/login`
42
+ * @see SPECIFICATIONS.md §8.5 — auth + hash upgrade
43
+ */
44
+ export declare const handleLogin: RouteHandler;
45
+ /**
46
+ * `POST /auth/change-password` — two distinct flows on one endpoint:
47
+ *
48
+ * - **Voluntary (session token):** caller supplies `current_password`; the
49
+ * server Argon2id-verifies it, then hashes + writes the new password. Does
50
+ * not touch `is_initial_password`. Returns a fresh session token (with the
51
+ * caller's current role).
52
+ * - **Forced (password-change-only token):** caller does not supply
53
+ * `current_password` (the change-only token already proves a successful
54
+ * prior login). The server hashes + writes the new password, clears
55
+ * `is_initial_password`, and returns a fresh session token. The client
56
+ * transitions to the normal chat at this point.
57
+ *
58
+ * Server-side validation: the new password must satisfy the §8.5 policy —
59
+ * at least 8 characters AND at least two of {letters, digits, special
60
+ * characters}. Length-only passwords (e.g. `"12345678"`) are rejected with
61
+ * 400 `password_too_simple`.
62
+ *
63
+ * Mismatched current password → 401 `unauthorized` with a "current
64
+ * password"-mentioning message. Voluntary path with no `current_password`
65
+ * is rejected as a validation error (the field IS required when the caller
66
+ * authenticated with a session token). Forced path with a `current_password`
67
+ * is rejected too — that signals client confusion.
68
+ *
69
+ * @see SPECIFICATIONS.md §6.3.4 — `POST /auth/change-password`
70
+ * @see SPECIFICATIONS.md §8.5 — change flows + password policy
71
+ */
72
+ export declare const handleChangePassword: RouteHandler;
73
+ /**
74
+ * `POST /auth/logout` — server-side no-op in v1.
75
+ *
76
+ * Per §6.3.4 and SPECIFICATIONS.md's "Implementer's call" note: v1's
77
+ * simplest implementation is for the client to discard its token; we don't
78
+ * maintain a blacklist. A future minor may add token revocation if a
79
+ * concrete threat model demands it. Returns 200 with a minimal body so the
80
+ * client has something to act on.
81
+ *
82
+ * Still routes through the protected middleware so an anonymous caller gets
83
+ * a 401 — that signals to the client that its token was already invalid (in
84
+ * which case it should already be on the login screen).
85
+ *
86
+ * @see SPECIFICATIONS.md §6.3.4
87
+ */
88
+ export declare const handleLogout: RouteHandler;
89
+ /**
90
+ * `GET /auth/me` — returns the `{ user, deployment }` envelope.
91
+ *
92
+ * `deployment.letta_ade_url` is included only for callers with role >=
93
+ * `power_user` (handled inside `buildDeploymentView`); `user`-role callers
94
+ * don't even see the field exists — by design (§6.3.4).
95
+ *
96
+ * The resolved theme is computed client-side from
97
+ * `user.theme_preference + deployment.{theme_default, theme_switcher_enabled}`
98
+ * per §7.6 — the server does not pre-compute it.
99
+ *
100
+ * @see SPECIFICATIONS.md §6.3.4 — `GET /auth/me`
101
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
102
+ */
103
+ export declare const handleMe: RouteHandler;
104
+ /**
105
+ * `PATCH /auth/me/preferences` — updates per-user preferences.
106
+ *
107
+ * Body: `{ theme?: "light" | "dark" }`. Empty body is a 200 no-op (returns
108
+ * the current `{ user, deployment }` envelope unchanged) — by an explicit
109
+ * decision recorded in the Step-3 prompt. Unknown keys → 400 (§13.4).
110
+ *
111
+ * Returns 409 `theme_switcher_disabled` when the deployment-wide
112
+ * `admin_setting.theme_switcher_enabled = false`: per §7.6 the user's
113
+ * stored preference is ignored while the switcher is off, and accepting a
114
+ * write in that state would be silently swallowed. We surface the
115
+ * configuration mismatch rather than pretend the write applied.
116
+ *
117
+ * @see SPECIFICATIONS.md §6.3.4 — `PATCH /auth/me/preferences`
118
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
119
+ */
120
+ export declare const handleMePreferences: RouteHandler;
121
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA6BH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAczD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,WAAW,EAAE,YAgHzB,CAAC;AAcF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,oBAAoB,EAAE,YAqHlC,CAAC;AAMF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,EAAE,YAEzB,CAAC;AAMH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,EAAE,YAWrB,CAAC;AAaH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,YA6DjC,CAAC"}