@palbase/backend 17.4.0 → 18.0.0

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 (72) hide show
  1. package/dist/bin/palbase-backend.cjs +1848 -0
  2. package/dist/bin/palbase-backend.cjs.map +1 -0
  3. package/dist/bin/palbase-backend.d.cts +1 -0
  4. package/dist/bin/palbase-backend.d.ts +1 -0
  5. package/dist/bin/palbase-backend.js +168 -0
  6. package/dist/bin/palbase-backend.js.map +1 -0
  7. package/dist/chunk-7D4SUZUM.js +38 -0
  8. package/dist/chunk-7D4SUZUM.js.map +1 -0
  9. package/dist/chunk-N32VDWKH.js +172 -0
  10. package/dist/chunk-N32VDWKH.js.map +1 -0
  11. package/dist/chunk-POYAFBLF.js +189 -0
  12. package/dist/chunk-POYAFBLF.js.map +1 -0
  13. package/dist/chunk-QMVK4X3V.js +200 -0
  14. package/dist/chunk-QMVK4X3V.js.map +1 -0
  15. package/dist/chunk-SSGAMC26.js +342 -0
  16. package/dist/chunk-SSGAMC26.js.map +1 -0
  17. package/dist/chunk-VYH4U7ZQ.js +1138 -0
  18. package/dist/chunk-VYH4U7ZQ.js.map +1 -0
  19. package/dist/{chunk-AAN642N5.js → chunk-W5ODXPY3.js} +2 -336
  20. package/dist/chunk-W5ODXPY3.js.map +1 -0
  21. package/dist/chunk-YL4C5NRY.js +90 -0
  22. package/dist/chunk-YL4C5NRY.js.map +1 -0
  23. package/dist/db/env.cjs.map +1 -1
  24. package/dist/db/env.d.cts +21 -1
  25. package/dist/db/env.d.ts +21 -1
  26. package/dist/db/index.cjs.map +1 -1
  27. package/dist/db/index.d.cts +2 -1
  28. package/dist/db/index.d.ts +2 -1
  29. package/dist/db/index.js +9 -6
  30. package/dist/{index-VLrU7rSW.d.ts → endpoint-B0LpZixz.d.cts} +124 -685
  31. package/dist/{index-BA_oFAz9.d.cts → endpoint-B0LpZixz.d.ts} +124 -685
  32. package/dist/engine/index.cjs +1797 -0
  33. package/dist/engine/index.cjs.map +1 -0
  34. package/dist/engine/index.d.cts +7 -0
  35. package/dist/engine/index.d.ts +7 -0
  36. package/dist/engine/index.js +43 -0
  37. package/dist/engine/index.js.map +1 -0
  38. package/dist/index-B46CGNvx.d.cts +839 -0
  39. package/dist/index-BGSCWlUa.d.cts +674 -0
  40. package/dist/index-DZDUMth5.d.ts +839 -0
  41. package/dist/index-g-EzitI-.d.ts +674 -0
  42. package/dist/index.cjs +1031 -11
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +290 -532
  45. package/dist/index.d.ts +290 -532
  46. package/dist/index.js +999 -509
  47. package/dist/index.js.map +1 -1
  48. package/dist/openapi/index.cjs +6464 -0
  49. package/dist/openapi/index.cjs.map +1 -0
  50. package/dist/openapi/index.d.cts +170 -0
  51. package/dist/openapi/index.d.ts +170 -0
  52. package/dist/openapi/index.js +6248 -0
  53. package/dist/openapi/index.js.map +1 -0
  54. package/dist/registry-3BLYv4si.d.ts +338 -0
  55. package/dist/registry-Cw0YEYCg.d.cts +338 -0
  56. package/dist/test/index.js +2 -0
  57. package/dist/test/index.js.map +1 -1
  58. package/docs/database.md +16 -3
  59. package/docs/llms-full.txt +16 -3
  60. package/package.json +43 -13
  61. package/stager/package.json +4 -0
  62. package/stager/return_types.js +338 -0
  63. package/stager/stage.js +78 -0
  64. package/stager/throw_analysis.js +726 -0
  65. package/template/AGENTS.md +261 -0
  66. package/template/config/secrets.ts +24 -0
  67. package/template/controllers/health.controller.ts +30 -0
  68. package/template/db/schema.ts +35 -0
  69. package/template/package.json +18 -0
  70. package/template/tsconfig.json +30 -0
  71. package/LICENSE +0 -21
  72. package/dist/chunk-AAN642N5.js.map +0 -1
