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