permdock 0.9.0 → 0.10.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 (108) hide show
  1. package/dist/active-tenant-DVee32-T.js +39 -0
  2. package/dist/{adoption-scan-Ch07MdxN.js → adoption-scan-0oFt2RD5.js} +1 -1
  3. package/dist/{arazzo-BpS7LnV0.js → arazzo-DA9PCXew.js} +1 -1
  4. package/dist/{arazzo-CmQaGPkM.js → arazzo-DhxDfYt-.js} +1 -1
  5. package/dist/better-supabase/index.d.ts +188 -23
  6. package/dist/better-supabase/index.js +246 -62
  7. package/dist/{casbin-bHyIxTNQ.js → casbin-BJu2liqU.js} +1 -1
  8. package/dist/{casbin-B7_yS-oo.js → casbin-BkhqGh2z.js} +1 -1
  9. package/dist/cli/bin.js +1 -1
  10. package/dist/cli/index.d.ts +6 -1
  11. package/dist/cli/index.js +10 -2
  12. package/dist/{cloud-3kWfjnYy.js → cloud-3RIuS4E9.js} +2 -2
  13. package/dist/{codemod-D9Aiii1r.js → codemod-DLcp-B6v.js} +2 -2
  14. package/dist/{codemod-Bv5i-Mzo.js → codemod-DRl_w-bY.js} +1 -1
  15. package/dist/{collect-B80GYEot.js → collect-D9Tt0Bcx.js} +1 -1
  16. package/dist/{completion-_bc0OUQ-.js → completion-VSWvaaPO.js} +1 -1
  17. package/dist/{config-BwPBfN0t.js → config-BR23fL8m.js} +2 -2
  18. package/dist/{config-D5XrLWpk.js → config-kL36AxkE.js} +1 -1
  19. package/dist/{config-report-BmWIskD7.js → config-report-osnfUNg5.js} +2 -2
  20. package/dist/{config-shape-ykV0Vm2n.js → config-shape-tsxHx71E.js} +2 -1
  21. package/dist/{create-BamXmeob.d.ts → create-CDd9c22W.d.ts} +10 -1
  22. package/dist/{doctor-LPMYjSqO.js → doctor-B9FY9H0F.js} +2 -2
  23. package/dist/{doctor-ClRVAtXQ.js → doctor-DfTnMaUt.js} +142 -14
  24. package/dist/{fields-D0vUI6dq.js → fields-DI8s3cPl.js} +1 -1
  25. package/dist/{gen-BtfFWGbz.js → gen-DuAhghDw.js} +1 -1
  26. package/dist/{gen-BFfzZSLH.js → gen-DwgCSC6V.js} +1 -1
  27. package/dist/{import-CST57XLp.js → import-CNBF96hB.js} +2 -2
  28. package/dist/{import-ACkOrv6g.js → import-Iuk9mx8A.js} +1 -1
  29. package/dist/{index-vYjMrXyK.d.ts → index-Cay5-Kvx.d.ts} +3 -51
  30. package/dist/index.d.ts +2 -1
  31. package/dist/{init-BjuFDYT1.js → init-DeoSvkh2.js} +1 -1
  32. package/dist/{init-txYTuW9u.js → init-fNF_XmTa.js} +3 -3
  33. package/dist/{login-CgfCTO3F.js → login-CutVtcmQ.js} +1 -1
  34. package/dist/{login-CfImTJGb.js → login-DYazeWc9.js} +3 -3
  35. package/dist/{manifest-B2QPh19C.js → manifest-B4ENCp2I.js} +33 -33
  36. package/dist/{manifest-CzzWtnAb.d.ts → manifest-B5yF3CIq.d.ts} +19 -0
  37. package/dist/{manifest-wg8WkkpD.js → manifest-BXTbHNIV.js} +1 -1
  38. package/dist/{manifest-schema-2r37XRhD.js → manifest-schema-DQkBK2Mr.js} +28 -1
  39. package/dist/{migrate-B7oP_sQU.js → migrate-BJHxQqT5.js} +2 -2
  40. package/dist/{module-values-BJTBdo6y.js → module-values-B1EVTkHP.js} +1 -1
  41. package/dist/next/index.d.ts +16 -3
  42. package/dist/next/index.js +1 -1
  43. package/dist/next/plugin.d.ts +1 -1
  44. package/dist/next/plugin.js +1 -1
  45. package/dist/{openapi-BGeuunWC.js → openapi-C-ADypaY.js} +1 -1
  46. package/dist/{openapi-BIpw3Mb7.js → openapi-DFj0cirS.js} +2 -2
  47. package/dist/{output-DV5dwOTs.js → output-LsbLe41I.js} +1 -1
  48. package/dist/{ownership-DA5YdYTm.js → ownership-qqBuGJXr.js} +3 -3
  49. package/dist/{postgrest-5Dr3W64L.js → postgrest-DAxFltvO.js} +1 -1
  50. package/dist/{powersync-Bg8u-JcR.js → powersync-B_8uwz7f.js} +2 -2
  51. package/dist/powersync-CSzDCHRu.js +1 -0
  52. package/dist/{powersync-CJrT1DSc.js → powersync-DUT-UKgw.js} +188 -36
  53. package/dist/{pull-2nJr0qg_.js → pull-DQCgvJSC.js} +2 -2
  54. package/dist/pull-DYbyod1f.js +1 -0
  55. package/dist/{push-Bn-lMEmI.js → push-Dq_ca4-j.js} +1 -1
  56. package/dist/{resolve-names-ConCSj1y.js → resolve-names-Y1Cb22cp.js} +4 -4
  57. package/dist/{rls-Cx0dtsB7.js → rls-B57mIeVq.js} +22 -15
  58. package/dist/{rls-Ba9wN0SP.js → rls-j0YrFPpW.js} +1 -1
  59. package/dist/run-BATawNVH.js +1 -0
  60. package/dist/{run-PbRlXTfh.js → run-CLP_fQUF.js} +3 -3
  61. package/dist/{run-ticket-Dv7aF7Dk.js → run-ticket-DfnZCh-u.js} +19 -6
  62. package/dist/{run-ticket-B2o2Kxz8.d.ts → run-ticket-ylovhyFQ.d.ts} +16 -1
  63. package/dist/{scan-CRTj6fke.js → scan-B7xrAvRm.js} +1 -1
  64. package/dist/{schema-BTsE1WRT.js → schema-3PJ8wAeL.js} +1 -1
  65. package/dist/server/index.d.ts +2 -2
  66. package/dist/server/index.js +1 -1
  67. package/dist/{shared-Afctg7B0.js → shared-DrGLwM3s.js} +1 -1
  68. package/dist/{skills-C9uKfSF5.js → skills-BUI8DMqy.js} +1 -1
  69. package/dist/{skills-CQQcuGBi.js → skills-iwbRxavw.js} +1 -1
  70. package/dist/{sources-CMAM-2A3.js → sources-C4vkzZJV.js} +12 -7
  71. package/dist/{types-BMVDHvI0.d.ts → sources-oB1Hg1Fm.d.ts} +316 -2
  72. package/dist/supabase/index.d.ts +2 -3
  73. package/dist/supabase/index.js +4 -4
  74. package/dist/{supabase-DnMMg5Fn.js → supabase-B1EfuD47.js} +2 -2
  75. package/dist/{supabase-SBs3MYUE.js → supabase-BaizG0n7.js} +2 -2
  76. package/dist/{supabase-hook-CrgU06WH.js → supabase-hook-BT2P9wah.js} +110 -47
  77. package/dist/supabase-hook-CbvGAutK.js +1 -0
  78. package/dist/{supautils-ByezN3b8.js → supautils-DBKOoyjn.js} +8 -8
  79. package/dist/{tables-BqrxBCir.js → tables-DimBcVWY.js} +2 -2
  80. package/dist/testing/fixtures/index.d.ts +1 -1
  81. package/dist/testing/fixtures/index.js +26 -28
  82. package/dist/testing/index.d.ts +4 -3
  83. package/dist/testing/index.js +3 -2
  84. package/dist/{types-CzOeVvcC.d.ts → types-Cklw5hJo.d.ts} +23 -2
  85. package/dist/unplugin/index.d.ts +1 -1
  86. package/dist/unplugin/index.js +1 -1
  87. package/dist/{usage-CBzgwfIs.js → usage-B1ZJOUdr.js} +1 -1
  88. package/dist/{usage-B9EtHWLa.js → usage-t_nXnwCW.js} +2 -2
  89. package/dist/usage-weohzv2f.js +1 -0
  90. package/dist/{verify-DlUcTDBz.js → verify-jHrbmn-E.js} +6 -6
  91. package/dist/version-cache-Df11C2Qp.d.ts +51 -0
  92. package/dist/{watch-BFDKqmUa.js → watch-Bpci3hBY.js} +1 -1
  93. package/dist/watch-COuvly20.js +1 -0
  94. package/dist/workflow/index.d.ts +1 -1
  95. package/dist/workflow/index.js +22 -6
  96. package/dist/workflow-sdk/index.d.ts +1 -1
  97. package/package.json +2 -2
  98. package/schemas/run-v1.json +16 -0
  99. package/schemas/supabase-manifest-v1.json +28 -0
  100. package/skills/permdock-agents/references/operations-automations-chat.md +1 -0
  101. package/skills/permdock-wire/references/adapters.md +2 -2
  102. package/dist/powersync-B701tvhE.js +0 -1
  103. package/dist/pull-C3EEGzSw.js +0 -1
  104. package/dist/run-Ck22anC2.js +0 -1
  105. package/dist/sources-CWG32C0G.d.ts +0 -312
  106. package/dist/supabase-hook-C0FyEACZ.js +0 -1
  107. package/dist/usage-0hjZPIRX.js +0 -1
  108. package/dist/watch-ClLuO2TL.js +0 -1
