@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
@@ -5,7 +5,7 @@ import {
5
5
  buildContainer,
6
6
  buildRelations,
7
7
  uniqueKeysOf
8
- } from "./chunk-6FEHCEB7.js";
8
+ } from "./chunk-YANGWAIM.js";
9
9
  import {
10
10
  validateInsertManyOptions
11
11
  } from "./chunk-GHNC2UHS.js";
@@ -13,11 +13,10 @@ import {
13
13
  __requestALS,
14
14
  __runStartHooks,
15
15
  __runWithRuntime
16
- } from "./chunk-YO5GYM73.js";
16
+ } from "./chunk-X7UR3VXA.js";
17
17
  import {
18
- CODECS,
19
- assertReadContract
20
- } from "./chunk-PRH45NZU.js";
18
+ CODECS
19
+ } from "./chunk-FFO36MQQ.js";
21
20
  import {
22
21
  assertNoExpressionHandles,
23
22
  assertUsableFilter,
@@ -32,9 +31,8 @@ import {
32
31
  looksLikeUnbrandedRef,
33
32
  qualifiedTableKey,
34
33
  retryBudget,
35
- validateAtomicOptions,
36
- validateWorkload
37
- } from "./chunk-P7MGSAFT.js";
34
+ validateAtomicOptions
35
+ } from "./chunk-LFUNRWUE.js";
38
36
  import {
39
37
  getRoutes,
40
38
  resolveEffectiveAuth
@@ -51,7 +49,7 @@ import {
51
49
  isHttpError,
52
50
  isRetryable,
53
51
  markEngineRaised
54
- } from "./chunk-UMESJJRS.js";
52
+ } from "./chunk-CS6NQ6PO.js";
55
53
  import {
56
54
  __name
57
55
  } from "./chunk-VXPNPVAG.js";
@@ -93,6 +91,16 @@ ${detail}`);
93
91
  if (!Number.isInteger(poolMax) || poolMax < 1) {
94
92
  throw new BootRefused([], `boot refused: DB_POOL_MAX must be a positive integer (got ${env.DB_POOL_MAX}).`);
95
93
  }
94
+ const capacityNumber = /* @__PURE__ */ __name((key2, minimum, maximum) => {
95
+ if (env[key2] === void 0) return void 0;
96
+ const value = Number(env[key2]);
97
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new BootRefused([], `boot refused: ${key2} must be between ${minimum} and ${maximum}.`);
98
+ return value;
99
+ }, "capacityNumber");
100
+ const queueLimit = capacityNumber("PALBASE_DB_QUEUE_LIMIT", 0, 1e4);
101
+ const queueTimeoutMs = capacityNumber("PALBASE_DB_QUEUE_TIMEOUT_MS", 1, 6e4);
102
+ const tenantClaim = env.PALBASE_DB_TENANT_CLAIM;
103
+ 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.");
96
104
  return {
97
105
  databaseUrl: env.DATABASE_URL.trim(),
98
106
  authJwksUrl: env.AUTH_JWKS_URL.trim(),
@@ -117,6 +125,17 @@ ${detail}`);
117
125
  // and the live database agrees (pg_roles.rolbypassrls = true).
118
126
  dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
119
127
  poolMax,
