@palbase/backend 35.0.0 → 36.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 (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 +84 -67
  53. package/docs/examples/database-dx.ts +12 -17
  54. package/docs/llms-full.txt +82 -65
  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 +1 -1
  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
@@ -663,25 +663,6 @@ function translateRejection(err, builder) {
663
663
  }
664
664
  __name(translateRejection, "translateRejection");
665
665
 
666
- // src/db/workload.ts
667
- function validateWorkload(value) {
668
- 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");
669
- for (const name of [
670
- "concurrency",
671
- "tenantConcurrency",
672
- "queueLimit",
673
- "tenantQueueLimit",
674
- "queueTimeoutMs"
675
- ]) {
676
- const n = value[name], min = name.endsWith("Limit") ? 0 : 1;
677
- const max = name === "queueTimeoutMs" ? 6e4 : name.endsWith("Limit") ? 1e4 : 1e3;
678
- if (!Number.isInteger(n) || n < min || n > max) throw new Error(`workload ${name} must be an integer between ${min} and ${max}`);
679
- }
680
- if (value.tenantConcurrency > value.concurrency || value.tenantQueueLimit > value.queueLimit) throw new Error("workload tenant limits cannot exceed global limits");
681
- 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");
682
- }
683
- __name(validateWorkload, "validateWorkload");
684
-
685
666
  // src/db/transaction-options.ts
686
667
  function retryBudget(value) {
687
668
  const n = value ?? 0;
@@ -713,10 +694,6 @@ function validateAtomicOptions(options) {
713
694
  throw new Error(`transaction ${name} must be a positive integer in milliseconds`);
714
695
  }
715
696
  }
716
- if (options.workload !== void 0) validateWorkload(options.workload);
717
- 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))) {
718
- throw new Error("conflictKeys requires a workload and at most 64 nonempty keys of at most 256 characters");
719
- }
720
697
  if (options.signal !== void 0 && (typeof options.signal.aborted !== "boolean" || typeof options.signal.addEventListener !== "function")) {
721
698
  throw new Error("transaction signal must be an AbortSignal");
722
699
  }
@@ -876,7 +853,9 @@ function makeTableProxy(ops, prefix) {
876
853
  // Older than this run, but the run rewrote this proxy for
877
854
  // `Database.schema(name).tables.*` and would have carried the gap onto
878
855
  // the new surface too.
879
- updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set), "updateMany"),
856
+ updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set, q.returning === void 0 ? void 0 : {
857
+ returning: q.returning
858
+ }), "updateMany"),
880
859
  deleteMany: /* @__PURE__ */ __name((q) => ops().deleteMany(name, q.where), "deleteMany"),
881
860
  count: /* @__PURE__ */ __name((q) => ops().count(name, q?.where), "count"),
882
861
  search: /* @__PURE__ */ __name((params) => ops().search(name, params), "search"),
