@palbase/backend 28.0.0 → 30.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 (49) hide show
  1. package/dist/bin/palbase-backend.cjs +166 -12
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +3 -3
  4. package/dist/{chunk-RVP6BTEZ.js → chunk-5J5W75A6.js} +15 -4
  5. package/dist/chunk-5J5W75A6.js.map +1 -0
  6. package/dist/{chunk-IVZERLTM.js → chunk-ENZ2RFFJ.js} +2 -2
  7. package/dist/{chunk-SNDXY565.js → chunk-SG4UTNOP.js} +5 -2
  8. package/dist/chunk-SG4UTNOP.js.map +1 -0
  9. package/dist/{chunk-75YROPRZ.js → chunk-VN7NMDUH.js} +166 -15
  10. package/dist/chunk-VN7NMDUH.js.map +1 -0
  11. package/dist/db/index.cjs +4 -1
  12. package/dist/db/index.cjs.map +1 -1
  13. package/dist/db/index.d.cts +1 -1
  14. package/dist/db/index.d.ts +1 -1
  15. package/dist/db/index.js +2 -2
  16. package/dist/engine/index.cjs +166 -12
  17. package/dist/engine/index.cjs.map +1 -1
  18. package/dist/engine/index.d.cts +3 -3
  19. package/dist/engine/index.d.ts +3 -3
  20. package/dist/engine/index.js +3 -3
  21. package/dist/{index-dTTLlHIn.d.ts → index-8qy3kIuA.d.ts} +228 -12
  22. package/dist/{index-DtISj9QX.d.cts → index-DtCgaZAg.d.cts} +228 -12
  23. package/dist/{index-9C3JHxg-.d.cts → index-Zi7MptvP.d.cts} +30 -8
  24. package/dist/{index-BbvOoZFr.d.ts → index-b-Q3l7W5.d.ts} +30 -8
  25. package/dist/index.cjs +66 -3
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +51 -11
  28. package/dist/index.d.ts +51 -11
  29. package/dist/index.js +52 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/openapi/index.d.cts +2 -2
  32. package/dist/openapi/index.d.ts +2 -2
  33. package/dist/{registry-JQNIX-eA.d.ts → registry-BGJ-Al6F.d.ts} +1 -1
  34. package/dist/{registry-qIPM5BQe.d.cts → registry-CEYLH-Iz.d.cts} +1 -1
  35. package/dist/test/index.cjs +56 -0
  36. package/dist/test/index.cjs.map +1 -1
  37. package/dist/test/index.d.cts +1 -1
  38. package/dist/test/index.d.ts +1 -1
  39. package/dist/test/index.js +56 -0
  40. package/dist/test/index.js.map +1 -1
  41. package/docs/README.md +1 -1
  42. package/docs/database.md +49 -0
  43. package/docs/llms-full.txt +50 -1
  44. package/package.json +1 -1
  45. package/template/package.json +1 -1
  46. package/dist/chunk-75YROPRZ.js.map +0 -1
  47. package/dist/chunk-RVP6BTEZ.js.map +0 -1
  48. package/dist/chunk-SNDXY565.js.map +0 -1
  49. /package/dist/{chunk-IVZERLTM.js.map → chunk-ENZ2RFFJ.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- export { f as AnyColumn, k as ColumnBuilder, l as ColumnDef, m as ColumnMap, n as ColumnType, E as EXTENSION_DEPENDENCIES, s as EmbeddingModelRef, u as EnvServiceDatabase, w as EnvTypedDatabase, N as InsertShape, V as Materialized, a0 as OnDeleteAction, a1 as PALBASE_EXTENSIONS, aA as PalbaseExtension, bx as PolicyBuilder, by as PolicyCommand, bz as PolicyDef, bC as PolicyMode, bE as RawConstraintDef, bG as Ref, bI as RowShape, S as SchemaDef, bR as TABLE_META, bS as TableDef, bT as TableHandle, bU as TableInput, bW as TxColumnExpr, bX as TxInsertShape, bZ as TxNow, b_ as TxPlan, T as TxPlanBody, b$ as TxPlanError, c0 as TxPlanHandle, c1 as TxPlanOpResult, c2 as TxPlanRejection, b as TxPlanResponse, c3 as TxRefError, c4 as TxRow, c5 as TxRows, c6 as TxSelectOptions, c7 as TxSetShape, c9 as TxTable, ca as TxTables, cb as TxWhere, ch as TypedDB, ci as TypedTable, cj as TypedTx, cs as bigint, ct as boolean, cv as dec, cy as defineSchema, cz as defineTable, cA as enumType, cC as inc, cF as installationRef, cG as integer, cH as isPalbaseExtension, cJ as jsonb, cK as makeTypedDB, cL as now, cP as openai, cQ as ownedByUser, cR as policy, cS as raw, cU as text, cV as timestamp, cW as userRef, cX as uuid, cY as vector } from '../index-DtISj9QX.cjs';
1
+ export { f as AnyColumn, l as ColumnBuilder, m as ColumnDef, n as ColumnMap, o as ColumnType, E as EXTENSION_DEPENDENCIES, t as EmbeddingModelRef, v as EnvServiceDatabase, x as EnvTypedDatabase, O as InsertShape, W as Materialized, a1 as OnDeleteAction, a2 as PALBASE_EXTENSIONS, aC as PalbaseExtension, bz as PolicyBuilder, bA as PolicyCommand, bB as PolicyDef, bE as PolicyMode, bG as RawConstraintDef, bI as Ref, bK as RowShape, S as SchemaDef, bT as TABLE_META, bU as TableDef, bV as TableHandle, bW as TableInput, bY as TxColumnExpr, bZ as TxInsertShape, b$ as TxNow, c0 as TxPlan, T as TxPlanBody, c1 as TxPlanError, c2 as TxPlanHandle, c3 as TxPlanOpResult, c4 as TxPlanRejection, b as TxPlanResponse, c5 as TxRefError, c6 as TxRow, c7 as TxRows, c8 as TxSelectOptions, c9 as TxSetShape, cb as TxTable, cc as TxTables, cd as TxWhere, cj as TypedDB, ck as TypedTable, cl as TypedTx, cu as bigint, cv as boolean, cx as dec, cA as defineSchema, cB as defineTable, cC as enumType, cE as inc, cH as installationRef, cI as integer, cJ as isPalbaseExtension, cL as jsonb, cM as makeTypedDB, cN as now, cR as openai, cS as ownedByUser, cT as policy, cU as raw, cW as text, cX as timestamp, cY as userRef, cZ as uuid, c_ as vector } from '../index-DtCgaZAg.cjs';
2
2
  import './env.cjs';
3
3
  import '../stack.cjs';
4
4
  import 'zod';
@@ -1,4 +1,4 @@
1
- export { f as AnyColumn, k as ColumnBuilder, l as ColumnDef, m as ColumnMap, n as ColumnType, E as EXTENSION_DEPENDENCIES, s as EmbeddingModelRef, u as EnvServiceDatabase, w as EnvTypedDatabase, N as InsertShape, V as Materialized, a0 as OnDeleteAction, a1 as PALBASE_EXTENSIONS, aA as PalbaseExtension, bx as PolicyBuilder, by as PolicyCommand, bz as PolicyDef, bC as PolicyMode, bE as RawConstraintDef, bG as Ref, bI as RowShape, S as SchemaDef, bR as TABLE_META, bS as TableDef, bT as TableHandle, bU as TableInput, bW as TxColumnExpr, bX as TxInsertShape, bZ as TxNow, b_ as TxPlan, T as TxPlanBody, b$ as TxPlanError, c0 as TxPlanHandle, c1 as TxPlanOpResult, c2 as TxPlanRejection, b as TxPlanResponse, c3 as TxRefError, c4 as TxRow, c5 as TxRows, c6 as TxSelectOptions, c7 as TxSetShape, c9 as TxTable, ca as TxTables, cb as TxWhere, ch as TypedDB, ci as TypedTable, cj as TypedTx, cs as bigint, ct as boolean, cv as dec, cy as defineSchema, cz as defineTable, cA as enumType, cC as inc, cF as installationRef, cG as integer, cH as isPalbaseExtension, cJ as jsonb, cK as makeTypedDB, cL as now, cP as openai, cQ as ownedByUser, cR as policy, cS as raw, cU as text, cV as timestamp, cW as userRef, cX as uuid, cY as vector } from '../index-dTTLlHIn.js';
1
+ export { f as AnyColumn, l as ColumnBuilder, m as ColumnDef, n as ColumnMap, o as ColumnType, E as EXTENSION_DEPENDENCIES, t as EmbeddingModelRef, v as EnvServiceDatabase, x as EnvTypedDatabase, O as InsertShape, W as Materialized, a1 as OnDeleteAction, a2 as PALBASE_EXTENSIONS, aC as PalbaseExtension, bz as PolicyBuilder, bA as PolicyCommand, bB as PolicyDef, bE as PolicyMode, bG as RawConstraintDef, bI as Ref, bK as RowShape, S as SchemaDef, bT as TABLE_META, bU as TableDef, bV as TableHandle, bW as TableInput, bY as TxColumnExpr, bZ as TxInsertShape, b$ as TxNow, c0 as TxPlan, T as TxPlanBody, c1 as TxPlanError, c2 as TxPlanHandle, c3 as TxPlanOpResult, c4 as TxPlanRejection, b as TxPlanResponse, c5 as TxRefError, c6 as TxRow, c7 as TxRows, c8 as TxSelectOptions, c9 as TxSetShape, cb as TxTable, cc as TxTables, cd as TxWhere, cj as TypedDB, ck as TypedTable, cl as TypedTx, cu as bigint, cv as boolean, cx as dec, cA as defineSchema, cB as defineTable, cC as enumType, cE as inc, cH as installationRef, cI as integer, cJ as isPalbaseExtension, cL as jsonb, cM as makeTypedDB, cN as now, cR as openai, cS as ownedByUser, cT as policy, cU as raw, cW as text, cX as timestamp, cY as userRef, cZ as uuid, c_ as vector } from '../index-8qy3kIuA.js';
2
2
  import './env.js';
3
3
  import '../stack.js';
4
4
  import 'zod';
package/dist/db/index.js CHANGED
@@ -21,10 +21,10 @@ import {
21
21
  userRef,
22
22
  uuid,
23
23
  vector
24
- } from "../chunk-IVZERLTM.js";
24
+ } from "../chunk-ENZ2RFFJ.js";
25
25
  import {
26
26
  makeTypedDB
27
- } from "../chunk-SNDXY565.js";
27
+ } from "../chunk-SG4UTNOP.js";
28
28
  import {
29
29
  TxPlanError,
30
30
  TxRefError,
@@ -948,6 +948,7 @@ function makeTableProxy(ops, prefix) {
948
948
  // DÖRDÜNCÜ FİİL, aynı gerekçeyle: tip söz veriyor, ops katmanı
949
949
  // uyguluyor, ve bir handler'ın gerçekten dokunduğu yer BURASI.
950
950
  // `runtime-table-verbs` kapısı bunu adıyla saydı.
951
+ aggregate: /* @__PURE__ */ __name((q) => ops().aggregate(name, q), "aggregate"),
951
952
  insertMany: /* @__PURE__ */ __name((rows, opts) => ops().insertMany(name, rows, opts), "insertMany"),
952
953
  update: /* @__PURE__ */ __name((q) => ops().update(name, q.where.id, q.set), "update"),
953
954
  delete: /* @__PURE__ */ __name((id) => ops().delete(name, id), "delete"),
@@ -1005,6 +1006,7 @@ function makeTypedSurface(raw) {
1005
1006
  $claim: /* @__PURE__ */ __name((table, unique, extra) => reco.claim(table, unique, extra), "$claim"),
1006
1007
  $lockRows: /* @__PURE__ */ __name((table, ids) => reco.lockRows(table, ids), "$lockRows"),
1007
1008
  $advisoryXactLock: /* @__PURE__ */ __name((key) => reco.advisoryXactLock(key), "$advisoryXactLock"),
1009
+ $aggregate: /* @__PURE__ */ __name((table, q) => raw.aggregate(table, q), "$aggregate"),
1008
1010
  $insertMany: /* @__PURE__ */ __name((table, rows, opts) => raw.insertMany(table, rows, opts), "$insertMany"),
1009
1011
  $supersede: /* @__PURE__ */ __name((table, id, row) => raw.supersede(table, id, row), "$supersede")
1010
1012
  };
@@ -1090,6 +1092,7 @@ var Storage = Object.assign({
1090
1092
  });
1091
1093
  var Cache = makeServiceProxy("Cache");
1092
1094
  var Secrets = makeServiceProxy("Secrets");
1095
+ var Auth = makeServiceProxy("Auth");
1093
1096
  var Log = makeServiceProxy("Log");
1094
1097
  var Notifications = makeServiceProxy("Notifications");
1095
1098
  var rawFlags = makeServiceProxy("Flags");
@@ -1339,9 +1342,11 @@ function effectiveAuth(routeAuth, controllerAuth) {
1339
1342
  };
1340
1343
  const o = spec;
1341
1344
  const role = typeof o.role === "string" && o.role.trim() !== "" ? o.role.trim() : void 0;
1345
+ const permission = typeof o.permission === "string" && o.permission.trim() !== "" ? o.permission.trim() : void 0;
1342
1346
  return {
1343
1347
  required: o.required !== false,
1344
1348
  role,
1349
+ permission,
1345
1350
  verifiedEmail: o.verifiedEmail === true
1346
1351
  };
1347
1352
  }
@@ -2329,6 +2334,26 @@ function buildValueTuple(table, cols, data, add, caller) {
2329
2334
  }).join(", ");
