@palbase/backend 35.0.1 → 36.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 (70) hide show
  1. package/dist/bin/palbase-backend.cjs +556 -487
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +6 -6
  4. package/dist/{chunk-L4R7WXLH.js → chunk-7WAGQ3VR.js} +4 -171
  5. package/dist/chunk-7WAGQ3VR.js.map +1 -0
  6. package/dist/{chunk-UMESJJRS.js → chunk-CS6NQ6PO.js} +2 -1
  7. package/dist/chunk-CS6NQ6PO.js.map +1 -0
  8. package/dist/{chunk-BOD3DT6D.js → chunk-EHKEMHB4.js} +560 -430
  9. package/dist/chunk-EHKEMHB4.js.map +1 -0
  10. package/dist/{chunk-PRH45NZU.js → chunk-FFO36MQQ.js} +2 -45
  11. package/dist/chunk-FFO36MQQ.js.map +1 -0
  12. package/dist/{chunk-P7MGSAFT.js → chunk-LFUNRWUE.js} +1 -40
  13. package/dist/chunk-LFUNRWUE.js.map +1 -0
  14. package/dist/{chunk-RZLY4BOY.js → chunk-PDD55QWN.js} +2 -2
  15. package/dist/{chunk-YO5GYM73.js → chunk-X7UR3VXA.js} +5 -6
  16. package/dist/chunk-X7UR3VXA.js.map +1 -0
  17. package/dist/{chunk-6FEHCEB7.js → chunk-YANGWAIM.js} +3 -3
  18. package/dist/db/index.cjs +1 -232
  19. package/dist/db/index.cjs.map +1 -1
  20. package/dist/db/index.d.cts +3 -46
  21. package/dist/db/index.d.ts +3 -46
  22. package/dist/db/index.js +4 -12
  23. package/dist/engine/index.cjs +556 -487
  24. package/dist/engine/index.cjs.map +1 -1
  25. package/dist/engine/index.d.cts +4 -4
  26. package/dist/engine/index.d.ts +4 -4
  27. package/dist/engine/index.js +6 -6
  28. package/dist/{index-DllNjpTl.d.cts → index-CI7S0Wqv.d.cts} +61 -8
  29. package/dist/{endpoint-BI0yGuEL.d.ts → index-CUy50OLU.d.ts} +6 -82
  30. package/dist/{endpoint-DaGnjt42.d.cts → index-DRx880KY.d.cts} +6 -82
  31. package/dist/{index-E8Sd6NFy.d.ts → index-Ogi30dbt.d.ts} +61 -8
  32. package/dist/index.cjs +5 -237
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +6 -7
  35. package/dist/index.d.ts +6 -7
  36. package/dist/index.js +7 -15
  37. package/dist/index.js.map +1 -1
  38. package/dist/openapi/index.cjs +1 -0
  39. package/dist/openapi/index.cjs.map +1 -1
  40. package/dist/openapi/index.d.cts +3 -3
  41. package/dist/openapi/index.d.ts +3 -3
  42. package/dist/openapi/index.js +2 -2
  43. package/dist/{registry-BnKvIyve.d.cts → registry-C7UCkQf0.d.cts} +1 -1
  44. package/dist/{registry-zDCj0eOU.d.ts → registry-DJcvbomD.d.ts} +1 -1
  45. package/dist/test/index.cjs +3 -11
  46. package/dist/test/index.cjs.map +1 -1
  47. package/dist/test/index.d.cts +2 -2
  48. package/dist/test/index.d.ts +2 -2
  49. package/dist/test/index.js +2 -9
  50. package/dist/test/index.js.map +1 -1
  51. package/docs/README.md +1 -1
  52. package/docs/database.md +2 -60
  53. package/docs/examples/database-dx.ts +12 -17
  54. package/docs/llms-full.txt +3 -61
  55. package/package.json +1 -1
  56. package/stager/inferred_returns.js +153 -0
  57. package/stager/return_types.js +9 -4
  58. package/stager/stage.js +3 -1
  59. package/template/AGENTS.md +5 -5
  60. package/template/package.json +2 -2
  61. package/dist/chunk-BOD3DT6D.js.map +0 -1
  62. package/dist/chunk-L4R7WXLH.js.map +0 -1
  63. package/dist/chunk-P7MGSAFT.js.map +0 -1
  64. package/dist/chunk-PRH45NZU.js.map +0 -1
  65. package/dist/chunk-UMESJJRS.js.map +0 -1
  66. package/dist/chunk-YO5GYM73.js.map +0 -1
  67. package/docs/examples/check-query-contracts.ts +0 -5
  68. package/docs/examples/database-dx.contract.json +0 -65
  69. /package/dist/{chunk-RZLY4BOY.js.map → chunk-PDD55QWN.js.map} +0 -0
  70. /package/dist/{chunk-6FEHCEB7.js.map → chunk-YANGWAIM.js.map} +0 -0
@@ -703,25 +703,6 @@ function translateRejection(err, builder) {
703
703
  }
704
704
  __name(translateRejection, "translateRejection");
705
705
 
706
- // src/db/workload.ts
707
- function validateWorkload(value) {
708
- if (typeof value.name !== "string" || !/^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$/.test(value.name)) throw new Error("workload name must be 1\u201364 letters/digits/dots/dashes");
709
- for (const name of [
710
- "concurrency",
711
- "tenantConcurrency",
712
- "queueLimit",
713
- "tenantQueueLimit",
714
- "queueTimeoutMs"
715
- ]) {
716
- const n = value[name], min = name.endsWith("Limit") ? 0 : 1;
717
- const max = name === "queueTimeoutMs" ? 6e4 : name.endsWith("Limit") ? 1e4 : 1e3;
718
- if (!Number.isInteger(n) || n < min || n > max) throw new Error(`workload ${name} must be an integer between ${min} and ${max}`);
719
- }
720
- if (value.tenantConcurrency > value.concurrency || value.tenantQueueLimit > value.queueLimit) throw new Error("workload tenant limits cannot exceed global limits");
721
- if (typeof value.tenantClaim !== "string" || !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(value.tenantClaim)) throw new Error("workload tenantClaim must be a top-level claim name");
722
- }
723
- __name(validateWorkload, "validateWorkload");
724
-
725
706
  // src/db/transaction-options.ts
726
707
  function retryBudget(value) {
727
708
  const n = value ?? 0;
@@ -753,10 +734,6 @@ function validateAtomicOptions(options) {
753
734
  throw new Error(`transaction ${name} must be a positive integer in milliseconds`);
754
735
  }
755
736
  }
756
- if (options.workload !== void 0) validateWorkload(options.workload);
757
- if (options.conflictKeys !== void 0 && (!options.workload || !Array.isArray(options.conflictKeys) || options.conflictKeys.length > 64 || options.conflictKeys.some((k) => typeof k !== "string" || k.length < 1 || k.length > 256))) {
758
- throw new Error("conflictKeys requires a workload and at most 64 nonempty keys of at most 256 characters");
759
- }
760
737
  if (options.signal !== void 0 && (typeof options.signal.aborted !== "boolean" || typeof options.signal.addEventListener !== "function")) {
761
738
  throw new Error("transaction signal must be an AbortSignal");
762
739
  }
@@ -916,7 +893,9 @@ function makeTableProxy(ops, prefix) {
916
893
  // Older than this run, but the run rewrote this proxy for
917
894
  // `Database.schema(name).tables.*` and would have carried the gap onto
918
895
  // the new surface too.
919
- updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set), "updateMany"),
896
+ updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set, q.returning === void 0 ? void 0 : {
897
+ returning: q.returning
898
+ }), "updateMany"),
920
899
  deleteMany: /* @__PURE__ */ __name((q) => ops().deleteMany(name, q.where), "deleteMany"),
921
900
  count: /* @__PURE__ */ __name((q) => ops().count(name, q?.where), "count"),
922
901
  search: /* @__PURE__ */ __name((params) => ops().search(name, params), "search"),
