@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
package/src/db/seed.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-boot seed.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities, both idempotent:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Super-admin seed (§8.5).** If the `user` table is empty, insert
|
|
7
|
+
* `username = "DataClub"`, `password_hash = argon2id("admin")`,
|
|
8
|
+
* `role = "super_admin"`, `is_initial_password = true`. Hardcoded
|
|
9
|
+
* credentials by design — the operator changes them on first login (the
|
|
10
|
+
* flag enforces it).
|
|
11
|
+
*
|
|
12
|
+
* 2. **Admin-setting bootstrap (§6.7 / §6.10 / §12.1).** If the singleton
|
|
13
|
+
* `admin_setting` row doesn't exist, create it with theme defaults
|
|
14
|
+
* (`theme_switcher_enabled = true`, `theme_default = "light"` per §7.6),
|
|
15
|
+
* seed `service_token_signing_key` from env `SERVICE_TOKEN_SIGNING_KEY`
|
|
16
|
+
* if set else random, seed `metrics_scrape_token` from env
|
|
17
|
+
* `METRICS_SCRAPE_TOKEN` if set else random, encrypt and store the env
|
|
18
|
+
* `LETTA_API_KEY`, and write `letta_base_url` / `letta_ade_url`. After
|
|
19
|
+
* first boot the env vars for the signing key + metrics token are no
|
|
20
|
+
* longer consulted — DB columns are authoritative.
|
|
21
|
+
*
|
|
22
|
+
* Both run inside a single SQLite transaction so a partial failure can't
|
|
23
|
+
* leave a half-seeded DB. Idempotency is enforced by the row-existence
|
|
24
|
+
* checks at the top of each branch.
|
|
25
|
+
*
|
|
26
|
+
* @see SPECIFICATIONS.md §8.5 — initial super admin seed
|
|
27
|
+
* @see SPECIFICATIONS.md §6.7 — single signing key
|
|
28
|
+
* @see SPECIFICATIONS.md §6.10 — observability / metrics token
|
|
29
|
+
* @see SPECIFICATIONS.md §12.1 — env-var semantics
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
33
|
+
|
|
34
|
+
import { hashPassword } from "../auth/argon2.js";
|
|
35
|
+
import type { Logger } from "../observability/logger.js";
|
|
36
|
+
|
|
37
|
+
import type { DbHandle } from "./connection.js";
|
|
38
|
+
import { adminSetting, user } from "./schema.js";
|
|
39
|
+
|
|
40
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
41
|
+
/* Hardcoded super-admin credentials (§8.5) */
|
|
42
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
/** Seeded super-admin username — see §8.5. */
|
|
45
|
+
export const SUPER_ADMIN_SEED_USERNAME = "DataClub";
|
|
46
|
+
|
|
47
|
+
/** Seeded super-admin password — see §8.5. Operator changes on first login. */
|
|
48
|
+
export const SUPER_ADMIN_SEED_PASSWORD = "admin";
|
|
49
|
+
|
|
50
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
51
|
+
/* Bootstrap inputs */
|
|
52
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Inputs the seed needs from the env (or a test harness). Kept as an explicit
|
|
56
|
+
* record so callers don't pass `process.env` directly through the seed —
|
|
57
|
+
* test harnesses can synthesize the exact shape they want without monkeying
|
|
58
|
+
* with the real environment.
|
|
59
|
+
*/
|
|
60
|
+
export interface SeedInputs {
|
|
61
|
+
/** Required — the AES-256 encryption key (parsed). */
|
|
62
|
+
encryptionKey: Buffer;
|
|
63
|
+
/** Optional bootstrap-only signing key. Random when undefined (§6.7). */
|
|
64
|
+
signingKeyFromEnv?: Buffer | undefined;
|
|
65
|
+
/** Optional bootstrap-only metrics token. Random when undefined (§6.10). */
|
|
66
|
+
metricsScrapeTokenFromEnv?: string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated Kept on the interface so existing callers compile, but
|
|
69
|
+
* **no longer seeded into the DB**. The Letta API key is read at runtime
|
|
70
|
+
* from `admin_setting.letta_api_key_encrypted` with a fallback to
|
|
71
|
+
* `env.LETTA_API_KEY` when the column is empty/whitespace
|
|
72
|
+
* (`letta/client.ts:getSdk`). Letting env stay authoritative until a
|
|
73
|
+
* super-admin overrides it via the admin panel is the precedence
|
|
74
|
+
* documented in env-vars.md. v2 will drop this field.
|
|
75
|
+
*/
|
|
76
|
+
lettaApiKey?: string | undefined;
|
|
77
|
+
/** Required — Letta base URL written into the row. */
|
|
78
|
+
lettaBaseUrl?: string | undefined;
|
|
79
|
+
/** Optional super-admin override for the ADE link target. */
|
|
80
|
+
lettaAdeUrl?: string | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What the seed actually wrote, so the caller can log first-boot specifics
|
|
85
|
+
* (e.g. "metrics scrape token: <random>" exactly once on stdout).
|
|
86
|
+
*/
|
|
87
|
+
export interface SeedReport {
|
|
88
|
+
/** `true` if the super-admin row was created on this boot. */
|
|
89
|
+
seededSuperAdmin: boolean;
|
|
90
|
+
/** `true` if the `admin_setting` row was created on this boot. */
|
|
91
|
+
seededAdminSetting: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* If `seededAdminSetting && metricsScrapeTokenFromEnv was undefined`, this
|
|
94
|
+
* is the random token that was generated. The dev runner logs it once on
|
|
95
|
+
* stdout for the operator to copy. `null` otherwise.
|
|
96
|
+
*/
|
|
97
|
+
randomMetricsScrapeToken: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
101
|
+
/* Seed entrypoint */
|
|
102
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Runs both seed branches in a single transaction. Idempotent — running on
|
|
106
|
+
* an already-seeded DB is a no-op (returns a report with both booleans
|
|
107
|
+
* `false` and `randomMetricsScrapeToken` null).
|
|
108
|
+
*
|
|
109
|
+
* @param handle - the DB handle.
|
|
110
|
+
* @param inputs - bootstrap-only env values.
|
|
111
|
+
* @param logger - pino logger for the once-on-first-boot notices (§6.10).
|
|
112
|
+
* @returns what was actually seeded.
|
|
113
|
+
*/
|
|
114
|
+
export async function runFirstBootSeed(
|
|
115
|
+
handle: DbHandle,
|
|
116
|
+
inputs: SeedInputs,
|
|
117
|
+
logger: Logger,
|
|
118
|
+
): Promise<SeedReport> {
|
|
119
|
+
const report: SeedReport = {
|
|
120
|
+
seededSuperAdmin: false,
|
|
121
|
+
seededAdminSetting: false,
|
|
122
|
+
randomMetricsScrapeToken: null,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Compute the password hash + encryption outside the transaction (Argon2
|
|
126
|
+
// is async; better-sqlite3 transactions are sync).
|
|
127
|
+
const userCount = handle.raw
|
|
128
|
+
.prepare("SELECT COUNT(*) as c FROM user")
|
|
129
|
+
.get() as { c: number };
|
|
130
|
+
const adminCount = handle.raw
|
|
131
|
+
.prepare("SELECT COUNT(*) as c FROM admin_setting")
|
|
132
|
+
.get() as { c: number };
|
|
133
|
+
|
|
134
|
+
let superAdminInsert: typeof user.$inferInsert | null = null;
|
|
135
|
+
if (userCount.c === 0) {
|
|
136
|
+
const passwordHash = await hashPassword(SUPER_ADMIN_SEED_PASSWORD);
|
|
137
|
+
superAdminInsert = {
|
|
138
|
+
id: randomUUID(),
|
|
139
|
+
username: SUPER_ADMIN_SEED_USERNAME,
|
|
140
|
+
passwordHash,
|
|
141
|
+
identitySource: "local",
|
|
142
|
+
isInitialPassword: true,
|
|
143
|
+
role: "super_admin",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let adminInsert: typeof adminSetting.$inferInsert | null = null;
|
|
148
|
+
if (adminCount.c === 0) {
|
|
149
|
+
const signingKey =
|
|
150
|
+
inputs.signingKeyFromEnv ?? randomBytes(64);
|
|
151
|
+
let metricsToken: string;
|
|
152
|
+
if (inputs.metricsScrapeTokenFromEnv) {
|
|
153
|
+
metricsToken = inputs.metricsScrapeTokenFromEnv;
|
|
154
|
+
} else {
|
|
155
|
+
metricsToken = randomBytes(32).toString("hex");
|
|
156
|
+
report.randomMetricsScrapeToken = metricsToken;
|
|
157
|
+
}
|
|
158
|
+
// **Deliberately left NULL.** `LETTA_API_KEY` is consulted at runtime
|
|
159
|
+
// as a fallback when this column is empty or whitespace (see
|
|
160
|
+
// `letta/client.ts:getSdk`). Seeding it into the DB on first boot would
|
|
161
|
+
// freeze the env value into the admin_setting row, after which
|
|
162
|
+
// changing `.env` + restart would silently have no effect — that was
|
|
163
|
+
// the rotation footgun before 2026-05. Leaving the column NULL by
|
|
164
|
+
// default means env stays authoritative until a super-admin explicitly
|
|
165
|
+
// PATCHes a value via /admin/settings; clearing the value there falls
|
|
166
|
+
// back to env again. See docs/operations/env-vars.md for the full
|
|
167
|
+
// precedence model.
|
|
168
|
+
const lettaApiKeyEncrypted: Buffer | null = null;
|
|
169
|
+
adminInsert = {
|
|
170
|
+
id: 1,
|
|
171
|
+
lettaApiKeyEncrypted,
|
|
172
|
+
lettaBaseUrl: inputs.lettaBaseUrl ?? null,
|
|
173
|
+
lettaAdeUrl: inputs.lettaAdeUrl ?? deriveLettaAdeUrl(inputs.lettaBaseUrl),
|
|
174
|
+
featureToggles: {},
|
|
175
|
+
themeSwitcherEnabled: true,
|
|
176
|
+
themeDefault: "light",
|
|
177
|
+
serviceTokenSigningKey: signingKey,
|
|
178
|
+
metricsScrapeToken: metricsToken,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!superAdminInsert && !adminInsert) {
|
|
183
|
+
return report;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tx = handle.raw.transaction(() => {
|
|
187
|
+
if (superAdminInsert) {
|
|
188
|
+
handle.db.insert(user).values(superAdminInsert).run();
|
|
189
|
+
report.seededSuperAdmin = true;
|
|
190
|
+
}
|
|
191
|
+
if (adminInsert) {
|
|
192
|
+
handle.db.insert(adminSetting).values(adminInsert).run();
|
|
193
|
+
report.seededAdminSetting = true;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
tx();
|
|
197
|
+
|
|
198
|
+
if (report.seededSuperAdmin) {
|
|
199
|
+
logger.info(
|
|
200
|
+
{
|
|
201
|
+
username: SUPER_ADMIN_SEED_USERNAME,
|
|
202
|
+
is_initial_password: true,
|
|
203
|
+
},
|
|
204
|
+
"Seeded initial super-admin user (change the password immediately on first login).",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (report.randomMetricsScrapeToken) {
|
|
208
|
+
// Per §6.10: log the random metrics token once on stdout so the operator
|
|
209
|
+
// can copy it into their Prometheus / Alloy config. We embed the value
|
|
210
|
+
// in the message string rather than as a structured field because the
|
|
211
|
+
// generic pino-redaction policy (§13.3) censors token-shaped keys — and
|
|
212
|
+
// here, this is the one place where we _do_ want the operator to see it.
|
|
213
|
+
// Subsequent reads (`GET /admin/settings`, request logs) never expose it
|
|
214
|
+
// because it stays out of the structured field list. Operators rotate via
|
|
215
|
+
// `/admin/settings/rotate-metrics-token`; only the rotation response
|
|
216
|
+
// re-reveals the new value.
|
|
217
|
+
logger.info(
|
|
218
|
+
{ event: "first_boot_metrics_token_generated" },
|
|
219
|
+
`Generated random METRICS_SCRAPE_TOKEN: ${report.randomMetricsScrapeToken} — copy this into your Prometheus / Alloy scrape config.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return report;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Derives `letta_ade_url` from `letta_base_url` by stripping a trailing
|
|
228
|
+
* `/v1` per §12.1. Returns `null` if the base url is itself null.
|
|
229
|
+
*
|
|
230
|
+
* @param baseUrl - the configured Letta base URL.
|
|
231
|
+
* @returns the ADE URL or `null`.
|
|
232
|
+
*/
|
|
233
|
+
function deriveLettaAdeUrl(baseUrl: string | undefined): string | null {
|
|
234
|
+
if (!baseUrl) return null;
|
|
235
|
+
return baseUrl.replace(/\/v1\/?$/, "");
|
|
236
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical entry point: `createFetchHandler({...})`.
|
|
3
|
+
*
|
|
4
|
+
* Every per-customer wrapper mounts this through one of the adapters. The
|
|
5
|
+
* Next.js App Router adapter (`createNextRouteHandler`) is the first-class
|
|
6
|
+
* one (§6.2); the bare `createFetchHandler` is what `node:http` /
|
|
7
|
+
* `Bun.serve` / Cloudflare Workers / etc. wrap directly with a 5-line
|
|
8
|
+
* adapter on the user's side.
|
|
9
|
+
*
|
|
10
|
+
* The handler:
|
|
11
|
+
*
|
|
12
|
+
* 1. Peels `basePath` off the URL.
|
|
13
|
+
* 2. Runs the matcher against the route table.
|
|
14
|
+
* 3. Invokes the matched handler with `(req, ctx)` and a unique
|
|
15
|
+
* `request_id` propagated through the per-request child logger.
|
|
16
|
+
* 4. Catches any thrown error, logs it, returns a generic 500.
|
|
17
|
+
*
|
|
18
|
+
* No auth check happens here — that's the role-enforcement middleware's
|
|
19
|
+
* job, and it lives inside individual handlers (or, in Step 3+, behind a
|
|
20
|
+
* `protected()` wrapper). Step 2's `POST /auth/login` does not require auth.
|
|
21
|
+
*
|
|
22
|
+
* @see SPECIFICATIONS.md §6.2 — Web-Standard Fetch handlers
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
|
|
27
|
+
import type Database from "better-sqlite3";
|
|
28
|
+
|
|
29
|
+
import type { DrizzleDB } from "../db/connection.js";
|
|
30
|
+
import {
|
|
31
|
+
createDefaultLettaClient,
|
|
32
|
+
type LettaClient,
|
|
33
|
+
} from "../letta/client.js";
|
|
34
|
+
import { resolveFilesDir } from "../letta/sandbox.js";
|
|
35
|
+
import {
|
|
36
|
+
readThrottleConfigFromEnv,
|
|
37
|
+
SubprocessThrottle,
|
|
38
|
+
} from "../letta/subprocessThrottle.js";
|
|
39
|
+
import type { Logger } from "../observability/logger.js";
|
|
40
|
+
import { ensureRotationScheduler } from "../state/rotationScheduler.js";
|
|
41
|
+
|
|
42
|
+
import { buildRouteTable } from "./routeTable.js";
|
|
43
|
+
import { matchRoute } from "./router.js";
|
|
44
|
+
import { internalError, methodNotAllowed, notFound } from "./responses.js";
|
|
45
|
+
import type { AIHubServerContext, RouteEntry } from "./types.js";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Dependency bundle accepted by `createFetchHandler`. The fetch entry point
|
|
49
|
+
* stays decoupled from process bootstrap (env loading, DB opening,
|
|
50
|
+
* migrations) so tests can pass a tmp DB + a captured logger directly.
|
|
51
|
+
*/
|
|
52
|
+
export interface CreateFetchHandlerOptions {
|
|
53
|
+
/** Drizzle client. */
|
|
54
|
+
db: DrizzleDB;
|
|
55
|
+
/** Raw better-sqlite3 handle (used for `transaction()` and PRAGMAs). */
|
|
56
|
+
sqlite: Database.Database;
|
|
57
|
+
/** Pino logger with redaction wired. */
|
|
58
|
+
logger: Logger;
|
|
59
|
+
/** Parsed `AI_HUB_ENCRYPTION_KEY`. */
|
|
60
|
+
encryptionKey: Buffer;
|
|
61
|
+
/** Route prefix. Default `/api/ai-hub`. */
|
|
62
|
+
basePath?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Override the route table (tests inject a stub table to assert matcher
|
|
65
|
+
* behavior in isolation). Defaults to `buildRouteTable()`.
|
|
66
|
+
*/
|
|
67
|
+
routes?: ReadonlyArray<RouteEntry>;
|
|
68
|
+
/**
|
|
69
|
+
* Project-shaped Letta client. Tests pass a mock so they never call
|
|
70
|
+
* out to a real Letta. When omitted, the default reads
|
|
71
|
+
* `admin_setting.letta_base_url` + decrypted API key on each use and
|
|
72
|
+
* memoizes one SDK instance per `(apiKey, baseURL)` pair.
|
|
73
|
+
*
|
|
74
|
+
* @see SPECIFICATIONS.md §6.4
|
|
75
|
+
*/
|
|
76
|
+
lettaClient?: LettaClient;
|
|
77
|
+
/**
|
|
78
|
+
* Throttle gating Letta Code subprocess spawns. Tests inject a configured
|
|
79
|
+
* instance (e.g. with a fixed `MemReader` returning low MemAvailable to
|
|
80
|
+
* drive the 503 overload path). Defaults to a fresh `SubprocessThrottle`
|
|
81
|
+
* with config from env (`AI_HUB_SUBPROCESS_RAM_THRESHOLD_PCT`,
|
|
82
|
+
* `AI_HUB_SUBPROCESS_HARD_CAP`) reading `/proc/meminfo`.
|
|
83
|
+
*
|
|
84
|
+
* @see ../letta/subprocessThrottle.ts
|
|
85
|
+
*/
|
|
86
|
+
subprocessThrottle?: SubprocessThrottle;
|
|
87
|
+
/**
|
|
88
|
+
* Env-var source for runtime fallbacks (currently: the Letta API key
|
|
89
|
+
* when the DB column is empty — see `letta/client.ts:resolveApiKey`).
|
|
90
|
+
* Defaults to `process.env`. Tests pass the synthesized record they
|
|
91
|
+
* boot with so the env-fallback behavior is exercisable.
|
|
92
|
+
*/
|
|
93
|
+
envSource?: Record<string, string | undefined>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The exposed Fetch handler — `(Request) => Promise<Response>`.
|
|
98
|
+
*/
|
|
99
|
+
export type FetchHandler = (req: Request) => Promise<Response>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds a Fetch handler from the given dependencies.
|
|
103
|
+
*
|
|
104
|
+
* @param opts - DB, logger, encryption key, etc.
|
|
105
|
+
* @returns the per-instance Fetch handler.
|
|
106
|
+
*/
|
|
107
|
+
export function createFetchHandler(
|
|
108
|
+
opts: CreateFetchHandlerOptions,
|
|
109
|
+
): FetchHandler {
|
|
110
|
+
const subprocessThrottle =
|
|
111
|
+
opts.subprocessThrottle ?? new SubprocessThrottle(readThrottleConfigFromEnv());
|
|
112
|
+
const envSource =
|
|
113
|
+
opts.envSource ?? (process.env as Record<string, string | undefined>);
|
|
114
|
+
// Step-12.5 — resolve the per-session sandbox + seed base directory.
|
|
115
|
+
// Creates `seeds/` + `sandboxes/` under the resolved path if missing
|
|
116
|
+
// (idempotent), so the admin agents validator + POST /sessions don't
|
|
117
|
+
// need to mkdir on the fly.
|
|
118
|
+
const filesDir = resolveFilesDir(envSource);
|
|
119
|
+
const ctx: AIHubServerContext = {
|
|
120
|
+
db: opts.db,
|
|
121
|
+
sqlite: opts.sqlite,
|
|
122
|
+
logger: opts.logger,
|
|
123
|
+
encryptionKey: opts.encryptionKey,
|
|
124
|
+
basePath: normalizeBasePath(opts.basePath ?? "/api/ai-hub"),
|
|
125
|
+
lettaClient:
|
|
126
|
+
opts.lettaClient ??
|
|
127
|
+
createDefaultLettaClient({
|
|
128
|
+
db: opts.db,
|
|
129
|
+
encryptionKey: opts.encryptionKey,
|
|
130
|
+
subprocessThrottle,
|
|
131
|
+
envSource,
|
|
132
|
+
}),
|
|
133
|
+
subprocessThrottle,
|
|
134
|
+
envSource,
|
|
135
|
+
filesDir,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Start the rotating-message scheduler once per process. Idempotent + a
|
|
139
|
+
// no-op under tests / when `AI_HUB_DISABLE_ROTATION_SCHEDULER=1` (see
|
|
140
|
+
// `ensureRotationScheduler`), so building multiple handlers — or booting
|
|
141
|
+
// the server in vitest — never spawns competing loops.
|
|
142
|
+
ensureRotationScheduler(
|
|
143
|
+
{ db: ctx.db, lettaClient: ctx.lettaClient, logger: ctx.logger },
|
|
144
|
+
envSource,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const routes = opts.routes ?? buildRouteTable();
|
|
148
|
+
|
|
149
|
+
return async (req) => {
|
|
150
|
+
const requestId = req.headers.get("x-request-id") ?? randomUUID();
|
|
151
|
+
const childLogger = ctx.logger.child({ request_id: requestId });
|
|
152
|
+
const childCtx: AIHubServerContext = { ...ctx, logger: childLogger };
|
|
153
|
+
|
|
154
|
+
const url = new URL(req.url);
|
|
155
|
+
const innerPath = stripBasePath(url.pathname, ctx.basePath);
|
|
156
|
+
if (innerPath === null) {
|
|
157
|
+
// Path is outside the basePath — the wrapper shouldn't have routed it
|
|
158
|
+
// here, but if it did we 404 cleanly rather than bubbling an
|
|
159
|
+
// ambiguous response.
|
|
160
|
+
return notFound();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const method = req.method.toUpperCase();
|
|
164
|
+
const match = matchRoute(routes, method, innerPath);
|
|
165
|
+
if (!match) {
|
|
166
|
+
// Distinguish "path doesn't exist" from "path exists, wrong method"
|
|
167
|
+
// — the React client treats them differently. We do the second-pass
|
|
168
|
+
// check by trying the wildcard method.
|
|
169
|
+
const anyMethod = matchRoute(routes, "*", innerPath);
|
|
170
|
+
if (anyMethod) return methodNotAllowed();
|
|
171
|
+
// Also check whether the same path exists for *any* method.
|
|
172
|
+
const samePath = routes.find((r) => pathMatchesRaw(r.path, innerPath));
|
|
173
|
+
return samePath ? methodNotAllowed() : notFound();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
return await match.entry.handler(req, childCtx, match.params);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
childLogger.error(
|
|
180
|
+
{ err: err instanceof Error ? err.message : String(err), method, path: innerPath },
|
|
181
|
+
"Unhandled error in route handler.",
|
|
182
|
+
);
|
|
183
|
+
return internalError();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
189
|
+
/* Helpers */
|
|
190
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Trims trailing slashes off `basePath` and ensures it begins with one.
|
|
194
|
+
*
|
|
195
|
+
* @param raw - the user-supplied base path.
|
|
196
|
+
* @returns the normalized base path (e.g. `"/api/ai-hub"`).
|
|
197
|
+
*/
|
|
198
|
+
function normalizeBasePath(raw: string): string {
|
|
199
|
+
if (!raw) return "";
|
|
200
|
+
let out = raw;
|
|
201
|
+
if (!out.startsWith("/")) out = "/" + out;
|
|
202
|
+
while (out.length > 1 && out.endsWith("/")) out = out.slice(0, -1);
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns `pathname` with `basePath` peeled off, or `null` if `pathname` is
|
|
208
|
+
* not under the prefix. An empty `basePath` returns `pathname` unchanged.
|
|
209
|
+
*
|
|
210
|
+
* @param pathname - request URL pathname.
|
|
211
|
+
* @param basePath - the configured prefix.
|
|
212
|
+
* @returns the inner path or `null`.
|
|
213
|
+
*/
|
|
214
|
+
function stripBasePath(pathname: string, basePath: string): string | null {
|
|
215
|
+
if (basePath === "" || basePath === "/") return pathname;
|
|
216
|
+
if (pathname === basePath) return "/";
|
|
217
|
+
if (pathname.startsWith(basePath + "/")) {
|
|
218
|
+
return pathname.slice(basePath.length);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Cheap "does this template match this path at all" predicate used for the
|
|
225
|
+
* 405-vs-404 disambiguation. Doesn't return params (that already happened
|
|
226
|
+
* in the main match).
|
|
227
|
+
*
|
|
228
|
+
* @param template - the route template.
|
|
229
|
+
* @param pathname - the actual path.
|
|
230
|
+
* @returns `true` when the template's segments match the path's.
|
|
231
|
+
*/
|
|
232
|
+
function pathMatchesRaw(template: string, pathname: string): boolean {
|
|
233
|
+
const tplParts = trimSlashes(template).split("/");
|
|
234
|
+
const pathParts = trimSlashes(pathname).split("/");
|
|
235
|
+
if (tplParts.length !== pathParts.length) return false;
|
|
236
|
+
for (let i = 0; i < tplParts.length; i++) {
|
|
237
|
+
const tp = tplParts[i] as string;
|
|
238
|
+
const pp = pathParts[i] as string;
|
|
239
|
+
if (tp.startsWith(":")) continue;
|
|
240
|
+
if (tp !== pp) return false;
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Same trim logic as in `router.ts`; inlined here to keep this file self-contained. */
|
|
246
|
+
function trimSlashes(s: string): string {
|
|
247
|
+
let out = s;
|
|
248
|
+
if (out.startsWith("/")) out = out.slice(1);
|
|
249
|
+
if (out.endsWith("/")) out = out.slice(0, -1);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router adapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `createFetchHandler` so per-customer wrapper apps can mount
|
|
5
|
+
* ai-hub-server with one line:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // app/api/ai-hub/[...path]/route.ts
|
|
9
|
+
* import { createNextRouteHandler } from "@data-club/ai-hub-server";
|
|
10
|
+
*
|
|
11
|
+
* const handler = createNextRouteHandler({ ...options });
|
|
12
|
+
* export const GET = handler;
|
|
13
|
+
* export const POST = handler;
|
|
14
|
+
* export const PATCH = handler;
|
|
15
|
+
* export const DELETE = handler;
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The adapter is intentionally thin — Next.js App Router already gives us
|
|
19
|
+
* Web-Standard `Request` / `Response` objects, so this is barely more than
|
|
20
|
+
* a re-export. The reason it exists at all (rather than telling users to
|
|
21
|
+
* import `createFetchHandler` directly) is so the package owns the contract:
|
|
22
|
+
* if Next.js ever tightens its route-handler signature (e.g. adds a context
|
|
23
|
+
* arg), we adapt here without breaking every wrapper app.
|
|
24
|
+
*
|
|
25
|
+
* @see SPECIFICATIONS.md §6.2 — stack
|
|
26
|
+
* @see SPECIFICATIONS.md §14 — demo app pattern
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
createFetchHandler,
|
|
31
|
+
type CreateFetchHandlerOptions,
|
|
32
|
+
type FetchHandler,
|
|
33
|
+
} from "./fetch.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds a Next.js App Router-compatible route handler.
|
|
37
|
+
*
|
|
38
|
+
* Each HTTP-method export (`GET`, `POST`, …) in `route.ts` should reference
|
|
39
|
+
* the same instance — Next.js dispatches by method name, but our internal
|
|
40
|
+
* matcher does the work; sharing the instance avoids re-opening the DB.
|
|
41
|
+
*
|
|
42
|
+
* @param opts - same shape as `createFetchHandler`.
|
|
43
|
+
* @returns a function suitable for `export const GET = ...`.
|
|
44
|
+
*/
|
|
45
|
+
export function createNextRouteHandler(
|
|
46
|
+
opts: CreateFetchHandlerOptions,
|
|
47
|
+
): FetchHandler {
|
|
48
|
+
return createFetchHandler(opts);
|
|
49
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small response helpers used by every handler.
|
|
3
|
+
*
|
|
4
|
+
* Centralizing them means error-shape drift can't happen — `unauthorized()`
|
|
5
|
+
* always emits the same `{ error: { code, message } }` envelope, so the
|
|
6
|
+
* React client's `AIHubError` reducer stays consistent.
|
|
7
|
+
*
|
|
8
|
+
* @see SPECIFICATIONS.md §13.4 — input safety / Zod validation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Default JSON content-type with utf-8 charset. */
|
|
12
|
+
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Writes `body` as JSON with the given `status`. Adds `application/json`
|
|
16
|
+
* content-type automatically.
|
|
17
|
+
*
|
|
18
|
+
* @param status - HTTP status code.
|
|
19
|
+
* @param body - JSON-serializable body.
|
|
20
|
+
* @param headers - extra headers to merge in.
|
|
21
|
+
* @returns the Response.
|
|
22
|
+
*/
|
|
23
|
+
export function jsonResponse(
|
|
24
|
+
status: number,
|
|
25
|
+
body: unknown,
|
|
26
|
+
headers?: Record<string, string>,
|
|
27
|
+
): Response {
|
|
28
|
+
return new Response(JSON.stringify(body), {
|
|
29
|
+
status,
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": JSON_CONTENT_TYPE,
|
|
32
|
+
...(headers ?? {}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Standard error envelope. Pairs every error with a stable machine code so
|
|
39
|
+
* the React client's `AIHubError` discriminator stays useful.
|
|
40
|
+
*
|
|
41
|
+
* @param status - HTTP status code.
|
|
42
|
+
* @param code - machine-readable code matching the §7.5 `AIHubError` codes.
|
|
43
|
+
* @param message - human-readable description; safe to surface to end-users.
|
|
44
|
+
* @returns the Response.
|
|
45
|
+
*/
|
|
46
|
+
export function errorResponse(
|
|
47
|
+
status: number,
|
|
48
|
+
code: string,
|
|
49
|
+
message: string,
|
|
50
|
+
): Response {
|
|
51
|
+
return jsonResponse(status, { error: { code, message } });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 401 with a generic invalid-credentials envelope (§13.1). */
|
|
55
|
+
export function unauthorized(message = "Invalid credentials"): Response {
|
|
56
|
+
return errorResponse(401, "unauthorized", message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 403 used for role-mismatch + shield + min-count violations (§8.6). */
|
|
60
|
+
export function forbidden(
|
|
61
|
+
message = "Forbidden",
|
|
62
|
+
code = "forbidden",
|
|
63
|
+
): Response {
|
|
64
|
+
return errorResponse(403, code, message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 400 with a `validation` envelope. */
|
|
68
|
+
export function validationError(message: string, code = "validation"): Response {
|
|
69
|
+
return errorResponse(400, code, message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 409 used for state-conflict errors (e.g. `theme_switcher_disabled`). */
|
|
73
|
+
export function conflictError(code: string, message: string): Response {
|
|
74
|
+
return errorResponse(409, code, message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 501 used for Step-2 stubs that Step 3+ replaces with real handlers. */
|
|
78
|
+
export function notImplemented(message = "Not implemented"): Response {
|
|
79
|
+
return errorResponse(501, "not_implemented", message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 404 with no enumeration leakage — used for unknown paths (§8.6). */
|
|
83
|
+
export function notFound(message = "Not found"): Response {
|
|
84
|
+
return errorResponse(404, "not_found", message);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 405 used by the matcher when the path matches but the method doesn't. */
|
|
88
|
+
export function methodNotAllowed(message = "Method not allowed"): Response {
|
|
89
|
+
return errorResponse(405, "method_not_allowed", message);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 500 for internal-error catch-alls. The body never leaks stack traces. */
|
|
93
|
+
export function internalError(message = "Internal server error"): Response {
|
|
94
|
+
return errorResponse(500, "internal", message);
|
|
95
|
+
}
|