@@ -11,6 +11,7 @@ import { r as sha256Digest } from "../sha256-C-4WXKIu.js";
11
11
  import { t as decideSupportSession } from "../support-session-BsmPCbz8.js";
12
12
  import { f as scopeList } from "../scopes-0Up_UA2t.js";
13
13
  import { o as flattenGrantee } from "../grantee-Dke-9rd1.js";
14
+ import { _ as claimsFirst } from "../instance-snapshot-DuWf1cT3.js";
14
15
  import { a as delegatedScopeMap, s as scopesReaching, t as getPermDock } from "../permdock-BvjivVb9.js";
15
16
  import { t as anonymousSubject } from "../subject-BGoM5o8B.js";
16
17
  import { i as linkPolicyViolation, n as capabilityOf, r as capabilitySubject } from "../capability-BqX94r07.js";
@@ -18,11 +19,12 @@ import { r as credentialSubject, s as parseCredential } from "../credential-Cyyy
18
19
  import { t as AccessReviewError } from "../errors-BDUwg2qC.js";
19
20
  import { t as policyRowConditionKeys } from "../row-conditions-BAGHOmVq.js";
20
21
  import { n as subscribersFor } from "../audience-DTjR8xet.js";
21
- import { l as sqlIdent, s as quoteSqlLiteral } from "../sql-0WjxIp9g.js";
22
- import "../sources-CMAM-2A3.js";
23
- import { c as subjectFromSupabaseSession, t as callRpc } from "../postgrest-5Dr3W64L.js";
24
- import { n as parseSupabaseManifest, t as hasTokenHook } from "../manifest-wg8WkkpD.js";
22
+ import { i as qualifySqlTable, l as sqlIdent, s as quoteSqlLiteral } from "../sql-0WjxIp9g.js";
23
+ import { o as junctionMembershipSource } from "../sources-C4vkzZJV.js";
24
+ import { c as subjectFromSupabaseSession, t as callRpc } from "../postgrest-DAxFltvO.js";
25
+ import { n as parseSupabaseManifest, t as hasTokenHook } from "../manifest-BXTbHNIV.js";
25
26
  import { t as parseCatalog } from "../parse-1_ZheTW5.js";
27
+ import { n as activeTenantMismatch, t as activeOrganizationFrom } from "../active-tenant-DVee32-T.js";
26
28
  import { AsyncResult, dbError } from "better-supabase";