@@ -936,9 +915,6 @@ function makeTypedSurface(raw) {
936
915
  const ops = {
937
916
  $query: /* @__PURE__ */ __name((sql, params) => raw.query(sql, params), "$query"),
938
917
  $diagnostics: /* @__PURE__ */ __name(() => raw.diagnostics(), "$diagnostics"),
939
- $readQuery: /* @__PURE__ */ __name((query) => raw.readQuery(query), "$readQuery"),
940
- $compileQuery: /* @__PURE__ */ __name((query) => raw.compileQuery(query), "$compileQuery"),
941
- $explainQuery: /* @__PURE__ */ __name((query) => raw.explainQuery(query), "$explainQuery"),
942
918
  $insert: /* @__PURE__ */ __name((table, data) => raw.insert(table, data), "$insert"),
943
919
  $update: /* @__PURE__ */ __name((table, id, data) => raw.update(table, id, data), "$update"),
944
920
  $delete: /* @__PURE__ */ __name((table, id) => raw.delete(table, id), "$delete"),
@@ -1143,6 +1119,7 @@ var Realtime = makeServiceProxy("Realtime");
1143
1119
  var SQLSTATE = Object.freeze({
1144
1120
  serializationFailure: "40001",
1145
1121
  deadlockDetected: "40P01",
1122
+ queryCancelled: "57014",
1146
1123
  commandGuardFailed: "PBC01"
1147
1124
  });
1148
1125
  function isTransactionRollbackState(state) {
@@ -1345,6 +1322,16 @@ ${detail}`);
1345
1322
  if (!Number.isInteger(poolMax) || poolMax < 1) {
1346
1323
  throw new BootRefused([], `boot refused: DB_POOL_MAX must be a positive integer (got ${env.DB_POOL_MAX}).`);
1347
1324
  }
1325
+ const capacityNumber = /* @__PURE__ */ __name((key2, minimum, maximum) => {
1326
+ if (env[key2] === void 0) return void 0;
1327
+ const value = Number(env[key2]);
1328
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new BootRefused([], `boot refused: ${key2} must be between ${minimum} and ${maximum}.`);
1329
+ return value;
1330
+ }, "capacityNumber");
1331
+ const queueLimit = capacityNumber("PALBASE_DB_QUEUE_LIMIT", 0, 1e4);
1332
+ const queueTimeoutMs = capacityNumber("PALBASE_DB_QUEUE_TIMEOUT_MS", 1, 6e4);
1333
+ const tenantClaim = env.PALBASE_DB_TENANT_CLAIM;
1334
+ if (tenantClaim !== void 0 && !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(tenantClaim)) throw new BootRefused([], "boot refused: PALBASE_DB_TENANT_CLAIM must be a top-level trusted claim name.");
1348
1335
  return {
1349
1336
  databaseUrl: env.DATABASE_URL.trim(),
1350
1337
  authJwksUrl: env.AUTH_JWKS_URL.trim(),
@@ -1369,6 +1356,17 @@ ${detail}`);
1369
1356
  // and the live database agrees (pg_roles.rolbypassrls = true).
1370
1357
  dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
1371
1358
  poolMax,
1359
+ databaseAdmission: {
1360
+ ...queueLimit === void 0 ? {} : {
1361
+ queueLimit
1362
+ },
1363
+ ...queueTimeoutMs === void 0 ? {} : {
1364
+ queueTimeoutMs
1365
+ },
1366
+ ...tenantClaim === void 0 ? {} : {
1367
+ tenantClaim
1368
+ }
1369
+ },
1372
1370
  ...env.PALBASE_DB_DIAGNOSTICS === "true" ? {
1373
1371
  databaseDiagnostics: {
1374
1372
  onComplete: /* @__PURE__ */ __name((diagnostics) => {
@@ -1892,46 +1890,6 @@ function validateInsertManyOptions(opts) {
1892
1890
  }
1893
1891
  __name(validateInsertManyOptions, "validateInsertManyOptions");
1894
1892
 
1895
- // src/db/query-contract.ts
1896
- function queryColumnContract(column) {
1897
- const def = column && typeof column === "object" && "_def" in column ? column._def : column;
1898
- if (!def?.type) throw new Error("Query contract requires declared column types");
1899
- if (def.transform !== void 0 && def.codec === void 0) throw new Error("Query contracts require a named codec instead of a free-form column transform");
1900
- return {
1901
- type: def.type,
1902
- nullable: def.nullable === true,
1903
- ...def.codec === void 0 ? {} : {
1904
- codec: def.codec
1905
- },
1906
- ...def.enumValues === void 0 ? {} : {
1907
- enumValues: [
1908
- ...def.enumValues
1909
- ]
1910
- },
1911
- ...def.dimensions === void 0 ? {} : {
1912
- dimensions: def.dimensions
1913
- }
1914
- };
1915
- }
1916
- __name(queryColumnContract, "queryColumnContract");
1917
- function stableContract(value) {
1918
- if (Array.isArray(value)) return `[${value.map(stableContract).join(",")}]`;
1919
- if (value && typeof value === "object") return `{${Object.keys(value).sort().map((key2) => `${JSON.stringify(key2)}:${stableContract(value[key2])}`).join(",")}}`;
1920
- return JSON.stringify(value) ?? "null";
1921
- }
1922
- __name(stableContract, "stableContract");
1923
- function assertReadContract(query, columns) {
1924
- if (!query.options.select.length || new Set(query.options.select).size !== query.options.select.length) {
1925
- throw new Error(`Query ${query.name}: select must contain distinct, explicit columns`);
1926
- }
1927
- for (const name of query.options.select) {
1928
- if (!columns || !Object.hasOwn(columns, name) || !Object.hasOwn(query.columns, name) || stableContract(queryColumnContract(columns[name])) !== stableContract(query.columns[name])) {
1929
- throw new Error(`Query ${query.name}: schema drift at ${query.table}.${name}; regenerate and review its response contract`);
1930
- }
1931
- }
1932
- }
1933
- __name(assertReadContract, "assertReadContract");
1934
-
1935
1893
  // src/db/unique.ts
1936
1894
  function uniqueKeysOf(table) {
1937
1895
  const columns = table.columns ?? {};
@@ -2105,142 +2063,167 @@ function parameterChunks(sql) {
2105
2063
  __name(parameterChunks, "parameterChunks");
2106
2064
 
2107
2065
  // src/engine/admission.ts
2108
- var WorkloadAdmission = class {
2066
+ var DatabaseAdmission = class {
2109
2067
  static {
2110
- __name(this, "WorkloadAdmission");
2068
+ __name(this, "DatabaseAdmission");
2111
2069
  }
2112
- definition;
2113
- pending = [];
2070
+ options;
2114
2071
  active = 0;
2115
- activeKeys = /* @__PURE__ */ new Set();
2072
+ queued = 0;
2116
2073
  tenants = /* @__PURE__ */ new Map();
2117
- draining = false;
2118
- constructor(definition) {
2119
- this.definition = definition;
2120
- validateWorkload(definition);
2074
+ // Map insertion order is the round-robin ring; each Set is FIFO. Grant and
2075
+ // cancellation remove nodes directly, without scanning a 10,000-item queue.
2076
+ pending = /* @__PURE__ */ new Map();
2077
+ lastTenant;
2078
+ accepted = 0;
2079
+ rejected = {
2080
+ full: 0,
2081
+ timeout: 0,
2082
+ cancelled: 0
2083
+ };
2084
+ limit;
2085
+ timeout;
2086
+ constructor(options) {
2087
+ this.options = options;
2088
+ this.limit = options.queueLimit ?? Math.min(512, Math.max(32, options.concurrency * 8));
2089
+ this.timeout = options.queueTimeoutMs ?? 1e3;
2090
+ if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1 || !Number.isSafeInteger(this.limit) || this.limit < 0 || this.limit > 1e4 || !Number.isSafeInteger(this.timeout) || this.timeout < 1 || this.timeout > 6e4) {
2091
+ throw new Error("Invalid database pool or queue deployment settings");
2092
+ }
2121
2093
  }
2122
2094
  get snapshot() {
2123
2095
  return {
2124
2096
  active: this.active,
2125
- queued: this.pending.length,
2097
+ queued: this.queued,
2126
2098
  tenants: this.tenants.size,
2127
- keys: this.activeKeys.size
2099
+ accepted: this.accepted,
2100
+ rejected: {
2101
+ ...this.rejected
2102
+ }
2103
+ };
2104
+ }
2105
+ rejection(reason2) {
2106
+ this.rejected[reason2]++;
2107
+ try {
2108
+ this.options.onReject?.({
2109
+ reason: reason2,
2110
+ active: this.active,
2111
+ queued: this.queued
2112
+ });
2113
+ } catch {
2114
+ }
2115
+ }
2116
+ take(tenant, entered) {
2117
+ this.active++;
2118
+ this.accepted++;
2119
+ this.lastTenant = tenant;
2120
+ this.tenants.set(tenant, (this.tenants.get(tenant) ?? 0) + 1);
2121
+ let released = false;
2122
+ return {
2123
+ queueMs: performance.now() - entered,
2124
+ release: /* @__PURE__ */ __name(() => {
2125
+ if (released) return;
2126
+ released = true;
2127
+ this.active--;
2128
+ const count = this.tenants.get(tenant) - 1;
2129
+ if (count) this.tenants.set(tenant, count);
2130
+ else this.tenants.delete(tenant);
2131
+ this.drain();
2132
+ }, "release")
2128
2133
  };
2129
2134
  }
2130
- acquire(tenant, keys, signal) {
2131
- if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Database admission cancelled"));
2132
- const normalized = [
2133
- ...new Set(keys)
2134
- ].sort();
2135
- if (normalized.length > 64 || normalized.some((k) => typeof k !== "string" || k.length === 0 || k.length > 256)) return Promise.reject(new Error("conflictKeys must contain at most 64 nonempty strings of at most 256 characters"));
2136
- const scoped = normalized;
2137
- const globalFull = this.pending.length >= this.definition.queueLimit;
2138
- const tenantFull = this.pending.filter((w) => w.tenant === tenant).length >= this.definition.tenantQueueLimit;
2139
- const canStart = this.active < this.definition.concurrency && (this.tenants.get(tenant) ?? 0) < this.definition.tenantConcurrency && scoped.every((k) => !this.activeKeys.has(k) && !this.pending.some((w) => w.keys.includes(k)));
2140
- if (!canStart && (globalFull || tenantFull)) {
2141
- return Promise.reject(new TooManyRequests({
2135
+ drain() {
2136
+ while (this.active < this.options.concurrency && this.queued) {
2137
+ let tenant = this.pending.keys().next().value;
2138
+ if (this.pending.size > 1 && tenant === this.lastTenant) {
2139
+ const queue2 = this.pending.get(tenant);
2140
+ this.pending.delete(tenant);
2141
+ this.pending.set(tenant, queue2);
2142
+ tenant = this.pending.keys().next().value;
2143
+ }
2144
+ const queue = this.pending.get(tenant), waiter = queue.values().next().value;
2145
+ queue.delete(waiter);
2146
+ this.pending.delete(tenant);
2147
+ if (queue.size) this.pending.set(tenant, queue);
2148
+ this.queued--;
2149
+ waiter.cleanup();
2150
+ waiter.grant(this.take(tenant, waiter.entered));
2151
+ }
2152
+ }
2153
+ async acquire(claimsJson, signal) {
2154
+ if (signal?.aborted) {
2155
+ this.rejection("cancelled");
2156
+ throw signal.reason ?? new Error("Database admission cancelled");
2157
+ }
2158
+ const claims = JSON.parse(claimsJson);
2159
+ const value = this.options.tenantClaim ? claims[this.options.tenantClaim] : claims.sub;
2160
+ const tenant = typeof value === "string" && value.length ? value : "__anonymous__";
2161
+ const entered = performance.now();
2162
+ if (this.active < this.options.concurrency && !this.queued) return this.take(tenant, entered);
2163
+ if (this.queued >= this.limit) {
2164
+ this.rejection("full");
2165
+ throw new TooManyRequests({
2142
2166
  retryAfter: 1
2143
- }, `Database workload ${this.definition.name}: ${tenantFull ? "tenant" : "global"} queue is full`));
2167
+ }, "Database queue is full");
2144
2168
  }
2145
2169
  return new Promise((grant, reject) => {
2170
+ const queue = this.pending.get(tenant) ?? /* @__PURE__ */ new Set();
2171
+ if (!this.pending.has(tenant)) this.pending.set(tenant, queue);
2146
2172
  const waiter = {
2147
- tenant,
2148
- keys: scoped,
2149
- entered: performance.now(),
2150
2173
  grant,
2151
2174
  reject,
2152
- cleanup: /* @__PURE__ */ __name(() => {
2153
- }, "cleanup")
2175
+ entered,
2176
+ cleanup() {
2177
+ }
2154
2178
  };
2155
- const remove = /* @__PURE__ */ __name((reason2) => {
2156
- const index = this.pending.indexOf(waiter);
2157
- if (index === -1) return;
2158
- this.pending.splice(index, 1);
2179
+ const remove = /* @__PURE__ */ __name((reason2, error) => {
2180
+ if (!queue.delete(waiter)) return;
2181
+ if (!queue.size) this.pending.delete(tenant);
2182
+ this.queued--;
2159
2183
  waiter.cleanup();
2160
- reject(reason2);
2184
+ this.rejection(reason2);
2185
+ reject(error);
2161
2186
  this.drain();
2162
2187
  }, "remove");
2163
- const abort = /* @__PURE__ */ __name(() => remove(signal?.reason ?? new Error("Database admission cancelled")), "abort");
2164
- const timer = setTimeout(() => remove(new TooManyRequests({
2188
+ const abort = /* @__PURE__ */ __name(() => remove("cancelled", signal?.reason ?? new Error("Database admission cancelled")), "abort");
2189
+ const timer = setTimeout(() => remove("timeout", new TooManyRequests({
2165
2190
  retryAfter: 1
2166
- }, `Database workload ${this.definition.name}: queue deadline exceeded`)), this.definition.queueTimeoutMs);
2191
+ }, "Database queue deadline exceeded")), this.timeout);
2167
2192
  waiter.cleanup = () => {
2168
2193
  clearTimeout(timer);
2169
2194
  signal?.removeEventListener("abort", abort);
2170
2195
  };
2196
+ queue.add(waiter);
2197
+ this.queued++;
2171
2198
  signal?.addEventListener("abort", abort, {
2172
2199
  once: true
2173
2200
  });
2174
- this.pending.push(waiter);
2175
2201
  if (signal?.aborted) abort();
2176
2202
  else this.drain();
2177
2203
  });
2178
2204
  }
2179
- drain() {
2180
- if (this.draining) return;
2181
- this.draining = true;
2182
- try {
2183
- const blockedKeys = /* @__PURE__ */ new Set();
2184
- for (let index = 0; index < this.pending.length && this.active < this.definition.concurrency; ) {
2185
- const waiter = this.pending[index];
2186
- if ((this.tenants.get(waiter.tenant) ?? 0) >= this.definition.tenantConcurrency || waiter.keys.some((key2) => this.activeKeys.has(key2) || blockedKeys.has(key2))) {
2187
- for (const key2 of waiter.keys) blockedKeys.add(key2);
2188
- index++;
2189
- continue;
2190
- }
2191
- this.pending.splice(index, 1);
2192
- waiter.cleanup();
2193
- this.active++;
2194
- this.tenants.set(waiter.tenant, (this.tenants.get(waiter.tenant) ?? 0) + 1);
2195
- for (const key2 of waiter.keys) this.activeKeys.add(key2);
2196
- let released = false;
2197
- waiter.grant({
2198
- queueMs: performance.now() - waiter.entered,
2199
- release: /* @__PURE__ */ __name(() => {
2200
- if (released) return;
2201
- released = true;
2202
- this.active--;
2203
- const left = this.tenants.get(waiter.tenant) - 1;
2204
- if (left === 0) this.tenants.delete(waiter.tenant);
2205
- else this.tenants.set(waiter.tenant, left);
2206
- for (const key2 of waiter.keys) this.activeKeys.delete(key2);
2207
- this.drain();
2208
- }, "release")
2209
- });
2210
- }
2211
- } finally {
2212
- this.draining = false;
2205
+ };
2206
+ var databases = /* @__PURE__ */ new WeakMap();
2207
+ function databaseAdmission(owner, options) {
2208
+ let admission = databases.get(owner);
2209
+ if (!admission) {
2210
+ admission = new DatabaseAdmission(options ?? {
2211
+ concurrency: 10
2212
+ });
2213
+ databases.set(owner, admission);
2214
+ } else if (options) {
2215
+ for (const key2 of [
2216
+ "concurrency",
2217
+ "queueLimit",
2218
+ "queueTimeoutMs",
2219
+ "tenantClaim"
2220
+ ]) {
2221
+ if (options[key2] !== admission.options[key2]) throw new Error("Database admission cannot be reconfigured while its pool is shared");
2213
2222
  }
2214
2223
  }
2215
- };
2216
- var registries = /* @__PURE__ */ new WeakMap();
2217
- function workloadAdmission(owner, definition) {
2218
- validateWorkload(definition);
2219
- let registry = registries.get(owner);
2220
- if (!registry) {
2221
- registry = /* @__PURE__ */ new Map();
2222
- registries.set(owner, registry);
2223
- }
2224
- const existing = registry.get(definition.name);
2225
- if (existing) {
2226
- const d = existing.definition;
2227
- if (Object.keys(d).some((k) => d[k] !== definition[k])) throw new Error(`Workload ${definition.name} was already registered with different limits`);
2228
- return existing;
2229
- }
2230
- if (registry.size >= 64) throw new Error("At most 64 named database workloads may be registered per engine; declare and reuse them at module scope");
2231
- const admission = new WorkloadAdmission(Object.freeze({
2232
- ...definition
2233
- }));
2234
- registry.set(definition.name, admission);
2235
2224
  return admission;
2236
2225
  }
2237
- __name(workloadAdmission, "workloadAdmission");
2238
- function workloadTenant(definition, claimsJson) {
2239
- const claims = JSON.parse(claimsJson);
2240
- const value = claims[definition.tenantClaim] ?? claims.sub;
2241
- return typeof value === "string" && value.length > 0 ? value : "__anonymous__";
2242
- }
2243
- __name(workloadTenant, "workloadTenant");
2226
+ __name(databaseAdmission, "databaseAdmission");
2244
2227
 
2245
2228
  // src/engine/transaction-control.ts
2246
2229
  var TransactionControl = class {
@@ -2363,6 +2346,7 @@ var DatabaseTrace = class {
2363
2346
  sqlMs: 0,
2364
2347
  commitMs: 0,
2365
2348
  rollbackMs: 0,
2349
+ connectionMs: 0,
2366
2350
  commits: 0,
2367
2351
  commitFailures: 0,
2368
2352
  rollbackRequests: 0,
@@ -2374,7 +2358,7 @@ var DatabaseTrace = class {
2374
2358
  if (this.failure) throw this.failure;
2375
2359
  }
2376
2360
  time(key2, started) {
2377
- this.state[key2] += performance.now() - started;
2361
+ this.state[key2] = (this.state[key2] ?? 0) + performance.now() - started;
2378
2362
  }
2379
2363
  retry() {
2380
2364
  this.state.retries++;
@@ -2698,31 +2682,53 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
2698
2682
  fail = rej;
2699
2683
  });
2700
2684
  void parked.catch(() => void 0);
2701
- const openingAt = options.trace ? performance.now() : 0;
2702
- settled = sql.begin(async (tx) => {
2703
- options.trace?.time("poolMs", openingAt);
2704
- const setupAt = options.trace ? performance.now() : 0;
2705
- options.control?.check();
2706
- if (options.isolation !== void 0 || options.readOnly !== void 0) {
2707
- const isolation = options.isolation ?? "read committed";
2708
- validateAtomicOptions({
2709
- isolation,
2710
- readOnly: options.readOnly
2711
- });
2712
- await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
2685
+ settled = (async () => {
2686
+ const queuedAt = performance.now();
2687
+ let lease;
2688
+ try {
2689
+ lease = await (sql.admission ?? databaseAdmission(sql)).acquire(claimsJson, options.control?.signal);
2690
+ } finally {
2691
+ options.trace?.time("queueMs", queuedAt);
2713
2692
  }
2714
- if (options.control && sql.cancelBackend) {
2715
- const ticket = await tx.unsafe("SELECT pg_backend_pid() AS pid, backend_start::text AS started FROM pg_stat_activity WHERE pid=pg_backend_pid()");
2716
- if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
2717
- options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
2718
- options.control.check();
2693
+ const openingAt = performance.now();
2694
+ let connectedAt;
2695
+ try {
2696
+ options.control?.check();
2697
+ if (closed) throw new Error("Database transaction closed while waiting for admission");
2698
+ return await sql.begin(async (tx) => {
2699
+ connectedAt = performance.now();
2700
+ options.trace?.time("poolMs", openingAt);
2701
+ const setupAt = options.trace ? performance.now() : 0;
2702
+ options.control?.check();
2703
+ if (closed) throw new Error("Database transaction closed while waiting for its connection");
2704
+ if (options.isolation !== void 0 || options.readOnly !== void 0) {
2705
+ const isolation = options.isolation ?? "read committed";
2706
+ validateAtomicOptions({
2707
+ isolation,
2708
+ readOnly: options.readOnly
2709
+ });
2710
+ await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
2711
+ }
2712
+ if (options.control && sql.cancelBackend) {
2713
+ const ticket = await tx.unsafe(`WITH backend AS MATERIALIZED (
2714
+ SELECT pid, backend_start::text AS started FROM pg_catalog.pg_stat_get_activity(pg_catalog.pg_backend_pid())
2715
+ ) ${bindSql}, backend.pid, backend.started FROM backend`, bindParams);
2716
+ if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
2717
+ options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
2718
+ options.control.check();
2719
+ } else {
2720
+ const binding = tx.unsafe(bindSql, bindParams);
2721
+ await (options.control ? options.control.watch(binding) : binding);
2722
+ }
2723
+ options.trace?.time("setupMs", setupAt);
2724
+ resolveTx2(scoped(tx));
2725
+ await parked;
2726
+ });
2727
+ } finally {
2728
+ if (connectedAt !== void 0) options.trace?.time("connectionMs", connectedAt);
2729
+ lease.release();
2719
2730
  }
2720
- const binding = tx.unsafe(bindSql, bindParams);
2721
- await (options.control ? options.control.watch(binding) : binding);
2722
- options.trace?.time("setupMs", setupAt);
2723
- resolveTx2(scoped(tx));
2724
- await parked;
2725
- }).catch((e) => {
2731
+ })().catch((e) => {
2726
2732
  rejectTx(e);
2727
2733
  throw e;
2728
2734
  });
@@ -2739,10 +2745,13 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
2739
2745
  if (pendingSavepoints.size) await Promise.allSettled([
2740
2746
  ...pendingSavepoints
2741
2747
  ]);
2742
- options.control?.check();
2743
2748
  options.trace?.check();
2749
+ if (!opening) {
2750
+ closed = true;
2751
+ return;
2752
+ }
2753
+ options.control?.check();
2744
2754
  closed = true;
2745
- if (!opening) return;
2746
2755
  const started = options.trace ? performance.now() : 0;
2747
2756
  release();
2748
2757
  try {
@@ -2869,10 +2878,11 @@ function asTableRows(table, rows, withSpec, partial = false) {
2869
2878
  const nested = typeof spec === "object" ? spec.with : void 0;
2870
2879
  for (const row of result) {
2871
2880
  const value = row[rel.name];
2872
- if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested);
2881
+ const selected = typeof spec === "object" && "select" in spec && spec.select !== void 0;
2882
+ if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested, selected);
2873
2883
  else if (value && typeof value === "object") row[rel.name] = asTableRows(rel.to, [
2874
2884
  value
2875
- ], nested)[0];
2885
+ ], nested, selected)[0];
2876
2886
  }
2877
2887
  }
2878
2888
  for (const row of result) foldCan(table, row, partial);
@@ -3816,6 +3826,9 @@ function withClauses(table, spec, add, caller, parentAlias = "t", depth = 0, cou
3816
3826
  const i = ++counter.value;
3817
3827
  const childAlias = `w${i}`;
3818
3828
  const childKnown = schemaColumns(rel.to);
3829
+ if (o["select"] !== void 0 && (!Array.isArray(o["select"]) || o["select"].length === 0)) {
3830
+ throw new Error(`${caller}(${table}): with.${name}.select must be a non-empty array of columns`);
3831
+ }
3819
3832
  const childCols = Array.isArray(o["select"]) && o["select"].length > 0 ? o["select"] : childKnown !== null ? [
3820
3833
  ...childKnown
3821
3834
  ] : [];
@@ -3919,7 +3932,10 @@ function pageSchema(table) {
3919
3932
  }
3920
3933
  __name(pageSchema, "pageSchema");
3921
3934
  function createOps(tx, context = {}) {
3922
- const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
3935
+ const at = /* @__PURE__ */ __name(() => {
3936
+ const target = typeof tx === "function" ? tx() : tx;
3937
+ return target instanceof Promise ? target.then(resolveTx) : resolveTx(target);
3938
+ }, "at");
3923
3939
  const ops = {
3924
3940
  diagnostics() {
3925
3941
  return context.trace?.snapshot ?? null;
@@ -4370,22 +4386,6 @@ function createOps(tx, context = {}) {
4370
4386
  ]);
4371
4387
  return rows[0] ? asTableRow(table, rows[0]) : null;
4372
4388
  },
4373
- compileQuery(query) {
4374
- assertReadContract(query, tableDefOf(query.table)?.columns);
4375
- return compileFindMany(query.table, query.where, query.options);
4376
- },
4377
- async readQuery(query) {
4378
- const compiled = ops.compileQuery(query);
4379
- return asTableRows(query.table, await (await at()).unsafe(compiled.sql, compiled.params, {
4380
- name: query.name
4381
- }), void 0, true);
4382
- },
4383
- async explainQuery(query) {
4384
- const compiled = ops.compileQuery(query);
4385
- return await (await at()).unsafe(`EXPLAIN (FORMAT JSON) ${compiled.sql}`, compiled.params, {
4386
- name: `${query.name}.explain`
4387
- });
4388
- },
4389
4389
  async findMany(table, query = {}, opts = {}) {
4390
4390
  const compiled = compileFindMany(table, query, opts);
4391
4391
  return asTableRows(table, await (await at()).unsafe(compiled.sql, compiled.params), opts.with, opts.select !== void 0);
@@ -4920,7 +4920,7 @@ function createOps(tx, context = {}) {
4920
4920
  if (options.mode === "compiled") throw new Error(`Command cannot compile: ${compiled.reason}. Use auto/sequential mode or installCommandExecutor at boot.`);
4921
4921
  return ops.txPlan(plan);
4922
4922
  }
4923
- if (compiled.steps.length === 0) return {
4923
+ if (compiled.steps.length === 0 && !compiled.pipeline?.length) return {
4924
4924
  results: []
4925
4925
  };
4926
4926
  if (compiled.pipeline) {
@@ -5193,6 +5193,7 @@ function diagnosingDriver(sql, surface = "service") {
5193
5193
  const state = sqlstateOf(e);
5194
5194
  if (state === SQLSTATE.serializationFailure) return markEngineRaised(new SerializationFailure(state));
5195
5195
  if (state === SQLSTATE.deadlockDetected) return markEngineRaised(new DeadlockDetected(state));
5196
+ if (state === SQLSTATE.queryCancelled) return markEngineRaised(new HttpError(503, "database_query_cancelled", "Database statement was cancelled"));
5196
5197
  if (surface === "service" && isForeignKeyViolation(e)) {
5197
5198
  return new Error(`Database.$asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`);
5198
5199
  }
@@ -5219,6 +5220,7 @@ function diagnosingDriver(sql, surface = "service") {
5219
5220
  }
5220
5221
  }), "wrapTx");
5221
5222
  return {
5223
+ admission: sql.admission ?? databaseAdmission(sql),
5222
5224
  unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
5223
5225
  cancelBackend: sql.cancelBackend?.bind(sql),
5224
5226
  begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))).catch((e) => {
@@ -5227,9 +5229,12 @@ function diagnosingDriver(sql, surface = "service") {
5227
5229
  };
5228
5230
  }
5229
5231
  __name(diagnosingDriver, "diagnosingDriver");
5230
- function createRequestDatabase(sql, identity, diagnostics) {
5232
+ function createRequestDatabase(sql, identity, diagnostics, signal) {
5231
5233
  const trace = diagnostics === void 0 ? void 0 : new DatabaseTrace(diagnostics);
5234
+ const requestControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
5232
5235
  let requestClosed = false;
5236
+ let authorized = false, applicationTouched = false;
5237
+ let authorizationRelease;
5233
5238
  const contextFor = /* @__PURE__ */ __name((role) => {
5234
5239
  let scope;
5235
5240
  return {
@@ -5248,19 +5253,21 @@ function createRequestDatabase(sql, identity, diagnostics) {
5248
5253
  validateAtomicOptions(options);
5249
5254
  if (requestClosed) throw new Error("Database request has settled; it cannot open another transaction");
5250
5255
  if (atomicScope.getStore() !== void 0) throw new Error("Nested Database.$atomic is not supported; use tx.$attempt for a savepoint");
5256
+ if (authorized && !applicationTouched && tx.opened) {
5257
+ authorizationRelease ??= (async () => {
5258
+ await tx.commit();
5259
+ tx = requestTransaction();
5260
+ })();
5261
+ }
5262
+ if (authorizationRelease) await authorizationRelease;
5251
5263
  const retries = retryBudget(options.retry);
5252
- const control = options.timeoutMs !== void 0 || options.signal !== void 0 ? new TransactionControl(options.timeoutMs, options.signal) : void 0;
5253
- let lease;
5264
+ const atomicSignal = options.signal && signal ? AbortSignal.any([
5265
+ options.signal,
5266
+ signal
5267
+ ]) : options.signal ?? signal;
5268
+ const control = options.timeoutMs !== void 0 || atomicSignal !== void 0 ? new TransactionControl(options.timeoutMs, atomicSignal) : void 0;
5254
5269
  try {
5255
5270
  control?.check();
5256
- if (options.workload) {
5257
- const queuedAt = trace ? performance.now() : 0;
5258
- try {
5259
- lease = await workloadAdmission(sql, options.workload).acquire(workloadTenant(options.workload, identity.claimsJson), options.conflictKeys ?? [], control?.signal);
5260
- } finally {
5261
- trace?.time("queueMs", queuedAt);
5262
- }
5263
- }
5264
5271
  for (let attempt = 0; ; attempt++) {
5265
5272
  control?.check();
5266
5273
  const timeouts = [
@@ -5293,19 +5300,23 @@ function createRequestDatabase(sql, identity, diagnostics) {
5293
5300
  }
5294
5301
  } finally {
5295
5302
  control?.dispose();
5296
- lease?.release();
5297
5303
  }
5298
5304
  }, "atomic");
5299
- const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
5300
- trace
5301
- });
5305
+ const requestTransaction = /* @__PURE__ */ __name(() => createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
5306
+ trace,
5307
+ control: requestControl
5308
+ }), "requestTransaction");
5309
+ let tx = requestTransaction();
5302
5310
  let serviceTx = null;
5311
+ let serviceControl;
5303
5312
  let serviceClient = null;
5304
5313
  const asService = /* @__PURE__ */ __name(() => {
5305
5314
  if (serviceClient === null) {
5315
+ serviceControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
5306
5316
  serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
5307
5317
  lockTimeout: SERVICE_LOCK_TIMEOUT,
5308
- trace
5318
+ trace,
5319
+ control: serviceControl
5309
5320
  });
5310
5321
  serviceClient = Object.assign(withTables(createOps(serviceTx, serviceContext)), {
5311
5322
  atomic: atomic(identity.serviceRole, "service")
@@ -5314,16 +5325,24 @@ function createRequestDatabase(sql, identity, diagnostics) {
5314
5325
  return serviceClient;
5315
5326
  }, "asService");
5316
5327
  return {
5317
- client: Object.assign(withTables(createOps(tx, userContext)), {
5328
+ client: Object.assign(withTables(createOps(() => {
5329
+ if (authorized) applicationTouched = true;
5330
+ return authorizationRelease ? authorizationRelease.then(() => tx) : tx;
5331
+ }, userContext)), {
5318
5332
  asService,
5319
5333
  atomic: atomic(identity.role, "user")
5320
5334
  }),
5335
+ authorizationComplete() {
5336
+ authorized = true;
5337
+ },
5321
5338
  async commit() {
5322
5339
  requestClosed = true;
5323
5340
  try {
5324
5341
  await tx.commit();
5325
5342
  await serviceTx?.commit();
5326
5343
  trace?.complete("committed");
5344
+ requestControl?.dispose();
5345
+ serviceControl?.dispose();
5327
5346
  } catch (error) {
5328
5347
  trace?.failed();
5329
5348
  throw error;
@@ -5333,6 +5352,8 @@ function createRequestDatabase(sql, identity, diagnostics) {
5333
5352
  requestClosed = true;
5334
5353
  await tx.rollback(reason2);
5335
5354
  await serviceTx?.rollback(reason2);
5355
+ requestControl?.dispose();
5356
+ serviceControl?.dispose();
5336
5357
  trace?.complete("failed");
5337
5358
  }
5338
5359
  };
@@ -5640,7 +5661,7 @@ function compileCommand(plan) {
5640
5661
  };
5641
5662
  });
5642
5663
  steps.push({
5643
- chunks: parameterChunks(sql),
5664
+ sql,
5644
5665
  bindings,
5645
5666
  guard: op.guard
5646
5667
  });
@@ -5663,11 +5684,15 @@ function compileCommand(plan) {
5663
5684
  rows_affected: 1
5664
5685
  });
5665
5686
  }
5687
+ if (!hasRefs) return {
5688
+ steps: [],
5689
+ pipeline
5690
+ };
5666
5691
  return {
5667
- steps,
5668
- ...hasRefs ? {} : {
5669
- pipeline
5670
- }
5692
+ steps: steps.map(({ sql, ...step }) => sql === void 0 ? step : {
5693
+ ...step,
5694
+ chunks: parameterChunks(sql)
5695
+ })
5671
5696
  };
5672
5697
  }
5673
5698
  __name(compileCommand, "compileCommand");
@@ -6130,9 +6155,11 @@ function makeSseWriter(opts) {
6130
6155
  if (!begun) {
6131
6156
  begun = true;
6132
6157
  chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
6158
+ void chain.catch(() => void 0);
6133
6159
  return;
6134
6160
  }
6135
6161
  chain = chain.then(() => opts.enqueue(frame));
6162
+ void chain.catch(() => void 0);
6136
6163
  },
6137
6164
  started() {
6138
6165
  return begun;
@@ -6727,6 +6754,23 @@ async function createApp(opts) {
6727
6754
  throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
6728
6755
  }
6729
6756
  const sql = opts.sql ?? await defaultSqlDriver(config);
6757
+ const admission = sql.admission ?? databaseAdmission(sql, {
6758
+ concurrency: config.poolMax,
6759
+ ...config.databaseAdmission
6760
+ });
6761
+ const inFlight = /* @__PURE__ */ new Set();
6762
+ let shuttingDown;
6763
+ let accepting = true;
6764
+ async function serving(run) {
6765
+ const work = run();
6766
+ inFlight.add(work);
6767
+ try {
6768
+ return await work;
6769
+ } finally {
6770
+ inFlight.delete(work);
6771
+ }
6772
+ }
6773
+ __name(serving, "serving");
6730
6774
  await sql.unsafe("select 1");
6731
6775
  setSchema(opts.schemas ?? []);
6732
6776
  setCursorKey(config.cursorSecret || config.serviceRoleKey || null);
@@ -6793,6 +6837,19 @@ async function createApp(opts) {
6793
6837
  }
6794
6838
  }
6795
6839
  __name(runInServiceScope, "runInServiceScope");
6840
+ function errorResponse(err, endpoint, requestId) {
6841
+ if (isHttpError(err)) {
6842
+ if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
6843
+ log.error(`[engine] unhandled ${err.error} in ${endpoint}`, err);
6844
+ }
6845
+ return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
6846
+ data: err.data
6847
+ } : void 0);
6848
+ }
6849
+ log.error(`[engine] unhandled error in ${endpoint}`, err);
6850
+ return envelope("internal_error", "The request could not be completed", 500, requestId);
6851
+ }
6852
+ __name(errorResponse, "errorResponse");
6796
6853
  async function handleAuthorize(req, requestId) {
6797
6854
  if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
6798
6855
  return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
@@ -6815,7 +6872,7 @@ async function createApp(opts) {
6815
6872
  ...config.databaseDiagnostics,
6816
6873
  endpoint: "upload authorization",
6817
6874
  requestId
6818
- });
6875
+ }, req.signal);
6819
6876
  try {
6820
6877
  const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
6821
6878
  if (refusal) {
@@ -6825,7 +6882,7 @@ async function createApp(opts) {
6825
6882
  await uploadDb.commit();
6826
6883
  } catch (err) {
6827
6884
  await uploadDb.rollback(err);
6828
- throw err;
6885
+ return errorResponse(err, "upload authorization", requestId);
6829
6886
  }
6830
6887
  const grant = grantFor(target?.entry, {
6831
6888
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -6867,256 +6924,257 @@ async function createApp(opts) {
6867
6924
  ...meta.options.databaseBudget,
6868
6925
  endpoint: hit.entry.id,
6869
6926
  requestId
6870
- });
6871
- const rolesOfCaller = makeRoleResolver(db.client);
6872
- let dispatched = false;
6873
- const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
6874
- if (dispatched) return;
6875
- await db.rollback(new Error("request refused before dispatch"));
6876
- }, "settleBeforeRefusal");
6877
- const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
6878
- if (refusal) {
6879
- await settleBeforeRefusal();
6880
- return refusal;
6881
- }
6882
- if (claims && spec.verifiedEmail && claims.email_verified !== true) {
6883
- await settleBeforeRefusal();
6884
- return envelope("email_not_verified", "A verified email address is required", 403, requestId);
6885
- }
6886
- let parsedBody;
6887
- let bodyRead = false;
6888
- let rawBody = null;
6889
- if (req.method !== "GET" && req.method !== "HEAD") {
6890
- const declared = Number(req.headers.get("content-length") ?? "");
6891
- if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
6927
+ }, req.signal);
6928
+ try {
6929
+ const rolesOfCaller = makeRoleResolver(db.client);
6930
+ let dispatched = false;
6931
+ const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
6932
+ if (dispatched) return;
6933
+ await db.rollback(new Error("request refused before dispatch"));
6934
+ }, "settleBeforeRefusal");
6935
+ const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
6936
+ if (refusal) {
6937
+ await settleBeforeRefusal();
6938
+ return refusal;
6939
+ }
6940
+ if (claims && spec.verifiedEmail && claims.email_verified !== true) {
6941
+ await settleBeforeRefusal();
6942
+ return envelope("email_not_verified", "A verified email address is required", 403, requestId);
6943
+ }
6944
+ let parsedBody;
6945
+ let bodyRead = false;
6946
+ let rawBody = null;
6947
+ if (req.method !== "GET" && req.method !== "HEAD") {
6948
+ const declared = Number(req.headers.get("content-length") ?? "");
6949
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
6950
+ await settleBeforeRefusal();
6951
+ return new Response(JSON.stringify({
6952
+ error: "payload_too_large",
6953
+ error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6954
+ status: 413,
6955
+ request_id: requestId
6956
+ }), {
6957
+ status: 413,
6958
+ headers: JSON_HEADERS
6959
+ });
6960
+ }
6961
+ const buf = await req.arrayBuffer().catch(() => null);
6962
+ if (buf && buf.byteLength > MAX_BODY_BYTES) {
6963
+ await settleBeforeRefusal();
6964
+ return new Response(JSON.stringify({
6965
+ error: "payload_too_large",
6966
+ error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6967
+ status: 413,
6968
+ request_id: requestId
6969
+ }), {
6970
+ status: 413,
6971
+ headers: JSON_HEADERS
6972
+ });
6973
+ }
6974
+ if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
6975
+ }
6976
+ const readParsedBody = /* @__PURE__ */ __name(() => {
6977
+ if (bodyRead) return parsedBody;
6978
+ bodyRead = true;
6979
+ if (!rawBody) {
6980
+ parsedBody = {};
6981
+ return parsedBody;
6982
+ }
6983
+ try {
6984
+ parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
6985
+ } catch {
6986
+ parsedBody = {};
6987
+ }
6988
+ return parsedBody;
6989
+ }, "readParsedBody");
6990
+ const attest = await attestGate(req, rawBody, {
6991
+ userId
6992
+ });
6993
+ if (attest.observation) observations.record(attest.observation);
6994
+ if (attest.outcome === "reject") {
6995
+ const headers = {
6996
+ ...JSON_HEADERS
6997
+ };
6998
+ if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
6892
6999
  await settleBeforeRefusal();
6893
7000
  return new Response(JSON.stringify({
6894
- error: "payload_too_large",
6895
- error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6896
- status: 413,
7001
+ error: attest.reason,
7002
+ error_description: "This endpoint requires a verified app instance",
7003
+ status: 401,
6897
7004
  request_id: requestId
6898
7005
  }), {
6899
- status: 413,
6900
- headers: JSON_HEADERS
7006
+ status: 401,
7007
+ headers
6901
7008
  });
6902
7009
  }
6903
- const buf = await req.arrayBuffer().catch(() => null);
6904
- if (buf && buf.byteLength > MAX_BODY_BYTES) {
7010
+ if (attest.device && claims) attestedDevice = attest.device;
7011
+ attestChallengeOut = attest.nextChallenge;
7012
+ const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
7013
+ if (retryAfter !== null) {
6905
7014
  await settleBeforeRefusal();
6906
7015
  return new Response(JSON.stringify({
6907
- error: "payload_too_large",
6908
- error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6909
- status: 413,
6910
- request_id: requestId
7016
+ error: "too_many_requests",
7017
+ error_description: "Rate limit exceeded for this endpoint",
7018
+ status: 429,
7019
+ request_id: requestId,
7020
+ data: {
7021
+ retryAfter
7022
+ }
6911
7023
  }), {
6912
- status: 413,
6913
- headers: JSON_HEADERS
7024
+ status: 429,
7025
+ headers: {
7026
+ ...JSON_HEADERS,
7027
+ "retry-after": String(retryAfter)
7028
+ }
6914
7029
  });
6915
7030
  }
6916
- if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
6917
- }
6918
- const readParsedBody = /* @__PURE__ */ __name(() => {
6919
- if (bodyRead) return parsedBody;
6920
- bodyRead = true;
6921
- if (!rawBody) {
6922
- parsedBody = {};
6923
- return parsedBody;
6924
- }
6925
- try {
6926
- parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
6927
- } catch {
6928
- parsedBody = {};
6929
- }
6930
- return parsedBody;
6931
- }, "readParsedBody");
6932
- const attest = await attestGate(req, rawBody, {
6933
- userId
6934
- });
6935
- if (attest.observation) observations.record(attest.observation);
6936
- if (attest.outcome === "reject") {
6937
- const headers = {
6938
- ...JSON_HEADERS
6939
- };
6940
- if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
6941
- await settleBeforeRefusal();
6942
- return new Response(JSON.stringify({
6943
- error: attest.reason,
6944
- error_description: "This endpoint requires a verified app instance",
6945
- status: 401,
6946
- request_id: requestId
6947
- }), {
6948
- status: 401,
6949
- headers
7031
+ let completionUploadId = null;
7032
+ const args = [];
7033
+ let sseEnqueue = null;
7034
+ let sseSettle = /* @__PURE__ */ __name(async () => {
7035
+ }, "sseSettle");
7036
+ const sseWriter = makeSseWriter({
7037
+ enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
7038
+ onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
6950
7039
  });
6951
- }
6952
- if (attest.device && claims) attestedDevice = attest.device;
6953
- attestChallengeOut = attest.nextChallenge;
6954
- const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
6955
- if (retryAfter !== null) {
6956
- await settleBeforeRefusal();
6957
- return new Response(JSON.stringify({
6958
- error: "too_many_requests",
6959
- error_description: "Rate limit exceeded for this endpoint",
6960
- status: 429,
6961
- request_id: requestId,
6962
- data: {
6963
- retryAfter
6964
- }
6965
- }), {
6966
- status: 429,
6967
- headers: {
6968
- ...JSON_HEADERS,
6969
- "retry-after": String(retryAfter)
6970
- }
6971
- });
6972
- }
6973
- let completionUploadId = null;
6974
- const args = [];
6975
- let sseEnqueue = null;
6976
- let sseSettle = /* @__PURE__ */ __name(async () => {
6977
- }, "sseSettle");
6978
- const sseWriter = makeSseWriter({
6979
- enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
6980
- onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
6981
- });
6982
- for (const p of meta.params ?? []) {
6983
- switch (p.kind) {
6984
- case "body": {
6985
- readParsedBody();
6986
- const r = p.schema.safeParse(parsedBody);
6987
- if (!r.success) {
6988
- await settleBeforeRefusal();
6989
- return envelope("bad_request", "Request body failed validation", 400, requestId, {
6990
- data: {
6991
- fields: fieldErrors(r.error)
6992
- }
6993
- });
6994
- }
6995
- args[p.index] = r.data;
6996
- break;
6997
- }
6998
- case "query": {
6999
- const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
7000
- if (!r.success) {
7001
- await settleBeforeRefusal();
7002
- return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
7003
- data: {
7004
- fields: fieldErrors(r.error)
7005
- }
7006
- });
7007
- }
7008
- args[p.index] = r.data;
7009
- break;
7010
- }
7011
- case "param":
7012
- args[p.index] = hit.params[p.name];
7013
- break;
7014
- case "headers": {
7015
- const raw = Object.fromEntries(req.headers);
7016
- if (!p.schema) {
7017
- args[p.index] = raw;
7040
+ for (const p of meta.params ?? []) {
7041
+ switch (p.kind) {
7042
+ case "body": {
7043
+ readParsedBody();
7044
+ const r = p.schema.safeParse(parsedBody);
7045
+ if (!r.success) {
7046
+ await settleBeforeRefusal();
7047
+ return envelope("bad_request", "Request body failed validation", 400, requestId, {
7048
+ data: {
7049
+ fields: fieldErrors(r.error)
7050
+ }
7051
+ });
7052
+ }
7053
+ args[p.index] = r.data;
7018
7054
  break;
7019
7055
  }
7020
- const r = p.schema.safeParse(headersFor(raw, p.schema));
7021
- if (!r.success) {
7022
- await settleBeforeRefusal();
7023
- return envelope("bad_request", "Request headers failed validation", 400, requestId, {
7024
- data: {
7025
- fields: fieldErrors(r.error)
7026
- }
7027
- });
7028
- }
7029
- args[p.index] = r.data;
7030
- break;
7031
- }
7032
- case "user":
7033
- case "optionalUser":
7034
- args[p.index] = claims ? {
7035
- id: userId,
7036
- email: claims.email,
7037
- role: claims.role,
7038
- emailVerified: claims.email_verified === true,
7039
- metadata: claims.metadata ?? {},
7040
- // The caller's APPLICATION roles, from `auth.user_roles` — the
7041
- // same resolution the gate used, so the handler cannot read a
7042
- // different set than the one it was admitted on. Never from a
7043
- // claim: a revoked role has to bite on the next request, and a
7044
- // token says only what was true when it was minted.
7045
- roles: await rolesOfCaller(),
7046
- // Server-owned and only ever filled from a verdict palauth
7047
- // returned. Never from a raw header, never from JWT metadata —
7048
- // the type has said so since it was written and this is the
7049
- // first writer.
7050
- device: attestedDevice
7051
- } : null;
7052
- break;
7053
- case "uploadedObject": {
7054
- if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
7055
- await settleBeforeRefusal();
7056
- return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
7057
- }
7058
- readParsedBody();
7059
- const envelopeIn = parsedBody;
7060
- if (!envelopeIn?.uploadedObject) {
7061
- await settleBeforeRefusal();
7062
- return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
7056
+ case "query": {
7057
+ const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
7058
+ if (!r.success) {
7059
+ await settleBeforeRefusal();
7060
+ return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
7061
+ data: {
7062
+ fields: fieldErrors(r.error)
7063
+ }
7064
+ });
7065
+ }
7066
+ args[p.index] = r.data;
7067
+ break;
7063
7068
  }
7064
- const uploadId = envelopeIn.uploadedObject.uploadId;
7065
- if (typeof uploadId === "string" && uploadId !== "") {
7066
- const already = completions.recall(uploadId);
7067
- if (already) {
7069
+ case "param":
7070
+ args[p.index] = hit.params[p.name];
7071
+ break;
7072
+ case "headers": {
7073
+ const raw = Object.fromEntries(req.headers);
7074
+ if (!p.schema) {
7075
+ args[p.index] = raw;
7076
+ break;
7077
+ }
7078
+ const r = p.schema.safeParse(headersFor(raw, p.schema));
7079
+ if (!r.success) {
7068
7080
  await settleBeforeRefusal();
7069
- return new Response(already.body, {
7070
- status: already.status,
7071
- headers: already.contentType ? {
7072
- "content-type": already.contentType
7073
- } : void 0
7081
+ return envelope("bad_request", "Request headers failed validation", 400, requestId, {
7082
+ data: {
7083
+ fields: fieldErrors(r.error)
7084
+ }
7074
7085
  });
7075
7086
  }
7076
- completionUploadId = uploadId;
7087
+ args[p.index] = r.data;
7088
+ break;
7077
7089
  }
7078
- args[p.index] = envelopeIn.uploadedObject;
7079
- parsedBody = envelopeIn.body ?? {};
7080
- break;
7090
+ case "user":
7091
+ case "optionalUser":
7092
+ args[p.index] = claims ? {
7093
+ id: userId,
7094
+ email: claims.email,
7095
+ role: claims.role,
7096
+ emailVerified: claims.email_verified === true,
7097
+ metadata: claims.metadata ?? {},
7098
+ // The caller's APPLICATION roles, from `auth.user_roles` — the
7099
+ // same resolution the gate used, so the handler cannot read a
7100
+ // different set than the one it was admitted on. Never from a
7101
+ // claim: a revoked role has to bite on the next request, and a
7102
+ // token says only what was true when it was minted.
7103
+ roles: await rolesOfCaller(),
7104
+ // Server-owned and only ever filled from a verdict palauth
7105
+ // returned. Never from a raw header, never from JWT metadata —
7106
+ // the type has said so since it was written and this is the
7107
+ // first writer.
7108
+ device: attestedDevice
7109
+ } : null;
7110
+ break;
7111
+ case "uploadedObject": {
7112
+ if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
7113
+ await settleBeforeRefusal();
7114
+ return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
7115
+ }
7116
+ readParsedBody();
7117
+ const envelopeIn = parsedBody;
7118
+ if (!envelopeIn?.uploadedObject) {
7119
+ await settleBeforeRefusal();
7120
+ return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
7121
+ }
7122
+ const uploadId = envelopeIn.uploadedObject.uploadId;
7123
+ if (typeof uploadId === "string" && uploadId !== "") {
7124
+ const already = completions.recall(uploadId);
7125
+ if (already) {
7126
+ await settleBeforeRefusal();
7127
+ return new Response(already.body, {
7128
+ status: already.status,
7129
+ headers: already.contentType ? {
7130
+ "content-type": already.contentType
7131
+ } : void 0
7132
+ });
7133
+ }
7134
+ completionUploadId = uploadId;
7135
+ }
7136
+ args[p.index] = envelopeIn.uploadedObject;
7137
+ parsedBody = envelopeIn.body ?? {};
7138
+ break;
7139
+ }
7140
+ case "requestId":
7141
+ args[p.index] = requestId;
7142
+ break;
7143
+ case "traceId":
7144
+ args[p.index] = requestId;
7145
+ break;
7146
+ case "client":
7147
+ args[p.index] = {
7148
+ sdkVersion: req.headers.get("x-palbase-sdk-version"),
7149
+ appVersion: req.headers.get("x-palbase-client-version"),
7150
+ platform: req.headers.get("x-platform"),
7151
+ osVersion: req.headers.get("x-os-version")
7152
+ };
7153
+ break;
7154
+ case "req":
7155
+ args[p.index] = req;
7156
+ break;
7157
+ // `@Signal()` — the raw request's AbortSignal, which enters the aborted
7158
+ // state when the client disconnects. Measured on Bun: an infinite
7159
+ // producer guarded by it stops within a few frames of the client dying.
7160
+ // It is deliberately NOT reachable through `@Req()`: PBRequest carries
7161
+ // request-scoped data and no signal.
7162
+ case "signal":
7163
+ args[p.index] = req.signal;
7164
+ break;
7165
+ // `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
7166
+ // on any other route it is an inert writer whose frames go nowhere,
7167
+ // which is the same shape `@UploadedObject()` has off an `@Upload`
7168
+ // route.
7169
+ case "sseOut":
7170
+ args[p.index] = sseWriter;
7171
+ break;
7172
+ default:
7173
+ args[p.index] = void 0;
7081
7174
  }
7082
- case "requestId":
7083
- args[p.index] = requestId;
7084
- break;
7085
- case "traceId":
7086
- args[p.index] = requestId;
7087
- break;
7088
- case "client":
7089
- args[p.index] = {
7090
- sdkVersion: req.headers.get("x-palbase-sdk-version"),
7091
- appVersion: req.headers.get("x-palbase-client-version"),
7092
- platform: req.headers.get("x-platform"),
7093
- osVersion: req.headers.get("x-os-version")
7094
- };
7095
- break;
7096
- case "req":
7097
- args[p.index] = req;
7098
- break;
7099
- // `@Signal()` — the raw request's AbortSignal, which enters the aborted
7100
- // state when the client disconnects. Measured on Bun: an infinite
7101
- // producer guarded by it stops within a few frames of the client dying.
7102
- // It is deliberately NOT reachable through `@Req()`: PBRequest carries
7103
- // request-scoped data and no signal.
7104
- case "signal":
7105
- args[p.index] = req.signal;
7106
- break;
7107
- // `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
7108
- // on any other route it is an inert writer whose frames go nowhere,
7109
- // which is the same shape `@UploadedObject()` has off an `@Upload`
7110
- // route.
7111
- case "sseOut":
7112
- args[p.index] = sseWriter;
7113
- break;
7114
- default:
7115
- args[p.index] = void 0;
7116
7175
  }
7117
- }
7118
- dispatched = true;
7119
- try {
7176
+ db.authorizationComplete?.();
7177
+ dispatched = true;
7120
7178
  const services = buildServices(db);
7121
7179
  if (meta.options?.sseConfig !== void 0) {
7122
7180
  let sseController;
@@ -7132,11 +7190,19 @@ async function createApp(opts) {
7132
7190
  } catch {
7133
7191
  }
7134
7192
  }, "sseEnqueue");
7135
- let sawFirstFrame;
7136
- const firstFrame = new Promise((r) => sawFirstFrame = r);
7193
+ let sawFirstFrame, refuseFirstFrame;
7194
+ const firstFrame = new Promise((resolve, reject) => {
7195
+ sawFirstFrame = resolve;
7196
+ refuseFirstFrame = reject;
7197
+ });
7137
7198
  sseSettle = /* @__PURE__ */ __name(async () => {
7138
- await db.commit();
7139
- sawFirstFrame();
7199
+ try {
7200
+ await db.commit();
7201
+ sawFirstFrame();
7202
+ } catch (error) {
7203
+ refuseFirstFrame(error);
7204
+ throw error;
7205
+ }
7140
7206
  }, "sseSettle");
7141
7207
  const guarded = {
7142
7208
  ...services,
@@ -7177,6 +7243,7 @@ async function createApp(opts) {
7177
7243
  status: 204
7178
7244
  });
7179
7245
  }
7246
+ await firstFrame;
7180
7247
  void (async () => {
7181
7248
  await ran;
7182
7249
  await sseWriter.drained();
@@ -7210,15 +7277,16 @@ async function createApp(opts) {
7210
7277
  }
7211
7278
  return method.apply(hit.entry.instance, args);
7212
7279
  });
7213
- await db.commit();
7214
7280
  if (meta.returnSchema) {
7215
7281
  const v = meta.returnSchema.safeParse(result);
7216
7282
  if (!v.success) {
7217
7283
  log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
7284
+ await db.rollback(new Error("output_invalid"));
7218
7285
  return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
7219
7286
  }
7220
7287
  }
7221
- if (result === void 0 || result === null) {
7288
+ if (result === void 0 || result === null && !meta.returnSchema) {
7289
+ await db.commit();
7222
7290
  if (completionUploadId) {
7223
7291
  completions.remember(completionUploadId, {
7224
7292
  status: 204,
@@ -7231,6 +7299,7 @@ async function createApp(opts) {
7231
7299
  });
7232
7300
  }
7233
7301
  const payload = JSON.stringify(result);
7302
+ await db.commit();
7234
7303
  if (completionUploadId) {
7235
7304
  completions.remember(completionUploadId, {
7236
7305
  status: 200,
@@ -7247,16 +7316,7 @@ async function createApp(opts) {
7247
7316
  });
7248
7317
  } catch (err) {
7249
7318
  await db.rollback(err);
7250
- if (isHttpError(err)) {
7251
- if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
7252
- log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
7253
- }
7254
- return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
7255
- data: err.data
7256
- } : void 0);
7257
- }
7258
- log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
7259
- return envelope("internal_error", "The request could not be completed", 500, requestId);
7319
+ return errorResponse(err, hit.entry.id, requestId);
7260
7320
  }
7261
7321
  }
7262
7322
  __name(handle, "handle");
@@ -7269,15 +7329,24 @@ async function createApp(opts) {
7269
7329
  throw err;
7270
7330
  }
7271
7331
  return {
7272
- handle,
7332
+ handle: /* @__PURE__ */ __name((req) => !accepting ? Promise.resolve(envelope("runtime_draining", "This application is shutting down", 503, `req_${crypto.randomUUID()}`)) : serving(() => handle(req)), "handle"),
7333
+ get databasePressure() {
7334
+ return admission.snapshot;
7335
+ },
7273
7336
  routes,
7274
7337
  config,
7275
7338
  container: liveContainer,
7276
- runInServiceScope,
7277
- async shutdown() {
7278
- await runShutdownHooks();
7279
- await closeDriver(sql);
7280
- released = true;
7339
+ runInServiceScope: /* @__PURE__ */ __name((fn) => !accepting ? Promise.reject(new Error("This application is shutting down; no new database scope can start")) : serving(() => runInServiceScope(fn)), "runInServiceScope"),
7340
+ shutdown() {
7341
+ accepting = false;
7342
+ return shuttingDown ??= (async () => {
7343
+ while (inFlight.size) await Promise.allSettled([
7344
+ ...inFlight
7345
+ ]);
7346
+ await runShutdownHooks();
7347
+ await closeDriver(sql);
7348
+ released = true;
7349
+ })();
7281
7350
  }
7282
7351
  };
7283
7352
  }