@@ -0,0 +1,1138 @@
1
+ import {
2
+ __requestALS,
3
+ __runWithRuntime
4
+ } from "./chunk-POYAFBLF.js";
5
+ import {
6
+ getRoutes,
7
+ isHttpError
8
+ } from "./chunk-QMVK4X3V.js";
9
+
10
+ // src/engine/config.ts
11
+ var BootRefused = class extends Error {
12
+ missing;
13
+ constructor(missing, message) {
14
+ super(message);
15
+ this.name = "BootRefused";
16
+ this.missing = missing;
17
+ }
18
+ };
19
+ var MANDATORY = [
20
+ { key: "DATABASE_URL", what: "the stack's Postgres (Database module)" },
21
+ { key: "AUTH_JWKS_URL", what: "where this stack publishes its token signing keys (Auth module)" }
22
+ ];
23
+ function loadConfig(env) {
24
+ const missing = MANDATORY.filter((m) => !env[m.key]?.trim()).map((m) => m.key);
25
+ if (missing.length > 0) {
26
+ const detail = MANDATORY.filter((m) => missing.includes(m.key)).map((m) => ` ${m.key.padEnd(16)}${m.what}`).join("\n");
27
+ throw new BootRefused(
28
+ missing,
29
+ `boot refused: mandatory module not configured \u2014 missing ${missing.join(", ")}.
30
+ ${detail}`
31
+ );
32
+ }
33
+ const port = Number(env.PORT ?? 3e3);
34
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
35
+ throw new BootRefused([], `boot refused: PORT is not a valid port number (got ${env.PORT}).`);
36
+ }
37
+ const poolMax = Number(env.DB_POOL_MAX ?? 10);
38
+ if (!Number.isInteger(poolMax) || poolMax < 1) {
39
+ throw new BootRefused([], `boot refused: DB_POOL_MAX must be a positive integer (got ${env.DB_POOL_MAX}).`);
40
+ }
41
+ return {
42
+ databaseUrl: env.DATABASE_URL.trim(),
43
+ authJwksUrl: env.AUTH_JWKS_URL.trim(),
44
+ authIssuer: env.AUTH_ISSUER?.trim() || void 0,
45
+ moduleBaseUrl: (env.MODULE_BASE_URL ?? "").replace(/\/+$/, ""),
46
+ // The secret storage signs its two internal calls with (authorize, and the
47
+ // completion that runs an @Upload handler). Empty means uploads are not
48
+ // wired, and both calls REFUSE — an unsigned completion would let anyone
49
+ // who knows a route path invent an upload that never happened.
50
+ uploadSecret: env.PALBASE_UPLOAD_SECRET ?? "",
51
+ anonKey: env.PALBASE_ANON_KEY ?? "",
52
+ serviceRoleKey: env.PALBASE_SERVICE_ROLE_KEY ?? "",
53
+ realtimeSecret: env.REALTIME_INGESTION_SECRET ?? "",
54
+ port,
55
+ dbRole: env.DB_ROLE ?? "backend_authenticated",
56
+ // Verified against the stack that provisions them, not from memory: the six
57
+ // roles and their attributes are declared in v2/internal/migrate/provision.go
58
+ // (`roleBackendServiceRole = "backend_service_role"`, NOLOGIN BYPASSRLS),
59
+ // and the live database agrees (pg_roles.rolbypassrls = true).
60
+ dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
61
+ poolMax
62
+ };
63
+ }
64
+
65
+ // src/engine/auth.ts
66
+ function b64urlToBytes(s) {
67
+ const pad = s.replace(/-/g, "+").replace(/_/g, "/");
68
+ const full = pad.padEnd(Math.ceil(pad.length / 4) * 4, "=");
69
+ const bin = atob(full);
70
+ const out = new Uint8Array(new ArrayBuffer(bin.length));
71
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
72
+ return out;
73
+ }
74
+ var AuthVerifier = class {
75
+ keys = /* @__PURE__ */ new Map();
76
+ fetchedAt = 0;
77
+ inflight = null;
78
+ jwksUrl;
79
+ issuer;
80
+ fetchImpl;
81
+ ttl;
82
+ constructor(opts) {
83
+ this.jwksUrl = opts.jwksUrl;
84
+ this.issuer = opts.issuer;
85
+ this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
86
+ this.ttl = opts.keysetTtlMs ?? 5 * 6e4;
87
+ }
88
+ /** Fetch the keyset at most once per TTL, and at most once concurrently. */
89
+ async refresh() {
90
+ if (this.inflight) return this.inflight;
91
+ this.inflight = (async () => {
92
+ try {
93
+ const res = await this.fetchImpl(this.jwksUrl);
94
+ if (!res.ok) return;
95
+ const body = await res.json();
96
+ const next = /* @__PURE__ */ new Map();
97
+ for (const jwk of body.keys ?? []) {
98
+ if (jwk.kty !== "EC" || jwk.crv !== "P-256") continue;
99
+ try {
100
+ next.set(
101
+ jwk.kid,
102
+ await crypto.subtle.importKey(
103
+ "jwk",
104
+ { kty: "EC", crv: jwk.crv, x: jwk.x, y: jwk.y, ext: true },
105
+ { name: "ECDSA", namedCurve: "P-256" },
106
+ true,
107
+ ["verify"]
108
+ )
109
+ );
110
+ } catch {
111
+ }
112
+ }
113
+ if (next.size > 0) {
114
+ this.keys = next;
115
+ this.fetchedAt = Date.now();
116
+ }
117
+ } finally {
118
+ this.inflight = null;
119
+ }
120
+ })();
121
+ return this.inflight;
122
+ }
123
+ async key(kid) {
124
+ const stale = Date.now() - this.fetchedAt > this.ttl;
125
+ if (!this.keys.has(kid) || stale) await this.refresh();
126
+ return this.keys.get(kid) ?? null;
127
+ }
128
+ /**
129
+ * Verify an `Authorization` header value.
130
+ *
131
+ * @returns the verified claims, or `null` for absent / malformed / expired /
132
+ * wrong-issuer / bad-signature. One `null` for every failure on purpose:
133
+ * the caller answers 401 either way, and a detailed reason is an oracle.
134
+ */
135
+ async verify(authorization) {
136
+ if (!authorization || !authorization.startsWith("Bearer ")) return null;
137
+ const parts = authorization.slice(7).trim().split(".");
138
+ if (parts.length !== 3) return null;
139
+ const h = parts[0];
140
+ const p = parts[1];
141
+ const sig = parts[2];
142
+ if (h === void 0 || p === void 0 || sig === void 0) return null;
143
+ let header;
144
+ let claims;
145
+ try {
146
+ header = JSON.parse(new TextDecoder().decode(b64urlToBytes(h)));
147
+ claims = JSON.parse(new TextDecoder().decode(b64urlToBytes(p)));
148
+ } catch {
149
+ return null;
150
+ }
151
+ if (header.alg !== "ES256" || !header.kid) return null;
152
+ const key = await this.key(header.kid);
153
+ if (!key) return null;
154
+ let ok = false;
155
+ try {
156
+ ok = await crypto.subtle.verify(
157
+ { name: "ECDSA", hash: "SHA-256" },
158
+ key,
159
+ b64urlToBytes(sig),
160
+ new TextEncoder().encode(`${h}.${p}`)
161
+ );
162
+ } catch {
163
+ return null;
164
+ }
165
+ if (!ok) return null;
166
+ if (typeof claims.exp === "number" && claims.exp * 1e3 <= Date.now()) return null;
167
+ if (this.issuer && claims.iss !== this.issuer) return null;
168
+ return claims;
169
+ }
170
+ };
171
+ function effectiveAuth(routeAuth, controllerAuth) {
172
+ const spec = routeAuth !== void 0 ? routeAuth : controllerAuth;
173
+ if (spec === false) return { required: false, verifiedEmail: false };
174
+ if (spec === true || spec === void 0 || spec === null) return { required: true, verifiedEmail: false };
175
+ if (typeof spec !== "object") return { required: true, verifiedEmail: false };
176
+ const o = spec;
177
+ const role = typeof o.role === "string" && o.role.trim() !== "" ? o.role.trim() : void 0;
178
+ return {
179
+ required: o.required !== false,
180
+ role,
181
+ verifiedEmail: o.verifiedEmail === true
182
+ };
183
+ }
184
+
185
+ // src/engine/ratelimit.ts
186
+ var RateLimiter = class {
187
+ /** Bound on distinct keys held, so an attacker cycling identities cannot
188
+ * grow this map without limit. On overflow the oldest windows are dropped —
189
+ * forgiving, consistent with the restart behaviour above. */
190
+ constructor(maxKeys = 1e5) {
191
+ this.maxKeys = maxKeys;
192
+ }
193
+ maxKeys;
194
+ buckets = /* @__PURE__ */ new Map();
195
+ /**
196
+ * Identify the caller: the signed-in user when the route resolved one,
197
+ * otherwise the address the edge forwarded. Callers the edge did not
198
+ * identify share one bucket — deliberately conservative, since the
199
+ * alternative is a limit anyone resets by omitting a header.
200
+ */
201
+ static key(routeId, userId, headers) {
202
+ if (userId) return `${routeId}\0u:${userId}`;
203
+ const fwd = headers.get("x-forwarded-for");
204
+ const addr = (fwd ? fwd.split(",")[0] ?? "" : headers.get("x-real-ip") ?? "").trim();
205
+ return `${routeId}\0a:${addr || "anonymous"}`;
206
+ }
207
+ /**
208
+ * @returns `null` when the request may proceed, or the number of seconds to
209
+ * wait (never 0 — a caller told to wait 0 comes straight back to the same
210
+ * refusal).
211
+ */
212
+ check(rule, key, now) {
213
+ if (!rule || !(rule.max > 0) || !(rule.window > 0)) return null;
214
+ const bucket = this.buckets.get(key);
215
+ if (!bucket || now >= bucket.resetAt) {
216
+ if (this.buckets.size >= this.maxKeys) this.evict(now);
217
+ this.buckets.set(key, { count: 1, resetAt: now + rule.window * 1e3 });
218
+ return null;
219
+ }
220
+ if (bucket.count < rule.max) {
221
+ bucket.count++;
222
+ return null;
223
+ }
224
+ return Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
225
+ }
226
+ /** Drop expired windows; if none are expired, drop the earliest-resetting
227
+ * quarter so the map cannot wedge at the ceiling. */
228
+ evict(now) {
229
+ let dropped = 0;
230
+ for (const [k, b] of this.buckets) {
231
+ if (now >= b.resetAt) {
232
+ this.buckets.delete(k);
233
+ dropped++;
234
+ }
235
+ }
236
+ if (dropped > 0) return;
237
+ const byReset = [...this.buckets.entries()].sort((a, b) => a[1].resetAt - b[1].resetAt);
238
+ for (let i = 0; i < Math.ceil(byReset.length / 4); i++) {
239
+ const victim = byReset[i];
240
+ if (victim) this.buckets.delete(victim[0]);
241
+ }
242
+ }
243
+ /** Test seam. */
244
+ get size() {
245
+ return this.buckets.size;
246
+ }
247
+ };
248
+
249
+ // src/engine/cache.ts
250
+ function makeMemoryCache(opts = {}) {
251
+ const maxEntries = opts.maxEntries ?? 5e4;
252
+ const now = opts.now ?? (() => Date.now());
253
+ const store = /* @__PURE__ */ new Map();
254
+ const inflight = /* @__PURE__ */ new Map();
255
+ const live = (key) => {
256
+ const e = store.get(key);
257
+ if (!e) return void 0;
258
+ if (e.expiresAt !== 0 && e.expiresAt <= now()) {
259
+ store.delete(key);
260
+ return void 0;
261
+ }
262
+ return e;
263
+ };
264
+ const evict = () => {
265
+ const t = now();
266
+ let dropped = 0;
267
+ for (const [k, e] of store) {
268
+ if (e.expiresAt !== 0 && e.expiresAt <= t) {
269
+ store.delete(k);
270
+ dropped++;
271
+ }
272
+ }
273
+ if (dropped > 0) return;
274
+ const order = [...store.entries()].sort(
275
+ (a, b) => (a[1].expiresAt || Infinity) - (b[1].expiresAt || Infinity)
276
+ );
277
+ for (let i = 0; i < Math.ceil(order.length / 4); i++) {
278
+ const victim = order[i];
279
+ if (victim) store.delete(victim[0]);
280
+ }
281
+ };
282
+ const set = async (key, value, ttl) => {
283
+ if (store.size >= maxEntries && !store.has(key)) evict();
284
+ store.set(key, { value, expiresAt: ttl && ttl > 0 ? now() + ttl * 1e3 : 0 });
285
+ };
286
+ return {
287
+ async get(key) {
288
+ const e = live(key);
289
+ return e ? e.value : null;
290
+ },
291
+ set,
292
+ async del(key) {
293
+ store.delete(key);
294
+ },
295
+ async incr(key) {
296
+ const e = live(key);
297
+ const next = (typeof e?.value === "number" ? e.value : 0) + 1;
298
+ store.set(key, { value: next, expiresAt: e?.expiresAt ?? 0 });
299
+ return next;
300
+ },
301
+ async getOrSet(key, ttl, fn) {
302
+ const hit = live(key);
303
+ if (hit) return hit.value;
304
+ const running = inflight.get(key);
305
+ if (running) return running;
306
+ const fill = (async () => {
307
+ try {
308
+ const value = await fn();
309
+ await set(key, value, ttl);
310
+ return value;
311
+ } finally {
312
+ inflight.delete(key);
313
+ }
314
+ })();
315
+ inflight.set(key, fill);
316
+ return fill;
317
+ }
318
+ };
319
+ }
320
+
321
+ // src/engine/db.ts
322
+ function quoteIdent(name) {
323
+ return `"${name.replace(/"/g, '""')}"`;
324
+ }
325
+ var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
326
+ function createLazyTransaction(sql, role, claimsJson, options = {}) {
327
+ const { lockTimeout } = options;
328
+ const bindSql = lockTimeout ? `${BIND_SQL}, set_config('lock_timeout',$3,true)` : BIND_SQL;
329
+ const bindParams = lockTimeout ? [role, claimsJson, lockTimeout] : [role, claimsJson];
330
+ let opening = null;
331
+ let release = null;
332
+ let fail = null;
333
+ let settled = null;
334
+ const ensure = () => {
335
+ if (opening) return opening;
336
+ opening = new Promise((resolveTx2, rejectTx) => {
337
+ const parked = new Promise((res, rej) => {
338
+ release = res;
339
+ fail = rej;
340
+ });
341
+ settled = sql.begin(async (tx) => {
342
+ await tx.unsafe(bindSql, bindParams);
343
+ resolveTx2(tx);
344
+ await parked;
345
+ }).catch((e) => {
346
+ rejectTx(e);
347
+ throw e;
348
+ });
349
+ });
350
+ return opening;
351
+ };
352
+ return {
353
+ ensure,
354
+ get opened() {
355
+ return opening !== null;
356
+ },
357
+ async commit() {
358
+ if (!opening) return;
359
+ release();
360
+ await settled;
361
+ },
362
+ async rollback(reason) {
363
+ if (!opening) return;
364
+ fail(reason);
365
+ await settled?.catch(() => void 0);
366
+ }
367
+ };
368
+ }
369
+ var resolveTx = async (tx) => typeof tx.ensure === "function" ? await tx.ensure() : tx;
370
+ function asWireValue(value) {
371
+ if (value instanceof Date) return value.toISOString();
372
+ if (Array.isArray(value)) return value.map(asWireValue);
373
+ return value;
374
+ }
375
+ function asWireRow(row) {
376
+ if (row === null || typeof row !== "object") return row;
377
+ const out = {};
378
+ for (const [key, value] of Object.entries(row)) out[key] = asWireValue(value);
379
+ return out;
380
+ }
381
+ function asWireRows(rows) {
382
+ return rows.map((row) => asWireRow(row));
383
+ }
384
+ function createOps(tx) {
385
+ const at = () => resolveTx(tx);
386
+ const ops = {
387
+ async query(sql, params = []) {
388
+ return asWireRows(await (await at()).unsafe(sql, params));
389
+ },
390
+ async insert(table, data) {
391
+ const cols = Object.keys(data);
392
+ if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
393
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
394
+ const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`;
395
+ const rows = await (await at()).unsafe(sql, cols.map((c) => data[c]));
396
+ const inserted = rows[0];
397
+ if (!inserted) {
398
+ throw new Error(
399
+ `insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
400
+ );
401
+ }
402
+ return asWireRow(inserted);
403
+ },
404
+ async update(table, id, data) {
405
+ const cols = Object.keys(data);
406
+ if (cols.length === 0) return ops.findById(table, id);
407
+ const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
408
+ const sql = `UPDATE ${quoteIdent(table)} SET ${assignments} WHERE id = $${cols.length + 1} RETURNING *`;
409
+ const rows = await (await at()).unsafe(sql, [...cols.map((c) => data[c]), id]);
410
+ return rows[0] ? asWireRow(rows[0]) : null;
411
+ },
412
+ async delete(table, id) {
413
+ await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
414
+ },
415
+ async findById(table, id) {
416
+ const rows = await (await at()).unsafe(
417
+ `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
418
+ [id]
419
+ );
420
+ return rows[0] ? asWireRow(rows[0]) : null;
421
+ },
422
+ async findMany(table, query = {}) {
423
+ const cols = Object.keys(query);
424
+ const where = cols.length ? ` WHERE ${cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ")}` : "";
425
+ return asWireRows(await (await at()).unsafe(
426
+ `SELECT * FROM ${quoteIdent(table)}${where}`,
427
+ cols.map((c) => query[c])
428
+ ));
429
+ },
430
+ /** A real SAVEPOINT inside the request's transaction. */
431
+ async transaction(cb) {
432
+ const live = await at();
433
+ return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
434
+ },
435
+ /**
436
+ * Execute a whole transaction plan — what `Database.transaction(fn)` builds.
437
+ *
438
+ * WHY IT RUNS HERE. The platform used to carry a complete implementation
439
+ * of this at `/internal-api/db/tx`, for tenant code that ran in an isolate
440
+ * with no connection of its own. Running the plan there means running it on
441
+ * a DIFFERENT connection: a transaction would not see the uncommitted
442
+ * writes of the request that started it, and the two would hold separate
443
+ * RLS bindings of the same identity. In this stack the tenant's code and
444
+ * the connection share a process, so the plan runs on the request's own
445
+ * transaction inside one SAVEPOINT — and that surface was removed on
446
+ * 2026-08-15, once this was the last thing that could have called it.
447
+ *
448
+ * Until 2026-08-15 it ran NOWHERE: `runTxPlan` called `transport.txPlan` and
449
+ * nothing here implemented it, so a live handler answered
450
+ * "transport.txPlan is not a function" while every test that covered
451
+ * transactions passed against a mock that did implement it.
452
+ */
453
+ async txPlan(plan) {
454
+ const live = await at();
455
+ return live.savepoint(async (sp) => {
456
+ const results = [];
457
+ for (const op of plan.ops) {
458
+ const rows = await runPlanOp(sp, op, results);
459
+ const result = { rows, rows_affected: rows.length };
460
+ results.push(result);
461
+ assertGuard(op, result);
462
+ }
463
+ return { results };
464
+ });
465
+ }
466
+ };
467
+ return ops;
468
+ }
469
+ var currentSchema = {};
470
+ function setSchema(schema) {
471
+ const s = schema;
472
+ currentSchema = (s && "default" in s ? s.default : s) ?? {};
473
+ }
474
+ function withTables(ops, schema = currentSchema) {
475
+ const tables = {};
476
+ for (const key of Object.keys(schema.tables ?? {})) {
477
+ const name = schema.tables?.[key]?.name ?? key;
478
+ tables[key] = {
479
+ insert: (data) => ops.insert(name, data),
480
+ update: (id, data) => ops.update(name, id, data),
481
+ delete: (id) => ops.delete(name, id),
482
+ findById: (id) => ops.findById(name, id),
483
+ findMany: (query) => ops.findMany(name, query ?? {})
484
+ };
485
+ }
486
+ const base = /* @__PURE__ */ Object.create(null);
487
+ return Object.assign(base, ops, { tables });
488
+ }
489
+ var SERVICE_LOCK_TIMEOUT = "5s";
490
+ function isLockTimeout(e) {
491
+ const code = e?.code;
492
+ const message = String(e?.message ?? "");
493
+ return code === "55P03" || /lock timeout/i.test(message);
494
+ }
495
+ function diagnosingDriver(sql) {
496
+ const explain = (e) => isLockTimeout(e) ? new Error(
497
+ `Database.asService() waited too long for a row lock. It runs in its OWN transaction, so a row this request already wrote through Database.* is locked against it until the request commits \u2014 a wait that cannot end. Do that row's work on one surface or the other. (${String(e?.message ?? e)})`
498
+ ) : e;
499
+ const wrapTx = (tx) => ({
500
+ async unsafe(text, params) {
501
+ try {
502
+ return await tx.unsafe(text, params);
503
+ } catch (e) {
504
+ throw explain(e);
505
+ }
506
+ },
507
+ savepoint(cb) {
508
+ return tx.savepoint((sp) => cb(wrapTx(sp)));
509
+ }
510
+ });
511
+ return {
512
+ unsafe: (text, params) => sql.unsafe(text, params),
513
+ begin: (cb) => sql.begin((tx) => cb(wrapTx(tx)))
514
+ };
515
+ }
516
+ function createRequestDatabase(sql, identity) {
517
+ const tx = createLazyTransaction(sql, identity.role, identity.claimsJson);
518
+ let serviceTx = null;
519
+ let serviceClient = null;
520
+ const asService = () => {
521
+ if (serviceClient === null) {
522
+ serviceTx = createLazyTransaction(
523
+ diagnosingDriver(sql),
524
+ identity.serviceRole,
525
+ identity.claimsJson,
526
+ { lockTimeout: SERVICE_LOCK_TIMEOUT }
527
+ );
528
+ serviceClient = withTables(createOps(serviceTx));
529
+ }
530
+ return serviceClient;
531
+ };
532
+ return {
533
+ client: Object.assign(withTables(createOps(tx)), { asService }),
534
+ async commit() {
535
+ await tx.commit();
536
+ await serviceTx?.commit();
537
+ },
538
+ async rollback(reason) {
539
+ await tx.rollback(reason);
540
+ await serviceTx?.rollback(reason);
541
+ }
542
+ };
543
+ }
544
+ var Args = class {
545
+ values = [];
546
+ bind(value) {
547
+ this.values.push(value);
548
+ return `$${this.values.length}`;
549
+ }
550
+ };
551
+ function isRef(v) {
552
+ return typeof v === "object" && v !== null && "$ref" in v;
553
+ }
554
+ function isExpr(v) {
555
+ return typeof v === "object" && v !== null && "$expr" in v;
556
+ }
557
+ function renderValue(value, column, args, results) {
558
+ if (isRef(value)) {
559
+ const source = results[value.$ref.op];
560
+ const row = source?.rows[0];
561
+ if (!row || !(value.$ref.field in row)) {
562
+ throw Object.assign(new Error(`op ${value.$ref.op} has no column "${value.$ref.field}" to reference`), {
563
+ error_code: "tx_ref_unresolved"
564
+ });
565
+ }
566
+ return args.bind(row[value.$ref.field]);
567
+ }
568
+ if (isExpr(value)) {
569
+ const fn = value.$expr;
570
+ if (fn.fn === "now") return "now()";
571
+ const operator = fn.fn === "inc" ? "+" : "-";
572
+ return `${quoteIdent(column)} ${operator} ${args.bind(fn.by)}`;
573
+ }
574
+ return args.bind(value);
575
+ }
576
+ function renderWhere(where, args, results) {
577
+ const cols = Object.keys(where ?? {});
578
+ if (cols.length === 0) return "";
579
+ const terms = cols.map((c) => {
580
+ const v = where[c];
581
+ if (v === null) return `${quoteIdent(c)} IS NULL`;
582
+ return `${quoteIdent(c)} = ${renderValue(v, c, args, results)}`;
583
+ });
584
+ return ` WHERE ${terms.join(" AND ")}`;
585
+ }
586
+ async function runPlanOp(sp, op, results) {
587
+ const args = new Args();
588
+ const table = quoteIdent(op.table);
589
+ let sql;
590
+ switch (op.op) {
591
+ case "insert": {
592
+ const cols = Object.keys(op.values ?? {});
593
+ const rendered = cols.map((c) => renderValue(op.values[c], c, args, results));
594
+ sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
595
+ break;
596
+ }
597
+ case "insertMany": {
598
+ const rows = op.rows ?? [];
599
+ if (rows.length === 0 || !rows[0]) return [];
600
+ const cols = Object.keys(rows[0]);
601
+ const tuples = rows.map(
602
+ (r) => `(${cols.map((c) => renderValue(r[c], c, args, results)).join(", ")})`
603
+ );
604
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
605
+ break;
606
+ }
607
+ case "update": {
608
+ const cols = Object.keys(op.set ?? {});
609
+ if (cols.length === 0) throw new Error(`update ${op.table}: nothing to set`);
610
+ const assignments = cols.map(
611
+ (c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results)}`
612
+ );
613
+ sql = `UPDATE ${table} SET ${assignments.join(", ")}${renderWhere(op.where, args, results)} RETURNING *`;
614
+ break;
615
+ }
616
+ case "delete": {
617
+ sql = `DELETE FROM ${table}${renderWhere(op.where, args, results)} RETURNING *`;
618
+ break;
619
+ }
620
+ case "select": {
621
+ const limit = op.limit !== void 0 ? ` LIMIT ${Number(op.limit)}` : "";
622
+ const lock = op.lock === "update" ? " FOR UPDATE" : "";
623
+ sql = `SELECT * FROM ${table}${renderWhere(op.where, args, results)}${limit}${lock}`;
624
+ break;
625
+ }
626
+ default:
627
+ throw new Error(`unknown operation "${String(op.op)}" in a transaction plan`);
628
+ }
629
+ return await sp.unsafe(sql, args.values);
630
+ }
631
+ function assertGuard(op, result) {
632
+ const guard = op.guard;
633
+ if (!guard) return;
634
+ const n = result.rows.length;
635
+ const ok = guard.kind === "one" ? n === 1 : guard.kind === "none" ? n === 0 : guard.kind === "atLeast" ? n >= guard.n : n <= guard.n;
636
+ if (ok) return;
637
+ throw Object.assign(new Error(`transaction expectation failed: ${guard.kind} (${n} row(s))`), {
638
+ error_code: "tx_guard_failed",
639
+ slot: guard.slot
640
+ });
641
+ }
642
+
643
+ // src/engine/router.ts
644
+ var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
645
+ function toSegments(path) {
646
+ return path.split("/").filter(Boolean).map((s) => s.startsWith("{") && s.endsWith("}") ? `:${s.slice(1, -1)}` : s);
647
+ }
648
+ function buildRouteTable(controllers) {
649
+ const table = [];
650
+ for (const Ctrl of controllers) {
651
+ const ctor = Ctrl;
652
+ const meta = ctor[CONTROLLER_META];
653
+ const basePath = meta?.basePath ?? "";
654
+ const routes = getRoutes(Ctrl);
655
+ if (routes.length === 0) {
656
+ const name = Ctrl.name ?? "<anonymous>";
657
+ throw new Error(
658
+ `controller ${name} collected zero routes. Either it declares no @Get/@Post/\u2026 , or its decorator metadata was erased at build time \u2014 check that the bundle was compiled with experimentalDecorators enabled.`
659
+ );
660
+ }
661
+ const instance = new ctor();
662
+ for (const r of routes) {
663
+ const full = `${basePath}${r.subpath ?? ""}` || "/";
664
+ table.push({
665
+ method: r.method,
666
+ segments: toSegments(full),
667
+ meta: r,
668
+ instance,
669
+ id: `${r.method} ${full}`,
670
+ controllerAuth: meta?.defaultAuth
671
+ });
672
+ }
673
+ }
674
+ return table;
675
+ }
676
+ function matchRoute(table, method, pathname) {
677
+ const parts = pathname.split("/").filter(Boolean);
678
+ for (const entry of table) {
679
+ if (entry.method !== method || entry.segments.length !== parts.length) continue;
680
+ const params = {};
681
+ let ok = true;
682
+ for (let i = 0; i < entry.segments.length; i++) {
683
+ const seg = entry.segments[i];
684
+ const got = parts[i];
685
+ if (seg === void 0 || got === void 0) {
686
+ ok = false;
687
+ break;
688
+ }
689
+ if (seg.charCodeAt(0) === 58) {
690
+ params[seg.slice(1)] = decodeURIComponent(got);
691
+ } else if (seg !== got) {
692
+ ok = false;
693
+ break;
694
+ }
695
+ }
696
+ if (ok) return { entry, params };
697
+ }
698
+ return null;
699
+ }
700
+
701
+ // src/engine/upload.ts
702
+ var AUTHORIZE_PATH = "/__palbase/upload/authorize";
703
+ var SIGNATURE_HEADER = "x-palbase-upload-signature";
704
+ function renderPath(template, tokens) {
705
+ return template.replaceAll("{userId}", sanitizeSegment(tokens.userId ?? "anonymous")).replaceAll("{uploadId}", sanitizeSegment(tokens.uploadId)).replaceAll("{filename}", sanitizeSegment(tokens.filename ?? "file"));
706
+ }
707
+ function sanitizeSegment(raw) {
708
+ const cleaned = raw.replace(/[-]/g, "").replace(/[/\\]/g, "-").replace(/\.{2,}/g, ".").replace(/^\.+/, "").trim();
709
+ return cleaned === "" ? "file" : cleaned.slice(0, 200);
710
+ }
711
+ function grantFor(entry, ctx, bucketLimits) {
712
+ const cfg = entry?.meta?.options?.uploadConfig;
713
+ if (!cfg) return null;
714
+ return {
715
+ bucket: cfg.bucket,
716
+ path: renderPath(cfg.pathTemplate, {
717
+ userId: ctx.userId,
718
+ uploadId: ctx.uploadId,
719
+ filename: ctx.filename
720
+ }),
721
+ maxBytes: bucketLimits?.maxBytes ?? null,
722
+ mimeTypes: bucketLimits?.mimeTypes ?? null
723
+ };
724
+ }
725
+ var CompletionLedger = class {
726
+ constructor(capacity = 1024) {
727
+ this.capacity = capacity;
728
+ }
729
+ capacity;
730
+ seen = /* @__PURE__ */ new Map();
731
+ recall(uploadId) {
732
+ return this.seen.get(uploadId);
733
+ }
734
+ remember(uploadId, response) {
735
+ this.seen.delete(uploadId);
736
+ this.seen.set(uploadId, response);
737
+ while (this.seen.size > this.capacity) {
738
+ const oldest = this.seen.keys().next();
739
+ if (oldest.done) break;
740
+ this.seen.delete(oldest.value);
741
+ }
742
+ }
743
+ get size() {
744
+ return this.seen.size;
745
+ }
746
+ };
747
+ function verifySignature(presented, expected) {
748
+ if (presented.length !== expected.length) return false;
749
+ let diff = 0;
750
+ for (let i = 0; i < presented.length; i++) {
751
+ diff |= presented.charCodeAt(i) ^ expected.charCodeAt(i);
752
+ }
753
+ return diff === 0;
754
+ }
755
+
756
+ // src/engine/fence.ts
757
+ var SECRET_ENV = [
758
+ "DATABASE_URL",
759
+ "PALBASE_SERVICE_ROLE_KEY",
760
+ "REALTIME_INGESTION_SECRET",
761
+ "INTERNAL_API_SECRET",
762
+ "STACK_ROOT_KEY",
763
+ "PEPPER",
764
+ "LOCAL_JWT_PEM"
765
+ ];
766
+ function scrubSecrets(env) {
767
+ const removed = [];
768
+ const kept = [];
769
+ for (const name of SECRET_ENV) {
770
+ if (env[name] === void 0) continue;
771
+ delete env[name];
772
+ removed.push(name);
773
+ }
774
+ for (const name of Object.keys(env)) if (name.startsWith("PALBASE_VAR_")) kept.push(name);
775
+ return { removed, kept };
776
+ }
777
+ function hostAllowed(host, allow) {
778
+ const h = host.toLowerCase();
779
+ for (const raw of allow) {
780
+ const pattern = raw.trim().toLowerCase();
781
+ if (!pattern) continue;
782
+ if (pattern === h) return true;
783
+ if (pattern.startsWith("*.") && h.endsWith(pattern.slice(1)) && h.length > pattern.length - 1) {
784
+ return true;
785
+ }
786
+ }
787
+ return false;
788
+ }
789
+ function installEgressFence(policy) {
790
+ const original = globalThis.fetch.bind(globalThis);
791
+ const declared = policy.allow.length > 0;
792
+ const platform = (policy.alwaysAllow ?? []).map((h) => h.toLowerCase()).filter(Boolean);
793
+ if (!declared && policy.whenUndeclared === "allow") return original;
794
+ const fenced = async (input, init) => {
795
+ const url = typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url);
796
+ if (platform.includes(url.hostname.toLowerCase())) return original(input, init);
797
+ if (!declared || !hostAllowed(url.hostname, policy.allow)) {
798
+ throw new Error(
799
+ `egress denied: ${url.hostname} is not in this backend's declared allowlist. Add it to config/egress.ts and deploy.`
800
+ );
801
+ }
802
+ if (policy.timeoutMs > 0) {
803
+ const controller = new AbortController();
804
+ const timer = setTimeout(() => controller.abort(), policy.timeoutMs);
805
+ try {
806
+ return await original(input, { ...init, signal: init?.signal ?? controller.signal });
807
+ } finally {
808
+ clearTimeout(timer);
809
+ }
810
+ }
811
+ return original(input, init);
812
+ };
813
+ globalThis.fetch = fenced;
814
+ return original;
815
+ }
816
+
817
+ // src/engine/index.ts
818
+ var JSON_HEADERS = { "content-type": "application/json" };
819
+ function envelope(error, description, status, requestId, extra) {
820
+ return new Response(
821
+ JSON.stringify({ error, error_description: description, status, request_id: requestId, ...extra }),
822
+ { status, headers: JSON_HEADERS }
823
+ );
824
+ }
825
+ function unavailable(name) {
826
+ throw new Error(
827
+ `${name} is unavailable. Either this backend was started without module clients (set MODULE_BASE_URL and the API keys so the engine can reach the module surface), or ${name} is not one of the modules this backend provides.`
828
+ );
829
+ }
830
+ function stubModule(name) {
831
+ return new Proxy(
832
+ {},
833
+ {
834
+ get: () => unavailable(name),
835
+ apply: () => unavailable(name)
836
+ }
837
+ );
838
+ }
839
+ async function defaultSqlDriver(config) {
840
+ const g = globalThis;
841
+ if (!g.Bun?.SQL) {
842
+ throw new BootRefused(
843
+ [],
844
+ "boot refused: no SQL driver. Running outside Bun means the driver must be supplied \u2014 pass `sql` to createApp()."
845
+ );
846
+ }
847
+ return new g.Bun.SQL({ url: config.databaseUrl, max: config.poolMax });
848
+ }
849
+ async function createApp(opts) {
850
+ const { config, controllers } = opts;
851
+ setSchema(opts.schema ?? {});
852
+ const routes = buildRouteTable(controllers);
853
+ if (routes.length === 0) {
854
+ throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
855
+ }
856
+ const sql = opts.sql ?? await defaultSqlDriver(config);
857
+ await sql.unsafe("select 1");
858
+ const auth = new AuthVerifier({ jwksUrl: config.authJwksUrl, issuer: config.authIssuer });
859
+ const limiter = new RateLimiter();
860
+ const cache = opts.cache ?? makeMemoryCache();
861
+ const log = opts.logger ?? console;
862
+ const modules = opts.modules ?? {};
863
+ const runWithRuntime = opts.runtimeHooks?.__runWithRuntime ?? __runWithRuntime;
864
+ const requestALS = opts.runtimeHooks?.__requestALS ?? __requestALS;
865
+ const uploadSecret = config.uploadSecret ?? "";
866
+ const completions = new CompletionLedger();
867
+ async function handleAuthorize(req, requestId) {
868
+ if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
869
+ return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
870
+ }
871
+ const body = await req.json().catch(() => null);
872
+ if (!body?.path || !body.uploadId) {
873
+ return envelope("bad_request", "authorize needs a path and an uploadId", 400, requestId);
874
+ }
875
+ const target = matchRoute(routes, body.method ?? "POST", body.path);
876
+ const spec = effectiveAuth(target?.entry.meta.options?.auth, target?.entry.controllerAuth);
877
+ const callerClaims = await auth.verify(req.headers.get("authorization"));
878
+ if (spec.required && !callerClaims) {
879
+ return envelope("unauthorized", "A valid access token is required", 401, requestId);
880
+ }
881
+ if (callerClaims && spec.role && callerClaims.role !== spec.role) {
882
+ return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
883
+ }
884
+ const grant = grantFor(target?.entry, {
885
+ userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
886
+ uploadId: body.uploadId,
887
+ filename: body.filename
888
+ });
889
+ if (!grant) {
890
+ return envelope(
891
+ "not_an_upload_route",
892
+ `${body.method ?? "POST"} ${body.path} does not declare @Upload`,
893
+ 400,
894
+ requestId
895
+ );
896
+ }
897
+ return new Response(JSON.stringify(grant), { status: 200, headers: JSON_HEADERS });
898
+ }
899
+ async function handle(req) {
900
+ const requestId = `req_${crypto.randomUUID()}`;
901
+ const url = new URL(req.url);
902
+ if (url.pathname === AUTHORIZE_PATH) {
903
+ return handleAuthorize(req, requestId);
904
+ }
905
+ const hit = matchRoute(routes, req.method, url.pathname);
906
+ if (!hit) return envelope("not_found", "No route matches this method and path", 404, requestId);
907
+ const { meta } = hit.entry;
908
+ const spec = effectiveAuth(meta.options?.auth, hit.entry.controllerAuth);
909
+ const claims = await auth.verify(req.headers.get("authorization"));
910
+ if (spec.required && !claims) {
911
+ return envelope("unauthorized", "A valid access token is required", 401, requestId);
912
+ }
913
+ const userId = typeof claims?.sub === "string" ? claims.sub : void 0;
914
+ if (claims && spec.role && claims.role !== spec.role) {
915
+ return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
916
+ }
917
+ if (claims && spec.verifiedEmail && claims.email_verified !== true) {
918
+ return envelope("email_not_verified", "A verified email address is required", 403, requestId);
919
+ }
920
+ const retryAfter = limiter.check(
921
+ meta.options?.rateLimit,
922
+ RateLimiter.key(hit.entry.id, userId, req.headers),
923
+ Date.now()
924
+ );
925
+ if (retryAfter !== null) {
926
+ return new Response(
927
+ JSON.stringify({
928
+ error: "too_many_requests",
929
+ error_description: "Rate limit exceeded for this endpoint",
930
+ status: 429,
931
+ request_id: requestId
932
+ }),
933
+ { status: 429, headers: { ...JSON_HEADERS, "retry-after": String(retryAfter) } }
934
+ );
935
+ }
936
+ let completionUploadId = null;
937
+ const args = [];
938
+ let parsedBody;
939
+ let bodyRead = false;
940
+ for (const p of meta.params ?? []) {
941
+ switch (p.kind) {
942
+ case "body": {
943
+ if (!bodyRead) {
944
+ parsedBody = await req.json().catch(() => ({}));
945
+ bodyRead = true;
946
+ }
947
+ const r = p.schema.safeParse(parsedBody);
948
+ if (!r.success) {
949
+ return envelope("bad_request", "Request body failed validation", 400, requestId, {
950
+ fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
951
+ });
952
+ }
953
+ args[p.index] = r.data;
954
+ break;
955
+ }
956
+ case "query": {
957
+ const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
958
+ if (!r.success) {
959
+ return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
960
+ fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
961
+ });
962
+ }
963
+ args[p.index] = r.data;
964
+ break;
965
+ }
966
+ case "param":
967
+ args[p.index] = hit.params[p.name];
968
+ break;
969
+ case "headers":
970
+ args[p.index] = Object.fromEntries(req.headers);
971
+ break;
972
+ case "user":
973
+ case "optionalUser":
974
+ args[p.index] = claims ? {
975
+ id: userId,
976
+ email: claims.email,
977
+ role: claims.role,
978
+ emailVerified: claims.email_verified === true,
979
+ metadata: claims.metadata ?? {}
980
+ } : null;
981
+ break;
982
+ case "uploadedObject": {
983
+ if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
984
+ return envelope(
985
+ "unauthorized",
986
+ "This endpoint accepts uploads through storage, not directly",
987
+ 401,
988
+ requestId
989
+ );
990
+ }
991
+ if (!bodyRead) {
992
+ parsedBody = await req.json().catch(() => ({}));
993
+ bodyRead = true;
994
+ }
995
+ const envelopeIn = parsedBody;
996
+ if (!envelopeIn?.uploadedObject) {
997
+ return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
998
+ }
999
+ const uploadId = envelopeIn.uploadedObject.uploadId;
1000
+ if (typeof uploadId === "string" && uploadId !== "") {
1001
+ const already = completions.recall(uploadId);
1002
+ if (already) {
1003
+ return new Response(already.body, {
1004
+ status: already.status,
1005
+ headers: already.contentType ? { "content-type": already.contentType } : void 0
1006
+ });
1007
+ }
1008
+ completionUploadId = uploadId;
1009
+ }
1010
+ args[p.index] = envelopeIn.uploadedObject;
1011
+ parsedBody = envelopeIn.body ?? {};
1012
+ break;
1013
+ }
1014
+ case "requestId":
1015
+ args[p.index] = requestId;
1016
+ break;
1017
+ case "traceId":
1018
+ args[p.index] = requestId;
1019
+ break;
1020
+ case "req":
1021
+ args[p.index] = req;
1022
+ break;
1023
+ default:
1024
+ args[p.index] = void 0;
1025
+ }
1026
+ }
1027
+ const db = createRequestDatabase(sql, {
1028
+ role: config.dbRole,
1029
+ serviceRole: config.dbServiceRole,
1030
+ claimsJson: JSON.stringify(claims ?? {})
1031
+ });
1032
+ try {
1033
+ const services = {
1034
+ Database: db.client,
1035
+ Cache: cache,
1036
+ Log: log,
1037
+ Documents: modules.Documents ?? stubModule("Documents"),
1038
+ Storage: modules.Storage ?? stubModule("Storage"),
1039
+ Notifications: modules.Notifications ?? stubModule("Notifications"),
1040
+ Flags: modules.Flags ?? stubModule("Flags"),
1041
+ Realtime: modules.Realtime ?? stubModule("Realtime"),
1042
+ Purchases: modules.Purchases ?? stubModule("Purchases"),
1043
+ // Named, never undefined. A backend started without a secrets client
1044
+ // that returned `undefined` here would fail inside the handler as
1045
+ // "Cannot read properties of undefined", which says nothing about what
1046
+ // to configure — the stub says the name and the variable.
1047
+ Secrets: modules.Secrets ?? stubModule("Secrets")
1048
+ };
1049
+ const result = await runWithRuntime(services, () => {
1050
+ const box = requestALS.getStore();
1051
+ if (box) {
1052
+ box.userId = userId ?? null;
1053
+ box.requestId = requestId;
1054
+ box.idempotencyKey = req.headers.get("idempotency-key");
1055
+ }
1056
+ const method = hit.entry.instance[meta.fnName];
1057
+ if (typeof method !== "function") {
1058
+ throw new Error(
1059
+ `route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
1060
+ );
1061
+ }
1062
+ return method.apply(hit.entry.instance, args);
1063
+ });
1064
+ await db.commit();
1065
+ if (meta.returnSchema) {
1066
+ const v = meta.returnSchema.safeParse(result);
1067
+ if (!v.success) {
1068
+ log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
1069
+ return envelope(
1070
+ "output_invalid",
1071
+ "The handler returned a value its declared return type rejects",
1072
+ 500,
1073
+ requestId
1074
+ );
1075
+ }
1076
+ }
1077
+ if (result === void 0 || result === null) {
1078
+ if (completionUploadId) {
1079
+ completions.remember(completionUploadId, { status: 204, body: null, contentType: null });
1080
+ }
1081
+ return new Response(null, { status: 204 });
1082
+ }
1083
+ const payload = JSON.stringify(result);
1084
+ if (completionUploadId) {
1085
+ completions.remember(completionUploadId, {
1086
+ status: 200,
1087
+ body: payload,
1088
+ contentType: JSON_HEADERS["content-type"] ?? "application/json"
1089
+ });
1090
+ }
1091
+ return new Response(payload, { status: 200, headers: JSON_HEADERS });
1092
+ } catch (err) {
1093
+ await db.rollback(err);
1094
+ if (isHttpError(err)) {
1095
+ return envelope(
1096
+ err.error,
1097
+ err.errorDescription,
1098
+ err.status,
1099
+ requestId,
1100
+ err.data !== void 0 ? { data: err.data } : void 0
1101
+ );
1102
+ }
1103
+ log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
1104
+ return envelope("internal_error", "The request could not be completed", 500, requestId);
1105
+ }
1106
+ }
1107
+ return {
1108
+ handle,
1109
+ routes,
1110
+ config,
1111
+ async shutdown() {
1112
+ const closable = sql;
1113
+ await closable.close?.();
1114
+ await closable.end?.();
1115
+ }
1116
+ };
1117
+ }
1118
+
1119
+ export {
1120
+ BootRefused,
1121
+ loadConfig,
1122
+ AuthVerifier,
1123
+ effectiveAuth,
1124
+ RateLimiter,
1125
+ makeMemoryCache,
1126
+ quoteIdent,
1127
+ createLazyTransaction,
1128
+ createOps,
1129
+ withTables,
1130
+ createRequestDatabase,
1131
+ buildRouteTable,
1132
+ matchRoute,
1133
+ scrubSecrets,
1134
+ hostAllowed,
1135
+ installEgressFence,
1136
+ createApp
1137
+ };
1138
+ //# sourceMappingURL=chunk-VYH4U7ZQ.js.map