@data-club/ai-hub-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/auth/argon2.d.ts +72 -0
  2. package/dist/auth/argon2.d.ts.map +1 -0
  3. package/dist/auth/argon2.js +145 -0
  4. package/dist/auth/argon2.js.map +1 -0
  5. package/dist/auth/encryption.d.ts +52 -0
  6. package/dist/auth/encryption.d.ts.map +1 -0
  7. package/dist/auth/encryption.js +98 -0
  8. package/dist/auth/encryption.js.map +1 -0
  9. package/dist/auth/jwt.d.ts +133 -0
  10. package/dist/auth/jwt.d.ts.map +1 -0
  11. package/dist/auth/jwt.js +147 -0
  12. package/dist/auth/jwt.js.map +1 -0
  13. package/dist/auth/middleware.d.ts +101 -0
  14. package/dist/auth/middleware.d.ts.map +1 -0
  15. package/dist/auth/middleware.js +161 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/passwordPolicy.d.ts +72 -0
  18. package/dist/auth/passwordPolicy.d.ts.map +1 -0
  19. package/dist/auth/passwordPolicy.js +88 -0
  20. package/dist/auth/passwordPolicy.js.map +1 -0
  21. package/dist/auth/roleRules.d.ts +200 -0
  22. package/dist/auth/roleRules.d.ts.map +1 -0
  23. package/dist/auth/roleRules.js +365 -0
  24. package/dist/auth/roleRules.js.map +1 -0
  25. package/dist/auth/userView.d.ts +55 -0
  26. package/dist/auth/userView.d.ts.map +1 -0
  27. package/dist/auth/userView.js +77 -0
  28. package/dist/auth/userView.js.map +1 -0
  29. package/dist/bootstrap.d.ts +104 -0
  30. package/dist/bootstrap.d.ts.map +1 -0
  31. package/dist/bootstrap.js +160 -0
  32. package/dist/bootstrap.js.map +1 -0
  33. package/dist/db/connection.d.ts +39 -0
  34. package/dist/db/connection.d.ts.map +1 -0
  35. package/dist/db/connection.js +62 -0
  36. package/dist/db/connection.js.map +1 -0
  37. package/dist/db/migrate.d.ts +31 -0
  38. package/dist/db/migrate.d.ts.map +1 -0
  39. package/dist/db/migrate.js +197 -0
  40. package/dist/db/migrate.js.map +1 -0
  41. package/dist/db/schema.d.ts +1902 -0
  42. package/dist/db/schema.d.ts.map +1 -0
  43. package/dist/db/schema.js +662 -0
  44. package/dist/db/schema.js.map +1 -0
  45. package/dist/db/seed.d.ts +92 -0
  46. package/dist/db/seed.d.ts.map +1 -0
  47. package/dist/db/seed.js +160 -0
  48. package/dist/db/seed.js.map +1 -0
  49. package/dist/handlers/fetch.d.ts +89 -0
  50. package/dist/handlers/fetch.d.ts.map +1 -0
  51. package/dist/handlers/fetch.js +173 -0
  52. package/dist/handlers/fetch.js.map +1 -0
  53. package/dist/handlers/nextjs.d.ts +40 -0
  54. package/dist/handlers/nextjs.d.ts.map +1 -0
  55. package/dist/handlers/nextjs.js +42 -0
  56. package/dist/handlers/nextjs.js.map +1 -0
  57. package/dist/handlers/responses.d.ts +46 -0
  58. package/dist/handlers/responses.d.ts.map +1 -0
  59. package/dist/handlers/responses.js +74 -0
  60. package/dist/handlers/responses.js.map +1 -0
  61. package/dist/handlers/routeTable.d.ts +21 -0
  62. package/dist/handlers/routeTable.d.ts.map +1 -0
  63. package/dist/handlers/routeTable.js +133 -0
  64. package/dist/handlers/routeTable.js.map +1 -0
  65. package/dist/handlers/router.d.ts +37 -0
  66. package/dist/handlers/router.d.ts.map +1 -0
  67. package/dist/handlers/router.js +82 -0
  68. package/dist/handlers/router.js.map +1 -0
  69. package/dist/handlers/sse.d.ts +38 -0
  70. package/dist/handlers/sse.d.ts.map +1 -0
  71. package/dist/handlers/sse.js +52 -0
  72. package/dist/handlers/sse.js.map +1 -0
  73. package/dist/handlers/types.d.ts +96 -0
  74. package/dist/handlers/types.d.ts.map +1 -0
  75. package/dist/handlers/types.js +14 -0
  76. package/dist/handlers/types.js.map +1 -0
  77. package/dist/index.d.ts +25 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +24 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/letta/client.d.ts +355 -0
  82. package/dist/letta/client.d.ts.map +1 -0
  83. package/dist/letta/client.js +529 -0
  84. package/dist/letta/client.js.map +1 -0
  85. package/dist/letta/eventTranslator.d.ts +166 -0
  86. package/dist/letta/eventTranslator.d.ts.map +1 -0
  87. package/dist/letta/eventTranslator.js +532 -0
  88. package/dist/letta/eventTranslator.js.map +1 -0
  89. package/dist/letta/index.d.ts +13 -0
  90. package/dist/letta/index.d.ts.map +1 -0
  91. package/dist/letta/index.js +12 -0
  92. package/dist/letta/index.js.map +1 -0
  93. package/dist/letta/modelSettings.d.ts +176 -0
  94. package/dist/letta/modelSettings.d.ts.map +1 -0
  95. package/dist/letta/modelSettings.js +314 -0
  96. package/dist/letta/modelSettings.js.map +1 -0
  97. package/dist/letta/promptWrapping.d.ts +141 -0
  98. package/dist/letta/promptWrapping.d.ts.map +1 -0
  99. package/dist/letta/promptWrapping.js +213 -0
  100. package/dist/letta/promptWrapping.js.map +1 -0
  101. package/dist/letta/sandbox.d.ts +100 -0
  102. package/dist/letta/sandbox.d.ts.map +1 -0
  103. package/dist/letta/sandbox.js +151 -0
  104. package/dist/letta/sandbox.js.map +1 -0
  105. package/dist/letta/settingsFileSeed.d.ts +146 -0
  106. package/dist/letta/settingsFileSeed.d.ts.map +1 -0
  107. package/dist/letta/settingsFileSeed.js +177 -0
  108. package/dist/letta/settingsFileSeed.js.map +1 -0
  109. package/dist/letta/subprocessThrottle.d.ts +151 -0
  110. package/dist/letta/subprocessThrottle.d.ts.map +1 -0
  111. package/dist/letta/subprocessThrottle.js +195 -0
  112. package/dist/letta/subprocessThrottle.js.map +1 -0
  113. package/dist/letta/tpmRecovery.d.ts +202 -0
  114. package/dist/letta/tpmRecovery.d.ts.map +1 -0
  115. package/dist/letta/tpmRecovery.js +314 -0
  116. package/dist/letta/tpmRecovery.js.map +1 -0
  117. package/dist/letta/types.d.ts +54 -0
  118. package/dist/letta/types.d.ts.map +1 -0
  119. package/dist/letta/types.js +16 -0
  120. package/dist/letta/types.js.map +1 -0
  121. package/dist/observability/logger.d.ts +48 -0
  122. package/dist/observability/logger.d.ts.map +1 -0
  123. package/dist/observability/logger.js +81 -0
  124. package/dist/observability/logger.js.map +1 -0
  125. package/dist/observability/metrics.d.ts +53 -0
  126. package/dist/observability/metrics.d.ts.map +1 -0
  127. package/dist/observability/metrics.js +92 -0
  128. package/dist/observability/metrics.js.map +1 -0
  129. package/dist/routes/admin/agents.d.ts +63 -0
  130. package/dist/routes/admin/agents.d.ts.map +1 -0
  131. package/dist/routes/admin/agents.js +461 -0
  132. package/dist/routes/admin/agents.js.map +1 -0
  133. package/dist/routes/admin/rotatingMessages.d.ts +51 -0
  134. package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
  135. package/dist/routes/admin/rotatingMessages.js +292 -0
  136. package/dist/routes/admin/rotatingMessages.js.map +1 -0
  137. package/dist/routes/admin/sessions.d.ts +61 -0
  138. package/dist/routes/admin/sessions.d.ts.map +1 -0
  139. package/dist/routes/admin/sessions.js +232 -0
  140. package/dist/routes/admin/sessions.js.map +1 -0
  141. package/dist/routes/admin/settings.d.ts +110 -0
  142. package/dist/routes/admin/settings.d.ts.map +1 -0
  143. package/dist/routes/admin/settings.js +401 -0
  144. package/dist/routes/admin/settings.js.map +1 -0
  145. package/dist/routes/admin/users.d.ts +115 -0
  146. package/dist/routes/admin/users.d.ts.map +1 -0
  147. package/dist/routes/admin/users.js +374 -0
  148. package/dist/routes/admin/users.js.map +1 -0
  149. package/dist/routes/agents.d.ts +30 -0
  150. package/dist/routes/agents.d.ts.map +1 -0
  151. package/dist/routes/agents.js +55 -0
  152. package/dist/routes/agents.js.map +1 -0
  153. package/dist/routes/auth.d.ts +121 -0
  154. package/dist/routes/auth.d.ts.map +1 -0
  155. package/dist/routes/auth.js +422 -0
  156. package/dist/routes/auth.js.map +1 -0
  157. package/dist/routes/chat.d.ts +97 -0
  158. package/dist/routes/chat.d.ts.map +1 -0
  159. package/dist/routes/chat.js +484 -0
  160. package/dist/routes/chat.js.map +1 -0
  161. package/dist/routes/featureToggles.d.ts +34 -0
  162. package/dist/routes/featureToggles.d.ts.map +1 -0
  163. package/dist/routes/featureToggles.js +47 -0
  164. package/dist/routes/featureToggles.js.map +1 -0
  165. package/dist/routes/health.d.ts +24 -0
  166. package/dist/routes/health.d.ts.map +1 -0
  167. package/dist/routes/health.js +37 -0
  168. package/dist/routes/health.js.map +1 -0
  169. package/dist/routes/metrics.d.ts +35 -0
  170. package/dist/routes/metrics.d.ts.map +1 -0
  171. package/dist/routes/metrics.js +106 -0
  172. package/dist/routes/metrics.js.map +1 -0
  173. package/dist/routes/sessions.d.ts +161 -0
  174. package/dist/routes/sessions.d.ts.map +1 -0
  175. package/dist/routes/sessions.js +1501 -0
  176. package/dist/routes/sessions.js.map +1 -0
  177. package/dist/routes/stubs.d.ts +25 -0
  178. package/dist/routes/stubs.d.ts.map +1 -0
  179. package/dist/routes/stubs.js +25 -0
  180. package/dist/routes/stubs.js.map +1 -0
  181. package/dist/state/cron.d.ts +58 -0
  182. package/dist/state/cron.d.ts.map +1 -0
  183. package/dist/state/cron.js +160 -0
  184. package/dist/state/cron.js.map +1 -0
  185. package/dist/state/rotationRunner.d.ts +96 -0
  186. package/dist/state/rotationRunner.d.ts.map +1 -0
  187. package/dist/state/rotationRunner.js +246 -0
  188. package/dist/state/rotationRunner.js.map +1 -0
  189. package/dist/state/rotationScheduler.d.ts +76 -0
  190. package/dist/state/rotationScheduler.d.ts.map +1 -0
  191. package/dist/state/rotationScheduler.js +124 -0
  192. package/dist/state/rotationScheduler.js.map +1 -0
  193. package/dist/state/turnRegistry.d.ts +95 -0
  194. package/dist/state/turnRegistry.d.ts.map +1 -0
  195. package/dist/state/turnRegistry.js +149 -0
  196. package/dist/state/turnRegistry.js.map +1 -0
  197. package/dist/state/turnRunner.d.ts +134 -0
  198. package/dist/state/turnRunner.d.ts.map +1 -0
  199. package/dist/state/turnRunner.js +476 -0
  200. package/dist/state/turnRunner.js.map +1 -0
  201. package/package.json +47 -0
  202. package/src/auth/argon2.ts +152 -0
  203. package/src/auth/encryption.ts +110 -0
  204. package/src/auth/jwt.ts +217 -0
  205. package/src/auth/middleware.ts +250 -0
  206. package/src/auth/passwordPolicy.ts +99 -0
  207. package/src/auth/roleRules.ts +497 -0
  208. package/src/auth/userView.ts +88 -0
  209. package/src/bootstrap.ts +254 -0
  210. package/src/db/connection.ts +75 -0
  211. package/src/db/migrate.ts +212 -0
  212. package/src/db/schema.ts +750 -0
  213. package/src/db/seed.ts +236 -0
  214. package/src/handlers/fetch.ts +251 -0
  215. package/src/handlers/nextjs.ts +49 -0
  216. package/src/handlers/responses.ts +95 -0
  217. package/src/handlers/routeTable.ts +188 -0
  218. package/src/handlers/router.ts +96 -0
  219. package/src/handlers/sse.ts +55 -0
  220. package/src/handlers/types.ts +104 -0
  221. package/src/index.ts +60 -0
  222. package/src/letta/client.ts +860 -0
  223. package/src/letta/eventTranslator.ts +667 -0
  224. package/src/letta/index.ts +46 -0
  225. package/src/letta/lettaCliShim.mjs +40 -0
  226. package/src/letta/sandbox.ts +167 -0
  227. package/src/letta/settingsFileSeed.ts +254 -0
  228. package/src/letta/subprocessThrottle.ts +251 -0
  229. package/src/letta/types.ts +65 -0
  230. package/src/observability/logger.ts +103 -0
  231. package/src/observability/metrics.ts +114 -0
  232. package/src/routes/admin/agents.ts +574 -0
  233. package/src/routes/admin/rotatingMessages.ts +351 -0
  234. package/src/routes/admin/sessions.ts +283 -0
  235. package/src/routes/admin/settings.ts +463 -0
  236. package/src/routes/admin/users.ts +455 -0
  237. package/src/routes/agents.ts +66 -0
  238. package/src/routes/auth.ts +513 -0
  239. package/src/routes/chat.ts +534 -0
  240. package/src/routes/featureToggles.ts +52 -0
  241. package/src/routes/health.ts +44 -0
  242. package/src/routes/metrics.ts +111 -0
  243. package/src/routes/sessions.ts +1878 -0
  244. package/src/state/cron.ts +179 -0
  245. package/src/state/rotationRunner.ts +326 -0
  246. package/src/state/rotationScheduler.ts +164 -0
  247. package/src/state/turnRegistry.ts +210 -0
  248. package/src/state/turnRunner.ts +588 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Boot orchestration: env → open DB → migrate → seed → ready.