128
+ databaseAdmission: {
129
+ ...queueLimit === void 0 ? {} : {
130
+ queueLimit
131
+ },
132
+ ...queueTimeoutMs === void 0 ? {} : {
133
+ queueTimeoutMs
134
+ },
135
+ ...tenantClaim === void 0 ? {} : {
136
+ tenantClaim
137
+ }
138
+ },
120
139
  ...env.PALBASE_DB_DIAGNOSTICS === "true" ? {
121
140
  databaseDiagnostics: {
122
141
  onComplete: /* @__PURE__ */ __name((diagnostics) => {
@@ -737,142 +756,167 @@ function parameterChunks(sql) {
737
756
  __name(parameterChunks, "parameterChunks");
738
757
 
739
758
  // src/engine/admission.ts
740
- var WorkloadAdmission = class {
759
+ var DatabaseAdmission = class {
741
760
  static {
742
- __name(this, "WorkloadAdmission");
761
+ __name(this, "DatabaseAdmission");
743
762
  }
744
- definition;
745
- pending = [];
763
+ options;
746
764
  active = 0;
747
- activeKeys = /* @__PURE__ */ new Set();
765
+ queued = 0;
748
766
  tenants = /* @__PURE__ */ new Map();
749
- draining = false;
750
- constructor(definition) {
751
- this.definition = definition;
752
- validateWorkload(definition);
767
+ // Map insertion order is the round-robin ring; each Set is FIFO. Grant and
768
+ // cancellation remove nodes directly, without scanning a 10,000-item queue.
769
+ pending = /* @__PURE__ */ new Map();
770
+ lastTenant;
771
+ accepted = 0;
772
+ rejected = {
773
+ full: 0,
774
+ timeout: 0,
775
+ cancelled: 0
776
+ };
777
+ limit;
778
+ timeout;
779
+ constructor(options) {
780
+ this.options = options;
781
+ this.limit = options.queueLimit ?? Math.min(512, Math.max(32, options.concurrency * 8));
782
+ this.timeout = options.queueTimeoutMs ?? 1e3;
783
+ 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) {
784
+ throw new Error("Invalid database pool or queue deployment settings");
785
+ }
753
786
  }
754
787
  get snapshot() {
755
788
  return {
756
789
  active: this.active,
757
- queued: this.pending.length,
790
+ queued: this.queued,
758
791
  tenants: this.tenants.size,
759
- keys: this.activeKeys.size
792
+ accepted: this.accepted,
793
+ rejected: {
794
+ ...this.rejected
795
+ }
796
+ };
797
+ }
798
+ rejection(reason) {
799
+ this.rejected[reason]++;
800
+ try {
801
+ this.options.onReject?.({
802
+ reason,
803
+ active: this.active,
804
+ queued: this.queued
805
+ });
806
+ } catch {
807
+ }
808
+ }
809
+ take(tenant, entered) {
810
+ this.active++;
811
+ this.accepted++;
812
+ this.lastTenant = tenant;
813
+ this.tenants.set(tenant, (this.tenants.get(tenant) ?? 0) + 1);
814
+ let released = false;
815
+ return {
816
+ queueMs: performance.now() - entered,
817
+ release: /* @__PURE__ */ __name(() => {
818
+ if (released) return;
819
+ released = true;
820
+ this.active--;
821
+ const count = this.tenants.get(tenant) - 1;
822
+ if (count) this.tenants.set(tenant, count);
823
+ else this.tenants.delete(tenant);
824
+ this.drain();
825
+ }, "release")
760
826
  };
761
827
  }
762
- acquire(tenant, keys, signal) {
763
- if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Database admission cancelled"));
764
- const normalized = [
765
- ...new Set(keys)
766
- ].sort();
767
- 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"));
768
- const scoped = normalized;
769
- const globalFull = this.pending.length >= this.definition.queueLimit;
770
- const tenantFull = this.pending.filter((w) => w.tenant === tenant).length >= this.definition.tenantQueueLimit;
771
- 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)));
772
- if (!canStart && (globalFull || tenantFull)) {
773
- return Promise.reject(new TooManyRequests({
828
+ drain() {
829
+ while (this.active < this.options.concurrency && this.queued) {
830
+ let tenant = this.pending.keys().next().value;
831
+ if (this.pending.size > 1 && tenant === this.lastTenant) {
832
+ const queue2 = this.pending.get(tenant);
833
+ this.pending.delete(tenant);
834
+ this.pending.set(tenant, queue2);
835
+ tenant = this.pending.keys().next().value;
836
+ }
837
+ const queue = this.pending.get(tenant), waiter = queue.values().next().value;
838
+ queue.delete(waiter);
839
+ this.pending.delete(tenant);
840
+ if (queue.size) this.pending.set(tenant, queue);
841
+ this.queued--;
842
+ waiter.cleanup();
843
+ waiter.grant(this.take(tenant, waiter.entered));
844
+ }
845
+ }
846
+ async acquire(claimsJson, signal) {
847
+ if (signal?.aborted) {
848
+ this.rejection("cancelled");
849
+ throw signal.reason ?? new Error("Database admission cancelled");
850
+ }
851
+ const claims = JSON.parse(claimsJson);
852
+ const value = this.options.tenantClaim ? claims[this.options.tenantClaim] : claims.sub;
853
+ const tenant = typeof value === "string" && value.length ? value : "__anonymous__";
854
+ const entered = performance.now();
855
+ if (this.active < this.options.concurrency && !this.queued) return this.take(tenant, entered);
856
+ if (this.queued >= this.limit) {
857
+ this.rejection("full");
858
+ throw new TooManyRequests({
774
859
  retryAfter: 1
775
- }, `Database workload ${this.definition.name}: ${tenantFull ? "tenant" : "global"} queue is full`));
860
+ }, "Database queue is full");
776
861
  }
777
862
  return new Promise((grant, reject) => {
863
+ const queue = this.pending.get(tenant) ?? /* @__PURE__ */ new Set();
864
+ if (!this.pending.has(tenant)) this.pending.set(tenant, queue);
778
865
  const waiter = {
779
- tenant,
780
- keys: scoped,
781
- entered: performance.now(),
782
866
  grant,
783
867
  reject,
784
- cleanup: /* @__PURE__ */ __name(() => {
785
- }, "cleanup")
868
+ entered,
869
+ cleanup() {
870
+ }
786
871
  };
787
- const remove = /* @__PURE__ */ __name((reason) => {
788
- const index = this.pending.indexOf(waiter);
789
- if (index === -1) return;
790
- this.pending.splice(index, 1);
872
+ const remove = /* @__PURE__ */ __name((reason, error) => {
873
+ if (!queue.delete(waiter)) return;
874
+ if (!queue.size) this.pending.delete(tenant);
875
+ this.queued--;
791
876
  waiter.cleanup();
792
- reject(reason);
877
+ this.rejection(reason);
878
+ reject(error);
793
879
  this.drain();
794
880
  }, "remove");
795
- const abort = /* @__PURE__ */ __name(() => remove(signal?.reason ?? new Error("Database admission cancelled")), "abort");
796
- const timer = setTimeout(() => remove(new TooManyRequests({
881
+ const abort = /* @__PURE__ */ __name(() => remove("cancelled", signal?.reason ?? new Error("Database admission cancelled")), "abort");
882
+ const timer = setTimeout(() => remove("timeout", new TooManyRequests({
797
883
  retryAfter: 1
798
- }, `Database workload ${this.definition.name}: queue deadline exceeded`)), this.definition.queueTimeoutMs);
884
+ }, "Database queue deadline exceeded")), this.timeout);
799
885
  waiter.cleanup = () => {
800
886
  clearTimeout(timer);
801
887
  signal?.removeEventListener("abort", abort);
802
888
  };
889
+ queue.add(waiter);
890
+ this.queued++;
803
891
  signal?.addEventListener("abort", abort, {
804
892
  once: true
805
893
  });
806
- this.pending.push(waiter);
807
894
  if (signal?.aborted) abort();
808
895
  else this.drain();
809
896
  });
810
897
  }
811
- drain() {
812
- if (this.draining) return;
813
- this.draining = true;
814
- try {
815
- const blockedKeys = /* @__PURE__ */ new Set();
816
- for (let index = 0; index < this.pending.length && this.active < this.definition.concurrency; ) {
817
- const waiter = this.pending[index];
818
- if ((this.tenants.get(waiter.tenant) ?? 0) >= this.definition.tenantConcurrency || waiter.keys.some((key2) => this.activeKeys.has(key2) || blockedKeys.has(key2))) {
819
- for (const key2 of waiter.keys) blockedKeys.add(key2);
820
- index++;
821
- continue;
822
- }
823
- this.pending.splice(index, 1);
824
- waiter.cleanup();
825
- this.active++;
826
- this.tenants.set(waiter.tenant, (this.tenants.get(waiter.tenant) ?? 0) + 1);
827
- for (const key2 of waiter.keys) this.activeKeys.add(key2);
828
- let released = false;
829
- waiter.grant({
830
- queueMs: performance.now() - waiter.entered,
831
- release: /* @__PURE__ */ __name(() => {
832
- if (released) return;
833
- released = true;
834
- this.active--;
835
- const left = this.tenants.get(waiter.tenant) - 1;
836
- if (left === 0) this.tenants.delete(waiter.tenant);
837
- else this.tenants.set(waiter.tenant, left);
838
- for (const key2 of waiter.keys) this.activeKeys.delete(key2);
839
- this.drain();
840
- }, "release")
841
- });
842
- }
843
- } finally {
844
- this.draining = false;
898
+ };
899
+ var databases = /* @__PURE__ */ new WeakMap();
900
+ function databaseAdmission(owner, options) {
901
+ let admission = databases.get(owner);
902
+ if (!admission) {
903
+ admission = new DatabaseAdmission(options ?? {
904
+ concurrency: 10
905
+ });
906
+ databases.set(owner, admission);
907
+ } else if (options) {
908
+ for (const key2 of [
909
+ "concurrency",
910
+ "queueLimit",
911
+ "queueTimeoutMs",
912
+ "tenantClaim"
913
+ ]) {
914
+ if (options[key2] !== admission.options[key2]) throw new Error("Database admission cannot be reconfigured while its pool is shared");
845
915
  }
846
916
  }
847
- };
848
- var registries = /* @__PURE__ */ new WeakMap();
849
- function workloadAdmission(owner, definition) {
850
- validateWorkload(definition);
851
- let registry = registries.get(owner);
852
- if (!registry) {
853
- registry = /* @__PURE__ */ new Map();
854
- registries.set(owner, registry);
855
- }
856
- const existing = registry.get(definition.name);
857
- if (existing) {
858
- const d = existing.definition;
859
- if (Object.keys(d).some((k) => d[k] !== definition[k])) throw new Error(`Workload ${definition.name} was already registered with different limits`);
860
- return existing;
861
- }
862
- 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");
863
- const admission = new WorkloadAdmission(Object.freeze({
864
- ...definition
865
- }));
866
- registry.set(definition.name, admission);
867
917
  return admission;
868
918
  }
869
- __name(workloadAdmission, "workloadAdmission");
870
- function workloadTenant(definition, claimsJson) {
871
- const claims = JSON.parse(claimsJson);
872
- const value = claims[definition.tenantClaim] ?? claims.sub;
873
- return typeof value === "string" && value.length > 0 ? value : "__anonymous__";
874
- }
875
- __name(workloadTenant, "workloadTenant");
919
+ __name(databaseAdmission, "databaseAdmission");
876
920
 
877
921
  // src/engine/transaction-control.ts
878
922
  var TransactionControl = class {
@@ -995,6 +1039,7 @@ var DatabaseTrace = class {
995
1039
  sqlMs: 0,
996
1040
  commitMs: 0,
997
1041
  rollbackMs: 0,
1042
+ connectionMs: 0,
998
1043
  commits: 0,
999
1044
  commitFailures: 0,
1000
1045
  rollbackRequests: 0,
@@ -1006,7 +1051,7 @@ var DatabaseTrace = class {
1006
1051
  if (this.failure) throw this.failure;
1007
1052
  }
1008
1053
  time(key2, started) {
1009
- this.state[key2] += performance.now() - started;
1054
+ this.state[key2] = (this.state[key2] ?? 0) + performance.now() - started;
1010
1055
  }
1011
1056
  retry() {
1012
1057
  this.state.retries++;
@@ -1212,31 +1257,53 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
1212
1257
  fail = rej;
1213
1258
  });
1214
1259
  void parked.catch(() => void 0);
1215
- const openingAt = options.trace ? performance.now() : 0;
1216
- settled = sql.begin(async (tx) => {
1217
- options.trace?.time("poolMs", openingAt);
1218
- const setupAt = options.trace ? performance.now() : 0;
1219
- options.control?.check();
1220
- if (options.isolation !== void 0 || options.readOnly !== void 0) {
1221
- const isolation = options.isolation ?? "read committed";
1222
- validateAtomicOptions({
1223
- isolation,
1224
- readOnly: options.readOnly
1225
- });
1226
- await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
1260
+ settled = (async () => {
1261
+ const queuedAt = performance.now();
1262
+ let lease;
1263
+ try {
1264
+ lease = await (sql.admission ?? databaseAdmission(sql)).acquire(claimsJson, options.control?.signal);
1265
+ } finally {
1266
+ options.trace?.time("queueMs", queuedAt);
1227
1267
  }
1228
- if (options.control && sql.cancelBackend) {
1229
- 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()");
1230
- if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
1231
- options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
1232
- options.control.check();
1268
+ const openingAt = performance.now();
1269
+ let connectedAt;
1270
+ try {
1271
+ options.control?.check();
1272
+ if (closed) throw new Error("Database transaction closed while waiting for admission");
1273
+ return await sql.begin(async (tx) => {
1274
+ connectedAt = performance.now();
1275
+ options.trace?.time("poolMs", openingAt);
1276
+ const setupAt = options.trace ? performance.now() : 0;
1277
+ options.control?.check();
1278
+ if (closed) throw new Error("Database transaction closed while waiting for its connection");
1279
+ if (options.isolation !== void 0 || options.readOnly !== void 0) {
1280
+ const isolation = options.isolation ?? "read committed";
1281
+ validateAtomicOptions({
1282
+ isolation,
1283
+ readOnly: options.readOnly
1284
+ });
1285
+ await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
1286
+ }
1287
+ if (options.control && sql.cancelBackend) {
1288
+ const ticket = await tx.unsafe(`WITH backend AS MATERIALIZED (
1289
+ SELECT pid, backend_start::text AS started FROM pg_catalog.pg_stat_get_activity(pg_catalog.pg_backend_pid())
1290
+ ) ${bindSql}, backend.pid, backend.started FROM backend`, bindParams);
1291
+ if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
1292
+ options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
1293
+ options.control.check();
1294
+ } else {
1295
+ const binding = tx.unsafe(bindSql, bindParams);
1296
+ await (options.control ? options.control.watch(binding) : binding);
1297
+ }
1298
+ options.trace?.time("setupMs", setupAt);
1299
+ resolveTx2(scoped(tx));
1300
+ await parked;
1301
+ });
1302
+ } finally {
1303
+ if (connectedAt !== void 0) options.trace?.time("connectionMs", connectedAt);
1304
+ lease.release();
1233
1305
  }
1234
- const binding = tx.unsafe(bindSql, bindParams);
1235
- await (options.control ? options.control.watch(binding) : binding);
1236
- options.trace?.time("setupMs", setupAt);
1237
- resolveTx2(scoped(tx));
1238
- await parked;
1239
- }).catch((e) => {
1306
+ })().catch((e) => {
1240
1307
  rejectTx(e);
1241
1308
  throw e;
1242
1309
  });
@@ -1253,10 +1320,13 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
1253
1320
  if (pendingSavepoints.size) await Promise.allSettled([
1254
1321
  ...pendingSavepoints
1255
1322
  ]);
1256
- options.control?.check();
1257
1323
  options.trace?.check();
1324
+ if (!opening) {
1325
+ closed = true;
1326
+ return;
1327
+ }
1328
+ options.control?.check();
1258
1329
  closed = true;
1259
- if (!opening) return;
1260
1330
  const started = options.trace ? performance.now() : 0;
1261
1331
  release();
1262
1332
  try {
@@ -1383,10 +1453,11 @@ function asTableRows(table, rows, withSpec, partial = false) {
1383
1453
  const nested = typeof spec === "object" ? spec.with : void 0;
1384
1454
  for (const row of result) {
1385
1455
  const value = row[rel.name];
1386
- if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested);
1456
+ const selected = typeof spec === "object" && "select" in spec && spec.select !== void 0;
1457
+ if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested, selected);
1387
1458
  else if (value && typeof value === "object") row[rel.name] = asTableRows(rel.to, [
1388
1459
  value
1389
- ], nested)[0];
1460
+ ], nested, selected)[0];
1390
1461
  }
1391
1462
  }
1392
1463
  for (const row of result) foldCan(table, row, partial);
@@ -2330,6 +2401,9 @@ function withClauses(table, spec, add, caller, parentAlias = "t", depth = 0, cou
2330
2401
  const i = ++counter.value;
2331
2402
  const childAlias = `w${i}`;
2332
2403
  const childKnown = schemaColumns(rel.to);
2404
+ if (o["select"] !== void 0 && (!Array.isArray(o["select"]) || o["select"].length === 0)) {
2405
+ throw new Error(`${caller}(${table}): with.${name}.select must be a non-empty array of columns`);
2406
+ }
2333
2407
  const childCols = Array.isArray(o["select"]) && o["select"].length > 0 ? o["select"] : childKnown !== null ? [
2334
2408
  ...childKnown
2335
2409
  ] : [];
@@ -2433,7 +2507,10 @@ function pageSchema(table) {
2433
2507
  }
2434
2508
  __name(pageSchema, "pageSchema");
2435
2509
  function createOps(tx, context = {}) {
2436
- const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
2510
+ const at = /* @__PURE__ */ __name(() => {
2511
+ const target = typeof tx === "function" ? tx() : tx;
2512
+ return target instanceof Promise ? target.then(resolveTx) : resolveTx(target);
2513
+ }, "at");
2437
2514
  const ops = {
2438
2515
  diagnostics() {
2439
2516
  return context.trace?.snapshot ?? null;
@@ -2884,22 +2961,6 @@ function createOps(tx, context = {}) {
2884
2961
  ]);
2885
2962
  return rows[0] ? asTableRow(table, rows[0]) : null;
2886
2963
  },
2887
- compileQuery(query) {
2888
- assertReadContract(query, tableDefOf(query.table)?.columns);
2889
- return compileFindMany(query.table, query.where, query.options);
2890
- },
2891
- async readQuery(query) {
2892
- const compiled = ops.compileQuery(query);
2893
- return asTableRows(query.table, await (await at()).unsafe(compiled.sql, compiled.params, {
2894
- name: query.name
2895
- }), void 0, true);
2896
- },
2897
- async explainQuery(query) {
2898
- const compiled = ops.compileQuery(query);
2899
- return await (await at()).unsafe(`EXPLAIN (FORMAT JSON) ${compiled.sql}`, compiled.params, {
2900
- name: `${query.name}.explain`
2901
- });
2902
- },
2903
2964
  async findMany(table, query = {}, opts = {}) {
2904
2965
  const compiled = compileFindMany(table, query, opts);
2905
2966
  return asTableRows(table, await (await at()).unsafe(compiled.sql, compiled.params), opts.with, opts.select !== void 0);
@@ -3434,7 +3495,7 @@ function createOps(tx, context = {}) {
3434
3495
  if (options.mode === "compiled") throw new Error(`Command cannot compile: ${compiled.reason}. Use auto/sequential mode or installCommandExecutor at boot.`);
3435
3496
  return ops.txPlan(plan);
3436
3497
  }
3437
- if (compiled.steps.length === 0) return {
3498
+ if (compiled.steps.length === 0 && !compiled.pipeline?.length) return {
3438
3499
  results: []
3439
3500
  };
3440
3501
  if (compiled.pipeline) {
@@ -3707,6 +3768,7 @@ function diagnosingDriver(sql, surface = "service") {
3707
3768
  const state = sqlstateOf(e);
3708
3769
  if (state === SQLSTATE.serializationFailure) return markEngineRaised(new SerializationFailure(state));
3709
3770
  if (state === SQLSTATE.deadlockDetected) return markEngineRaised(new DeadlockDetected(state));
3771
+ if (state === SQLSTATE.queryCancelled) return markEngineRaised(new HttpError(503, "database_query_cancelled", "Database statement was cancelled"));
3710
3772
  if (surface === "service" && isForeignKeyViolation(e)) {
3711
3773
  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)})`);
3712
3774
  }
@@ -3733,6 +3795,7 @@ function diagnosingDriver(sql, surface = "service") {
3733
3795
  }
3734
3796
  }), "wrapTx");
3735
3797
  return {
3798
+ admission: sql.admission ?? databaseAdmission(sql),
3736
3799
  unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
3737
3800
  cancelBackend: sql.cancelBackend?.bind(sql),
3738
3801
  begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))).catch((e) => {
@@ -3741,9 +3804,12 @@ function diagnosingDriver(sql, surface = "service") {
3741
3804
  };
3742
3805
  }
3743
3806
  __name(diagnosingDriver, "diagnosingDriver");
3744
- function createRequestDatabase(sql, identity, diagnostics) {
3807
+ function createRequestDatabase(sql, identity, diagnostics, signal) {
3745
3808
  const trace = diagnostics === void 0 ? void 0 : new DatabaseTrace(diagnostics);
3809
+ const requestControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
3746
3810
  let requestClosed = false;
3811
+ let authorized = false, applicationTouched = false;
3812
+ let authorizationRelease;
3747
3813
  const contextFor = /* @__PURE__ */ __name((role) => {
3748
3814
  let scope;
3749
3815
  return {
@@ -3762,19 +3828,21 @@ function createRequestDatabase(sql, identity, diagnostics) {
3762
3828
  validateAtomicOptions(options);
3763
3829
  if (requestClosed) throw new Error("Database request has settled; it cannot open another transaction");
3764
3830
  if (atomicScope.getStore() !== void 0) throw new Error("Nested Database.$atomic is not supported; use tx.$attempt for a savepoint");
3831
+ if (authorized && !applicationTouched && tx.opened) {
3832
+ authorizationRelease ??= (async () => {
3833
+ await tx.commit();
3834
+ tx = requestTransaction();
3835
+ })();
3836
+ }
3837
+ if (authorizationRelease) await authorizationRelease;
3765
3838
  const retries = retryBudget(options.retry);
3766
- const control = options.timeoutMs !== void 0 || options.signal !== void 0 ? new TransactionControl(options.timeoutMs, options.signal) : void 0;
3767
- let lease;
3839
+ const atomicSignal = options.signal && signal ? AbortSignal.any([
3840
+ options.signal,
3841
+ signal
3842
+ ]) : options.signal ?? signal;
3843
+ const control = options.timeoutMs !== void 0 || atomicSignal !== void 0 ? new TransactionControl(options.timeoutMs, atomicSignal) : void 0;
3768
3844
  try {
3769
3845
  control?.check();
3770
- if (options.workload) {
3771
- const queuedAt = trace ? performance.now() : 0;
3772
- try {
3773
- lease = await workloadAdmission(sql, options.workload).acquire(workloadTenant(options.workload, identity.claimsJson), options.conflictKeys ?? [], control?.signal);
3774
- } finally {
3775
- trace?.time("queueMs", queuedAt);
3776
- }
3777
- }
3778
3846
  for (let attempt = 0; ; attempt++) {
3779
3847
  control?.check();
3780
3848
  const timeouts = [
@@ -3807,19 +3875,23 @@ function createRequestDatabase(sql, identity, diagnostics) {
3807
3875
  }
3808
3876
  } finally {
3809
3877
  control?.dispose();
3810
- lease?.release();
3811
3878
  }
3812
3879
  }, "atomic");
3813
- const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
3814
- trace
3815
- });
3880
+ const requestTransaction = /* @__PURE__ */ __name(() => createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
3881
+ trace,
3882
+ control: requestControl
3883
+ }), "requestTransaction");
3884
+ let tx = requestTransaction();
3816
3885
  let serviceTx = null;
3886
+ let serviceControl;
3817
3887
  let serviceClient = null;
3818
3888
  const asService = /* @__PURE__ */ __name(() => {
3819
3889
  if (serviceClient === null) {
3890
+ serviceControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
3820
3891
  serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
3821
3892
  lockTimeout: SERVICE_LOCK_TIMEOUT,
3822
- trace
3893
+ trace,
3894
+ control: serviceControl
3823
3895
  });
3824
3896
  serviceClient = Object.assign(withTables(createOps(serviceTx, serviceContext)), {
3825
3897
  atomic: atomic(identity.serviceRole, "service")
@@ -3828,16 +3900,24 @@ function createRequestDatabase(sql, identity, diagnostics) {
3828
3900
  return serviceClient;
3829
3901
  }, "asService");
3830
3902
  return {
3831
- client: Object.assign(withTables(createOps(tx, userContext)), {
3903
+ client: Object.assign(withTables(createOps(() => {
3904
+ if (authorized) applicationTouched = true;
3905
+ return authorizationRelease ? authorizationRelease.then(() => tx) : tx;
3906
+ }, userContext)), {
3832
3907
  asService,
3833
3908
  atomic: atomic(identity.role, "user")
3834
3909
  }),
3910
+ authorizationComplete() {
3911
+ authorized = true;
3912
+ },
3835
3913
  async commit() {
3836
3914
  requestClosed = true;
3837
3915
  try {
3838
3916
  await tx.commit();
3839
3917
  await serviceTx?.commit();
3840
3918
  trace?.complete("committed");
3919
+ requestControl?.dispose();
3920
+ serviceControl?.dispose();
3841
3921
  } catch (error) {
3842
3922
  trace?.failed();
3843
3923
  throw error;
@@ -3847,6 +3927,8 @@ function createRequestDatabase(sql, identity, diagnostics) {
3847
3927
  requestClosed = true;
3848
3928
  await tx.rollback(reason);
3849
3929
  await serviceTx?.rollback(reason);
3930
+ requestControl?.dispose();
3931
+ serviceControl?.dispose();
3850
3932
  trace?.complete("failed");
3851
3933
  }
3852
3934
  };
@@ -4154,7 +4236,7 @@ function compileCommand(plan) {
4154
4236
  };
4155
4237
  });
4156
4238
  steps.push({
4157
- chunks: parameterChunks(sql),
4239
+ sql,
4158
4240
  bindings,
4159
4241
  guard: op.guard
4160
4242
  });
@@ -4177,11 +4259,15 @@ function compileCommand(plan) {
4177
4259
  rows_affected: 1
4178
4260
  });
4179
4261
  }
4262
+ if (!hasRefs) return {
4263
+ steps: [],
4264
+ pipeline
4265
+ };
4180
4266
  return {
4181
- steps,
4182
- ...hasRefs ? {} : {
4183
- pipeline
4184
- }
4267
+ steps: steps.map(({ sql, ...step }) => sql === void 0 ? step : {
4268
+ ...step,
4269
+ chunks: parameterChunks(sql)
4270
+ })
4185
4271
  };
4186
4272
  }
4187
4273
  __name(compileCommand, "compileCommand");
@@ -4589,9 +4675,11 @@ function makeSseWriter(opts) {
4589
4675
  if (!begun) {
4590
4676
  begun = true;
4591
4677
  chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
4678
+ void chain.catch(() => void 0);
4592
4679
  return;
4593
4680
  }
4594
4681
  chain = chain.then(() => opts.enqueue(frame));
4682
+ void chain.catch(() => void 0);
4595
4683
  },
4596
4684
  started() {
4597
4685
  return begun;
@@ -4804,6 +4892,23 @@ async function createApp(opts) {
4804
4892
  throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
4805
4893
  }
4806
4894
  const sql = opts.sql ?? await defaultSqlDriver(config);
4895
+ const admission = sql.admission ?? databaseAdmission(sql, {
4896
+ concurrency: config.poolMax,
4897
+ ...config.databaseAdmission
4898
+ });
4899
+ const inFlight = /* @__PURE__ */ new Set();
4900
+ let shuttingDown;
4901
+ let accepting = true;
4902
+ async function serving(run) {
4903
+ const work = run();
4904
+ inFlight.add(work);
4905
+ try {
4906
+ return await work;
4907
+ } finally {
4908
+ inFlight.delete(work);
4909
+ }
4910
+ }
4911
+ __name(serving, "serving");
4807
4912
  await sql.unsafe("select 1");
4808
4913
  setSchema(opts.schemas ?? []);
4809
4914
  setCursorKey(config.cursorSecret || config.serviceRoleKey || null);
@@ -4870,6 +4975,19 @@ async function createApp(opts) {
4870
4975
  }
4871
4976
  }
4872
4977
  __name(runInServiceScope, "runInServiceScope");
4978
+ function errorResponse(err, endpoint, requestId) {
4979
+ if (isHttpError(err)) {
4980
+ if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
4981
+ log.error(`[engine] unhandled ${err.error} in ${endpoint}`, err);
4982
+ }
4983
+ return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
4984
+ data: err.data
4985
+ } : void 0);
4986
+ }
4987
+ log.error(`[engine] unhandled error in ${endpoint}`, err);
4988
+ return envelope("internal_error", "The request could not be completed", 500, requestId);
4989
+ }
4990
+ __name(errorResponse, "errorResponse");
4873
4991
  async function handleAuthorize(req, requestId) {
4874
4992
  if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
4875
4993
  return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
@@ -4892,7 +5010,7 @@ async function createApp(opts) {
4892
5010
  ...config.databaseDiagnostics,
4893
5011
  endpoint: "upload authorization",
4894
5012
  requestId
4895
- });
5013
+ }, req.signal);
4896
5014
  try {
4897
5015
  const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
4898
5016
  if (refusal) {
@@ -4902,7 +5020,7 @@ async function createApp(opts) {
4902
5020
  await uploadDb.commit();
4903
5021
  } catch (err) {
4904
5022
  await uploadDb.rollback(err);
4905
- throw err;
5023
+ return errorResponse(err, "upload authorization", requestId);
4906
5024
  }
4907
5025
  const grant = grantFor(target?.entry, {
4908
5026
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -4944,256 +5062,257 @@ async function createApp(opts) {
4944
5062
  ...meta.options.databaseBudget,
4945
5063
  endpoint: hit.entry.id,
4946
5064
  requestId
4947
- });
4948
- const rolesOfCaller = makeRoleResolver(db.client);
4949
- let dispatched = false;
4950
- const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
4951
- if (dispatched) return;
4952
- await db.rollback(new Error("request refused before dispatch"));
4953
- }, "settleBeforeRefusal");
4954
- const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
4955
- if (refusal) {
4956
- await settleBeforeRefusal();
4957
- return refusal;
4958
- }
4959
- if (claims && spec.verifiedEmail && claims.email_verified !== true) {
4960
- await settleBeforeRefusal();
4961
- return envelope("email_not_verified", "A verified email address is required", 403, requestId);
4962
- }
4963
- let parsedBody;
4964
- let bodyRead = false;
4965
- let rawBody = null;
4966
- if (req.method !== "GET" && req.method !== "HEAD") {
4967
- const declared = Number(req.headers.get("content-length") ?? "");
4968
- if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
5065
+ }, req.signal);
5066
+ try {
5067
+ const rolesOfCaller = makeRoleResolver(db.client);
5068
+ let dispatched = false;
5069
+ const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
5070
+ if (dispatched) return;
5071
+ await db.rollback(new Error("request refused before dispatch"));
5072
+ }, "settleBeforeRefusal");
5073
+ const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
5074
+ if (refusal) {
5075
+ await settleBeforeRefusal();
5076
+ return refusal;
5077
+ }
5078
+ if (claims && spec.verifiedEmail && claims.email_verified !== true) {
5079
+ await settleBeforeRefusal();
5080
+ return envelope("email_not_verified", "A verified email address is required", 403, requestId);
5081
+ }
5082
+ let parsedBody;
5083
+ let bodyRead = false;
5084
+ let rawBody = null;
5085
+ if (req.method !== "GET" && req.method !== "HEAD") {
5086
+ const declared = Number(req.headers.get("content-length") ?? "");
5087
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
5088
+ await settleBeforeRefusal();
5089
+ return new Response(JSON.stringify({
5090
+ error: "payload_too_large",
5091
+ error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
5092
+ status: 413,
5093
+ request_id: requestId
5094
+ }), {
5095
+ status: 413,
5096
+ headers: JSON_HEADERS
5097
+ });
5098
+ }
5099
+ const buf = await req.arrayBuffer().catch(() => null);
5100
+ if (buf && buf.byteLength > MAX_BODY_BYTES) {
5101
+ await settleBeforeRefusal();
5102
+ return new Response(JSON.stringify({
5103
+ error: "payload_too_large",
5104
+ error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
5105
+ status: 413,
5106
+ request_id: requestId
5107
+ }), {
5108
+ status: 413,
5109
+ headers: JSON_HEADERS
5110
+ });
5111
+ }
5112
+ if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
5113
+ }
5114
+ const readParsedBody = /* @__PURE__ */ __name(() => {
5115
+ if (bodyRead) return parsedBody;
5116
+ bodyRead = true;
5117
+ if (!rawBody) {
5118
+ parsedBody = {};
5119
+ return parsedBody;
5120
+ }
5121
+ try {
5122
+ parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
5123
+ } catch {
5124
+ parsedBody = {};
5125
+ }
5126
+ return parsedBody;
5127
+ }, "readParsedBody");
5128
+ const attest = await attestGate(req, rawBody, {
5129
+ userId
5130
+ });
5131
+ if (attest.observation) observations.record(attest.observation);
5132
+ if (attest.outcome === "reject") {
5133
+ const headers = {
5134
+ ...JSON_HEADERS
5135
+ };
5136
+ if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
4969
5137
  await settleBeforeRefusal();
4970
5138
  return new Response(JSON.stringify({
4971
- error: "payload_too_large",
4972
- error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
4973
- status: 413,
5139
+ error: attest.reason,
5140
+ error_description: "This endpoint requires a verified app instance",
5141
+ status: 401,
4974
5142
  request_id: requestId
4975
5143
  }), {
4976
- status: 413,
4977
- headers: JSON_HEADERS
5144
+ status: 401,
5145
+ headers
4978
5146
  });
4979
5147
  }
4980
- const buf = await req.arrayBuffer().catch(() => null);
4981
- if (buf && buf.byteLength > MAX_BODY_BYTES) {
5148
+ if (attest.device && claims) attestedDevice = attest.device;
5149
+ attestChallengeOut = attest.nextChallenge;
5150
+ const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
5151
+ if (retryAfter !== null) {
4982
5152
  await settleBeforeRefusal();
4983
5153
  return new Response(JSON.stringify({
4984
- error: "payload_too_large",
4985
- error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
4986
- status: 413,
4987
- request_id: requestId
5154
+ error: "too_many_requests",
5155
+ error_description: "Rate limit exceeded for this endpoint",
5156
+ status: 429,
5157
+ request_id: requestId,
5158
+ data: {
5159
+ retryAfter
5160
+ }
4988
5161
  }), {
4989
- status: 413,
4990
- headers: JSON_HEADERS
5162
+ status: 429,
5163
+ headers: {
5164
+ ...JSON_HEADERS,
5165
+ "retry-after": String(retryAfter)
5166
+ }
4991
5167
  });
4992
5168
  }
4993
- if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
4994
- }
4995
- const readParsedBody = /* @__PURE__ */ __name(() => {
4996
- if (bodyRead) return parsedBody;
4997
- bodyRead = true;
4998
- if (!rawBody) {
4999
- parsedBody = {};
5000
- return parsedBody;
5001
- }
5002
- try {
5003
- parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
5004
- } catch {
5005
- parsedBody = {};
5006
- }
5007
- return parsedBody;
5008
- }, "readParsedBody");
5009
- const attest = await attestGate(req, rawBody, {
5010
- userId
5011
- });
5012
- if (attest.observation) observations.record(attest.observation);
5013
- if (attest.outcome === "reject") {
5014
- const headers = {
5015
- ...JSON_HEADERS
5016
- };
5017
- if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
5018
- await settleBeforeRefusal();
5019
- return new Response(JSON.stringify({
5020
- error: attest.reason,
5021
- error_description: "This endpoint requires a verified app instance",
5022
- status: 401,
5023
- request_id: requestId
5024
- }), {
5025
- status: 401,
5026
- headers
5027
- });
5028
- }
5029
- if (attest.device && claims) attestedDevice = attest.device;
5030
- attestChallengeOut = attest.nextChallenge;
5031
- const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
5032
- if (retryAfter !== null) {
5033
- await settleBeforeRefusal();
5034
- return new Response(JSON.stringify({
5035
- error: "too_many_requests",
5036
- error_description: "Rate limit exceeded for this endpoint",
5037
- status: 429,
5038
- request_id: requestId,
5039
- data: {
5040
- retryAfter
5041
- }
5042
- }), {
5043
- status: 429,
5044
- headers: {
5045
- ...JSON_HEADERS,
5046
- "retry-after": String(retryAfter)
5047
- }
5169
+ let completionUploadId = null;
5170
+ const args = [];
5171
+ let sseEnqueue = null;
5172
+ let sseSettle = /* @__PURE__ */ __name(async () => {
5173
+ }, "sseSettle");
5174
+ const sseWriter = makeSseWriter({
5175
+ enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
5176
+ onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
5048
5177
  });
5049
- }
5050
- let completionUploadId = null;
5051
- const args = [];
5052
- let sseEnqueue = null;
5053
- let sseSettle = /* @__PURE__ */ __name(async () => {
5054
- }, "sseSettle");
5055
- const sseWriter = makeSseWriter({
5056
- enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
5057
- onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
5058
- });
5059
- for (const p of meta.params ?? []) {
5060
- switch (p.kind) {
5061
- case "body": {
5062
- readParsedBody();
5063
- const r = p.schema.safeParse(parsedBody);
5064
- if (!r.success) {
5065
- await settleBeforeRefusal();
5066
- return envelope("bad_request", "Request body failed validation", 400, requestId, {
5067
- data: {
5068
- fields: fieldErrors(r.error)
5069
- }
5070
- });
5071
- }
5072
- args[p.index] = r.data;
5073
- break;
5074
- }
5075
- case "query": {
5076
- const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
5077
- if (!r.success) {
5078
- await settleBeforeRefusal();
5079
- return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
5080
- data: {
5081
- fields: fieldErrors(r.error)
5082
- }
5083
- });
5084
- }
5085
- args[p.index] = r.data;
5086
- break;
5087
- }
5088
- case "param":
5089
- args[p.index] = hit.params[p.name];
5090
- break;
5091
- case "headers": {
5092
- const raw = Object.fromEntries(req.headers);
5093
- if (!p.schema) {
5094
- args[p.index] = raw;
5178
+ for (const p of meta.params ?? []) {
5179
+ switch (p.kind) {
5180
+ case "body": {
5181
+ readParsedBody();
5182
+ const r = p.schema.safeParse(parsedBody);
5183
+ if (!r.success) {
5184
+ await settleBeforeRefusal();
5185
+ return envelope("bad_request", "Request body failed validation", 400, requestId, {
5186
+ data: {
5187
+ fields: fieldErrors(r.error)
5188
+ }
5189
+ });
5190
+ }
5191
+ args[p.index] = r.data;
5095
5192
  break;
5096
5193
  }
5097
- const r = p.schema.safeParse(headersFor(raw, p.schema));
5098
- if (!r.success) {
5099
- await settleBeforeRefusal();
5100
- return envelope("bad_request", "Request headers failed validation", 400, requestId, {
5101
- data: {
5102
- fields: fieldErrors(r.error)
5103
- }
5104
- });
5105
- }
5106
- args[p.index] = r.data;
5107
- break;
5108
- }
5109
- case "user":
5110
- case "optionalUser":
5111
- args[p.index] = claims ? {
5112
- id: userId,
5113
- email: claims.email,
5114
- role: claims.role,
5115
- emailVerified: claims.email_verified === true,
5116
- metadata: claims.metadata ?? {},
5117
- // The caller's APPLICATION roles, from `auth.user_roles` — the
5118
- // same resolution the gate used, so the handler cannot read a
5119
- // different set than the one it was admitted on. Never from a
5120
- // claim: a revoked role has to bite on the next request, and a
5121
- // token says only what was true when it was minted.
5122
- roles: await rolesOfCaller(),
5123
- // Server-owned and only ever filled from a verdict palauth
5124
- // returned. Never from a raw header, never from JWT metadata —
5125
- // the type has said so since it was written and this is the
5126
- // first writer.
5127
- device: attestedDevice
5128
- } : null;
5129
- break;
5130
- case "uploadedObject": {
5131
- if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
5132
- await settleBeforeRefusal();
5133
- return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
5134
- }
5135
- readParsedBody();
5136
- const envelopeIn = parsedBody;
5137
- if (!envelopeIn?.uploadedObject) {
5138
- await settleBeforeRefusal();
5139
- return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
5194
+ case "query": {
5195
+ const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
5196
+ if (!r.success) {
5197
+ await settleBeforeRefusal();
5198
+ return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
5199
+ data: {
5200
+ fields: fieldErrors(r.error)
5201
+ }
5202
+ });
5203
+ }
5204
+ args[p.index] = r.data;
5205
+ break;
5140
5206
  }
5141
- const uploadId = envelopeIn.uploadedObject.uploadId;
5142
- if (typeof uploadId === "string" && uploadId !== "") {
5143
- const already = completions.recall(uploadId);
5144
- if (already) {
5207
+ case "param":
5208
+ args[p.index] = hit.params[p.name];
5209
+ break;
5210
+ case "headers": {
5211
+ const raw = Object.fromEntries(req.headers);
5212
+ if (!p.schema) {
5213
+ args[p.index] = raw;
5214
+ break;
5215
+ }
5216
+ const r = p.schema.safeParse(headersFor(raw, p.schema));
5217
+ if (!r.success) {
5145
5218
  await settleBeforeRefusal();
5146
- return new Response(already.body, {
5147
- status: already.status,
5148
- headers: already.contentType ? {
5149
- "content-type": already.contentType
5150
- } : void 0
5219
+ return envelope("bad_request", "Request headers failed validation", 400, requestId, {
5220
+ data: {
5221
+ fields: fieldErrors(r.error)
5222
+ }
5151
5223
  });
5152
5224
  }
5153
- completionUploadId = uploadId;
5225
+ args[p.index] = r.data;
5226
+ break;
5227
+ }
5228
+ case "user":
5229
+ case "optionalUser":
5230
+ args[p.index] = claims ? {
5231
+ id: userId,
5232
+ email: claims.email,
5233
+ role: claims.role,
5234
+ emailVerified: claims.email_verified === true,
5235
+ metadata: claims.metadata ?? {},
5236
+ // The caller's APPLICATION roles, from `auth.user_roles` — the
5237
+ // same resolution the gate used, so the handler cannot read a
5238
+ // different set than the one it was admitted on. Never from a
5239
+ // claim: a revoked role has to bite on the next request, and a
5240
+ // token says only what was true when it was minted.
5241
+ roles: await rolesOfCaller(),
5242
+ // Server-owned and only ever filled from a verdict palauth
5243
+ // returned. Never from a raw header, never from JWT metadata —
5244
+ // the type has said so since it was written and this is the
5245
+ // first writer.
5246
+ device: attestedDevice
5247
+ } : null;
5248
+ break;
5249
+ case "uploadedObject": {
5250
+ if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
5251
+ await settleBeforeRefusal();
5252
+ return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
5253
+ }
5254
+ readParsedBody();
5255
+ const envelopeIn = parsedBody;
5256
+ if (!envelopeIn?.uploadedObject) {
5257
+ await settleBeforeRefusal();
5258
+ return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
5259
+ }
5260
+ const uploadId = envelopeIn.uploadedObject.uploadId;
5261
+ if (typeof uploadId === "string" && uploadId !== "") {
5262
+ const already = completions.recall(uploadId);
5263
+ if (already) {
5264
+ await settleBeforeRefusal();
5265
+ return new Response(already.body, {
5266
+ status: already.status,
5267
+ headers: already.contentType ? {
5268
+ "content-type": already.contentType
5269
+ } : void 0
5270
+ });
5271
+ }
5272
+ completionUploadId = uploadId;
5273
+ }
5274
+ args[p.index] = envelopeIn.uploadedObject;
5275
+ parsedBody = envelopeIn.body ?? {};
5276
+ break;
5154
5277
  }
5155
- args[p.index] = envelopeIn.uploadedObject;
5156
- parsedBody = envelopeIn.body ?? {};
5157
- break;
5278
+ case "requestId":
5279
+ args[p.index] = requestId;
5280
+ break;
5281
+ case "traceId":
5282
+ args[p.index] = requestId;
5283
+ break;
5284
+ case "client":
5285
+ args[p.index] = {
5286
+ sdkVersion: req.headers.get("x-palbase-sdk-version"),
5287
+ appVersion: req.headers.get("x-palbase-client-version"),
5288
+ platform: req.headers.get("x-platform"),
5289
+ osVersion: req.headers.get("x-os-version")
5290
+ };
5291
+ break;
5292
+ case "req":
5293
+ args[p.index] = req;
5294
+ break;
5295
+ // `@Signal()` — the raw request's AbortSignal, which enters the aborted
5296
+ // state when the client disconnects. Measured on Bun: an infinite
5297
+ // producer guarded by it stops within a few frames of the client dying.
5298
+ // It is deliberately NOT reachable through `@Req()`: PBRequest carries
5299
+ // request-scoped data and no signal.
5300
+ case "signal":
5301
+ args[p.index] = req.signal;
5302
+ break;
5303
+ // `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
5304
+ // on any other route it is an inert writer whose frames go nowhere,
5305
+ // which is the same shape `@UploadedObject()` has off an `@Upload`
5306
+ // route.
5307
+ case "sseOut":
5308
+ args[p.index] = sseWriter;
5309
+ break;
5310
+ default:
5311
+ args[p.index] = void 0;
5158
5312
  }
5159
- case "requestId":
5160
- args[p.index] = requestId;
5161
- break;
5162
- case "traceId":
5163
- args[p.index] = requestId;
5164
- break;
5165
- case "client":
5166
- args[p.index] = {
5167
- sdkVersion: req.headers.get("x-palbase-sdk-version"),
5168
- appVersion: req.headers.get("x-palbase-client-version"),
5169
- platform: req.headers.get("x-platform"),
5170
- osVersion: req.headers.get("x-os-version")
5171
- };
5172
- break;
5173
- case "req":
5174
- args[p.index] = req;
5175
- break;
5176
- // `@Signal()` — the raw request's AbortSignal, which enters the aborted
5177
- // state when the client disconnects. Measured on Bun: an infinite
5178
- // producer guarded by it stops within a few frames of the client dying.
5179
- // It is deliberately NOT reachable through `@Req()`: PBRequest carries
5180
- // request-scoped data and no signal.
5181
- case "signal":
5182
- args[p.index] = req.signal;
5183
- break;
5184
- // `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
5185
- // on any other route it is an inert writer whose frames go nowhere,
5186
- // which is the same shape `@UploadedObject()` has off an `@Upload`
5187
- // route.
5188
- case "sseOut":
5189
- args[p.index] = sseWriter;
5190
- break;
5191
- default:
5192
- args[p.index] = void 0;
5193
5313
  }
5194
- }
5195
- dispatched = true;
5196
- try {
5314
+ db.authorizationComplete?.();
5315
+ dispatched = true;
5197
5316
  const services = buildServices(db);
5198
5317
  if (meta.options?.sseConfig !== void 0) {
5199
5318
  let sseController;
@@ -5209,11 +5328,19 @@ async function createApp(opts) {
5209
5328
  } catch {
5210
5329
  }
5211
5330
  }, "sseEnqueue");
5212
- let sawFirstFrame;
5213
- const firstFrame = new Promise((r) => sawFirstFrame = r);
5331
+ let sawFirstFrame, refuseFirstFrame;
5332
+ const firstFrame = new Promise((resolve, reject) => {
5333
+ sawFirstFrame = resolve;
5334
+ refuseFirstFrame = reject;
5335
+ });
5214
5336
  sseSettle = /* @__PURE__ */ __name(async () => {
5215
- await db.commit();
5216
- sawFirstFrame();
5337
+ try {
5338
+ await db.commit();
5339
+ sawFirstFrame();
5340
+ } catch (error) {
5341
+ refuseFirstFrame(error);
5342
+ throw error;
5343
+ }
5217
5344
  }, "sseSettle");
5218
5345
  const guarded = {
5219
5346
  ...services,
@@ -5254,6 +5381,7 @@ async function createApp(opts) {
5254
5381
  status: 204
5255
5382
  });
5256
5383
  }
5384
+ await firstFrame;
5257
5385
  void (async () => {
5258
5386
  await ran;
5259
5387
  await sseWriter.drained();
@@ -5287,15 +5415,16 @@ async function createApp(opts) {
5287
5415
  }
5288
5416
  return method.apply(hit.entry.instance, args);
5289
5417
  });
5290
- await db.commit();
5291
5418
  if (meta.returnSchema) {
5292
5419
  const v = meta.returnSchema.safeParse(result);
5293
5420
  if (!v.success) {
5294
5421
  log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
5422
+ await db.rollback(new Error("output_invalid"));
5295
5423
  return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
5296
5424
  }
5297
5425
  }
5298
- if (result === void 0 || result === null) {
5426
+ if (result === void 0 || result === null && !meta.returnSchema) {
5427
+ await db.commit();
5299
5428
  if (completionUploadId) {
5300
5429
  completions.remember(completionUploadId, {
5301
5430
  status: 204,
@@ -5308,6 +5437,7 @@ async function createApp(opts) {
5308
5437
  });
5309
5438
  }
5310
5439
  const payload = JSON.stringify(result);
5440
+ await db.commit();
5311
5441
  if (completionUploadId) {
5312
5442
  completions.remember(completionUploadId, {
5313
5443
  status: 200,
@@ -5324,16 +5454,7 @@ async function createApp(opts) {
5324
5454
  });
5325
5455
  } catch (err) {
5326
5456
  await db.rollback(err);
5327
- if (isHttpError(err)) {
5328
- if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
5329
- log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
5330
- }
5331
- return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
5332
- data: err.data
5333
- } : void 0);
5334
- }
5335
- log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
5336
- return envelope("internal_error", "The request could not be completed", 500, requestId);
5457
+ return errorResponse(err, hit.entry.id, requestId);
5337
5458
  }
5338
5459
  }
5339
5460
  __name(handle, "handle");
@@ -5346,15 +5467,24 @@ async function createApp(opts) {
5346
5467
  throw err;
5347
5468
  }
5348
5469
  return {
5349
- handle,
5470
+ handle: /* @__PURE__ */ __name((req) => !accepting ? Promise.resolve(envelope("runtime_draining", "This application is shutting down", 503, `req_${crypto.randomUUID()}`)) : serving(() => handle(req)), "handle"),
5471
+ get databasePressure() {
5472
+ return admission.snapshot;
5473
+ },
5350
5474
  routes,
5351
5475
  config,
5352
5476
  container: liveContainer,
5353
- runInServiceScope,
5354
- async shutdown() {
5355
- await runShutdownHooks();
5356
- await closeDriver(sql);
5357
- released = true;
5477
+ runInServiceScope: /* @__PURE__ */ __name((fn) => !accepting ? Promise.reject(new Error("This application is shutting down; no new database scope can start")) : serving(() => runInServiceScope(fn)), "runInServiceScope"),
5478
+ shutdown() {
5479
+ accepting = false;
5480
+ return shuttingDown ??= (async () => {
5481
+ while (inFlight.size) await Promise.allSettled([
5482
+ ...inFlight
5483
+ ]);
5484
+ await runShutdownHooks();
5485
+ await closeDriver(sql);
5486
+ released = true;
5487
+ })();
5358
5488
  }
5359
5489
  };
5360
5490
  }
@@ -5389,4 +5519,4 @@ export {
5389
5519
  installEgressFence,
5390
5520
  createApp
5391
5521
  };
5392
- //# sourceMappingURL=chunk-BOD3DT6D.js.map
5522
+ //# sourceMappingURL=chunk-EHKEMHB4.js.map