27
29
  //#region src/better-supabase/access-reviews.ts
28
30
  const NOT_MEMBER = "ORGANIZATION_NOT_MEMBER";
@@ -289,6 +291,17 @@ function isApiKeySession(session) {
289
291
  const record = Object.fromEntries(Object.entries(session));
290
292
  return record["kind"] === "apiKey" && typeof record["keyId"] === "string" && typeof record["name"] === "string" && Array.isArray(record["scopes"]) && record["scopes"].every((scope) => typeof scope === "string");
291
293
  }
294
+ function isSessionSnapshot(session) {
295
+ return isRecord(session) && !Object.hasOwn(session, "kind") && typeof session["accessToken"] === "string" && typeof session["userId"] === "string" && isRecord(session["claims"]);
296
+ }
297
+ /** A snapshot as a user session; one whose `sub` is not its `userId` is none. */
298
+ function snapshotSession(snapshot) {
299
+ const { claims } = snapshot;
300
+ return isRecord(claims) && claims["sub"] === snapshot.userId ? {
301
+ kind: "user",
302
+ claims
303
+ } : null;
304
+ }
292
305
  function apiKeySubject(session, options) {
293
306
  const roles = typeof options.serviceRoles === "function" ? options.serviceRoles(session) : options.serviceRoles ?? manifestServiceRoles(options.manifest);
294
307
  const credential = keyCredential({
@@ -309,14 +322,16 @@ function apiKeySubject(session, options) {
309
322
  * verified `apiKey` session is the key's credential subject: a personal key
310
323
  * acts as its user within the key's scopes, a tenant key as a `service`
311
324
  * principal holding `serviceRoles`. Without `apiKeys`, and for `anon`,
312
- * `service` and `invalid` sessions, the subject is anonymous.
325
+ * `service` and `invalid` sessions, the subject is anonymous. A
326
+ * `SessionSnapshot` maps as the user session of its claims.
313
327
  */
314
328
  function subjectFromBetterSupabase(session, options = {}) {
315
329
  const { apiKeys, plans = "features", claims, ...rest } = options;
316
330
  if (isApiKeySession(session)) return apiKeys === void 0 ? anonymousSubject() : apiKeySubject(session, apiKeys);
331
+ const user = isSessionSnapshot(session) ? snapshotSession(session) : session;
317
332
  const claim = typeof plans === "string" ? plans : plans.claim ?? "features";
318
333
  const keys = typeof plans === "string" ? void 0 : plans.keys;
319
- return subjectFromSupabaseSession(keys === void 0 ? session : decodedFeatures(session, claim, keys), {
334
+ return subjectFromSupabaseSession(keys === void 0 ? user : decodedFeatures(user, claim, keys), {
320
335
  ...rest,
321
336
  claims: {
322
337
  memberships: "memberships",
@@ -442,8 +457,12 @@ function supportPolicy(options) {
442
457
  }
443
458
  //#endregion
444
459
  //#region src/better-supabase/hooks.ts
445
- /** better-supabase's claim that caps a context to the permission keys it lists. */
446
- const PERMISSION_CAP_CLAIM = "bs_permission_cap";
460
+ /**
461
+ * better-supabase's claim that caps a context to the permission keys it lists:
462
+ * `bs_permission_cap` before 0.15, `bsb_permission_cap` from 0.15. A run sets
463
+ * both, so the cap holds on every version in the peer range.
464
+ */
465
+ const PERMISSION_CAP_CLAIMS = ["bs_permission_cap", "bsb_permission_cap"];
447
466
  /**
448
467
  * Whether `permdock` is granted `key` with no row; an unknown key and
449
468
  * `approval-required` are `false`.
@@ -522,7 +541,7 @@ function authorizationHooks(options) {
522
541
  if (!isRecord(claims) || !await holdsAll(claims, ref)) return null;
523
542
  return Object.freeze({
524
543
  ...claims,
525
- [PERMISSION_CAP_CLAIM]: [...ref.permissions]
544
+ ...Object.fromEntries(PERMISSION_CAP_CLAIMS.map((claim) => [claim, [...ref.permissions]]))
526
545
  });
527
546
  } catch {
528
547
  return null;
@@ -745,7 +764,8 @@ const HOLDERS_OF = "permitted_{scope}_users";
745
764
  /**
746
765
  * better-supabase calls `teamIdsOf` from RLS policies as `authenticated`, so it
747
766
  * fills from the caller-bound `member_<scope>_ids_of`, never from the `_for`
748
- * form only `supabase_auth_admin` may execute.
767
+ * form only `supabase_auth_admin` may execute. Without a team scope, the hook
768
+ * writes `member_team_ids_of` from `supabase.hook.teams`.
749
769
  */
750
770
  const TEAM_IDS_OF = "member_{scope}_ids_of";
751
771
  /** The scope names better-supabase reads as its teams (doctor BS417). */
@@ -806,12 +826,25 @@ function membershipOf(source, scope, problems) {
806
826
  table: source.table,
807
827
  userColumn: source.user.column,
808
828
  scope: "column" in source.scope ? { column: source.scope.column } : { value: scope },
809
- idColumn: source.id.column
829
+ idColumn: source.id.column,
830
+ ...rolesTableOf(source)
810
831
  };
811
832
  }
833
+ /**
834
+ * The role set a member's roles come from, one row per scope id, user and role:
835
+ * a role read through a reference over several columns.
836
+ */
837
+ function rolesTableOf(source) {
838
+ const roles = isRoleList(source.role) ? source.role : [source.role];
839
+ for (const role of roles) if ("through" in role && role.through.on !== void 0) return { rolesTable: role.through.table };
840
+ return {};
841
+ }
842
+ function isRoleList(role) {
843
+ return Array.isArray(role);
844
+ }
812
845
  function roleSourceOf(source) {
813
846
  const role = source.role;
814
- if (Array.isArray(role) || !("column" in role) || !("through" in role)) return;
847
+ if (Array.isArray(role) || !("column" in role) || !("through" in role) || role.through.on !== void 0) return;
815
848
  return {
816
849
  table: source.table,
817
850
  role: {
@@ -846,13 +879,15 @@ function permissionsOf(catalog, scopes) {
846
879
  };
847
880
  });
848
881
  }
849
- function tokenHookOf(manifest) {
882
+ function tokenHookOf(manifest, activeTenant) {
850
883
  const owned = manifest.claims.filter((claim) => claim.source === "permdock");
851
884
  const registered = manifest.claims.filter((claim) => claim.source !== "permdock");
852
885
  const budgeted = manifest.claims.filter((claim) => claim.budget);
886
+ const activeFrom = activeTenant === "resolver" || manifest.hook.activeFrom === void 0 ? void 0 : activeOrganizationFrom(manifest.hook.activeFrom, manifest.tenantClaim);
853
887
  return {
854
888
  function: `${manifest.hook.schema}.${manifest.hook.function}`,
855
889
  tenantClaim: manifest.tenantClaim,
890
+ ...activeFrom === void 0 ? {} : { activeOrganizationFrom: activeFrom },
856
891
  ownedClaims: owned.map((claim) => claim.name),
857
892
  ...registered.length > 0 ? { registeredClaims: registered.map((claim) => ({
858
893
  name: claim.name,
@@ -944,13 +979,14 @@ function delegatedScopes(catalog) {
944
979
  * templates, the membership tables, suspension rows, the token hook and every
945
980
  * catalog key with `sqlComplete` set from its `rowConditions`. A `_for`
946
981
  * template and `permissionsFor` are set only when the manifest lists their
947
- * helper, which `rls.mode: 'database'` writes; `isApprover` only with
948
- * `approvers` and `permdock_is_approver`, and otherwise `canApprove` only with
949
- * `approver`. `holdersOf` (`permitted_<tenant scope>_users`) and
950
- * `actorMayWrite` (`permdock_actor_may_write()`) are set when the manifest
951
- * lists their helper, and so is `teamIdsOf` (`member_<scope>_ids_of({user})`)
952
- * for a scope named `team` or `teams`. Throws when the manifest or catalog is
953
- * invalid, or when the tenant scope is ambiguous.
982
+ * helper, which `rls.mode: 'database'` writes. `isApprover` is set with
983
+ * `approvers` or `approver` and `permdock_is_approver`: `approver` fills its
984
+ * `'ai-tool'` entry. `canApprove` is never set. `holdersOf` (`permitted_<tenant
985
+ * scope>_users`) and `actorMayWrite` (`permdock_actor_may_write()`) are set
986
+ * when the manifest lists their helper, and so is `teamIdsOf`
987
+ * (`member_<scope>_ids_of({user})`) for a scope named `team` or `teams`. Throws
988
+ * when the manifest or catalog is invalid, or when the tenant scope is
989
+ * ambiguous.
954
990
  */
955
991
  function authorizationProvider(options) {
956
992
  const manifest = parseSupabaseManifest(options.manifest);
@@ -958,6 +994,11 @@ function authorizationProvider(options) {
958
994
  const rls = manifest.rls;
959
995
  const problems = [];
960
996
  if (catalog === void 0) problems.push("No catalog: better-supabase cannot check that the permission keys its SQL modules use (ai_chat, workflow, inbox and the rest) exist. Pass `catalog`.");
997
+ const activeFrom = hasTokenHook(manifest) ? manifest.hook.activeFrom : void 0;
998
+ if (options.activeTenant !== void 0 && activeFrom !== void 0) {
999
+ const mismatch = activeTenantMismatch(activeFrom, manifest.tenantClaim, options.activeTenant);
1000
+ if (mismatch !== void 0) problems.push(mismatch);
1001
+ }
961
1002
  const tenantScope = tenantScopeOf(manifest, options.scope);
962
1003
  const scopeNames = rls.scopes.map((scope) => scope.name);
963
1004
  const helpers = new Map(rls.helpers.map((helper) => [helper.name, helper]));
@@ -1037,7 +1078,7 @@ function authorizationProvider(options) {
1037
1078
  });
1038
1079
  }
1039
1080
  let teamIdsOf;
1040
- const teamHelper = scopeNames.filter((scope) => TEAM_SCOPE.test(scope)).map((scope) => helpers.get(TEAM_IDS_OF.replace("{scope}", scope))).find((helper) => helper !== void 0);
1081
+ const teamHelper = scopeNames.filter((scope) => TEAM_SCOPE.test(scope)).map((scope) => helpers.get(TEAM_IDS_OF.replace("{scope}", scope))).find((helper) => helper !== void 0) ?? helpers.get(TEAM_IDS_OF.replace("{scope}", "team"));
1041
1082
  if (teamHelper !== void 0) {
1042
1083
  const fn = `${schema}.${sqlIdent(teamHelper.name)}`;
1043
1084
  teamIdsOf = `${fn}({user})`;
@@ -1098,11 +1139,24 @@ function authorizationProvider(options) {
1098
1139
  ...Object.keys(operations).length > 0 ? { operations } : {},
1099
1140
  ...Object.keys(delegated).length > 0 ? { delegation: { scopes: delegated } } : {},
1100
1141
  decidingColumns: manifest.decidingColumns,
1101
- ...hasTokenHook(manifest) ? { tokenHook: tokenHookOf(manifest) } : {},
1142
+ ...hasTokenHook(manifest) ? { tokenHook: tokenHookOf(manifest, options.activeTenant) } : {},
1102
1143
  ...problems.length > 0 ? { problems } : {}
1103
1144
  });
1104
1145
  }
1105
1146
  //#endregion
1147
+ //#region src/better-supabase/report.ts
1148
+ /** `PermDock denied <key>: <first denial reason>.` */
1149
+ function deniedMessage(key, decision) {
1150
+ const why = decision.denials[0]?.reason;
1151
+ return why === void 0 ? `PermDock denied ${key}.` : `PermDock denied ${key}: ${why}.`;
1152
+ }
1153
+ function report(sink, error, context) {
1154
+ if (sink === void 0) return;
1155
+ try {
1156
+ sink(error, context);
1157
+ } catch {}
1158
+ }
1159
+ //#endregion
1106
1160
  //#region src/better-supabase/authorizer.ts
1107
1161
  const OPERATIONS = /* @__PURE__ */ new Set([
1108
1162
  "list",
@@ -1111,10 +1165,18 @@ const OPERATIONS = /* @__PURE__ */ new Set([
1111
1165
  "update",
1112
1166
  "delete"
1113
1167
  ]);
1168
+ const READ_ACTIONS = /* @__PURE__ */ new Set([
1169
+ "read",
1170
+ "list",
1171
+ "view"
1172
+ ]);
1114
1173
  const denied = (reason) => ({
1115
1174
  outcome: "denied",
1116
1175
  reason
1117
1176
  });
1177
+ function isVersionCache(value) {
1178
+ return isRecord(value) && typeof value["maxStaleMs"] === "number" && typeof value["current"] === "function" && typeof value["evict"] === "function";
1179
+ }
1118
1180
  function outcomeOf(decision, key) {
1119
1181
  switch (decision.outcome) {
1120
1182
  case "granted": return { outcome: "granted" };
@@ -1122,10 +1184,7 @@ function outcomeOf(decision, key) {
1122
1184
  outcome: "approval-required",
1123
1185
  approval: { id: decision.token }
1124
1186
  };
1125
- case "denied": {
1126
- const why = decision.denials[0]?.reason;
1127
- return denied(why === void 0 ? `PermDock denied ${key}.` : `PermDock denied ${key}: ${why}.`);
1128
- }
1187
+ case "denied": return denied(deniedMessage(key, decision));
1129
1188
  default: return decision;
1130
1189
  }
1131
1190
  }
@@ -1144,23 +1203,59 @@ function claimsOf(request) {
1144
1203
  const claims = subject.properties?.["claims"];
1145
1204
  return isRecord(claims) ? claims : void 0;
1146
1205
  }
1206
+ const tenantOf = (request) => {
1207
+ const tenant = request.context["tenant"];
1208
+ return typeof tenant === "string" ? tenant : null;
1209
+ };
1210
+ const keyOf = (request) => isPermission(request.action) ? request.action.key : null;
1147
1211
  /**
1148
1212
  * better-supabase's `authorizer` for PermDock: every resource, action, route
1149
1213
  * guard and MCP tool that declares a `permission` is decided with `decide`. The
1150
1214
  * subject is mapped from the verified claims with `subjectFromBetterSupabase`;
1151
1215
  * the request's `context.tenant` selects the active tenant only among the
1152
1216
  * memberships the claims hold. A caller without claims is anonymous, and a
1153
- * throw denies.
1217
+ * throw denies. With better-supabase 0.15's runtime `memo`, the checks of one
1218
+ * request scope share an instance per claims, tenant and intent.
1154
1219
  */
1155
1220
  function authorizer(options) {
1156
1221
  const { operations } = options;
1157
- const instanceFor = async (claims, tenant) => {
1222
+ const intentOf = (request, batch) => {
1223
+ if (options.intent !== void 0) return options.intent(request);
1224
+ if (batch) return "read";
1225
+ return isPermission(request.action) && READ_ACTIONS.has(request.action.action) ? "read" : "write";
1226
+ };
1227
+ const membershipsFor = (runtime) => {
1228
+ if (options.memberships === void 0) return {};
1229
+ const versions = isVersionCache(runtime?.versions) ? runtime.versions : options.versions;
1230
+ return { memberships: claimsFirst(options.memberships, {
1231
+ onStale: "reread",
1232
+ ...options.version === void 0 ? {} : { version: options.version },
1233
+ ...versions === void 0 ? {} : { versions }
1234
+ }) };
1235
+ };
1236
+ const instanceFor = async (claims, tenant, intent, runtime) => {
1158
1237
  const session = claims === void 0 ? null : {
1159
1238
  kind: "user",
1160
1239
  claims
1161
1240
  };
1162
- const base = await getPermDock(options.policy, subjectFromBetterSupabase(session, options.subject), options.options);
1163
- return typeof tenant === "string" ? base.tenant(tenant) : base;
1241
+ const base = await getPermDock(options.policy, subjectFromBetterSupabase(session, options.subject), {
1242
+ ...options.options,
1243
+ ...membershipsFor(runtime),
1244
+ intent
1245
+ });
1246
+ return tenant === null ? base : base.tenant(tenant);
1247
+ };
1248
+ const scoped = (claims, tenant, intent, runtime) => runtime?.memo === void 0 ? instanceFor(claims, tenant, intent, runtime) : runtime.memo.once(`permdock:authorizer:${JSON.stringify([
1249
+ claims ?? null,
1250
+ tenant,
1251
+ intent
1252
+ ])}`, () => instanceFor(claims, tenant, intent, runtime));
1253
+ const failed = (error, request) => {
1254
+ report(options.onError, error, {
1255
+ permission: keyOf(request),
1256
+ tenant: tenantOf(request)
1257
+ });
1258
+ return denied("The permission check failed.");
1164
1259
  };
1165
1260
  return Object.freeze({
1166
1261
  apiVersion: 1,
@@ -1173,39 +1268,98 @@ function authorizer(options) {
1173
1268
  if (!OPERATIONS.has(operation) || !Object.hasOwn(operations, table)) return void 0;
1174
1269
  return operations[table]?.[operation];
1175
1270
  } },
1176
- async evaluate(request) {
1271
+ async evaluate(request, runtime) {
1177
1272
  try {
1178
- return decideWith(await instanceFor(claimsOf(request), request.context["tenant"]), request);
1179
- } catch {
1180
- return denied("The permission check failed.");
1273
+ return decideWith(await scoped(claimsOf(request), tenantOf(request), intentOf(request, false), runtime), request);
1274
+ } catch (error) {
1275
+ return failed(error, request);
1181
1276
  }
1182
1277
  },
1183
- evaluations(requests) {
1278
+ evaluations(requests, runtime) {
1184
1279
  const cache = /* @__PURE__ */ new Map();
1185
- const shared = (claims, tenant) => {
1186
- let byTenant = cache.get(claims);
1187
- if (byTenant === void 0) {
1188
- byTenant = /* @__PURE__ */ new Map();
1189
- cache.set(claims, byTenant);
1280
+ const shared = (request) => {
1281
+ const claims = claimsOf(request);
1282
+ const tenant = tenantOf(request);
1283
+ const intent = intentOf(request, true);
1284
+ let byKey = cache.get(claims);
1285
+ if (byKey === void 0) {
1286
+ byKey = /* @__PURE__ */ new Map();
1287
+ cache.set(claims, byKey);
1190
1288
  }
1191
- let instance = byTenant.get(tenant);
1289
+ const key = JSON.stringify([tenant, intent]);
1290
+ let instance = byKey.get(key);
1192
1291
  if (instance === void 0) {
1193
- instance = instanceFor(claims, tenant);
1194
- byTenant.set(tenant, instance);
1292
+ instance = scoped(claims, tenant, intent, runtime);
1293
+ byKey.set(key, instance);
1195
1294
  }
1196
1295
  return instance;
1197
1296
  };
1198
1297
  return Promise.all(requests.map(async (request) => {
1199
1298
  try {
1200
- return decideWith(await shared(claimsOf(request), request.context["tenant"]), request);
1201
- } catch {
1202
- return denied("The permission check failed.");
1299
+ return decideWith(await shared(request), request);
1300
+ } catch (error) {
1301
+ return failed(error, request);
1203
1302
  }
1204
1303
  }));
1205
1304
  }
1206
1305
  });
1207
1306
  }
1208
1307
  //#endregion
1308
+ //#region src/better-supabase/block-memberships.ts
1309
+ function roleOf(options, columns) {
1310
+ const column = columns.role;
1311
+ if (options.roles !== void 0 && options.roleThrough !== void 0) throw new TypeError("PermDock: blockMembershipSource takes roles or roleThrough, not both; a junction holds role keys");
1312
+ if (options.roles === "many") return {
1313
+ through: `${qualifySqlTable(columns.table).split(".")[0] ?? "better_supabase"}.membership_roles`,
1314
+ on: {
1315
+ [columns.tenant]: columns.tenant,
1316
+ [columns.user]: columns.user
1317
+ },
1318
+ column: "role"
1319
+ };
1320
+ if (options.roles !== void 0) return {
1321
+ through: options.roles.table,
1322
+ on: { [options.roles.id ?? "id"]: options.roles.membership },
1323
+ column: options.roles.role
1324
+ };
1325
+ if (options.roleThrough !== void 0) return {
1326
+ through: options.roleThrough.table,
1327
+ on: { [column]: options.roleThrough.id },
1328
+ column: options.roleThrough.column
1329
+ };
1330
+ return column;
1331
+ }
1332
+ /**
1333
+ * Memberships from better-supabase's `memberships` block table: one row per
1334
+ * user and organization, its role key in `role`, a custom role's key through
1335
+ * `roleThrough`, or several roles from a `roles` junction or, with `roles:
1336
+ * "many"`, from better-supabase's `membership_roles`. A row with `disabled_at`
1337
+ * set is no membership.
1338
+ */
1339
+ function blockMembershipSource(options = {}) {
1340
+ const c = options.columns ?? {};
1341
+ const scope = options.scope ?? "organization";
1342
+ const disabledAt = c.disabledAt === null ? void 0 : c.disabledAt ?? "disabled_at";
1343
+ const table = options.table ?? "better_supabase.memberships";
1344
+ const tenant = c.tenant ?? "organization_id";
1345
+ const user = c.user ?? "user_id";
1346
+ return junctionMembershipSource({
1347
+ table,
1348
+ scope,
1349
+ id: tenant,
1350
+ user,
1351
+ roles: roleOf(options, {
1352
+ table,
1353
+ role: c.role ?? "role",
1354
+ tenant,
1355
+ user
1356
+ }),
1357
+ ...disabledAt === void 0 ? {} : { disabledAt },
1358
+ ...options.query === void 0 ? {} : { query: options.query },
1359
+ ...options.suspension === void 0 ? {} : { suspension: options.suspension }
1360
+ });
1361
+ }
1362
+ //#endregion
1209
1363
  //#region src/better-supabase/policies.ts
1210
1364
  /**
1211
1365
  * PermDock splits a key with row conditions into `key#1`, `key#2`, ... in the
@@ -1339,7 +1493,8 @@ const refused = (reason) => ({
1339
1493
  * tools whose `meta` is a PermDock permission. A tool without a permission is
1340
1494
  * hidden and refused. `authorize` grants only a `granted` decision: an
1341
1495
  * approval-required call is refused, because `createMcp` has no approval flow.
1342
- * A throw anywhere refuses.
1496
+ * A denial's text names the first denial reason. A throw anywhere refuses and
1497
+ * reaches `onError`.
1343
1498
  */
1344
1499
  function toolPolicy(options) {
1345
1500
  return Object.freeze({
@@ -1353,10 +1508,14 @@ function toolPolicy(options) {
1353
1508
  switch (decision.outcome) {
1354
1509
  case "granted": return { allowed: true };
1355
1510
  case "approval-required": return refused(`${permission.key} needs approval, which this server does not collect.`);
1356
- case "denied": return refused(`PermDock denied ${permission.key}.`);
1511
+ case "denied": return refused(deniedMessage(permission.key, decision));
1357
1512
  default: return decision;
1358
1513
  }
1359
- } catch {
1514
+ } catch (error) {
1515
+ report(options.onError, error, {
1516
+ permission: permission.key,
1517
+ tenant: null
1518
+ });
1360
1519
  return refused("The permission check failed.");
1361
1520
  }
1362
1521
  },
@@ -1365,7 +1524,11 @@ function toolPolicy(options) {
1365
1524
  if (!isPermission(permission)) return false;
1366
1525
  try {
1367
1526
  return (await options.permdock(ctx)).mayUse(permission);
1368
- } catch {
1527
+ } catch (error) {
1528
+ report(options.onError, error, {
1529
+ permission: permission.key,
1530
+ tenant: null
1531
+ });
1369
1532
  return false;
1370
1533
  }
1371
1534
  }
@@ -1585,12 +1748,12 @@ async function subjectFromShareLink(token, options) {
1585
1748
  //#region src/better-supabase/workflow.ts
1586
1749
  /**
1587
1750
  * Whether `permdock` is granted the permission `key` (a catalog key or scope)
1588
- * with no row.
1751
+ * on `row`.
1589
1752
  */
1590
- function grants(permdock, key) {
1753
+ function grants(permdock, key, row) {
1591
1754
  try {
1592
1755
  const permission = findPermission(permdock.permissions, key);
1593
- return permission !== void 0 && permdock.decide(permission, void 0).outcome === "granted";
1756
+ return permission !== void 0 && permdock.decide(permission, row).outcome === "granted";
1594
1757
  } catch {
1595
1758
  return false;
1596
1759
  }
@@ -1598,25 +1761,40 @@ function grants(permdock, key) {
1598
1761
  /**
1599
1762
  * The `can` option of better-supabase's `authorizeHook`, for the caller's
1600
1763
  * `permdock`: `true` only when the caller is a member of the hook's tenant and
1601
- * `decide` grants the permission its `hookMetadata` names there. An unknown
1602
- * key, `approval-required` and a throw are `false`.
1764
+ * `decide` grants the permission its `hookMetadata` names there, on the row
1765
+ * `options.data` loads. An unknown key, `approval-required` and a throw are
1766
+ * `false`.
1603
1767
  */
1604
- function hookCan(permdock) {
1605
- return (tenant, key) => {
1768
+ function hookCan(permdock, options = {}) {
1769
+ const { data } = options;
1770
+ const decideIn = (tenant, key, row) => {
1771
+ try {
1772
+ return permdock.tenants().includes(tenant) && grants(permdock.tenant(tenant), key, row);
1773
+ } catch {
1774
+ return false;
1775
+ }
1776
+ };
1777
+ if (data === void 0) return (tenant, key) => decideIn(tenant, key);
1778
+ return async (tenant, key) => {
1779
+ let row;
1606
1780
  try {
1607
- return permdock.tenants().includes(tenant) && grants(permdock.tenant(tenant), key);
1781
+ if (!permdock.tenants().includes(tenant)) return false;
1782
+ row = await data(tenant, key);
1608
1783
  } catch {
1609
1784
  return false;
1610
1785
  }
1786
+ return decideIn(tenant, key, row);
1611
1787
  };
1612
1788
  }
1613
1789
  /**
1614
1790
  * The `can` option of better-supabase's `protectWebHandler`: `undefined` (401)
1615
1791
  * when the request has no signed-in principal or `getPermDock` throws,
1616
1792
  * otherwise whether `decide` grants the permission in the request's active
1617
- * tenant. An unknown key and `approval-required` are `false` (403).
1793
+ * tenant, on the row `options.data` loads. An unknown key, `approval-required`
1794
+ * and a throwing `data` are `false` (403).
1618
1795
  */
1619
- function handlerCan(source) {
1796
+ function handlerCan(source, options = {}) {
1797
+ const { data } = options;
1620
1798
  return async (request, key) => {
1621
1799
  let permdock;
1622
1800
  try {
@@ -1625,8 +1803,14 @@ function handlerCan(source) {
1625
1803
  return;
1626
1804
  }
1627
1805
  if (permdock.subject.principal === null) return;
1628
- return grants(permdock, key);
1806
+ if (data === void 0) return grants(permdock, key);
1807
+ try {
1808
+ const row = await data(request, key);
1809
+ return grants(permdock, key, row);
1810
+ } catch {
1811
+ return false;
1812
+ }
1629
1813
  };
1630
1814
  }
1631
1815
  //#endregion
1632
- export { accessReviewApplier, apiKeyClaimOptions, apiKeyVerifier, authorizationHooks, authorizationProvider, authorizer, bucketPolicy, credentialGuard, handlerCan, hookCan, principalFromMemberships, principalSource, roleOrderFromCatalog, subjectFromBetterSupabase, subjectFromShareLink, subscriberPolicy, supportPolicy, toolPolicy, topicPolicy };
1816
+ export { accessReviewApplier, apiKeyClaimOptions, apiKeyVerifier, authorizationHooks, authorizationProvider, authorizer, blockMembershipSource, bucketPolicy, credentialGuard, handlerCan, hookCan, principalFromMemberships, principalSource, roleOrderFromCatalog, subjectFromBetterSupabase, subjectFromShareLink, subscriberPolicy, supportPolicy, toolPolicy, topicPolicy };
@@ -1,4 +1,4 @@
1
- import { a as nonEmptyOr } from "./output-DV5dwOTs.js";
1
+ import { a as nonEmptyOr } from "./output-LsbLe41I.js";
2
2
  import { t as applyFiles } from "./files-B3AA05f1.js";
3
3
  import { n as isIdentifier, t as emitPolicyModule } from "./policy-module-xym9fnSq.js";
4
4
  import { existsSync, readFileSync } from "node:fs";
@@ -16,7 +16,7 @@ const casbinImport = defineLeaf({
16
16
  ...outFlag
17
17
  },
18
18
  async run(ctx, { flags }) {
19
- const { runCasbinImport } = await import("./casbin-bHyIxTNQ.js");
19
+ const { runCasbinImport } = await import("./casbin-BJu2liqU.js");
20
20
  return runCasbinImport({
21
21
  cwd: ctx.cwd,
22
22
  model: given(flags.model),
package/dist/cli/bin.js CHANGED
@@ -11,7 +11,7 @@ if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
11
11
  const [{ isCi }, { trapSignals }, { run }, { streamHasColors }] = await Promise.all([
12
12
  import("../env-DJZrPjRi.js"),
13
13
  import("../signals-B1vHR3fN.js"),
14
- import("../run-Ck22anC2.js"),
14
+ import("../run-BATawNVH.js"),
15
15
  import("../style-DblOtSga.js")
16
16
  ]);
17
17
  const signals = trapSignals();
@@ -3,7 +3,7 @@ import { f as PermissionTree } from "../permission-types-CAY-GU7e.js";
3
3
  import { o as OperationMap } from "../operations-Ci_rT6Uo.js";
4
4
  import { a as CatalogDocument, d as CatalogRole, f as CatalogScope, l as CatalogPermission, m as CatalogUsage, n as CatalogApproval, p as CatalogSupportAccess, r as CatalogBreakGlass, t as CatalogActivation, u as CatalogResource } from "../types-ZzRGoViD.js";
5
5
  import { a as pushedPolicy, i as parsePushedPolicy, n as PushedGrant, r as PushedPolicy, t as PUSHED_POLICY_SCHEMA } from "../pushed-policy-CryQheKq.js";
6
- import { _ as RlsSuspension, a as GenKind, c as RlsActiveRow, d as RlsDialect, f as RlsFunctionMapping, g as RlsSuspendedScope, h as RlsSuspendedMembership, i as GenConfig, l as RlsApiKeys, m as RlsMemberships, n as CliIo, o as PermDockConfig, p as RlsMembershipTable, r as CollectConfig, t as CatalogConfig, u as RlsConfig, v as RlsTarget, y as RunResult } from "../types-CzOeVvcC.js";
6
+ import { _ as RlsSuspension, a as GenKind, c as RlsActiveRow, d as RlsDialect, f as RlsFunctionMapping, g as RlsSuspendedScope, h as RlsSuspendedMembership, i as GenConfig, l as RlsApiKeys, m as RlsMemberships, n as CliIo, o as PermDockConfig, p as RlsMembershipTable, r as CollectConfig, t as CatalogConfig, u as RlsConfig, v as RlsTarget, y as RunResult } from "../types-Cklw5hJo.js";
7
7
  import { o as ResourceNameOf, r as PermissionKeyOf, s as ScopeNameOf, t as ActionNameOf } from "../type-helpers-c7Xo_2Cu.js";
8
8
  //#region src/cli/catalog-path.d.ts
9
9
  /**
@@ -76,6 +76,11 @@ type ConfigChecks<C> = [PolicyOf<C>] extends [never] ? unknown : {
76
76
  readonly powersync?: {
77
77
  readonly resources?: readonly ResourceNameOf<PolicyOf<C>>[];
78
78
  readonly action?: ActionNameOf<PolicyOf<C>>;
79
+ readonly tables?: Readonly<Record<string, {
80
+ readonly follow?: readonly {
81
+ readonly resource: ResourceNameOf<PolicyOf<C>>;
82
+ }[];
83
+ }>>;
79
84
  };
80
85
  };
81
86
  //#endregion
package/dist/cli/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { t as catalogPath } from "../catalog-path-CnI0XTFK.js";
2
- import { t as defineConfig } from "../config-BwPBfN0t.js";
2
+ import { t as defineConfig } from "../config-BR23fL8m.js";
3
3
  import { n as parsePushedPolicy, r as pushedPolicy, t as PUSHED_POLICY_SCHEMA } from "../pushed-policy-CDuOL2de.js";
4
4
  import { c as parseGrantsMarker, l as parseHookMarker } from "../markers-BeHZ4x9Q.js";
5
- import { t as run } from "../run-PbRlXTfh.js";
5
+ import { t as run } from "../run-CLP_fQUF.js";
6
6
  //#region src/cli/token-schemas.ts
7
7
  const ID = {
8
8
  type: "string",
@@ -85,6 +85,14 @@ const RUN_TICKET_SCHEMA = {
85
85
  maxItems: 64
86
86
  }
87
87
  }
88
+ },
89
+ resource: {
90
+ type: "object",
91
+ required: ["type", "id"],
92
+ properties: {
93
+ type: ID,
94
+ id: ID
95
+ }
88
96
  }
89
97
  },
90
98
  anyOf: [{
@@ -41,7 +41,7 @@ const push = defineLeaf({
41
41
  },
42
42
  async run(ctx, { flags }) {
43
43
  const env = await withStdinSecret(ctx, "key", flags["key-stdin"], "PERMDOCK_CLOUD_KEY");
44
- return (await import("./push-Bn-lMEmI.js")).runCloud({
44
+ return (await import("./push-Dq_ca4-j.js")).runCloud({
45
45
  cwd: ctx.cwd,
46
46
  config: ctx.config,
47
47
  url: given(flags.url),
@@ -70,7 +70,7 @@ const pull = defineLeaf({
70
70
  },
71
71
  async run(ctx, { flags }) {
72
72
  const env = await withStdinSecret(ctx, "token", flags["token-stdin"], "PERMDOCK_CLOUD_TOKEN");
73
- return (await import("./pull-C3EEGzSw.js")).runCloudPull({
73
+ return (await import("./pull-DYbyod1f.js")).runCloudPull({
74
74
  cwd: ctx.cwd,
75
75
  config: ctx.config,
76
76
  url: given(flags.url),
@@ -7,7 +7,7 @@ const codemod = defineLeaf({
7
7
  description: "The release to migrate to (0.5)",
8
8
  required: true,
9
9
  async complete() {
10
- return (await import("./codemod-Bv5i-Mzo.js")).codemodVersions();
10
+ return (await import("./codemod-DRl_w-bY.js")).codemodVersions();
11
11
  }
12
12
  }, {
13
13
  name: "paths",
@@ -22,7 +22,7 @@ const codemod = defineLeaf({
22
22
  }
23
23
  },
24
24
  async run(ctx, { flags, args, rest }) {
25
- return (await import("./codemod-Bv5i-Mzo.js")).runCodemod({
25
+ return (await import("./codemod-DRl_w-bY.js")).runCodemod({
26
26
  cwd: ctx.cwd,
27
27
  version: args["version"],
28
28
  paths: rest,
@@ -1,7 +1,7 @@
1
1
  import { n as CliError } from "./errors-Vpu6rKHl.js";
2
2
  import { a as rel } from "./files-BSjrtIHw.js";
3
3
  import { t as beforeFirst } from "./text-BMFRfBqc.js";
4
- import { n as ROOT } from "./manifest-B2QPh19C.js";
4
+ import { n as ROOT } from "./manifest-B4ENCp2I.js";
5
5
  import { i as renameMessage, n as DEFAULT_CHANGES, r as flagRename, t as COMMAND_MOVES } from "./renames-CGj7YwcQ.js";
6
6
  import { i as refuseDirty, t as applyFiles } from "./files-B3AA05f1.js";
7
7
  import { readFileSync, readdirSync, statSync } from "node:fs";