@@ -896,9 +875,6 @@ function makeTypedSurface(raw) {
896
875
  const ops = {
897
876
  $query: /* @__PURE__ */ __name((sql, params) => raw.query(sql, params), "$query"),
898
877
  $diagnostics: /* @__PURE__ */ __name(() => raw.diagnostics(), "$diagnostics"),
899
- $readQuery: /* @__PURE__ */ __name((query) => raw.readQuery(query), "$readQuery"),
900
- $compileQuery: /* @__PURE__ */ __name((query) => raw.compileQuery(query), "$compileQuery"),
901
- $explainQuery: /* @__PURE__ */ __name((query) => raw.explainQuery(query), "$explainQuery"),
902
878
  $insert: /* @__PURE__ */ __name((table, data) => raw.insert(table, data), "$insert"),
903
879
  $update: /* @__PURE__ */ __name((table, id, data) => raw.update(table, id, data), "$update"),
904
880
  $delete: /* @__PURE__ */ __name((table, id) => raw.delete(table, id), "$delete"),
@@ -1103,6 +1079,7 @@ var Realtime = makeServiceProxy("Realtime");
1103
1079
  var SQLSTATE = Object.freeze({
1104
1080
  serializationFailure: "40001",
1105
1081
  deadlockDetected: "40P01",
1082
+ queryCancelled: "57014",
1106
1083
  commandGuardFailed: "PBC01"
1107
1084
  });
1108
1085
  function isTransactionRollbackState(state) {
@@ -1305,6 +1282,16 @@ ${detail}`);
1305
1282
  if (!Number.isInteger(poolMax) || poolMax < 1) {
1306
1283
  throw new BootRefused([], `boot refused: DB_POOL_MAX must be a positive integer (got ${env.DB_POOL_MAX}).`);
1307
1284
  }
1285
+ const capacityNumber = /* @__PURE__ */ __name((key2, minimum, maximum) => {
1286
+ if (env[key2] === void 0) return void 0;
1287
+ const value = Number(env[key2]);
1288
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new BootRefused([], `boot refused: ${key2} must be between ${minimum} and ${maximum}.`);
1289
+ return value;
1290
+ }, "capacityNumber");
1291
+ const queueLimit = capacityNumber("PALBASE_DB_QUEUE_LIMIT", 0, 1e4);
1292
+ const queueTimeoutMs = capacityNumber("PALBASE_DB_QUEUE_TIMEOUT_MS", 1, 6e4);
1293
+ const tenantClaim = env.PALBASE_DB_TENANT_CLAIM;
1294
+ 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.");
1308
1295
  return {
1309
1296
  databaseUrl: env.DATABASE_URL.trim(),
1310
1297
  authJwksUrl: env.AUTH_JWKS_URL.trim(),
@@ -1329,6 +1316,17 @@ ${detail}`);
1329
1316
  // and the live database agrees (pg_roles.rolbypassrls = true).
1330
1317
  dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
1331
1318
  poolMax,
1319
+ databaseAdmission: {
1320
+ ...queueLimit === void 0 ? {} : {
1321
+ queueLimit
1322
+ },
1323
+ ...queueTimeoutMs === void 0 ? {} : {
1324
+ queueTimeoutMs
1325
+ },
1326
+ ...tenantClaim === void 0 ? {} : {
1327
+ tenantClaim
1328
+ }
1329
+ },
1332
1330
  ...env.PALBASE_DB_DIAGNOSTICS === "true" ? {
1333
1331
  databaseDiagnostics: {
1334
1332
  onComplete: /* @__PURE__ */ __name((diagnostics) => {
@@ -1848,46 +1846,6 @@ function validateInsertManyOptions(opts) {
1848
1846
  }
1849
1847
  __name(validateInsertManyOptions, "validateInsertManyOptions");
1850
1848
 
1851
- // src/db/query-contract.ts
1852
- function queryColumnContract(column) {
1853
- const def = column && typeof column === "object" && "_def" in column ? column._def : column;
1854
- if (!def?.type) throw new Error("Query contract requires declared column types");
1855
- 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");
1856
- return {
1857
- type: def.type,
1858
- nullable: def.nullable === true,
1859
- ...def.codec === void 0 ? {} : {
1860
- codec: def.codec
1861
- },
1862
- ...def.enumValues === void 0 ? {} : {
1863
- enumValues: [
1864
- ...def.enumValues
1865
- ]
1866
- },
1867
- ...def.dimensions === void 0 ? {} : {
1868
- dimensions: def.dimensions
1869
- }
1870
- };
1871
- }
1872
- __name(queryColumnContract, "queryColumnContract");
1873
- function stableContract(value) {
1874
- if (Array.isArray(value)) return `[${value.map(stableContract).join(",")}]`;
1875
- if (value && typeof value === "object") return `{${Object.keys(value).sort().map((key2) => `${JSON.stringify(key2)}:${stableContract(value[key2])}`).join(",")}}`;
1876
- return JSON.stringify(value) ?? "null";
1877
- }
1878
- __name(stableContract, "stableContract");
1879
- function assertReadContract(query, columns) {
1880
- if (!query.options.select.length || new Set(query.options.select).size !== query.options.select.length) {
1881
- throw new Error(`Query ${query.name}: select must contain distinct, explicit columns`);
1882
- }
1883
- for (const name of query.options.select) {
1884
- if (!columns || !Object.hasOwn(columns, name) || !Object.hasOwn(query.columns, name) || stableContract(queryColumnContract(columns[name])) !== stableContract(query.columns[name])) {
1885
- throw new Error(`Query ${query.name}: schema drift at ${query.table}.${name}; regenerate and review its response contract`);
1886
- }
1887
- }
1888
- }
1889
- __name(assertReadContract, "assertReadContract");
1890
-
1891
1849
  // src/db/unique.ts
1892
1850
  function uniqueKeysOf(table) {
1893
1851
  const columns = table.columns ?? {};
@@ -2061,142 +2019,167 @@ function parameterChunks(sql) {
2061
2019
  __name(parameterChunks, "parameterChunks");
2062
2020
 
2063
2021
  // src/engine/admission.ts
2064
- var WorkloadAdmission = class {
2022
+ var DatabaseAdmission = class {
2065
2023
  static {
2066
- __name(this, "WorkloadAdmission");
2024
+ __name(this, "DatabaseAdmission");
2067
2025
  }
2068
- definition;
2069
- pending = [];
2026
+ options;
2070
2027
  active = 0;
2071
- activeKeys = /* @__PURE__ */ new Set();
2028
+ queued = 0;
2072
2029
  tenants = /* @__PURE__ */ new Map();
2073
- draining = false;
2074
- constructor(definition) {
2075
- this.definition = definition;
2076
- validateWorkload(definition);
2030
+ // Map insertion order is the round-robin ring; each Set is FIFO. Grant and
2031
+ // cancellation remove nodes directly, without scanning a 10,000-item queue.
2032
+ pending = /* @__PURE__ */ new Map();
2033
+ lastTenant;
2034
+ accepted = 0;
2035
+ rejected = {
2036
+ full: 0,
2037
+ timeout: 0,
2038
+ cancelled: 0
2039
+ };
2040
+ limit;
2041
+ timeout;
2042
+ constructor(options) {
2043
+ this.options = options;
2044
+ this.limit = options.queueLimit ?? Math.min(512, Math.max(32, options.concurrency * 8));
2045
+ this.timeout = options.queueTimeoutMs ?? 1e3;
2046
+ 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) {
2047
+ throw new Error("Invalid database pool or queue deployment settings");
2048
+ }
2077
2049
  }
2078
2050
  get snapshot() {
2079
2051
  return {
2080
2052
  active: this.active,
2081
- queued: this.pending.length,
2053
+ queued: this.queued,
2082
2054
  tenants: this.tenants.size,
2083
- keys: this.activeKeys.size
2055
+ accepted: this.accepted,
2056
+ rejected: {
2057
+ ...this.rejected
2058
+ }
2059
+ };
2060
+ }
2061
+ rejection(reason2) {
2062
+ this.rejected[reason2]++;
2063
+ try {
2064
+ this.options.onReject?.({
2065
+ reason: reason2,
2066
+ active: this.active,
2067
+ queued: this.queued
2068
+ });
2069
+ } catch {
2070
+ }
2071
+ }
2072
+ take(tenant, entered) {
2073
+ this.active++;
2074
+ this.accepted++;
2075
+ this.lastTenant = tenant;
2076
+ this.tenants.set(tenant, (this.tenants.get(tenant) ?? 0) + 1);
2077
+ let released = false;
2078
+ return {
2079
+ queueMs: performance.now() - entered,
2080
+ release: /* @__PURE__ */ __name(() => {
2081
+ if (released) return;
2082
+ released = true;
2083
+ this.active--;
2084
+ const count = this.tenants.get(tenant) - 1;
2085
+ if (count) this.tenants.set(tenant, count);
2086
+ else this.tenants.delete(tenant);
2087
+ this.drain();
2088
+ }, "release")
2084
2089
  };
2085
2090
  }
2086
- acquire(tenant, keys, signal) {
2087
- if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Database admission cancelled"));
2088
- const normalized = [
2089
- ...new Set(keys)
2090
- ].sort();
2091
- 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"));
2092
- const scoped = normalized;
2093
- const globalFull = this.pending.length >= this.definition.queueLimit;
2094
- const tenantFull = this.pending.filter((w) => w.tenant === tenant).length >= this.definition.tenantQueueLimit;
2095
- 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)));
2096
- if (!canStart && (globalFull || tenantFull)) {
2097
- return Promise.reject(new TooManyRequests({
2091
+ drain() {
2092
+ while (this.active < this.options.concurrency && this.queued) {
2093
+ let tenant = this.pending.keys().next().value;
2094
+ if (this.pending.size > 1 && tenant === this.lastTenant) {
2095
+ const queue2 = this.pending.get(tenant);
2096
+ this.pending.delete(tenant);
2097
+ this.pending.set(tenant, queue2);
2098
+ tenant = this.pending.keys().next().value;
2099
+ }
2100
+ const queue = this.pending.get(tenant), waiter = queue.values().next().value;
2101
+ queue.delete(waiter);
2102
+ this.pending.delete(tenant);
2103
+ if (queue.size) this.pending.set(tenant, queue);
2104
+ this.queued--;
2105
+ waiter.cleanup();
2106
+ waiter.grant(this.take(tenant, waiter.entered));
2107
+ }
2108
+ }
2109
+ async acquire(claimsJson, signal) {
2110
+ if (signal?.aborted) {
2111
+ this.rejection("cancelled");
2112
+ throw signal.reason ?? new Error("Database admission cancelled");
2113
+ }
2114
+ const claims = JSON.parse(claimsJson);
2115
+ const value = this.options.tenantClaim ? claims[this.options.tenantClaim] : claims.sub;
2116
+ const tenant = typeof value === "string" && value.length ? value : "__anonymous__";
2117
+ const entered = performance.now();
2118
+ if (this.active < this.options.concurrency && !this.queued) return this.take(tenant, entered);
2119
+ if (this.queued >= this.limit) {
2120
+ this.rejection("full");
2121
+ throw new TooManyRequests({
2098
2122
  retryAfter: 1
2099
- }, `Database workload ${this.definition.name}: ${tenantFull ? "tenant" : "global"} queue is full`));
2123
+ }, "Database queue is full");
2100
2124
  }
2101
2125
  return new Promise((grant, reject) => {
2126
+ const queue = this.pending.get(tenant) ?? /* @__PURE__ */ new Set();
2127
+ if (!this.pending.has(tenant)) this.pending.set(tenant, queue);
2102
2128
  const waiter = {
2103
- tenant,
2104
- keys: scoped,
2105
- entered: performance.now(),
2106
2129
  grant,
2107
2130
  reject,
2108
- cleanup: /* @__PURE__ */ __name(() => {
2109
- }, "cleanup")
2131
+ entered,
2132
+ cleanup() {
2133
+ }
2110
2134
  };
2111
- const remove = /* @__PURE__ */ __name((reason2) => {
2112
- const index = this.pending.indexOf(waiter);
2113
- if (index === -1) return;
2114
- this.pending.splice(index, 1);
2135
+ const remove = /* @__PURE__ */ __name((reason2, error) => {
2136
+ if (!queue.delete(waiter)) return;
2137
+ if (!queue.size) this.pending.delete(tenant);
2138
+ this.queued--;
2115
2139
  waiter.cleanup();
2116
- reject(reason2);
2140
+ this.rejection(reason2);
2141
+ reject(error);
2117
2142
  this.drain();
2118
2143
  }, "remove");
2119
- const abort = /* @__PURE__ */ __name(() => remove(signal?.reason ?? new Error("Database admission cancelled")), "abort");
2120
- const timer = setTimeout(() => remove(new TooManyRequests({
2144
+ const abort = /* @__PURE__ */ __name(() => remove("cancelled", signal?.reason ?? new Error("Database admission cancelled")), "abort");
2145
+ const timer = setTimeout(() => remove("timeout", new TooManyRequests({
2121
2146
  retryAfter: 1
2122
- }, `Database workload ${this.definition.name}: queue deadline exceeded`)), this.definition.queueTimeoutMs);
2147
+ }, "Database queue deadline exceeded")), this.timeout);
2123
2148
  waiter.cleanup = () => {
2124
2149
  clearTimeout(timer);
2125
2150
  signal?.removeEventListener("abort", abort);
2126
2151
  };
2152
+ queue.add(waiter);
2153
+ this.queued++;
2127
2154
  signal?.addEventListener("abort", abort, {
2128
2155
  once: true
2129
2156
  });
2130
- this.pending.push(waiter);
2131
2157
  if (signal?.aborted) abort();
2132
2158
  else this.drain();
2133
2159
  });
2134
2160
  }
2135
- drain() {
2136
- if (this.draining) return;
2137
- this.draining = true;
2138
- try {
2139
- const blockedKeys = /* @__PURE__ */ new Set();
2140
- for (let index = 0; index < this.pending.length && this.active < this.definition.concurrency; ) {
2141
- const waiter = this.pending[index];
2142
- if ((this.tenants.get(waiter.tenant) ?? 0) >= this.definition.tenantConcurrency || waiter.keys.some((key2) => this.activeKeys.has(key2) || blockedKeys.has(key2))) {
2143
- for (const key2 of waiter.keys) blockedKeys.add(key2);
2144
- index++;
2145
- continue;
2146
- }
2147
- this.pending.splice(index, 1);
2148
- waiter.cleanup();
2149
- this.active++;
2150
- this.tenants.set(waiter.tenant, (this.tenants.get(waiter.tenant) ?? 0) + 1);
2151
- for (const key2 of waiter.keys) this.activeKeys.add(key2);
2152
- let released = false;
2153
- waiter.grant({
2154
- queueMs: performance.now() - waiter.entered,
2155
- release: /* @__PURE__ */ __name(() => {
2156
- if (released) return;
2157
- released = true;
2158
- this.active--;
2159
- const left = this.tenants.get(waiter.tenant) - 1;
2160
- if (left === 0) this.tenants.delete(waiter.tenant);
2161
- else this.tenants.set(waiter.tenant, left);
2162
- for (const key2 of waiter.keys) this.activeKeys.delete(key2);
2163
- this.drain();
2164
- }, "release")
2165
- });
2166
- }
2167
- } finally {
2168
- this.draining = false;
2161
+ };
2162
+ var databases = /* @__PURE__ */ new WeakMap();
2163
+ function databaseAdmission(owner, options) {
2164
+ let admission = databases.get(owner);
2165
+ if (!admission) {
2166
+ admission = new DatabaseAdmission(options ?? {
2167
+ concurrency: 10
2168
+ });
2169
+ databases.set(owner, admission);
2170
+ } else if (options) {
2171
+ for (const key2 of [
2172
+ "concurrency",
2173
+ "queueLimit",
2174
+ "queueTimeoutMs",
2175
+ "tenantClaim"
2176
+ ]) {
2177
+ if (options[key2] !== admission.options[key2]) throw new Error("Database admission cannot be reconfigured while its pool is shared");
2169
2178
  }
2170
2179
  }
2171
- };
2172
- var registries = /* @__PURE__ */ new WeakMap();
2173
- function workloadAdmission(owner, definition) {
2174
- validateWorkload(definition);
2175
- let registry2 = registries.get(owner);
2176
- if (!registry2) {
2177
- registry2 = /* @__PURE__ */ new Map();
2178
- registries.set(owner, registry2);
2179
- }
2180
- const existing = registry2.get(definition.name);
2181
- if (existing) {
2182
- const d = existing.definition;
2183
- if (Object.keys(d).some((k) => d[k] !== definition[k])) throw new Error(`Workload ${definition.name} was already registered with different limits`);
2184
- return existing;
2185
- }
2186
- if (registry2.size >= 64) throw new Error("At most 64 named database workloads may be registered per engine; declare and reuse them at module scope");
2187
- const admission = new WorkloadAdmission(Object.freeze({
2188
- ...definition
2189
- }));
2190
- registry2.set(definition.name, admission);
2191
2180
  return admission;
2192
2181
  }
2193
- __name(workloadAdmission, "workloadAdmission");
2194
- function workloadTenant(definition, claimsJson) {
2195
- const claims = JSON.parse(claimsJson);
2196
- const value = claims[definition.tenantClaim] ?? claims.sub;
2197
- return typeof value === "string" && value.length > 0 ? value : "__anonymous__";
2198
- }
2199
- __name(workloadTenant, "workloadTenant");
2182
+ __name(databaseAdmission, "databaseAdmission");
2200
2183
 
2201
2184
  // src/engine/transaction-control.ts
2202
2185
  var TransactionControl = class {
@@ -2319,6 +2302,7 @@ var DatabaseTrace = class {
2319
2302
  sqlMs: 0,
2320
2303
  commitMs: 0,
2321
2304
  rollbackMs: 0,
2305
+ connectionMs: 0,
2322
2306
  commits: 0,
2323
2307
  commitFailures: 0,
2324
2308
  rollbackRequests: 0,
@@ -2330,7 +2314,7 @@ var DatabaseTrace = class {
2330
2314
  if (this.failure) throw this.failure;
2331
2315
  }
2332
2316
  time(key2, started) {
2333
- this.state[key2] += performance.now() - started;
2317
+ this.state[key2] = (this.state[key2] ?? 0) + performance.now() - started;
2334
2318
  }
2335
2319
  retry() {
2336
2320
  this.state.retries++;
@@ -2654,31 +2638,53 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
2654
2638
  fail = rej;
2655
2639
  });
2656
2640
  void parked.catch(() => void 0);
2657
- const openingAt = options.trace ? performance.now() : 0;
2658
- settled = sql.begin(async (tx) => {
2659
- options.trace?.time("poolMs", openingAt);
2660
- const setupAt = options.trace ? performance.now() : 0;
2661
- options.control?.check();
2662
- if (options.isolation !== void 0 || options.readOnly !== void 0) {
2663
- const isolation = options.isolation ?? "read committed";
2664
- validateAtomicOptions({
2665
- isolation,
2666
- readOnly: options.readOnly
2667
- });
2668
- await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
2641
+ settled = (async () => {
2642
+ const queuedAt = performance.now();
2643
+ let lease;
2644
+ try {
2645
+ lease = await (sql.admission ?? databaseAdmission(sql)).acquire(claimsJson, options.control?.signal);
2646
+ } finally {
2647
+ options.trace?.time("queueMs", queuedAt);
2669
2648
  }
2670
- if (options.control && sql.cancelBackend) {
2671
- 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()");
2672
- if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
2673
- options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
2674
- options.control.check();
2649
+ const openingAt = performance.now();
2650
+ let connectedAt;
2651
+ try {
2652
+ options.control?.check();
2653
+ if (closed) throw new Error("Database transaction closed while waiting for admission");
2654
+ return await sql.begin(async (tx) => {
2655
+ connectedAt = performance.now();
2656
+ options.trace?.time("poolMs", openingAt);
2657
+ const setupAt = options.trace ? performance.now() : 0;
2658
+ options.control?.check();
2659
+ if (closed) throw new Error("Database transaction closed while waiting for its connection");
2660
+ if (options.isolation !== void 0 || options.readOnly !== void 0) {
2661
+ const isolation = options.isolation ?? "read committed";
2662
+ validateAtomicOptions({
2663
+ isolation,
2664
+ readOnly: options.readOnly
2665
+ });
2666
+ await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
2667
+ }
2668
+ if (options.control && sql.cancelBackend) {
2669
+ const ticket = await tx.unsafe(`WITH backend AS MATERIALIZED (
2670
+ SELECT pid, backend_start::text AS started FROM pg_catalog.pg_stat_get_activity(pg_catalog.pg_backend_pid())
2671
+ ) ${bindSql}, backend.pid, backend.started FROM backend`, bindParams);
2672
+ if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
2673
+ options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
2674
+ options.control.check();
2675
+ } else {
2676
+ const binding = tx.unsafe(bindSql, bindParams);
2677
+ await (options.control ? options.control.watch(binding) : binding);
2678
+ }
2679
+ options.trace?.time("setupMs", setupAt);
2680
+ resolveTx2(scoped(tx));
2681
+ await parked;
2682
+ });
2683
+ } finally {
2684
+ if (connectedAt !== void 0) options.trace?.time("connectionMs", connectedAt);
2685
+ lease.release();
2675
2686
  }
2676
- const binding = tx.unsafe(bindSql, bindParams);
2677
- await (options.control ? options.control.watch(binding) : binding);
2678
- options.trace?.time("setupMs", setupAt);
2679
- resolveTx2(scoped(tx));
2680
- await parked;
2681
- }).catch((e) => {
2687
+ })().catch((e) => {
2682
2688
  rejectTx(e);
2683
2689
  throw e;
2684
2690
  });
@@ -2695,10 +2701,13 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
2695
2701
  if (pendingSavepoints.size) await Promise.allSettled([
2696
2702
  ...pendingSavepoints
2697
2703
  ]);
2698
- options.control?.check();
2699
2704
  options.trace?.check();
2705
+ if (!opening) {
2706
+ closed = true;
2707
+ return;
2708
+ }
2709
+ options.control?.check();
2700
2710
  closed = true;
2701
- if (!opening) return;
2702
2711
  const started = options.trace ? performance.now() : 0;
2703
2712
  release();
2704
2713
  try {
@@ -2825,10 +2834,11 @@ function asTableRows(table, rows, withSpec, partial = false) {
2825
2834
  const nested = typeof spec === "object" ? spec.with : void 0;
2826
2835
  for (const row of result) {
2827
2836
  const value = row[rel.name];
2828
- if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested);
2837
+ const selected = typeof spec === "object" && "select" in spec && spec.select !== void 0;
2838
+ if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested, selected);
2829
2839
  else if (value && typeof value === "object") row[rel.name] = asTableRows(rel.to, [
2830
2840
  value
2831
- ], nested)[0];
2841
+ ], nested, selected)[0];
2832
2842
  }
2833
2843
  }
2834
2844
  for (const row of result) foldCan(table, row, partial);
@@ -3772,6 +3782,9 @@ function withClauses(table, spec, add, caller, parentAlias = "t", depth = 0, cou
3772
3782
  const i = ++counter.value;
3773
3783
  const childAlias = `w${i}`;
3774
3784
  const childKnown = schemaColumns(rel.to);
3785
+ if (o["select"] !== void 0 && (!Array.isArray(o["select"]) || o["select"].length === 0)) {
3786
+ throw new Error(`${caller}(${table}): with.${name}.select must be a non-empty array of columns`);
3787
+ }
3775
3788
  const childCols = Array.isArray(o["select"]) && o["select"].length > 0 ? o["select"] : childKnown !== null ? [
3776
3789
  ...childKnown
3777
3790
  ] : [];
@@ -3875,7 +3888,10 @@ function pageSchema(table) {
3875
3888
  }
3876
3889
  __name(pageSchema, "pageSchema");
3877
3890
  function createOps(tx, context = {}) {
3878
- const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
3891
+ const at = /* @__PURE__ */ __name(() => {
3892
+ const target = typeof tx === "function" ? tx() : tx;
3893
+ return target instanceof Promise ? target.then(resolveTx) : resolveTx(target);
3894
+ }, "at");
3879
3895
  const ops = {
3880
3896
  diagnostics() {
3881
3897
  return context.trace?.snapshot ?? null;
@@ -4326,22 +4342,6 @@ function createOps(tx, context = {}) {
4326
4342
  ]);
4327
4343
  return rows[0] ? asTableRow(table, rows[0]) : null;
4328
4344
  },
4329
- compileQuery(query) {
4330
- assertReadContract(query, tableDefOf(query.table)?.columns);
4331
- return compileFindMany(query.table, query.where, query.options);
4332
- },
4333
- async readQuery(query) {
4334
- const compiled = ops.compileQuery(query);
4335
- return asTableRows(query.table, await (await at()).unsafe(compiled.sql, compiled.params, {
4336
- name: query.name
4337
- }), void 0, true);
4338
- },
4339
- async explainQuery(query) {
4340
- const compiled = ops.compileQuery(query);
4341
- return await (await at()).unsafe(`EXPLAIN (FORMAT JSON) ${compiled.sql}`, compiled.params, {
4342
- name: `${query.name}.explain`
4343
- });
4344
- },
4345
4345
  async findMany(table, query = {}, opts = {}) {
4346
4346
  const compiled = compileFindMany(table, query, opts);
4347
4347
  return asTableRows(table, await (await at()).unsafe(compiled.sql, compiled.params), opts.with, opts.select !== void 0);
@@ -4876,7 +4876,7 @@ function createOps(tx, context = {}) {
4876
4876
  if (options.mode === "compiled") throw new Error(`Command cannot compile: ${compiled.reason}. Use auto/sequential mode or installCommandExecutor at boot.`);
4877
4877
  return ops.txPlan(plan);
4878
4878
  }
4879
- if (compiled.steps.length === 0) return {
4879
+ if (compiled.steps.length === 0 && !compiled.pipeline?.length) return {
4880
4880
  results: []
4881
4881
  };
4882
4882
  if (compiled.pipeline) {
@@ -5149,6 +5149,7 @@ function diagnosingDriver(sql, surface = "service") {
5149
5149
  const state = sqlstateOf(e);
5150
5150
  if (state === SQLSTATE.serializationFailure) return markEngineRaised(new SerializationFailure(state));
5151
5151
  if (state === SQLSTATE.deadlockDetected) return markEngineRaised(new DeadlockDetected(state));
5152
+ if (state === SQLSTATE.queryCancelled) return markEngineRaised(new HttpError(503, "database_query_cancelled", "Database statement was cancelled"));
5152
5153
  if (surface === "service" && isForeignKeyViolation(e)) {
5153
5154
  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)})`);
5154
5155
  }
@@ -5175,6 +5176,7 @@ function diagnosingDriver(sql, surface = "service") {
5175
5176
  }
5176
5177
  }), "wrapTx");
5177
5178
  return {
5179
+ admission: sql.admission ?? databaseAdmission(sql),
5178
5180
  unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
5179
5181
  cancelBackend: sql.cancelBackend?.bind(sql),
5180
5182
  begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))).catch((e) => {
@@ -5183,9 +5185,12 @@ function diagnosingDriver(sql, surface = "service") {
5183
5185
  };
5184
5186
  }
5185
5187
  __name(diagnosingDriver, "diagnosingDriver");
5186
- function createRequestDatabase(sql, identity, diagnostics) {
5188
+ function createRequestDatabase(sql, identity, diagnostics, signal) {
5187
5189
  const trace = diagnostics === void 0 ? void 0 : new DatabaseTrace(diagnostics);
5190
+ const requestControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
5188
5191
  let requestClosed = false;
5192
+ let authorized = false, applicationTouched = false;
5193
+ let authorizationRelease;
5189
5194
  const contextFor = /* @__PURE__ */ __name((role) => {
5190
5195
  let scope;
5191
5196
  return {
@@ -5204,19 +5209,21 @@ function createRequestDatabase(sql, identity, diagnostics) {
5204
5209
  validateAtomicOptions(options);
5205
5210
  if (requestClosed) throw new Error("Database request has settled; it cannot open another transaction");
5206
5211
  if (atomicScope.getStore() !== void 0) throw new Error("Nested Database.$atomic is not supported; use tx.$attempt for a savepoint");
5212
+ if (authorized && !applicationTouched && tx.opened) {
5213
+ authorizationRelease ??= (async () => {
5214
+ await tx.commit();
5215
+ tx = requestTransaction();
5216
+ })();
5217
+ }
5218
+ if (authorizationRelease) await authorizationRelease;
5207
5219
  const retries = retryBudget(options.retry);
5208
- const control = options.timeoutMs !== void 0 || options.signal !== void 0 ? new TransactionControl(options.timeoutMs, options.signal) : void 0;
5209
- let lease;
5220
+ const atomicSignal = options.signal && signal ? AbortSignal.any([
5221
+ options.signal,
5222
+ signal
5223
+ ]) : options.signal ?? signal;
5224
+ const control = options.timeoutMs !== void 0 || atomicSignal !== void 0 ? new TransactionControl(options.timeoutMs, atomicSignal) : void 0;
5210
5225
  try {
5211
5226
  control?.check();
5212
- if (options.workload) {
5213
- const queuedAt = trace ? performance.now() : 0;
5214
- try {
5215
- lease = await workloadAdmission(sql, options.workload).acquire(workloadTenant(options.workload, identity.claimsJson), options.conflictKeys ?? [], control?.signal);
5216
- } finally {
5217
- trace?.time("queueMs", queuedAt);
5218
- }
5219
- }
5220
5227
  for (let attempt = 0; ; attempt++) {
5221
5228
  control?.check();
5222
5229
  const timeouts = [
@@ -5249,19 +5256,23 @@ function createRequestDatabase(sql, identity, diagnostics) {
5249
5256
  }
5250
5257
  } finally {
5251
5258
  control?.dispose();
5252
- lease?.release();
5253
5259
  }
5254
5260
  }, "atomic");
5255
- const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
5256
- trace
5257
- });
5261
+ const requestTransaction = /* @__PURE__ */ __name(() => createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
5262
+ trace,
5263
+ control: requestControl
5264
+ }), "requestTransaction");
5265
+ let tx = requestTransaction();
5258
5266
  let serviceTx = null;
5267
+ let serviceControl;
5259
5268
  let serviceClient = null;
5260
5269
  const asService = /* @__PURE__ */ __name(() => {
5261
5270
  if (serviceClient === null) {
5271
+ serviceControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
5262
5272
  serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
5263
5273
  lockTimeout: SERVICE_LOCK_TIMEOUT,
5264
- trace
5274
+ trace,
5275
+ control: serviceControl
5265
5276
  });
5266
5277
  serviceClient = Object.assign(withTables(createOps(serviceTx, serviceContext)), {
5267
5278
  atomic: atomic(identity.serviceRole, "service")
@@ -5270,16 +5281,24 @@ function createRequestDatabase(sql, identity, diagnostics) {
5270
5281
  return serviceClient;
5271
5282
  }, "asService");
5272
5283
  return {
5273
- client: Object.assign(withTables(createOps(tx, userContext)), {
5284
+ client: Object.assign(withTables(createOps(() => {
5285
+ if (authorized) applicationTouched = true;
5286
+ return authorizationRelease ? authorizationRelease.then(() => tx) : tx;
5287
+ }, userContext)), {
5274
5288
  asService,
5275
5289
  atomic: atomic(identity.role, "user")
5276
5290
  }),
5291
+ authorizationComplete() {
5292
+ authorized = true;
5293
+ },
5277
5294
  async commit() {
5278
5295
  requestClosed = true;
5279
5296
  try {
5280
5297
  await tx.commit();
5281
5298
  await serviceTx?.commit();
5282
5299
  trace?.complete("committed");
5300
+ requestControl?.dispose();
5301
+ serviceControl?.dispose();
5283
5302
  } catch (error) {
5284
5303
  trace?.failed();
5285
5304
  throw error;
@@ -5289,6 +5308,8 @@ function createRequestDatabase(sql, identity, diagnostics) {
5289
5308
  requestClosed = true;
5290
5309
  await tx.rollback(reason2);
5291
5310
  await serviceTx?.rollback(reason2);
5311
+ requestControl?.dispose();
5312
+ serviceControl?.dispose();
5292
5313
  trace?.complete("failed");
5293
5314
  }
5294
5315
  };
@@ -5596,7 +5617,7 @@ function compileCommand(plan) {
5596
5617
  };
5597
5618
  });
5598
5619
  steps.push({
5599
- chunks: parameterChunks(sql),
5620
+ sql,
5600
5621
  bindings,
5601
5622
  guard: op.guard
5602
5623
  });
@@ -5619,11 +5640,15 @@ function compileCommand(plan) {
5619
5640
  rows_affected: 1
5620
5641
  });
5621
5642
  }
5643
+ if (!hasRefs) return {
5644
+ steps: [],
5645
+ pipeline
5646
+ };
5622
5647
  return {
5623
- steps,
5624
- ...hasRefs ? {} : {
5625
- pipeline
5626
- }
5648
+ steps: steps.map(({ sql, ...step }) => sql === void 0 ? step : {
5649
+ ...step,
5650
+ chunks: parameterChunks(sql)
5651
+ })
5627
5652
  };
5628
5653
  }
5629
5654
  __name(compileCommand, "compileCommand");
@@ -6100,9 +6125,11 @@ function makeSseWriter(opts) {
6100
6125
  if (!begun) {
6101
6126
  begun = true;
6102
6127
  chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
6128
+ void chain.catch(() => void 0);
6103
6129
  return;
6104
6130
  }
6105
6131
  chain = chain.then(() => opts.enqueue(frame));
6132
+ void chain.catch(() => void 0);
6106
6133
  },
6107
6134
  started() {
6108
6135
  return begun;
@@ -6629,6 +6656,23 @@ async function createApp(opts) {
6629
6656
  throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
6630
6657
  }
6631
6658
  const sql = opts.sql ?? await defaultSqlDriver(config);
6659
+ const admission = sql.admission ?? databaseAdmission(sql, {
6660
+ concurrency: config.poolMax,
6661
+ ...config.databaseAdmission
6662
+ });
6663
+ const inFlight = /* @__PURE__ */ new Set();
6664
+ let shuttingDown;
6665
+ let accepting = true;
6666
+ async function serving(run) {
6667
+ const work = run();
6668
+ inFlight.add(work);
6669
+ try {
6670
+ return await work;
6671
+ } finally {
6672
+ inFlight.delete(work);
6673
+ }
6674
+ }
6675
+ __name(serving, "serving");
6632
6676
  await sql.unsafe("select 1");
6633
6677
  setSchema(opts.schemas ?? []);
6634
6678
  setCursorKey(config.cursorSecret || config.serviceRoleKey || null);
@@ -6695,6 +6739,19 @@ async function createApp(opts) {
6695
6739
  }
6696
6740
  }
6697
6741
  __name(runInServiceScope, "runInServiceScope");
6742
+ function errorResponse(err, endpoint, requestId) {
6743
+ if (isHttpError(err)) {
6744
+ if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
6745
+ log.error(`[engine] unhandled ${err.error} in ${endpoint}`, err);
6746
+ }
6747
+ return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
6748
+ data: err.data
6749
+ } : void 0);
6750
+ }
6751
+ log.error(`[engine] unhandled error in ${endpoint}`, err);
6752
+ return envelope("internal_error", "The request could not be completed", 500, requestId);
6753
+ }
6754
+ __name(errorResponse, "errorResponse");
6698
6755
  async function handleAuthorize(req, requestId) {
6699
6756
  if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
6700
6757
  return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
@@ -6717,7 +6774,7 @@ async function createApp(opts) {
6717
6774
  ...config.databaseDiagnostics,
6718
6775
  endpoint: "upload authorization",
6719
6776
  requestId
6720
- });
6777
+ }, req.signal);
6721
6778
  try {
6722
6779
  const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
6723
6780
  if (refusal) {
@@ -6727,7 +6784,7 @@ async function createApp(opts) {
6727
6784
  await uploadDb.commit();
6728
6785
  } catch (err) {
6729
6786
  await uploadDb.rollback(err);
6730
- throw err;
6787
+ return errorResponse(err, "upload authorization", requestId);
6731
6788
  }
6732
6789
  const grant = grantFor(target?.entry, {
6733
6790
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -6769,256 +6826,257 @@ async function createApp(opts) {
6769
6826
  ...meta.options.databaseBudget,
6770
6827
  endpoint: hit.entry.id,
6771
6828
  requestId
6772
- });
6773
- const rolesOfCaller = makeRoleResolver(db.client);
6774
- let dispatched = false;
6775
- const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
6776
- if (dispatched) return;
6777
- await db.rollback(new Error("request refused before dispatch"));
6778
- }, "settleBeforeRefusal");
6779
- const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
6780
- if (refusal) {
6781
- await settleBeforeRefusal();
6782
- return refusal;
6783
- }
6784
- if (claims && spec.verifiedEmail && claims.email_verified !== true) {
6785
- await settleBeforeRefusal();
6786
- return envelope("email_not_verified", "A verified email address is required", 403, requestId);
6787
- }
6788
- let parsedBody;
6789
- let bodyRead = false;
6790
- let rawBody = null;
6791
- if (req.method !== "GET" && req.method !== "HEAD") {
6792
- const declared = Number(req.headers.get("content-length") ?? "");
6793
- if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
6829
+ }, req.signal);
6830
+ try {
6831
+ const rolesOfCaller = makeRoleResolver(db.client);
6832
+ let dispatched = false;
6833
+ const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
6834
+ if (dispatched) return;
6835
+ await db.rollback(new Error("request refused before dispatch"));
6836
+ }, "settleBeforeRefusal");
6837
+ const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
6838
+ if (refusal) {
6839
+ await settleBeforeRefusal();
6840
+ return refusal;
6841
+ }
6842
+ if (claims && spec.verifiedEmail && claims.email_verified !== true) {
6843
+ await settleBeforeRefusal();
6844
+ return envelope("email_not_verified", "A verified email address is required", 403, requestId);
6845
+ }
6846
+ let parsedBody;
6847
+ let bodyRead = false;
6848
+ let rawBody = null;
6849
+ if (req.method !== "GET" && req.method !== "HEAD") {
6850
+ const declared = Number(req.headers.get("content-length") ?? "");
6851
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
6852
+ await settleBeforeRefusal();
6853
+ return new Response(JSON.stringify({
6854
+ error: "payload_too_large",
6855
+ error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6856
+ status: 413,
6857
+ request_id: requestId
6858
+ }), {
6859
+ status: 413,
6860
+ headers: JSON_HEADERS
6861
+ });
6862
+ }
6863
+ const buf = await req.arrayBuffer().catch(() => null);
6864
+ if (buf && buf.byteLength > MAX_BODY_BYTES) {
6865
+ await settleBeforeRefusal();
6866
+ return new Response(JSON.stringify({
6867
+ error: "payload_too_large",
6868
+ error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6869
+ status: 413,
6870
+ request_id: requestId
6871
+ }), {
6872
+ status: 413,
6873
+ headers: JSON_HEADERS
6874
+ });
6875
+ }
6876
+ if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
6877
+ }
6878
+ const readParsedBody = /* @__PURE__ */ __name(() => {
6879
+ if (bodyRead) return parsedBody;
6880
+ bodyRead = true;
6881
+ if (!rawBody) {
6882
+ parsedBody = {};
6883
+ return parsedBody;
6884
+ }
6885
+ try {
6886
+ parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
6887
+ } catch {
6888
+ parsedBody = {};
6889
+ }
6890
+ return parsedBody;
6891
+ }, "readParsedBody");
6892
+ const attest = await attestGate(req, rawBody, {
6893
+ userId
6894
+ });
6895
+ if (attest.observation) observations.record(attest.observation);
6896
+ if (attest.outcome === "reject") {
6897
+ const headers = {
6898
+ ...JSON_HEADERS
6899
+ };
6900
+ if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
6794
6901
  await settleBeforeRefusal();
6795
6902
  return new Response(JSON.stringify({
6796
- error: "payload_too_large",
6797
- error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6798
- status: 413,
6903
+ error: attest.reason,
6904
+ error_description: "This endpoint requires a verified app instance",
6905
+ status: 401,
6799
6906
  request_id: requestId
6800
6907
  }), {
6801
- status: 413,
6802
- headers: JSON_HEADERS
6908
+ status: 401,
6909
+ headers
6803
6910
  });
6804
6911
  }
6805
- const buf = await req.arrayBuffer().catch(() => null);
6806
- if (buf && buf.byteLength > MAX_BODY_BYTES) {
6912
+ if (attest.device && claims) attestedDevice = attest.device;
6913
+ attestChallengeOut = attest.nextChallenge;
6914
+ const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
6915
+ if (retryAfter !== null) {
6807
6916
  await settleBeforeRefusal();
6808
6917
  return new Response(JSON.stringify({
6809
- error: "payload_too_large",
6810
- error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
6811
- status: 413,
6812
- request_id: requestId
6918
+ error: "too_many_requests",
6919
+ error_description: "Rate limit exceeded for this endpoint",
6920
+ status: 429,
6921
+ request_id: requestId,
6922
+ data: {
6923
+ retryAfter
6924
+ }
6813
6925
  }), {
6814
- status: 413,
6815
- headers: JSON_HEADERS
6926
+ status: 429,
6927
+ headers: {
6928
+ ...JSON_HEADERS,
6929
+ "retry-after": String(retryAfter)
6930
+ }
6816
6931
  });
6817
6932
  }
6818
- if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
6819
- }
6820
- const readParsedBody = /* @__PURE__ */ __name(() => {
6821
- if (bodyRead) return parsedBody;
6822
- bodyRead = true;
6823
- if (!rawBody) {
6824
- parsedBody = {};
6825
- return parsedBody;
6826
- }
6827
- try {
6828
- parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
6829
- } catch {
6830
- parsedBody = {};
6831
- }
6832
- return parsedBody;
6833
- }, "readParsedBody");
6834
- const attest = await attestGate(req, rawBody, {
6835
- userId
6836
- });
6837
- if (attest.observation) observations.record(attest.observation);
6838
- if (attest.outcome === "reject") {
6839
- const headers = {
6840
- ...JSON_HEADERS
6841
- };
6842
- if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
6843
- await settleBeforeRefusal();
6844
- return new Response(JSON.stringify({
6845
- error: attest.reason,
6846
- error_description: "This endpoint requires a verified app instance",
6847
- status: 401,
6848
- request_id: requestId
6849
- }), {
6850
- status: 401,
6851
- headers
6933
+ let completionUploadId = null;
6934
+ const args = [];
6935
+ let sseEnqueue = null;
6936
+ let sseSettle = /* @__PURE__ */ __name(async () => {
6937
+ }, "sseSettle");
6938
+ const sseWriter = makeSseWriter({
6939
+ enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
6940
+ onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
6852
6941
  });
6853
- }
6854
- if (attest.device && claims) attestedDevice = attest.device;
6855
- attestChallengeOut = attest.nextChallenge;
6856
- const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
6857
- if (retryAfter !== null) {
6858
- await settleBeforeRefusal();
6859
- return new Response(JSON.stringify({
6860
- error: "too_many_requests",
6861
- error_description: "Rate limit exceeded for this endpoint",
6862
- status: 429,
6863
- request_id: requestId,
6864
- data: {
6865
- retryAfter
6866
- }
6867
- }), {
6868
- status: 429,
6869
- headers: {
6870
- ...JSON_HEADERS,
6871
- "retry-after": String(retryAfter)
6872
- }
6873
- });
6874
- }
6875
- let completionUploadId = null;
6876
- const args = [];
6877
- let sseEnqueue = null;
6878
- let sseSettle = /* @__PURE__ */ __name(async () => {
6879
- }, "sseSettle");
6880
- const sseWriter = makeSseWriter({
6881
- enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
6882
- onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
6883
- });
6884
- for (const p of meta.params ?? []) {
6885
- switch (p.kind) {
6886
- case "body": {
6887
- readParsedBody();
6888
- const r = p.schema.safeParse(parsedBody);
6889
- if (!r.success) {
6890
- await settleBeforeRefusal();
6891
- return envelope("bad_request", "Request body failed validation", 400, requestId, {
6892
- data: {
6893
- fields: fieldErrors(r.error)
6894
- }
6895
- });
6896
- }
6897
- args[p.index] = r.data;
6898
- break;
6899
- }
6900
- case "query": {
6901
- const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
6902
- if (!r.success) {
6903
- await settleBeforeRefusal();
6904
- return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
6905
- data: {
6906
- fields: fieldErrors(r.error)
6907
- }
6908
- });
6909
- }
6910
- args[p.index] = r.data;
6911
- break;
6912
- }
6913
- case "param":
6914
- args[p.index] = hit.params[p.name];
6915
- break;
6916
- case "headers": {
6917
- const raw = Object.fromEntries(req.headers);
6918
- if (!p.schema) {
6919
- args[p.index] = raw;
6942
+ for (const p of meta.params ?? []) {
6943
+ switch (p.kind) {
6944
+ case "body": {
6945
+ readParsedBody();
6946
+ const r = p.schema.safeParse(parsedBody);
6947
+ if (!r.success) {
6948
+ await settleBeforeRefusal();
6949
+ return envelope("bad_request", "Request body failed validation", 400, requestId, {
6950
+ data: {
6951
+ fields: fieldErrors(r.error)
6952
+ }
6953
+ });
6954
+ }
6955
+ args[p.index] = r.data;
6920
6956
  break;
6921
6957
  }
6922
- const r = p.schema.safeParse(headersFor(raw, p.schema));
6923
- if (!r.success) {
6924
- await settleBeforeRefusal();
6925
- return envelope("bad_request", "Request headers failed validation", 400, requestId, {
6926
- data: {
6927
- fields: fieldErrors(r.error)
6928
- }
6929
- });
6930
- }
6931
- args[p.index] = r.data;
6932
- break;
6933
- }
6934
- case "user":
6935
- case "optionalUser":
6936
- args[p.index] = claims ? {
6937
- id: userId,
6938
- email: claims.email,
6939
- role: claims.role,
6940
- emailVerified: claims.email_verified === true,
6941
- metadata: claims.metadata ?? {},
6942
- // The caller's APPLICATION roles, from `auth.user_roles` — the
6943
- // same resolution the gate used, so the handler cannot read a
6944
- // different set than the one it was admitted on. Never from a
6945
- // claim: a revoked role has to bite on the next request, and a
6946
- // token says only what was true when it was minted.
6947
- roles: await rolesOfCaller(),
6948
- // Server-owned and only ever filled from a verdict palauth
6949
- // returned. Never from a raw header, never from JWT metadata —
6950
- // the type has said so since it was written and this is the
6951
- // first writer.
6952
- device: attestedDevice
6953
- } : null;
6954
- break;
6955
- case "uploadedObject": {
6956
- if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
6957
- await settleBeforeRefusal();
6958
- return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
6959
- }
6960
- readParsedBody();
6961
- const envelopeIn = parsedBody;
6962
- if (!envelopeIn?.uploadedObject) {
6963
- await settleBeforeRefusal();
6964
- return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
6958
+ case "query": {
6959
+ const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
6960
+ if (!r.success) {
6961
+ await settleBeforeRefusal();
6962
+ return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
6963
+ data: {
6964
+ fields: fieldErrors(r.error)
6965
+ }
6966
+ });
6967
+ }
6968
+ args[p.index] = r.data;
6969
+ break;
6965
6970
  }
6966
- const uploadId = envelopeIn.uploadedObject.uploadId;
6967
- if (typeof uploadId === "string" && uploadId !== "") {
6968
- const already = completions.recall(uploadId);
6969
- if (already) {
6971
+ case "param":
6972
+ args[p.index] = hit.params[p.name];
6973
+ break;
6974
+ case "headers": {
6975
+ const raw = Object.fromEntries(req.headers);
6976
+ if (!p.schema) {
6977
+ args[p.index] = raw;
6978
+ break;
6979
+ }
6980
+ const r = p.schema.safeParse(headersFor(raw, p.schema));
6981
+ if (!r.success) {
6970
6982
  await settleBeforeRefusal();
6971
- return new Response(already.body, {
6972
- status: already.status,
6973
- headers: already.contentType ? {
6974
- "content-type": already.contentType
6975
- } : void 0
6983
+ return envelope("bad_request", "Request headers failed validation", 400, requestId, {
6984
+ data: {
6985
+ fields: fieldErrors(r.error)
6986
+ }
6976
6987
  });
6977
6988
  }
6978
- completionUploadId = uploadId;
6989
+ args[p.index] = r.data;
6990
+ break;
6979
6991
  }
6980
- args[p.index] = envelopeIn.uploadedObject;
6981
- parsedBody = envelopeIn.body ?? {};
6982
- break;
6992
+ case "user":
6993
+ case "optionalUser":
6994
+ args[p.index] = claims ? {
6995
+ id: userId,
6996
+ email: claims.email,
6997
+ role: claims.role,
6998
+ emailVerified: claims.email_verified === true,
6999
+ metadata: claims.metadata ?? {},
7000
+ // The caller's APPLICATION roles, from `auth.user_roles` — the
7001
+ // same resolution the gate used, so the handler cannot read a
7002
+ // different set than the one it was admitted on. Never from a
7003
+ // claim: a revoked role has to bite on the next request, and a
7004
+ // token says only what was true when it was minted.
7005
+ roles: await rolesOfCaller(),
7006
+ // Server-owned and only ever filled from a verdict palauth
7007
+ // returned. Never from a raw header, never from JWT metadata —
7008
+ // the type has said so since it was written and this is the
7009
+ // first writer.
7010
+ device: attestedDevice
7011
+ } : null;
7012
+ break;
7013
+ case "uploadedObject": {
7014
+ if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
7015
+ await settleBeforeRefusal();
7016
+ return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
7017
+ }
7018
+ readParsedBody();
7019
+ const envelopeIn = parsedBody;
7020
+ if (!envelopeIn?.uploadedObject) {
7021
+ await settleBeforeRefusal();
7022
+ return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
7023
+ }
7024
+ const uploadId = envelopeIn.uploadedObject.uploadId;
7025
+ if (typeof uploadId === "string" && uploadId !== "") {
7026
+ const already = completions.recall(uploadId);
7027
+ if (already) {
7028
+ await settleBeforeRefusal();
7029
+ return new Response(already.body, {
7030
+ status: already.status,
7031
+ headers: already.contentType ? {
7032
+ "content-type": already.contentType
7033
+ } : void 0
7034
+ });
7035
+ }
7036
+ completionUploadId = uploadId;
7037
+ }
7038
+ args[p.index] = envelopeIn.uploadedObject;
7039
+ parsedBody = envelopeIn.body ?? {};
7040
+ break;
7041
+ }
7042
+ case "requestId":
7043
+ args[p.index] = requestId;
7044
+ break;
7045
+ case "traceId":
7046
+ args[p.index] = requestId;
7047
+ break;
7048
+ case "client":
7049
+ args[p.index] = {
7050
+ sdkVersion: req.headers.get("x-palbase-sdk-version"),
7051
+ appVersion: req.headers.get("x-palbase-client-version"),
7052
+ platform: req.headers.get("x-platform"),
7053
+ osVersion: req.headers.get("x-os-version")
7054
+ };
7055
+ break;
7056
+ case "req":
7057
+ args[p.index] = req;
7058
+ break;
7059
+ // `@Signal()` — the raw request's AbortSignal, which enters the aborted
7060
+ // state when the client disconnects. Measured on Bun: an infinite
7061
+ // producer guarded by it stops within a few frames of the client dying.
7062
+ // It is deliberately NOT reachable through `@Req()`: PBRequest carries
7063
+ // request-scoped data and no signal.
7064
+ case "signal":
7065
+ args[p.index] = req.signal;
7066
+ break;
7067
+ // `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
7068
+ // on any other route it is an inert writer whose frames go nowhere,
7069
+ // which is the same shape `@UploadedObject()` has off an `@Upload`
7070
+ // route.
7071
+ case "sseOut":
7072
+ args[p.index] = sseWriter;
7073
+ break;
7074
+ default:
7075
+ args[p.index] = void 0;
6983
7076
  }
6984
- case "requestId":
6985
- args[p.index] = requestId;
6986
- break;
6987
- case "traceId":
6988
- args[p.index] = requestId;
6989
- break;
6990
- case "client":
6991
- args[p.index] = {
6992
- sdkVersion: req.headers.get("x-palbase-sdk-version"),
6993
- appVersion: req.headers.get("x-palbase-client-version"),
6994
- platform: req.headers.get("x-platform"),
6995
- osVersion: req.headers.get("x-os-version")
6996
- };
6997
- break;
6998
- case "req":
6999
- args[p.index] = req;
7000
- break;
7001
- // `@Signal()` — the raw request's AbortSignal, which enters the aborted
7002
- // state when the client disconnects. Measured on Bun: an infinite
7003
- // producer guarded by it stops within a few frames of the client dying.
7004
- // It is deliberately NOT reachable through `@Req()`: PBRequest carries
7005
- // request-scoped data and no signal.
7006
- case "signal":
7007
- args[p.index] = req.signal;
7008
- break;
7009
- // `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
7010
- // on any other route it is an inert writer whose frames go nowhere,
7011
- // which is the same shape `@UploadedObject()` has off an `@Upload`
7012
- // route.
7013
- case "sseOut":
7014
- args[p.index] = sseWriter;
7015
- break;
7016
- default:
7017
- args[p.index] = void 0;
7018
7077
  }
7019
- }
7020
- dispatched = true;
7021
- try {
7078
+ db.authorizationComplete?.();
7079
+ dispatched = true;
7022
7080
  const services = buildServices(db);
7023
7081
  if (meta.options?.sseConfig !== void 0) {
7024
7082
  let sseController;
@@ -7034,11 +7092,19 @@ async function createApp(opts) {
7034
7092
  } catch {
7035
7093
  }
7036
7094
  }, "sseEnqueue");
7037
- let sawFirstFrame;
7038
- const firstFrame = new Promise((r) => sawFirstFrame = r);
7095
+ let sawFirstFrame, refuseFirstFrame;
7096
+ const firstFrame = new Promise((resolve2, reject) => {
7097
+ sawFirstFrame = resolve2;
7098
+ refuseFirstFrame = reject;
7099
+ });
7039
7100
  sseSettle = /* @__PURE__ */ __name(async () => {
7040
- await db.commit();
7041
- sawFirstFrame();
7101
+ try {
7102
+ await db.commit();
7103
+ sawFirstFrame();
7104
+ } catch (error) {
7105
+ refuseFirstFrame(error);
7106
+ throw error;
7107
+ }
7042
7108
  }, "sseSettle");
7043
7109
  const guarded = {
7044
7110
  ...services,
@@ -7079,6 +7145,7 @@ async function createApp(opts) {
7079
7145
  status: 204
7080
7146
  });
7081
7147
  }
7148
+ await firstFrame;
7082
7149
  void (async () => {
7083
7150
  await ran;
7084
7151
  await sseWriter.drained();
@@ -7112,15 +7179,16 @@ async function createApp(opts) {
7112
7179
  }
7113
7180
  return method.apply(hit.entry.instance, args);
7114
7181
  });
7115
- await db.commit();
7116
7182
  if (meta.returnSchema) {
7117
7183
  const v = meta.returnSchema.safeParse(result);
7118
7184
  if (!v.success) {
7119
7185
  log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
7186
+ await db.rollback(new Error("output_invalid"));
7120
7187
  return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
7121
7188
  }
7122
7189
  }
7123
- if (result === void 0 || result === null) {
7190
+ if (result === void 0 || result === null && !meta.returnSchema) {
7191
+ await db.commit();
7124
7192
  if (completionUploadId) {
7125
7193
  completions.remember(completionUploadId, {
7126
7194
  status: 204,
@@ -7133,6 +7201,7 @@ async function createApp(opts) {
7133
7201
  });
7134
7202
  }
7135
7203
  const payload = JSON.stringify(result);
7204
+ await db.commit();
7136
7205
  if (completionUploadId) {
7137
7206
  completions.remember(completionUploadId, {
7138
7207
  status: 200,
@@ -7149,16 +7218,7 @@ async function createApp(opts) {
7149
7218
  });
7150
7219
  } catch (err) {
7151
7220
  await db.rollback(err);
7152
- if (isHttpError(err)) {
7153
- if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
7154
- log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
7155
- }
7156
- return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
7157
- data: err.data
7158
- } : void 0);
7159
- }
7160
- log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
7161
- return envelope("internal_error", "The request could not be completed", 500, requestId);
7221
+ return errorResponse(err, hit.entry.id, requestId);
7162
7222
  }
7163
7223
  }
7164
7224
  __name(handle, "handle");
@@ -7171,15 +7231,24 @@ async function createApp(opts) {
7171
7231
  throw err;
7172
7232
  }
7173
7233
  return {
7174
- handle,
7234
+ handle: /* @__PURE__ */ __name((req) => !accepting ? Promise.resolve(envelope("runtime_draining", "This application is shutting down", 503, `req_${crypto.randomUUID()}`)) : serving(() => handle(req)), "handle"),
7235
+ get databasePressure() {
7236
+ return admission.snapshot;
7237
+ },
7175
7238
  routes,
7176
7239
  config,
7177
7240
  container: liveContainer,
7178
- runInServiceScope,
7179
- async shutdown() {
7180
- await runShutdownHooks();
7181
- await closeDriver(sql);
7182
- released = true;
7241
+ runInServiceScope: /* @__PURE__ */ __name((fn) => !accepting ? Promise.reject(new Error("This application is shutting down; no new database scope can start")) : serving(() => runInServiceScope(fn)), "runInServiceScope"),
7242
+ shutdown() {
7243
+ accepting = false;
7244
+ return shuttingDown ??= (async () => {
7245
+ while (inFlight.size) await Promise.allSettled([
7246
+ ...inFlight
7247
+ ]);
7248
+ await runShutdownHooks();
7249
+ await closeDriver(sql);
7250
+ released = true;
7251
+ })();
7183
7252
  }
7184
7253
  };
7185
7254
  }