2330
2335
  }
2331
2336
  __name(buildValueTuple, "buildValueTuple");
2337
+ function columnTypeOf(table, col) {
2338
+ const def = tableDefOf(table);
2339
+ const c = def?.columns?.[col];
2340
+ if (c === void 0 || c === null) return null;
2341
+ const d = typeof c === "object" && "_def" in c ? c._def : c;
2342
+ return d !== null && typeof d === "object" && typeof d.type === "string" ? d.type : null;
2343
+ }
2344
+ __name(columnTypeOf, "columnTypeOf");
2345
+ var NUMERIC_PG_TYPES = /* @__PURE__ */ new Set([
2346
+ "integer",
2347
+ "bigint",
2348
+ "numeric"
2349
+ ]);
2350
+ function aggregateExpr(table, fn, col, alias) {
2351
+ const q = `t.${quoteIdent(col)}`;
2352
+ const t = columnTypeOf(table, col);
2353
+ if (fn === "sum" || fn === "avg") return `${fn}(${q})::text AS ${quoteIdent(alias)}`;
2354
+ return t !== null && NUMERIC_PG_TYPES.has(t) && t !== "integer" ? `${fn}(${q})::text AS ${quoteIdent(alias)}` : `${fn}(${q}) AS ${quoteIdent(alias)}`;
2355
+ }
2356
+ __name(aggregateExpr, "aggregateExpr");
2332
2357
  function createOps(tx) {
2333
2358
  const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
2334
2359
  const ops = {
@@ -2609,7 +2634,7 @@ function createOps(tx) {
2609
2634
  * should not silently succeed. Callers who mean every row say so with a
2610
2635
  * predicate that is true for every row.
2611
2636
  */
2612
- async updateMany(table, where, set) {
2637
+ async updateMany(table, where, set, opts) {
2613
2638
  assertNotAppendOnly("updateMany", table);
2614
2639
  const cols = Object.keys(set);
2615
2640
  if (cols.length === 0) {
@@ -2632,8 +2657,15 @@ function createOps(tx) {
2632
2657
  const whereSql = compileWhere(table, known, where, add, "updateMany");
2633
2658
  assertHasPredicate(whereSql, "updateMany", table);
2634
2659
  const pk = primaryKeyOf(table);
2635
- const sql = isSinglePkEquality(where, pk) || pk === null ? `UPDATE ${quoteTable(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *` : `WITH locked AS (SELECT t.${quoteIdent(pk)} FROM ${quoteTable(table)} t WHERE true${whereSql} ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE) UPDATE ${quoteTable(table)} AS t SET ${assignments} FROM locked WHERE t.${quoteIdent(pk)} = locked.${quoteIdent(pk)} RETURNING t.*`;
2636
- return asTableRows(table, await (await at()).unsafe(sql, params));
2660
+ const wantRows = opts?.returning !== false;
2661
+ const tail = wantRows ? "RETURNING *" : "RETURNING 1";
2662
+ const simple = isSinglePkEquality(where, pk) || pk === null;
2663
+ const body = simple ? `UPDATE ${quoteTable(table)} AS t SET ${assignments} WHERE true${whereSql} ${tail}` : `UPDATE ${quoteTable(table)} AS t SET ${assignments} FROM locked WHERE t.${quoteIdent(pk)} = locked.${quoteIdent(pk)} ` + (wantRows ? "RETURNING t.*" : "RETURNING 1");
2664
+ const lockedCte = simple ? "" : `WITH locked AS (SELECT t.${quoteIdent(pk)} FROM ${quoteTable(table)} t WHERE true${whereSql} ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE) `;
2665
+ const sql = wantRows ? `${lockedCte}${body}` : simple ? `WITH upd AS (${body}) SELECT count(*)::int AS n FROM upd` : `${lockedCte.replace(/ $/, "")}, upd AS (${body}) SELECT count(*)::int AS n FROM upd`;
2666
+ const out = await (await at()).unsafe(sql, params);
2667
+ if (!wantRows) return Number(out[0]?.["n"] ?? 0);
2668
+ return asTableRows(table, out);
2637
2669
  },
2638
2670
  /**
2639
2671
  * Delete every row the filter matches, in ONE statement; resolves to how
@@ -2663,6 +2695,78 @@ function createOps(tx) {
2663
2695
  * An empty filter is legitimate HERE: counting a whole table is a read, and
2664
2696
  * reads do not destroy anything.
2665
2697
  */
2698
+ /**
2699
+ * Toplama — `count`'un yanına `sum`/`avg`/`min`/`max` ve `groupBy`.
2700
+ *
2701
+ * Var olma nedeni: bunlar olmadan "bu hesabın toplamı" ya `$query`'ye ya da
2702
+ * TÜM satırları çekip JS'te toplamaya düşüyor. İkincisi hem N+1 hem de para
2703
+ * için YANLIŞ — `numeric` JS `number`'ına uğradığı anda kesinliğini
2704
+ * kaybeder.
2705
+ *
2706
+ * `groupBy` verilirse SATIRLAR döner, verilmezse TEK sonuç. Boş kümede
2707
+ * Postgres NULL döndürür, 0 değil — ve bu SDK onu `null` olarak geçiriyor:
2708
+ * "hiç satır yoktu" ile "toplam sıfırdı" farklı şeyler.
2709
+ */
2710
+ async aggregate(table, q = {}) {
2711
+ const known = schemaColumns(table);
2712
+ const check = /* @__PURE__ */ __name((col, where) => {
2713
+ if (known !== null && !known.has(col)) {
2714
+ throw new Error(`aggregate(${table}): ${where} kolonu "${col}" tabloda yok (FR-016)`);
2715
+ }
2716
+ }, "check");
2717
+ const parts = [];
2718
+ const wants = [];
2719
+ for (const fn of [
2720
+ "sum",
2721
+ "avg",
2722
+ "min",
2723
+ "max"
2724
+ ]) {
2725
+ for (const col of q[fn] ?? []) {
2726
+ check(col, fn);
2727
+ if (fn === "sum" || fn === "avg") {
2728
+ const t = columnTypeOf(table, col);
2729
+ if (t !== null && !NUMERIC_PG_TYPES.has(t)) {
2730
+ throw new Error(`aggregate(${table}): ${fn}("${col}") say\u0131sal olmayan bir kolonda \u2014 "${col}" bir ${t} kolonu. Toplama yaln\u0131z integer/bigint/numeric kolonlarda anlaml\u0131; ka\xE7 sat\u0131r oldu\u011Funu saymak i\xE7in count: true kullan\u0131n.`);
2731
+ }
2732
+ }
2733
+ const alias = `${fn}__${col}`;
2734
+ wants.push({
2735
+ fn,
2736
+ col,
2737
+ alias
2738
+ });
2739
+ parts.push(aggregateExpr(table, fn, col, alias));
2740
+ }
2741
+ }
2742
+ if (q.count === true) parts.push('count(*) AS "_count"');
2743
+ if (parts.length === 0) {
2744
+ throw new Error(`aggregate(${table}): hi\xE7bir toplama istenmedi \u2014 sonu\xE7 bo\u015F bir nesne olurdu. sum/avg/min/max kolonlar\u0131n\u0131 ya da count: true'yu verin.`);
2745
+ }
2746
+ const groups = q.groupBy ?? [];
2747
+ for (const col of groups) check(col, "groupBy");
2748
+ const groupSel = groups.map((c) => `t.${quoteIdent(c)} AS ${quoteIdent(`g__${c}`)}`);
2749
+ const params = [];
2750
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
2751
+ const whereSql = compileWhere(table, known, q.where ?? {}, add, "aggregate");
2752
+ const groupSql = groups.length > 0 ? ` GROUP BY ${groups.map((c) => `t.${quoteIdent(c)}`).join(", ")}` : "";
2753
+ const sql = `SELECT ${[
2754
+ ...groupSel,
2755
+ ...parts
2756
+ ].join(", ")} FROM ${quoteTable(table)} t WHERE true${whereSql}${groupSql}`;
2757
+ const rows = await (await at()).unsafe(sql, params);
2758
+ const shape = /* @__PURE__ */ __name((r) => {
2759
+ const out = {};
2760
+ for (const c of groups) out[c] = r[`g__${c}`];
2761
+ for (const w of wants) {
2762
+ const bucket = out[w.fn] ??= {};
2763
+ bucket[w.col] = r[w.alias] ?? null;
2764
+ }
2765
+ if (q.count === true) out["count"] = Number(r["_count"] ?? 0);
2766
+ return out;
2767
+ }, "shape");
2768
+ return groups.length > 0 ? rows.map(shape) : shape(rows[0] ?? {});
2769
+ },
2666
2770
  async count(table, where = {}) {
2667
2771
  const params = [];
2668
2772
  const add = /* @__PURE__ */ __name((v) => {
@@ -4505,6 +4609,32 @@ var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
4505
4609
  409,
4506
4610
  429
4507
4611
  ]);
4612
+ async function refuseUnlessAuthorized(client, spec, requestId, rolesOfCaller) {
4613
+ if (spec.permission !== void 0) {
4614
+ const [row] = await client.query("select auth.has_permission($1) as ok", [
4615
+ spec.permission
4616
+ ]);
4617
+ if (row?.ok !== true) {
4618
+ return envelope("forbidden", `This endpoint requires the "${spec.permission}" permission`, 403, requestId);
4619
+ }
4620
+ }
4621
+ if (spec.role !== void 0) {
4622
+ const held = await rolesOfCaller();
4623
+ if (!held.includes(spec.role)) {
4624
+ return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4625
+ }
4626
+ }
4627
+ return null;
4628
+ }
4629
+ __name(refuseUnlessAuthorized, "refuseUnlessAuthorized");
4630
+ function makeRoleResolver(client) {
4631
+ let pending = null;
4632
+ return () => {
4633
+ pending ??= client.query("select unnest(auth.app_roles()) as role").then((rows) => rows.map((r) => r.role));
4634
+ return pending;
4635
+ };
4636
+ }
4637
+ __name(makeRoleResolver, "makeRoleResolver");
4508
4638
  async function createApp(opts) {
4509
4639
  const { config, controllers } = opts;
4510
4640
  const container = buildContainer();
@@ -4553,6 +4683,7 @@ async function createApp(opts) {
4553
4683
  Database: db?.client ?? stubModule("Database (a start hook runs before any request \u2014 open your own connection here)"),
4554
4684
  Cache: cache,
4555
4685
  Log: log,
4686
+ Auth: modules.Auth ?? stubModule("Auth"),
4556
4687
  Documents: modules.Documents ?? stubModule("Documents"),
4557
4688
  Storage: modules.Storage ?? stubModule("Storage"),
4558
4689
  Notifications: modules.Notifications ?? stubModule("Notifications"),
@@ -4596,8 +4727,23 @@ async function createApp(opts) {
4596
4727
  if (spec.required && !callerClaims) {
4597
4728
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
4598
4729
  }
4599
- if (callerClaims && spec.role && callerClaims.role !== spec.role) {
4600
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4730
+ if (callerClaims) {
4731
+ const uploadDb = createRequestDatabase(sql, {
4732
+ role: config.dbRole,
4733
+ serviceRole: config.dbServiceRole,
4734
+ claimsJson: JSON.stringify(callerClaims)
4735
+ });
4736
+ try {
4737
+ const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
4738
+ if (refusal) {
4739
+ await uploadDb.commit();
4740
+ return refusal;
4741
+ }
4742
+ await uploadDb.commit();
4743
+ } catch (err) {
4744
+ await uploadDb.rollback(err);
4745
+ throw err;
4746
+ }
4601
4747
  }
4602
4748
  const grant = grantFor(target?.entry, {
4603
4749
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -4630,8 +4776,15 @@ async function createApp(opts) {
4630
4776
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
4631
4777
  }
4632
4778
  const userId = typeof claims?.sub === "string" ? claims.sub : void 0;
4633
- if (claims && spec.role && claims.role !== spec.role) {
4634
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4779
+ const db = createRequestDatabase(sql, {
4780
+ role: config.dbRole,
4781
+ serviceRole: config.dbServiceRole,
4782
+ claimsJson: JSON.stringify(claims ?? {})
4783
+ });
4784
+ const rolesOfCaller = makeRoleResolver(db.client);
4785
+ if (claims) {
4786
+ const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
4787
+ if (refusal) return refusal;
4635
4788
  }
4636
4789
  if (claims && spec.verifiedEmail && claims.email_verified !== true) {
4637
4790
  return envelope("email_not_verified", "A verified email address is required", 403, requestId);
@@ -4783,6 +4936,12 @@ async function createApp(opts) {
4783
4936
  role: claims.role,
4784
4937
  emailVerified: claims.email_verified === true,
4785
4938
  metadata: claims.metadata ?? {},
4939
+ // The caller's APPLICATION roles, from `auth.user_roles` — the
4940
+ // same resolution the gate used, so the handler cannot read a
4941
+ // different set than the one it was admitted on. Never from a
4942
+ // claim: a revoked role has to bite on the next request, and a
4943
+ // token says only what was true when it was minted.
4944
+ roles: await rolesOfCaller(),
4786
4945
  // Server-owned and only ever filled from a verdict palauth
4787
4946
  // returned. Never from a raw header, never from JWT metadata —
4788
4947
  // the type has said so since it was written and this is the
@@ -4852,11 +5011,6 @@ async function createApp(opts) {
4852
5011
  args[p.index] = void 0;
4853
5012
  }
4854
5013
  }
4855
- const db = createRequestDatabase(sql, {
4856
- role: config.dbRole,
4857
- serviceRole: config.dbServiceRole,
4858
- claimsJson: JSON.stringify(claims ?? {})
4859
- });
4860
5014
  try {
4861
5015
  const services = buildServices(db);
4862
5016
  if (meta.options?.sseConfig !== void 0) {