3
+ *
4
+ * The pieces that compose this (env loader, `openDb`, `runMigrations`,
5
+ * `runFirstBootSeed`, `createLogger`) are independently importable so tests
6
+ * and embedders can wire bespoke combinations. `bootServer` is the
7
+ * convenience entry point the `dev/server.ts` runner and the per-customer
8
+ * wrapper's Next.js mount call.
9
+ *
10
+ * @see SPECIFICATIONS.md §12.1 — env vars
11
+ * @see SPECIFICATIONS.md §6.7 — signing key bootstrap
12
+ * @see SPECIFICATIONS.md §6.10 — metrics token bootstrap
13
+ */
14
+
15
+ import { join } from "node:path";
16
+
17
+ import type Database from "better-sqlite3";
18
+
19
+ import { parseEncryptionKey } from "./auth/encryption.js";
20
+ import { openDb, type DbHandle, type DrizzleDB } from "./db/connection.js";
21
+ import { runMigrations } from "./db/migrate.js";
22
+ import { runFirstBootSeed, type SeedReport } from "./db/seed.js";
23
+ import { createFetchHandler, type FetchHandler } from "./handlers/fetch.js";
24
+ import type { LettaClient } from "./letta/client.js";
25
+ import { createLogger, type Logger } from "./observability/logger.js";
26
+
27
+ /* ────────────────────────────────────────────────────────────────────────── */
28
+ /* Config */
29
+ /* ────────────────────────────────────────────────────────────────────────── */
30
+
31
+ /** Subset of `process.env` we care about — narrows for tests. */
32
+ export type EnvLike = Record<string, string | undefined>;
33
+
34
+ /**
35
+ * Parsed env config used by `bootServer`. Everything is optional except
36
+ * `encryptionKey`, which `parseEnvConfig` will raise on if missing.
37
+ */
38
+ export interface EnvConfig {
39
+ /** Filesystem dir for SQLite + state. Default `./data`. */
40
+ dataDir: string;
41
+ /** SQLite connection string (file path or `:memory:`). */
42
+ databaseUrl: string;
43
+ /** Required AES-256 key parsed from `AI_HUB_ENCRYPTION_KEY`. */
44
+ encryptionKey: Buffer;
45
+ /** Bootstrap-only signing key (env value, decoded). */
46
+ signingKeyFromEnv: Buffer | undefined;
47
+ /** Bootstrap-only metrics scrape token (env value). */
48
+ metricsScrapeTokenFromEnv: string | undefined;
49
+ /** Letta API key from env. */
50
+ lettaApiKey: string | undefined;
51
+ /** Letta base URL from env. */
52
+ lettaBaseUrl: string | undefined;
53
+ /** Letta ADE URL override from env. */
54
+ lettaAdeUrl: string | undefined;
55
+ /** Pino log level. */
56
+ logLevel: string;
57
+ /** CORS origins (comma-separated). */
58
+ corsAllowedOrigins: string;
59
+ }
60
+
61
+ /**
62
+ * Parses `process.env` (or a test-supplied stand-in) into an `EnvConfig`.
63
+ * Throws on any required-but-missing variable so the operator gets a clear
64
+ * error on first boot instead of a confusing runtime failure.
65
+ *
66
+ * @param env - the env-var record to read from.
67
+ * @returns the parsed config.
68
+ */
69
+ export function parseEnvConfig(env: EnvLike): EnvConfig {
70
+ const encRaw = env.AI_HUB_ENCRYPTION_KEY;
71
+ if (!encRaw) {
72
+ throw new Error(
73
+ "AI_HUB_ENCRYPTION_KEY is required (see docs/operations/env-vars.md).",
74
+ );
75
+ }
76
+ const encryptionKey = parseEncryptionKey(encRaw);
77
+
78
+ const dataDir = env.AI_HUB_DATA_DIR ?? "./data";
79
+ const databaseUrl = env.DATABASE_URL ?? `file:${join(dataDir, "ai_hub.db")}`;
80
+
81
+ return {
82
+ dataDir,
83
+ databaseUrl,
84
+ encryptionKey,
85
+ signingKeyFromEnv: parseSigningKey(env.SERVICE_TOKEN_SIGNING_KEY),
86
+ metricsScrapeTokenFromEnv: env.METRICS_SCRAPE_TOKEN || undefined,
87
+ lettaApiKey: env.LETTA_API_KEY || undefined,
88
+ // Letta Code's CLI reads `LETTA_BASE_URL` from process.env and
89
+ // appends `/v1/...` to it. Operators who set the env value
90
+ // matching the Letta Client SDK's expectation (`http://host/v1`)
91
+ // would otherwise get double-v1 URLs in the spawned CLI subprocess.
92
+ // Strip the trailing /v1 once at boot so both ai-hub-server's own
93
+ // direct API calls (already strip defensively in `letta/client.ts`)
94
+ // and Letta Code subprocess spawns see the bare host:port form.
95
+ // Mutating process.env here propagates to every later subprocess
96
+ // spawn — Letta Code SDK forwards `{...process.env}` verbatim.
97
+ lettaBaseUrl: normalizeLettaBaseUrl(env.LETTA_BASE_URL),
98
+ lettaAdeUrl: env.LETTA_ADE_URL || undefined,
99
+ logLevel: env.LOG_LEVEL ?? "info",
100
+ corsAllowedOrigins: env.CORS_ALLOWED_ORIGINS ?? "",
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Decodes the bootstrap `SERVICE_TOKEN_SIGNING_KEY` env value.
106
+ *
107
+ * Accepts hex (any length, even number of chars) or base64 / base64url. If
108
+ * the input is neither, treats it as raw utf-8 bytes (operators who paste
109
+ * a passphrase get a deterministic key from it). Returns `undefined` when
110
+ * the env is unset.
111
+ *
112
+ * @param raw - the raw env value, if present.
113
+ * @returns the decoded key, or undefined.
114
+ */
115
+ /**
116
+ * Strips a trailing `/v1` from `LETTA_BASE_URL` and writes the cleaned
117
+ * value back to `process.env` so subprocess spawns inherit the normalized
118
+ * form. Idempotent — bare-host inputs pass through unchanged. Returns
119
+ * the normalized value (or `undefined` when the env var is unset).
120
+ *
121
+ * @param raw - the raw `process.env.LETTA_BASE_URL`, possibly undefined.
122
+ * @returns the normalized base URL, or undefined when unset.
123
+ */
124
+ function normalizeLettaBaseUrl(raw: string | undefined): string | undefined {
125
+ if (!raw) return undefined;
126
+ const stripped = raw.replace(/\/v1\/?$/i, "");
127
+ if (stripped !== raw) {
128
+ process.env.LETTA_BASE_URL = stripped;
129
+ }
130
+ return stripped;
131
+ }
132
+
133
+ function parseSigningKey(raw: string | undefined): Buffer | undefined {
134
+ if (!raw) return undefined;
135
+ const trimmed = raw.trim();
136
+ if (/^[0-9a-fA-F]+$/.test(trimmed) && trimmed.length % 2 === 0) {
137
+ return Buffer.from(trimmed, "hex");
138
+ }
139
+ // Try base64 / base64url.
140
+ const normalized = trimmed.replace(/-/g, "+").replace(/_/g, "/");
141
+ const padded = normalized + "===".slice((normalized.length + 3) % 4);
142
+ try {
143
+ const buf = Buffer.from(padded, "base64");
144
+ // Reject obviously broken decodes (base64 length != original).
145
+ if (buf.length > 0 && buf.toString("base64").replace(/=+$/, "") === normalized.replace(/=+$/, "")) {
146
+ return buf;
147
+ }
148
+ } catch {
149
+ /* fall through */
150
+ }
151
+ // Fallback: treat as raw utf-8 bytes.
152
+ return Buffer.from(trimmed, "utf8");
153
+ }
154
+
155
+ /* ────────────────────────────────────────────────────────────────────────── */
156
+ /* bootServer */
157
+ /* ────────────────────────────────────────────────────────────────────────── */
158
+
159
+ /** Returned by `bootServer` — everything the dev/Next.js mount needs. */
160
+ export interface BootedServer {
161
+ /** The Fetch handler — `(Request) => Promise<Response>`. */
162
+ fetchHandler: FetchHandler;
163
+ /** Drizzle client (in case the embedder wants to read directly). */
164
+ db: DrizzleDB;
165
+ /** Raw better-sqlite3 handle. */
166
+ sqlite: Database.Database;
167
+ /** Logger configured per env. */
168
+ logger: Logger;
169
+ /** Parsed env config. */
170
+ config: EnvConfig;
171
+ /** What the seed actually wrote on this boot. */
172
+ seedReport: SeedReport;
173
+ /** Closes the DB handle. Call this on graceful shutdown. */
174
+ close: () => void;
175
+ }
176
+
177
+ /** Optional override: pass a pre-built logger or destination to capture. */
178
+ export interface BootServerOptions {
179
+ /**
180
+ * Custom env source. Defaults to `process.env`. Tests pass a synthesized
181
+ * record so they don't have to mutate the actual environment.
182
+ */
183
+ env?: EnvLike;
184
+ /** Override the logger (e.g. test capture). */
185
+ logger?: Logger;
186
+ /** Override the connection — tests use `:memory:` here. */
187
+ databaseUrl?: string;
188
+ /**
189
+ * Override the Letta client (§6.4). Tests pass a mock so the server
190
+ * never reaches a real Letta. Production omits this — the default reads
191
+ * `admin_setting.letta_base_url` + decrypted API key on each use.
192
+ */
193
+ lettaClient?: LettaClient;
194
+ }
195
+
196
+ /**
197
+ * One-shot boot: parses env, opens the DB, runs migrations, runs the
198
+ * first-boot seed (idempotent), and returns the Fetch handler.
199
+ *
200
+ * Called by `dev/server.ts` and by per-customer wrappers' Next.js mount
201
+ * (typically wrapped in a module-level singleton so it runs once per
202
+ * Next.js process).
203
+ *
204
+ * @param opts - optional overrides for testing.
205
+ * @returns the booted server bundle.
206
+ */
207
+ export async function bootServer(
208
+ opts: BootServerOptions = {},
209
+ ): Promise<BootedServer> {
210
+ const env = opts.env ?? (process.env as EnvLike);
211
+ const config = parseEnvConfig(env);
212
+ const logger = opts.logger ?? createLogger({ level: config.logLevel });
213
+
214
+ const databaseUrl = opts.databaseUrl ?? config.databaseUrl;
215
+ const handle: DbHandle = openDb(databaseUrl);
216
+
217
+ runMigrations(handle);
218
+ const seedReport = await runFirstBootSeed(
219
+ handle,
220
+ {
221
+ encryptionKey: config.encryptionKey,
222
+ signingKeyFromEnv: config.signingKeyFromEnv,
223
+ metricsScrapeTokenFromEnv: config.metricsScrapeTokenFromEnv,
224
+ lettaApiKey: config.lettaApiKey,
225
+ lettaBaseUrl: config.lettaBaseUrl,
226
+ lettaAdeUrl: config.lettaAdeUrl,
227
+ },
228
+ logger,
229
+ );
230
+
231
+ const fetchHandler = createFetchHandler({
232
+ db: handle.db,
233
+ sqlite: handle.raw,
234
+ logger,
235
+ encryptionKey: config.encryptionKey,
236
+ // Pass the booted env through as the runtime fallback source. Tests
237
+ // synthesize an env record; production uses process.env (the default
238
+ // inside createFetchHandler). Threading it explicitly here means
239
+ // bootTestServer's `env:` parameter actually reaches resolveApiKey
240
+ // in letta/client.ts.
241
+ envSource: env,
242
+ ...(opts.lettaClient !== undefined ? { lettaClient: opts.lettaClient } : {}),
243
+ });
244
+
245
+ return {
246
+ fetchHandler,
247
+ db: handle.db,
248
+ sqlite: handle.raw,
249
+ logger,
250
+ config,
251
+ seedReport,
252
+ close: () => handle.raw.close(),
253
+ };
254
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * better-sqlite3 connection + Drizzle wiring.
3
+ *
4
+ * On open we set the two PRAGMAs the spec requires (§6.8): `journal_mode=WAL`
5
+ * for concurrency under multiple readers + one writer, and `foreign_keys=ON`
6
+ * because SQLite ships them off by default.
7
+ *
8
+ * The connection is intentionally a per-server-instance singleton — Drizzle's
9
+ * client wraps better-sqlite3, and better-sqlite3 itself is fully synchronous
10
+ * (no connection pool needed; one handle is enough). Tests open their own
11
+ * handle against a `:memory:` or tmp-file path.
12
+ *
13
+ * @see SPECIFICATIONS.md §6.8 — persistence
14
+ * @see SPECIFICATIONS.md §11 — data model
15
+ */
16
+
17
+ import { mkdirSync } from "node:fs";
18
+ import { dirname, isAbsolute, resolve as pathResolve } from "node:path";
19
+
20
+ import Database from "better-sqlite3";
21
+ import { drizzle } from "drizzle-orm/better-sqlite3";
22
+
23
+ import * as schema from "./schema.js";
24
+
25
+ /** The Drizzle client type our handlers consume. */
26
+ export type DrizzleDB = ReturnType<typeof drizzle<typeof schema>>;
27
+
28
+ /** Bundles the raw better-sqlite3 handle alongside the Drizzle wrapper. */
29
+ export interface DbHandle {
30
+ /** Drizzle client — typed against the schema. */
31
+ db: DrizzleDB;
32
+ /** Raw better-sqlite3 handle (used for PRAGMAs and `.close()`). */
33
+ raw: Database.Database;
34
+ }
35
+
36
+ /**
37
+ * Opens the SQLite database at `path` and applies the spec's PRAGMAs.
38
+ *
39
+ * `:memory:` is recognized verbatim and skipped from path-resolution +
40
+ * directory-creation logic (used by tests).
41
+ *
42
+ * @param path - filesystem path, a `file:`-prefixed URI, or `:memory:`.
43
+ * @returns Drizzle wrapper + raw better-sqlite3 handle.
44
+ */
45
+ export function openDb(path: string): DbHandle {
46
+ const resolvedPath = resolveDbPath(path);
47
+ if (resolvedPath !== ":memory:") {
48
+ mkdirSync(dirname(resolvedPath), { recursive: true });
49
+ }
50
+ const raw = new Database(resolvedPath);
51
+ raw.pragma("journal_mode = WAL");
52
+ raw.pragma("foreign_keys = ON");
53
+ const db = drizzle(raw, { schema });
54
+ return { db, raw };
55
+ }
56
+
57
+ /**
58
+ * Resolves a path that may be `:memory:`, a `file:` URI from `DATABASE_URL`,
59
+ * or an ordinary filesystem path (relative or absolute).
60
+ *
61
+ * @param path - raw path string from env or caller.
62
+ * @returns absolute filesystem path or `:memory:`.
63
+ */
64
+ function resolveDbPath(path: string): string {
65
+ if (path === ":memory:") return path;
66
+ let stripped = path;
67
+ if (stripped.startsWith("file:")) {
68
+ stripped = stripped.slice("file:".length);
69
+ if (stripped.startsWith("//")) stripped = stripped.slice(2);
70
+ }
71
+ if (stripped === ":memory:") return stripped;
72
+ return isAbsolute(stripped) ? stripped : pathResolve(stripped);
73
+ }
74
+
75
+ export { schema };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Boot-time migrations runner.
3
+ *
4
+ * Currently a single `0001_init` migration that builds the entire §11 schema
5
+ * idempotently via `CREATE TABLE IF NOT EXISTS`. Tracked in
6
+ * `_ai_hub_migrations` so future migrations can append additional steps and
7
+ * skip already-applied ones.
8
+ *
9
+ * **Why hand-rolled SQL instead of `drizzle-kit migrate`**: shipping
10
+ * `drizzle-kit` and a generated `drizzle/` SQL folder as runtime artifacts
11
+ * adds a build step that the per-customer wrapper doesn't want to think about.
12
+ * The schema is small and stable; CREATE-TABLE-IF-NOT-EXISTS is sufficient,
13
+ * and Drizzle's typed query builder already protects us from drift between
14
+ * the runtime SQL and `schema.ts`. (If migrations ever get complex enough
15
+ * that this breaks down, we move to drizzle-kit then — not pre-emptively.)
16
+ *
17
+ * @see SPECIFICATIONS.md §11 — data model the schema mirrors 1:1
18
+ */
19
+
20
+ import type { DbHandle } from "./connection.js";
21
+
22
+ /**
23
+ * Runs every not-yet-applied migration on `handle.raw`.
24
+ *
25
+ * Idempotent: running this on a fresh SQLite produces the v1 schema; running
26
+ * it on an already-migrated DB is a no-op. The migrations are wrapped in a
27
+ * single transaction so a partial failure can't leave the schema in a
28
+ * half-applied state.
29
+ *
30
+ * @param handle - the db handle returned from `openDb`.
31
+ */
32
+ export function runMigrations(handle: DbHandle): void {
33
+ const { raw } = handle;
34
+
35
+ raw.exec(`
36
+ CREATE TABLE IF NOT EXISTS _ai_hub_migrations (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ name TEXT NOT NULL UNIQUE,
39
+ applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
40
+ )
41
+ `);
42
+
43
+ for (const m of MIGRATIONS) {
44
+ const already = raw
45
+ .prepare("SELECT 1 FROM _ai_hub_migrations WHERE name = ?")
46
+ .get(m.name) as { 1: number } | undefined;
47
+ if (already) continue;
48
+ const apply = raw.transaction(() => {
49
+ raw.exec(m.sql);
50
+ raw
51
+ .prepare("INSERT INTO _ai_hub_migrations (name) VALUES (?)")
52
+ .run(m.name);
53
+ });
54
+ apply();
55
+ }
56
+ }
57
+
58
+ /* ────────────────────────────────────────────────────────────────────────── */
59
+ /* Migrations table */
60
+ /* ────────────────────────────────────────────────────────────────────────── */
61
+
62
+ interface Migration {
63
+ /** Stable name used as the dedupe key — `NNNN_<slug>`. */
64
+ name: string;
65
+ /** Raw SQL applied verbatim inside a transaction. */
66
+ sql: string;
67
+ }
68
+
69
+ /**
70
+ * The canonical migration list. Append-only — never edit a past entry, only
71
+ * add a new one with a higher number. (Same convention as Drizzle / Prisma /
72
+ * Rails.)
73
+ */
74
+ const MIGRATIONS: ReadonlyArray<Migration> = [
75
+ {
76
+ name: "0001_init",
77
+ sql: `
78
+ CREATE TABLE user (
79
+ id TEXT NOT NULL PRIMARY KEY,
80
+ username TEXT NOT NULL UNIQUE,
81
+ display_name TEXT,
82
+ password_hash TEXT,
83
+ identity_source TEXT NOT NULL DEFAULT 'local',
84
+ is_initial_password INTEGER NOT NULL DEFAULT 0,
85
+ role TEXT NOT NULL DEFAULT 'user',
86
+ theme_preference TEXT,
87
+ password_changed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
88
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
89
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
90
+ );
91
+ CREATE INDEX user_role_idx ON user (role);
92
+
93
+ CREATE TABLE agent (
94
+ id TEXT NOT NULL PRIMARY KEY,
95
+ display_name TEXT NOT NULL,
96
+ letta_agent_id TEXT NOT NULL,
97
+ tpm_recovery_enabled INTEGER NOT NULL DEFAULT 0,
98
+ max_recovery_attempts INTEGER NOT NULL DEFAULT 2,
99
+ default_retry_after_seconds INTEGER NOT NULL DEFAULT 65,
100
+ stream_idle_timeout_seconds INTEGER NOT NULL DEFAULT 60,
101
+ sandbox_seed_name TEXT,
102
+ fork_source_conversation_id TEXT,
103
+ is_archived INTEGER NOT NULL DEFAULT 0,
104
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
105
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
106
+ );
107
+
108
+ CREATE TABLE session (
109
+ id TEXT NOT NULL PRIMARY KEY,
110
+ user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
111
+ agent_id TEXT NOT NULL REFERENCES agent(id) ON DELETE RESTRICT,
112
+ letta_conversation_id TEXT NOT NULL,
113
+ letta_agent_id TEXT NOT NULL,
114
+ title TEXT NOT NULL,
115
+ is_archived INTEGER NOT NULL DEFAULT 0,
116
+ reasoning_effort TEXT,
117
+ verbosity TEXT,
118
+ thinking_budget INTEGER,
119
+ sandbox_dir TEXT,
120
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
121
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
122
+ );
123
+ CREATE INDEX session_user_agent_idx ON session (user_id, agent_id);
124
+
125
+ CREATE TABLE turn (
126
+ id TEXT NOT NULL PRIMARY KEY,
127
+ session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE,
128
+ status TEXT NOT NULL,
129
+ phase TEXT,
130
+ last_seq INTEGER NOT NULL DEFAULT 0,
131
+ idempotency_key TEXT,
132
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
133
+ finished_at TEXT
134
+ );
135
+
136
+ CREATE TABLE turn_event (
137
+ turn_id TEXT NOT NULL REFERENCES turn(id) ON DELETE CASCADE,
138
+ seq INTEGER NOT NULL,
139
+ event_name TEXT NOT NULL,
140
+ data TEXT NOT NULL,
141
+ state TEXT NOT NULL,
142
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
143
+ PRIMARY KEY (turn_id, seq)
144
+ );
145
+
146
+ CREATE TABLE admin_setting (
147
+ id INTEGER NOT NULL PRIMARY KEY,
148
+ letta_api_key_encrypted BLOB,
149
+ letta_base_url TEXT,
150
+ letta_ade_url TEXT,
151
+ feature_toggles TEXT NOT NULL DEFAULT '{}',
152
+ theme_switcher_enabled INTEGER NOT NULL DEFAULT 1,
153
+ theme_default TEXT NOT NULL DEFAULT 'light',
154
+ service_token_signing_key BLOB,
155
+ metrics_scrape_token TEXT,
156
+ metrics_scrape_token_rotated_at TEXT,
157
+ signing_key_rotated_at TEXT,
158
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
159
+ );
160
+
161
+ CREATE TABLE message_override (
162
+ conversation_id TEXT NOT NULL,
163
+ ordinal_position INTEGER NOT NULL,
164
+ system_override TEXT,
165
+ user_override TEXT,
166
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
167
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
168
+ PRIMARY KEY (conversation_id, ordinal_position),
169
+ CHECK (system_override IS NOT NULL OR user_override IS NOT NULL)
170
+ );
171
+
172
+ CREATE TABLE message_usage (
173
+ letta_message_id TEXT NOT NULL PRIMARY KEY,
174
+ session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE,
175
+ prompt_tokens INTEGER,
176
+ completion_tokens INTEGER,
177
+ total_tokens INTEGER,
178
+ cached_input_tokens INTEGER,
179
+ cache_write_tokens INTEGER,
180
+ reasoning_tokens INTEGER,
181
+ context_tokens INTEGER,
182
+ run_id TEXT,
183
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
184
+ );
185
+ CREATE INDEX message_usage_session_idx ON message_usage(session_id);
186
+ `,
187
+ },
188
+ {
189
+ name: "0002_rotating_message",
190
+ sql: `
191
+ CREATE TABLE rotating_message (
192
+ id TEXT NOT NULL PRIMARY KEY,
193
+ display_name TEXT NOT NULL,
194
+ prompt TEXT NOT NULL,
195
+ letta_agent_id TEXT NOT NULL,
196
+ schedule_kind TEXT NOT NULL,
197
+ interval_seconds INTEGER,
198
+ daily_time TEXT,
199
+ cron_expression TEXT,
200
+ current_conversation_id TEXT,
201
+ last_status TEXT NOT NULL DEFAULT 'pending',
202
+ last_error TEXT,
203
+ last_rotated_at TEXT,
204
+ next_rotation_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
205
+ is_archived INTEGER NOT NULL DEFAULT 0,
206
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
207
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
208
+ );
209
+ CREATE INDEX rotating_message_due_idx ON rotating_message (is_archived, next_rotation_at);
210
+ `,
211
+ },
212
+ ];