@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.
- package/dist/auth/argon2.d.ts +72 -0
- package/dist/auth/argon2.d.ts.map +1 -0
- package/dist/auth/argon2.js +145 -0
- package/dist/auth/argon2.js.map +1 -0
- package/dist/auth/encryption.d.ts +52 -0
- package/dist/auth/encryption.d.ts.map +1 -0
- package/dist/auth/encryption.js +98 -0
- package/dist/auth/encryption.js.map +1 -0
- package/dist/auth/jwt.d.ts +133 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/jwt.js +147 -0
- package/dist/auth/jwt.js.map +1 -0
- package/dist/auth/middleware.d.ts +101 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +161 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/passwordPolicy.d.ts +72 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +88 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/roleRules.d.ts +200 -0
- package/dist/auth/roleRules.d.ts.map +1 -0
- package/dist/auth/roleRules.js +365 -0
- package/dist/auth/roleRules.js.map +1 -0
- package/dist/auth/userView.d.ts +55 -0
- package/dist/auth/userView.d.ts.map +1 -0
- package/dist/auth/userView.js +77 -0
- package/dist/auth/userView.js.map +1 -0
- package/dist/bootstrap.d.ts +104 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +160 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/db/connection.d.ts +39 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +62 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +31 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +197 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +1902 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +662 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/seed.d.ts +92 -0
- package/dist/db/seed.d.ts.map +1 -0
- package/dist/db/seed.js +160 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/handlers/fetch.d.ts +89 -0
- package/dist/handlers/fetch.d.ts.map +1 -0
- package/dist/handlers/fetch.js +173 -0
- package/dist/handlers/fetch.js.map +1 -0
- package/dist/handlers/nextjs.d.ts +40 -0
- package/dist/handlers/nextjs.d.ts.map +1 -0
- package/dist/handlers/nextjs.js +42 -0
- package/dist/handlers/nextjs.js.map +1 -0
- package/dist/handlers/responses.d.ts +46 -0
- package/dist/handlers/responses.d.ts.map +1 -0
- package/dist/handlers/responses.js +74 -0
- package/dist/handlers/responses.js.map +1 -0
- package/dist/handlers/routeTable.d.ts +21 -0
- package/dist/handlers/routeTable.d.ts.map +1 -0
- package/dist/handlers/routeTable.js +133 -0
- package/dist/handlers/routeTable.js.map +1 -0
- package/dist/handlers/router.d.ts +37 -0
- package/dist/handlers/router.d.ts.map +1 -0
- package/dist/handlers/router.js +82 -0
- package/dist/handlers/router.js.map +1 -0
- package/dist/handlers/sse.d.ts +38 -0
- package/dist/handlers/sse.d.ts.map +1 -0
- package/dist/handlers/sse.js +52 -0
- package/dist/handlers/sse.js.map +1 -0
- package/dist/handlers/types.d.ts +96 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/types.js +14 -0
- package/dist/handlers/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/letta/client.d.ts +355 -0
- package/dist/letta/client.d.ts.map +1 -0
- package/dist/letta/client.js +529 -0
- package/dist/letta/client.js.map +1 -0
- package/dist/letta/eventTranslator.d.ts +166 -0
- package/dist/letta/eventTranslator.d.ts.map +1 -0
- package/dist/letta/eventTranslator.js +532 -0
- package/dist/letta/eventTranslator.js.map +1 -0
- package/dist/letta/index.d.ts +13 -0
- package/dist/letta/index.d.ts.map +1 -0
- package/dist/letta/index.js +12 -0
- package/dist/letta/index.js.map +1 -0
- package/dist/letta/modelSettings.d.ts +176 -0
- package/dist/letta/modelSettings.d.ts.map +1 -0
- package/dist/letta/modelSettings.js +314 -0
- package/dist/letta/modelSettings.js.map +1 -0
- package/dist/letta/promptWrapping.d.ts +141 -0
- package/dist/letta/promptWrapping.d.ts.map +1 -0
- package/dist/letta/promptWrapping.js +213 -0
- package/dist/letta/promptWrapping.js.map +1 -0
- package/dist/letta/sandbox.d.ts +100 -0
- package/dist/letta/sandbox.d.ts.map +1 -0
- package/dist/letta/sandbox.js +151 -0
- package/dist/letta/sandbox.js.map +1 -0
- package/dist/letta/settingsFileSeed.d.ts +146 -0
- package/dist/letta/settingsFileSeed.d.ts.map +1 -0
- package/dist/letta/settingsFileSeed.js +177 -0
- package/dist/letta/settingsFileSeed.js.map +1 -0
- package/dist/letta/subprocessThrottle.d.ts +151 -0
- package/dist/letta/subprocessThrottle.d.ts.map +1 -0
- package/dist/letta/subprocessThrottle.js +195 -0
- package/dist/letta/subprocessThrottle.js.map +1 -0
- package/dist/letta/tpmRecovery.d.ts +202 -0
- package/dist/letta/tpmRecovery.d.ts.map +1 -0
- package/dist/letta/tpmRecovery.js +314 -0
- package/dist/letta/tpmRecovery.js.map +1 -0
- package/dist/letta/types.d.ts +54 -0
- package/dist/letta/types.d.ts.map +1 -0
- package/dist/letta/types.js +16 -0
- package/dist/letta/types.js.map +1 -0
- package/dist/observability/logger.d.ts +48 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +81 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +53 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +92 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/routes/admin/agents.d.ts +63 -0
- package/dist/routes/admin/agents.d.ts.map +1 -0
- package/dist/routes/admin/agents.js +461 -0
- package/dist/routes/admin/agents.js.map +1 -0
- package/dist/routes/admin/rotatingMessages.d.ts +51 -0
- package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
- package/dist/routes/admin/rotatingMessages.js +292 -0
- package/dist/routes/admin/rotatingMessages.js.map +1 -0
- package/dist/routes/admin/sessions.d.ts +61 -0
- package/dist/routes/admin/sessions.d.ts.map +1 -0
- package/dist/routes/admin/sessions.js +232 -0
- package/dist/routes/admin/sessions.js.map +1 -0
- package/dist/routes/admin/settings.d.ts +110 -0
- package/dist/routes/admin/settings.d.ts.map +1 -0
- package/dist/routes/admin/settings.js +401 -0
- package/dist/routes/admin/settings.js.map +1 -0
- package/dist/routes/admin/users.d.ts +115 -0
- package/dist/routes/admin/users.d.ts.map +1 -0
- package/dist/routes/admin/users.js +374 -0
- package/dist/routes/admin/users.js.map +1 -0
- package/dist/routes/agents.d.ts +30 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +55 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/auth.d.ts +121 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +422 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/chat.d.ts +97 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +484 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/featureToggles.d.ts +34 -0
- package/dist/routes/featureToggles.d.ts.map +1 -0
- package/dist/routes/featureToggles.js +47 -0
- package/dist/routes/featureToggles.js.map +1 -0
- package/dist/routes/health.d.ts +24 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +37 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/metrics.d.ts +35 -0
- package/dist/routes/metrics.d.ts.map +1 -0
- package/dist/routes/metrics.js +106 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/sessions.d.ts +161 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +1501 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/stubs.d.ts +25 -0
- package/dist/routes/stubs.d.ts.map +1 -0
- package/dist/routes/stubs.js +25 -0
- package/dist/routes/stubs.js.map +1 -0
- package/dist/state/cron.d.ts +58 -0
- package/dist/state/cron.d.ts.map +1 -0
- package/dist/state/cron.js +160 -0
- package/dist/state/cron.js.map +1 -0
- package/dist/state/rotationRunner.d.ts +96 -0
- package/dist/state/rotationRunner.d.ts.map +1 -0
- package/dist/state/rotationRunner.js +246 -0
- package/dist/state/rotationRunner.js.map +1 -0
- package/dist/state/rotationScheduler.d.ts +76 -0
- package/dist/state/rotationScheduler.d.ts.map +1 -0
- package/dist/state/rotationScheduler.js +124 -0
- package/dist/state/rotationScheduler.js.map +1 -0
- package/dist/state/turnRegistry.d.ts +95 -0
- package/dist/state/turnRegistry.d.ts.map +1 -0
- package/dist/state/turnRegistry.js +149 -0
- package/dist/state/turnRegistry.js.map +1 -0
- package/dist/state/turnRunner.d.ts +134 -0
- package/dist/state/turnRunner.d.ts.map +1 -0
- package/dist/state/turnRunner.js +476 -0
- package/dist/state/turnRunner.js.map +1 -0
- package/package.json +47 -0
- package/src/auth/argon2.ts +152 -0
- package/src/auth/encryption.ts +110 -0
- package/src/auth/jwt.ts +217 -0
- package/src/auth/middleware.ts +250 -0
- package/src/auth/passwordPolicy.ts +99 -0
- package/src/auth/roleRules.ts +497 -0
- package/src/auth/userView.ts +88 -0
- package/src/bootstrap.ts +254 -0
- package/src/db/connection.ts +75 -0
- package/src/db/migrate.ts +212 -0
- package/src/db/schema.ts +750 -0
- package/src/db/seed.ts +236 -0
- package/src/handlers/fetch.ts +251 -0
- package/src/handlers/nextjs.ts +49 -0
- package/src/handlers/responses.ts +95 -0
- package/src/handlers/routeTable.ts +188 -0
- package/src/handlers/router.ts +96 -0
- package/src/handlers/sse.ts +55 -0
- package/src/handlers/types.ts +104 -0
- package/src/index.ts +60 -0
- package/src/letta/client.ts +860 -0
- package/src/letta/eventTranslator.ts +667 -0
- package/src/letta/index.ts +46 -0
- package/src/letta/lettaCliShim.mjs +40 -0
- package/src/letta/sandbox.ts +167 -0
- package/src/letta/settingsFileSeed.ts +254 -0
- package/src/letta/subprocessThrottle.ts +251 -0
- package/src/letta/types.ts +65 -0
- package/src/observability/logger.ts +103 -0
- package/src/observability/metrics.ts +114 -0
- package/src/routes/admin/agents.ts +574 -0
- package/src/routes/admin/rotatingMessages.ts +351 -0
- package/src/routes/admin/sessions.ts +283 -0
- package/src/routes/admin/settings.ts +463 -0
- package/src/routes/admin/users.ts +455 -0
- package/src/routes/agents.ts +66 -0
- package/src/routes/auth.ts +513 -0
- package/src/routes/chat.ts +534 -0
- package/src/routes/featureToggles.ts +52 -0
- package/src/routes/health.ts +44 -0
- package/src/routes/metrics.ts +111 -0
- package/src/routes/sessions.ts +1878 -0
- package/src/state/cron.ts +179 -0
- package/src/state/rotationRunner.ts +326 -0
- package/src/state/rotationScheduler.ts +164 -0
- package/src/state/turnRegistry.ts +210 -0
- 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":"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"}
|
|
@@ -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"}
|