permdock 0.11.0 → 0.14.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 (84) hide show
  1. package/dist/a2a/index.js +1 -1
  2. package/dist/{adoption-scan-DKCOsfcF.js → adoption-scan-BGtwGfoH.js} +1 -1
  3. package/dist/agent/index.js +1 -1
  4. package/dist/ai-sdk/index.js +2 -2
  5. package/dist/{arazzo-CTH5iN--.js → arazzo-9uhbmKf5.js} +1 -1
  6. package/dist/{arazzo-BXaEhGHM.js → arazzo-Bol9OgF-.js} +1 -1
  7. package/dist/better-supabase/index.d.ts +19 -17
  8. package/dist/better-supabase/index.js +31 -16
  9. package/dist/better-supabase-83Gsetrv.js +56 -0
  10. package/dist/{casbin-BuTZnSPv.js → casbin-1PVgMHOR.js} +1 -1
  11. package/dist/{casbin-DTi6HSsl.js → casbin-CBXD5rFx.js} +1 -1
  12. package/dist/claude-agent/index.js +1 -1
  13. package/dist/cli/bin.js +1 -1
  14. package/dist/cli/index.js +2 -2
  15. package/dist/{cloud-DiAiYFRo.js → cloud-BfJVKpcN.js} +1 -1
  16. package/dist/{codemod-C1U8_Zu3.js → codemod-BXLd4K3j.js} +1 -1
  17. package/dist/{codemod-_q_ZI01S.js → codemod-DWfH-KHk.js} +2 -2
  18. package/dist/{collect-D9Tt0Bcx.js → collect-lZgCKMe_.js} +1 -1
  19. package/dist/{completion-DLTuCf53.js → completion-fJ19i8Jk.js} +1 -1
  20. package/dist/{config-CmydKZr2.js → config-BYSk3iKV.js} +1 -1
  21. package/dist/{config-BR23fL8m.js → config-ZZdtyQZy.js} +4 -1
  22. package/dist/{config-report-5AwptLr2.js → config-report-BpztJ3HP.js} +1 -1
  23. package/dist/{create-6EwRQlig.js → create-Coq5fIrq.js} +1 -1
  24. package/dist/{diff-BwR1JdP2.js → diff-CxtbOi2l.js} +1 -1
  25. package/dist/{diff-CQegtBAo.js → diff-DfmYaIhB.js} +1 -1
  26. package/dist/{doctor-DxyVeUEE.js → doctor-NxCr_hba.js} +2 -2
  27. package/dist/{doctor-ktitU5Dl.js → doctor-eNc7A_7I.js} +13 -47
  28. package/dist/eve/index.js +1 -1
  29. package/dist/{fixtures-A1_sp_tR.js → fixtures-Bm4_K1ke.js} +7 -9
  30. package/dist/{import-t1yqnCyG.js → import-efm6qPgx.js} +1 -1
  31. package/dist/{init-CY7THM2v.js → init-BtWY1L0J.js} +3 -3
  32. package/dist/{init-DFwylmiN.js → init-D6rpLPwM.js} +1 -1
  33. package/dist/{kernel-qv4cr9w8.js → kernel-BD8QuMnv.js} +9 -2
  34. package/dist/{login-C5XLYZSF.js → login-B8s6VI8Z.js} +3 -3
  35. package/dist/{login-B92-dZMK.js → login-cLflB63Z.js} +1 -1
  36. package/dist/{manifest-Cav1rjNQ.js → manifest-BWIR_EEB.js} +33 -33
  37. package/dist/mcp/index.d.ts +12 -3
  38. package/dist/mcp/index.js +60 -16
  39. package/dist/next/plugin.js +1 -1
  40. package/dist/openai/index.js +2 -2
  41. package/dist/{openapi-gMQQSzNn.js → openapi-CT55v3WD.js} +2 -2
  42. package/dist/{openapi-CuePQL4z.js → openapi-DDENzRbG.js} +1 -1
  43. package/dist/{output-CtyBwTNB.js → output-3iB3_Hfg.js} +1 -1
  44. package/dist/{powersync-CBcsEvdr.js → powersync-COjQTHRD.js} +2 -2
  45. package/dist/{powersync-CD8-Ypao.js → powersync-DVbskWD9.js} +126 -39
  46. package/dist/powersync-k8lUeC37.js +1 -0
  47. package/dist/pull-BY1METGf.js +1 -0
  48. package/dist/{pull-CAeaAioC.js → pull-Er4aBHlL.js} +1 -1
  49. package/dist/react-native/index.d.ts +7 -7
  50. package/dist/react-native/index.js +57 -16
  51. package/dist/{rls-CtRaRrn3.js → rls-DY3Fc18U.js} +30 -15
  52. package/dist/{rls-BYHN5bcJ.js → rls-DjjOHlKE.js} +1 -1
  53. package/dist/{run-B5nyNKe8.js → run-BlxXmaDS.js} +3 -3
  54. package/dist/run-DGSLvGty.js +1 -0
  55. package/dist/{scan-CvFWW_fU.js → scan-CR_xm71k.js} +1 -1
  56. package/dist/{schema-CQzIJdEd.js → schema-CnrIP94n.js} +1 -1
  57. package/dist/{skills-Bar7ZqwM.js → skills-CY-MY_Oj.js} +1 -1
  58. package/dist/{skills-BZc81VLt.js → skills-uzsVqEi9.js} +1 -1
  59. package/dist/supabase/index.js +1 -1
  60. package/dist/{supabase-CBEK6KEo.js → supabase-DYWnT800.js} +2 -2
  61. package/dist/{supabase-hook-CyuD9Wau.js → supabase-hook-HPgwYb9V.js} +2 -2
  62. package/dist/supabase-hook-qsewAVZY.js +1 -0
  63. package/dist/{supautils-C-_jqylY.js → supautils-DZ95xxW-.js} +3 -3
  64. package/dist/testing/core.js +1 -1
  65. package/dist/testing/fixtures/index.js +1 -1
  66. package/dist/testing/index.js +2 -2
  67. package/dist/unplugin/index.js +1 -1
  68. package/dist/usage-CH7oISI6.js +1 -0
  69. package/dist/{usage-heV5KOiU.js → usage-Dm2hn0pS.js} +1 -1
  70. package/dist/{usage-Y6xZUfE-.js → usage-WYRc7Kqg.js} +2 -2
  71. package/dist/{verify-6EauWrkP.js → verify-CHQgLrtS.js} +3 -3
  72. package/dist/watch-C44FtM79.js +1 -0
  73. package/dist/{watch-Bpci3hBY.js → watch-CTfSlIGD.js} +1 -1
  74. package/package.json +16 -16
  75. package/skills/permdock-credentials/references/tokens-keys-links.md +1 -1
  76. package/skills/permdock-wire/references/adapters.md +1 -1
  77. package/dist/powersync-6vgKevSi.js +0 -1
  78. package/dist/pull-DfJALUbS.js +0 -1
  79. package/dist/run-0vOholnN.js +0 -1
  80. package/dist/supabase-hook-CWvEpdCh.js +0 -1
  81. package/dist/usage-CNca1i1W.js +0 -1
  82. package/dist/watch-COuvly20.js +0 -1
  83. package/dist/{core-DzVINFbU.js → core-lxoANyai.js} +1 -1
  84. package/dist/{supabase-CC6rIri4.js → supabase-CDHMsDD0.js} +1 -1
@@ -1,7 +1,7 @@
1
1
  import { t as compact } from "../compact-CAf557ZB.js";
2
2
  import { l as withApp } from "../permdock-BvjivVb9.js";
3
- import { t as approvalTokenOf } from "../kernel-qv4cr9w8.js";
4
- import { t as agentRuntime } from "../create-6EwRQlig.js";
3
+ import { t as approvalTokenOf } from "../kernel-BD8QuMnv.js";
4
+ import { t as agentRuntime } from "../create-Coq5fIrq.js";
5
5
  //#region src/openai/create.ts
6
6
  function appContext(runContext) {
7
7
  const context = runContext?.context;
@@ -80,7 +80,7 @@ const emit = defineLeaf({
80
80
  }
81
81
  },
82
82
  async run(ctx, { flags }) {
83
- return (await import("./openapi-CuePQL4z.js")).runOpenapi({
83
+ return (await import("./openapi-DDENzRbG.js")).runOpenapi({
84
84
  cwd: ctx.cwd,
85
85
  config: ctx.config,
86
86
  rest: ["emit"],
@@ -122,7 +122,7 @@ const importDocument = defineLeaf({
122
122
  async run(ctx, { flags }) {
123
123
  const doc = given(flags.doc);
124
124
  if (doc === void 0) throw missingInput("openapi import", ["--doc"], "the OpenAPI document to import");
125
- return (await import("./import-t1yqnCyG.js")).runOpenapiImport({
125
+ return (await import("./import-efm6qPgx.js")).runOpenapiImport({
126
126
  cwd: ctx.cwd,
127
127
  doc,
128
128
  out: fileOut("openapi import", flags.out),
@@ -7,7 +7,7 @@ import { a as loadModule, n as asPolicy, s as pickNamed, t as asPermissionTree }
7
7
  import { t as shortDiff } from "./text-diff-BtfSmdUo.js";
8
8
  import { c as openapiVersion, o as createPermDock } from "./openapi-BOsx0xn9.js";
9
9
  import { i as fileDrift } from "./output-files-BIQ_9pIs.js";
10
- import { n as validateOverlay, t as validateOpenapi } from "./schema-CQzIJdEd.js";
10
+ import { n as validateOverlay, t as validateOpenapi } from "./schema-CnrIP94n.js";
11
11
  import { existsSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
13
  //#region src/cli/openapi/openapi.ts
@@ -1,5 +1,5 @@
1
1
  import { s as cliVersion } from "./version-DMQ0dnN5.js";
2
- import { t as DOCS_BASE } from "./manifest-Cav1rjNQ.js";
2
+ import { t as DOCS_BASE } from "./manifest-BWIR_EEB.js";
3
3
  //#region src/cli/core/output.ts
4
4
  const CLI_RESULT_SCHEMA = "https://permdock.com/schemas/cli-result-v1.json";
5
5
  function envelope(input) {
@@ -14,7 +14,7 @@ const generate = defineLeaf({
14
14
  async run(ctx, { flags }) {
15
15
  const out = fileOut("powersync", flags.out);
16
16
  const from = given(flags.policy);
17
- const { runPowerSync } = await import("./powersync-6vgKevSi.js");
17
+ const { runPowerSync } = await import("./powersync-k8lUeC37.js");
18
18
  return runPowerSync({
19
19
  cwd: ctx.cwd,
20
20
  config: ctx.config,
@@ -45,7 +45,7 @@ const verify = defineLeaf({
45
45
  const db = await dbUrl(ctx, flags, { required: true });
46
46
  const fixtures = given(flags.fixtures);
47
47
  const from = given(flags.policy);
48
- const { runPowerSync } = await import("./powersync-6vgKevSi.js");
48
+ const { runPowerSync } = await import("./powersync-k8lUeC37.js");
49
49
  return runPowerSync({
50
50
  cwd: ctx.cwd,
51
51
  config: ctx.config,
@@ -1,4 +1,4 @@
1
- import { t as compact } from "./compact-CAf557ZB.js";
1
+ import { r as isRecord, t as compact } from "./compact-CAf557ZB.js";
2
2
  import { c as isConditionDate, l as isConditionRef, o as hasConditionOp } from "./refs-C8BKxc_e.js";
3
3
  import { g as localSnapshotManifest, j as memoryCustomRoleSource } from "./src-C_pi2Rnh.js";
4
4
  import { n as findPermission } from "./permission-registry-B8tYxPIt.js";
@@ -10,16 +10,16 @@ import { u as usageResult } from "./errors-Vpu6rKHl.js";
10
10
  import { c as requirePolicy } from "./load-DjSU5KXO.js";
11
11
  import { s as quoteSqlLiteral, t as SQL_IDENT } from "./sql-0WjxIp9g.js";
12
12
  import { a as scopeMembershipTable, i as scopeColumn } from "./compile-EzEMB-7v.js";
13
- import "./sources-C4vkzZJV.js";
13
+ import { d as keptKeys } from "./sources-C4vkzZJV.js";
14
14
  import { t as applyFiles } from "./files-B3AA05f1.js";
15
15
  import { i as fileDrift } from "./output-files-BIQ_9pIs.js";
16
- import { n as fixtureSubject, r as loadFixtures, t as fixtureRow } from "./fixtures-A1_sp_tR.js";
17
- import { O as quoteIdent, a as customRolesOn, t as CUSTOM_ROLES } from "./shared-DrGLwM3s.js";
18
- import { r as tableNames } from "./tables-DimBcVWY.js";
16
+ import { n as fixtureSubject, r as loadFixtures, t as fixtureRow } from "./fixtures-Bm4_K1ke.js";
17
+ import { A as quoteQualified, O as quoteIdent, a as customRolesOn, t as CUSTOM_ROLES } from "./shared-DrGLwM3s.js";
18
+ import { n as tableNameOf, r as tableNames } from "./tables-DimBcVWY.js";
19
19
  import { d as andConditions, l as collectGrants, o as assignableRoles, p as contextRefs, t as ownershipRules, u as roleNames } from "./ownership-qqBuGJXr.js";
20
20
  import { t as membershipSourcesOf } from "./sources-qoIJQrJv.js";
21
21
  import { n as pgConnector } from "./pg-DQTqRrou.js";
22
- import { m as tableFor, t as canFixture } from "./verify-6EauWrkP.js";
22
+ import { m as tableFor, t as canFixture } from "./verify-CHQgLrtS.js";
23
23
  import { resolve } from "node:path";
24
24
  //#region src/cli/powersync/names.ts
25
25
  function ident(name) {
@@ -89,7 +89,6 @@ function streamOf(source) {
89
89
  const { stream } = source.sql;
90
90
  if (stream === void 0) return `the ${source.sql.table} membership source has no stream form: build it with tableMembershipSource, junctionMembershipSource or blockMembershipSource`;
91
91
  if (stream.expiresAt !== void 0) return `the ${stream.table} membership source has no stream: it has an expiry`;
92
- if (stream.disabledAt !== void 0) return `the ${stream.table} membership source has no stream: it has disabledAt`;
93
92
  return stream;
94
93
  }
95
94
  function pairOf(through) {
@@ -111,22 +110,32 @@ function anyOf(clauses) {
111
110
  }
112
111
  /**
113
112
  * The user's own rows of the source, of scope `scope` when the source has a
114
- * scope column.
113
+ * scope column. `keep` drops the suspension filter: the grant keeps a suspended
114
+ * membership.
115
115
  */
116
- function ownRows(stream, dialect, scope) {
116
+ function ownRows(stream, dialect, scope, keep = false) {
117
117
  const table = bare(stream.table, dialect);
118
118
  const scoped = scope !== void 0 && "column" in stream.scope ? [`${dialect.column(table, stream.scope.column)} = ${dialect.literal(scope)}`] : [];
119
+ const suspended = !keep && stream.disabledAt !== void 0 ? [`${dialect.column(table, stream.disabledAt)} IS NULL`] : [];
119
120
  if (typeof stream.user === "string") return {
120
121
  table,
121
122
  from: fromTable(stream.table, dialect),
122
- where: [`${dialect.column(table, stream.user)} = ${dialect.user}`, ...scoped]
123
+ where: [
124
+ `${dialect.column(table, stream.user)} = ${dialect.user}`,
125
+ ...scoped,
126
+ ...suspended
127
+ ]
123
128
  };
124
129
  const users = bare(stream.user.through, dialect);
125
130
  const [ref, id] = pairOf(stream.user);
126
131
  return {
127
132
  table,
128
133
  from: `${fromTable(stream.table, dialect)} INNER JOIN ${fromTable(stream.user.through, dialect)} ON ${dialect.column(users, id)} = ${dialect.column(table, ref)}`,
129
- where: [`${dialect.column(users, stream.user.column)} = ${dialect.user}`, ...scoped]
134
+ where: [
135
+ `${dialect.column(users, stream.user.column)} = ${dialect.user}`,
136
+ ...scoped,
137
+ ...suspended
138
+ ]
130
139
  };
131
140
  }
132
141
  /**
@@ -170,9 +179,10 @@ function kindFilter(stream, role, table, ctx) {
170
179
  * One `IN (SELECT …)` per role source of the stream that can hold one of
171
180
  * `roles` in scope `scope`: the row's `left` is an instance the user holds it
172
181
  * in. A null role never equals a key, so a row without a role grants nothing.
182
+ * `keep` drops the suspension filter of a membership the grant keeps.
173
183
  */
174
- function sourceIn(stream, scope, left, roles, ctx) {
175
- const own = ownRows(stream, ctx.dialect, scope);
184
+ function sourceIn(stream, scope, left, roles, ctx, keep = false) {
185
+ const own = ownRows(stream, ctx.dialect, scope, keep);
176
186
  const select = (join, filters) => `${left} IN (SELECT ${ctx.dialect.column(own.table, stream.id)} FROM ${own.from}${join} WHERE ${[...own.where, ...filters].join(" AND ")})`;
177
187
  const out = [];
178
188
  for (const role of roles) {
@@ -193,9 +203,9 @@ function sourceIn(stream, scope, left, roles, ctx) {
193
203
  * The roles tables the source joins through, each with the query of the rows
194
204
  * the user's own rows reference.
195
205
  */
196
- function roleTableRows(stream, dialect) {
206
+ function roleTableRows(stream, dialect, keep = false) {
197
207
  if (isFixed(stream.roles)) return [];
198
- const own = ownRows(stream, dialect);
208
+ const own = ownRows(stream, dialect, void 0, keep);
199
209
  return roleSources(stream.roles).flatMap((source) => {
200
210
  if (typeof source === "string" || "map" in source) return [];
201
211
  const keys = bare(source.through, dialect);
@@ -341,7 +351,7 @@ function customRoleIn(ctx, root, left, key, level) {
341
351
  continue;
342
352
  }
343
353
  if (isFixed(stream.roles)) continue;
344
- const own = ownRows(stream, dialect, root);
354
+ const own = ownRows(stream, dialect, root, keptKeys(rls?.suspension?.memberships).includes(key));
345
355
  const id = dialect.column(own.table, stream.id);
346
356
  for (const role of roleSources(stream.roles)) {
347
357
  const keyed = roleKeyOf(role, own.table, dialect);
@@ -518,7 +528,7 @@ const COMPARE = {
518
528
  * A row condition as alternatives of ANDed clauses over `table`. `undefined`
519
529
  * when part of it has no Sync Streams form, so the grant syncs nothing.
520
530
  */
521
- function conditionSql(condition, table, ctx) {
531
+ function conditionSql(condition, table, ctx, permission) {
522
532
  const column = (field) => SQL_IDENT.test(field) ? ctx.dialect.column(table, field) : void 0;
523
533
  switch (condition.op) {
524
534
  case "eq":
@@ -551,7 +561,7 @@ function conditionSql(condition, table, ctx) {
551
561
  }
552
562
  case "and":
553
563
  case "or": {
554
- const parts = every(condition.conditions.map((child) => conditionSql(child, table, ctx)));
564
+ const parts = every(condition.conditions.map((child) => conditionSql(child, table, ctx, permission)));
555
565
  if (parts === void 0) return;
556
566
  return condition.op === "and" ? product(parts) : parts.flat();
557
567
  }
@@ -560,9 +570,9 @@ function conditionSql(condition, table, ctx) {
560
570
  const scope = resolveScope(ctx.scopes, condition.scope);
561
571
  const left = column(condition.field);
562
572
  if (scope === void 0 || left === void 0) return;
563
- return membershipSql(scope, left, condition.roles, ctx);
573
+ return membershipSql(scope, left, condition.roles, ctx, permission);
564
574
  }
565
- case "sqlFunction": return conditionSql(condition.twin, table, ctx);
575
+ case "sqlFunction": return conditionSql(condition.twin, table, ctx, permission);
566
576
  case "related": return relatedSql(condition, table, ctx);
567
577
  case "not":
568
578
  case "contains":
@@ -632,7 +642,7 @@ function permittedSql(resource, key, ctx) {
632
642
  }))];
633
643
  const queries = /* @__PURE__ */ new Set();
634
644
  for (const { item, access } of reaches) {
635
- const where = typeof access === "string" || item.where === void 0 ? [[]] : conditionSql(item.where, table, ctx);
645
+ const where = typeof access === "string" || item.where === void 0 ? [[]] : conditionSql(item.where, table, ctx, item.grant.permission.key);
636
646
  if (typeof access === "string" || where === void 0) {
637
647
  ctx.gaps.push(`inherit() of ${key} leaves out ${item.label}: ${typeof access === "string" ? access : "its condition has no Sync Streams form"}`);
638
648
  continue;
@@ -642,11 +652,20 @@ function permittedSql(resource, key, ctx) {
642
652
  return queries.size === 0 && reaches.length > 0 ? void 0 : [...queries];
643
653
  }
644
654
  /**
655
+ * Whether `permission` keeps a suspended membership: its key is in
656
+ * `rls.suspension.memberships.keep`, so the stream drops the suspension
657
+ * filter.
658
+ */
659
+ function keepsSuspended(permission, ctx) {
660
+ return permission !== void 0 && keptKeys(ctx.config.rls?.suspension?.memberships).includes(permission);
661
+ }
662
+ /**
645
663
  * One `IN (SELECT …)` per role source of the membership table: the row's `left`
646
- * is an instance the user holds `roles` in.
664
+ * is an instance the user holds `roles` in. `keep` drops the suspension
665
+ * filter.
647
666
  */
648
- function membershipIn(table, idColumn, left, roles, ctx) {
649
- if (table.expiresAt !== void 0 || table.disabledAt !== void 0) return;
667
+ function membershipIn(table, idColumn, left, roles, ctx, keep = false) {
668
+ if (table.expiresAt !== void 0) return;
650
669
  const name = bare(table.table, ctx.dialect);
651
670
  const sources = Array.isArray(table.role) ? table.role : [table.role];
652
671
  const kinds = [...new Set(roles.flatMap((role) => ctx.kinds[role] ?? []))];
@@ -654,7 +673,11 @@ function membershipIn(table, idColumn, left, roles, ctx) {
654
673
  if (restricted && (table.via === void 0 || roles.length !== 1)) return;
655
674
  const via = table.via;
656
675
  if (restricted && typeof via === "object" && !kinds.includes(via.value)) return [];
657
- const filters = [`${ctx.dialect.column(name, table.user)} = ${ctx.dialect.user}`, ...restricted && typeof via === "string" ? [`${ctx.dialect.column(name, via)} IN (${kinds.map((kind) => ctx.dialect.literal(kind)).join(", ")})`] : []];
676
+ const filters = [
677
+ `${ctx.dialect.column(name, table.user)} = ${ctx.dialect.user}`,
678
+ ...restricted && typeof via === "string" ? [`${ctx.dialect.column(name, via)} IN (${kinds.map((kind) => ctx.dialect.literal(kind)).join(", ")})`] : [],
679
+ ...!keep && table.disabledAt !== void 0 ? [`${ctx.dialect.column(name, table.disabledAt)} IS NULL`] : []
680
+ ];
658
681
  const list = roles.map((role) => ctx.dialect.literal(role)).join(", ");
659
682
  return sources.map((source) => {
660
683
  if (typeof source === "string") return [`${left} IN (SELECT ${ctx.dialect.column(name, idColumn)} FROM ${fromTable(table.table, ctx.dialect)} WHERE ${[...filters, `${ctx.dialect.column(name, source)} IN (${list})`].join(" AND ")})`];
@@ -673,11 +696,12 @@ function membershipIn(table, idColumn, left, roles, ctx) {
673
696
  * The membership filter of scope `scope`: its `rls.memberships` table, else the
674
697
  * union of its membership sources.
675
698
  */
676
- function membershipSql(scope, left, roles, ctx) {
699
+ function membershipSql(scope, left, roles, ctx, permission) {
700
+ const keep = keepsSuspended(permission, ctx);
677
701
  const table = scopeMembershipTable(ctx.config.rls?.memberships, ctx.scopes, scope);
678
702
  if (table !== void 0) {
679
703
  const idColumn = scopeColumn(table, ctx.scopes, scope);
680
- return idColumn === void 0 ? void 0 : membershipIn(table, idColumn, left, roles, ctx);
704
+ return idColumn === void 0 ? void 0 : membershipIn(table, idColumn, left, roles, ctx, keep);
681
705
  }
682
706
  const sources = scopeSourcesOf(ctx.sources, scope);
683
707
  if (sources.length === 0) return;
@@ -687,7 +711,7 @@ function membershipSql(scope, left, roles, ctx) {
687
711
  ctx.gaps.push(stream);
688
712
  return [];
689
713
  }
690
- return sourceIn(stream, scope, left, roles, ctx);
714
+ return sourceIn(stream, scope, left, roles, ctx, keep);
691
715
  });
692
716
  }
693
717
  /**
@@ -703,14 +727,14 @@ function accessSql(item, table, ctx) {
703
727
  if (access.scope === "global") return `global role ${access.role} is not a row filter`;
704
728
  const field = ctx.policy.scopes.find((scope) => scope.name === access.scope)?.key;
705
729
  if (field === void 0 || !SQL_IDENT.test(field)) return `scope ${access.scope} has no row key`;
706
- return membershipSql(access.scope, ctx.dialect.column(table, field), [access.role], ctx) ?? `no rls.memberships table without an expiry or disabledAt and with the kinds of ${access.role}, and no membership source, for scope ${access.scope}`;
730
+ return membershipSql(access.scope, ctx.dialect.column(table, field), [access.role], ctx, item.grant.permission.key) ?? `no rls.memberships table without an expiry and with the kinds of ${access.role}, and no membership source, for scope ${access.scope}`;
707
731
  }
708
732
  case "resource": {
709
733
  const holder = ctx.config.rls?.memberships?.resource?.[access.resource];
710
734
  const node = ctx.policy.resources.get(item.grant.permission.resource);
711
735
  const field = node?.name === access.resource ? node.id : node?.parent?.resource === access.resource ? node.parent.field : void 0;
712
736
  if (holder === void 0 || field === void 0 || !SQL_IDENT.test(field)) return `no rls.memberships.resource table for ${access.resource} on the row`;
713
- return membershipIn(holder, holder.id ?? "id", ctx.dialect.column(table, field), [access.role], ctx) ?? `the ${access.resource} memberships table has an expiry or disabledAt, or lacks via`;
737
+ return membershipIn(holder, holder.id ?? "id", ctx.dialect.column(table, field), [access.role], ctx, keepsSuspended(item.grant.permission.key, ctx)) ?? `the ${access.resource} memberships table has an expiry, or lacks via`;
714
738
  }
715
739
  case "actor": return "actor grantees are not in the sync token";
716
740
  default: return access;
@@ -740,12 +764,13 @@ function userTenantIds(ctx) {
740
764
  if (tenants !== void 0) {
741
765
  const column = scopeColumn(tenants, ctx.scopes, root);
742
766
  const members = bare(tenants.table, dialect);
743
- return column === void 0 ? [] : [`SELECT ${dialect.column(members, column)} FROM ${fromTable(tenants.table, dialect)} WHERE ${dialect.column(members, tenants.user)} = ${dialect.user}`];
767
+ const active = tenants.disabledAt !== void 0 && keptKeys(ctx.config.rls?.suspension?.memberships).length === 0 ? ` AND ${dialect.column(members, tenants.disabledAt)} IS NULL` : "";
768
+ return column === void 0 ? [] : [`SELECT ${dialect.column(members, column)} FROM ${fromTable(tenants.table, dialect)} WHERE ${dialect.column(members, tenants.user)} = ${dialect.user}${active}`];
744
769
  }
745
770
  return scopeSourcesOf(ctx.sources, root).flatMap((source) => {
746
771
  const stream = streamOf(source);
747
772
  if (typeof stream === "string") return [];
748
- const rows = ownRows(stream, dialect, root);
773
+ const rows = ownRows(stream, dialect, root, keptKeys(ctx.config.rls?.suspension?.memberships).length > 0);
749
774
  return [`SELECT ${dialect.column(rows.table, stream.id)} FROM ${rows.from} WHERE ${rows.where.join(" AND ")}`];
750
775
  });
751
776
  }
@@ -781,9 +806,10 @@ function subjectStreams(ctx) {
781
806
  entry.queries.add(query);
782
807
  tables.set(table, entry);
783
808
  };
784
- const own = (table, user, role) => {
809
+ const own = (table, user, role, disabledAt) => {
785
810
  const name = bare(table, dialect);
786
- const mine = `${dialect.column(name, user)} = ${dialect.user}`;
811
+ const active = disabledAt !== void 0 && keptKeys(rls?.suspension?.memberships).length === 0 ? ` AND ${dialect.column(name, disabledAt)} IS NULL` : "";
812
+ const mine = `${dialect.column(name, user)} = ${dialect.user}${active}`;
787
813
  add(name, `SELECT * FROM ${fromTable(table, dialect)} WHERE ${mine}`, user);
788
814
  for (const through of roleThroughs(role)) {
789
815
  const keys = bare(through.through, dialect);
@@ -794,15 +820,16 @@ function subjectStreams(ctx) {
794
820
  }
795
821
  };
796
822
  const memberships = rls?.memberships;
797
- for (const table of [...Object.values(memberships?.scopes ?? {}), ...Object.values(memberships?.resource ?? {})]) own(table.table, table.user, table.role);
823
+ for (const table of [...Object.values(memberships?.scopes ?? {}), ...Object.values(memberships?.resource ?? {})]) own(table.table, table.user, table.role, table.disabledAt);
798
824
  for (const stream of unmappedStreams(ctx)) {
799
- const rows = ownRows(stream, dialect);
825
+ const keep = keptKeys(rls?.suspension?.memberships).length > 0;
826
+ const rows = ownRows(stream, dialect, void 0, keep);
800
827
  add(rows.table, typeof stream.user === "string" ? `SELECT * FROM ${rows.from} WHERE ${rows.where.join(" AND ")}` : `SELECT ${rows.table}.* FROM ${rows.from} WHERE ${rows.where.join(" AND ")}`, typeof stream.user === "string" ? stream.user : void 0);
801
828
  if (typeof stream.user !== "string") {
802
829
  const users = bare(stream.user.through, dialect);
803
830
  add(users, `SELECT * FROM ${fromTable(stream.user.through, dialect)} WHERE ${dialect.column(users, stream.user.column)} = ${dialect.user}`, stream.user.column);
804
831
  }
805
- for (const keys of roleTableRows(stream, dialect)) add(keys.table, keys.query);
832
+ for (const keys of roleTableRows(stream, dialect, keep)) add(keys.table, keys.query);
806
833
  }
807
834
  const schema = rls?.schema ?? "permdock";
808
835
  const database = rls?.authorize === "database";
@@ -931,7 +958,7 @@ function planWith(policy, config, dialect) {
931
958
  const queries = /* @__PURE__ */ new Set();
932
959
  const compile = (label, gap, item, reach) => {
933
960
  const access = unsupported(item) ?? reach();
934
- const where = typeof access === "string" || item.where === void 0 ? [[]] : conditionSql(item.where, table, ctx);
961
+ const where = typeof access === "string" || item.where === void 0 ? [[]] : conditionSql(item.where, table, ctx, item.grant.permission.key);
935
962
  const gaps = [...new Set(ctx.gaps.splice(0))];
936
963
  for (const reason of gaps) warnings.push(`${label} under-syncs: ${reason}`);
937
964
  if (typeof access === "string") warnings.push(`${label} does not sync: ${access}`);
@@ -1007,6 +1034,53 @@ function planWith(policy, config, dialect) {
1007
1034
  };
1008
1035
  }
1009
1036
  //#endregion
1037
+ //#region src/cli/powersync/verify-followed.ts
1038
+ /** Check a non-resource row against its own-user rule or readable parents. */
1039
+ async function verifyFollowedRow(input) {
1040
+ const { fixture, config, policy, mismatches } = input;
1041
+ const label = `${fixture.table} for ${fixture.subject.id}`;
1042
+ const spec = config.powersync?.tables?.[fixture.table];
1043
+ const stream = input.plan.tables.find((item) => item.table === bare(fixture.table, {
1044
+ ...POSTGRES,
1045
+ names: {
1046
+ renamed: input.plan.devices,
1047
+ seen: /* @__PURE__ */ new Set()
1048
+ }
1049
+ }));
1050
+ if (spec === void 0 || stream === void 0 || !isRecord(fixture.row)) {
1051
+ mismatches.push(`${label}: no configured table stream or invalid row`);
1052
+ return;
1053
+ }
1054
+ const row = fixture.row;
1055
+ let granted = spec.user !== void 0 && row[spec.user] === fixture.subject.id;
1056
+ for (const follow of spec.follow ?? []) {
1057
+ if (!Object.entries(follow.match ?? {}).every(([key, value]) => row[key] === value)) continue;
1058
+ const permission = findPermission(policy.permissions, `${follow.resource}.${config.powersync?.action ?? "read"}`);
1059
+ const resource = policy.resources.get(follow.resource);
1060
+ const parentId = row[follow.field];
1061
+ if (permission === void 0 || resource?.id === void 0 || typeof parentId !== "string" && typeof parentId !== "number") continue;
1062
+ const parents = await input.query(`select * from ${quoteQualified(tableNameOf(config.rls?.tables?.[follow.resource], follow.resource))} where ${quoteIdent(resource.id)}::text = $1::text`, [String(parentId)]);
1063
+ for (const parent of parents) {
1064
+ const root = policy.scopes.find((scope) => scope.within === void 0)?.key;
1065
+ const tenant = root !== void 0 && isRecord(parent) ? parent[root] : void 0;
1066
+ const permdock = await getPermDock(policy, fixtureSubject({
1067
+ ...fixture.subject,
1068
+ ...typeof tenant === "string" ? { tenant } : {}
1069
+ }), { customRoles: memoryCustomRoleSource(input.customRoles) });
1070
+ granted ||= canFixture(permdock, permission, parent);
1071
+ }
1072
+ }
1073
+ if (fixture.expected === "granted" !== granted) mismatches.push(`${label}: expected ${fixture.expected}, but parent access is ${granted ? "granted" : "denied"}`);
1074
+ const holds = await input.synced({
1075
+ ...stream,
1076
+ resource: fixture.table,
1077
+ id: "id"
1078
+ }, fixture);
1079
+ if (holds === void 0) mismatches.push(`${label}: fixture row needs id`);
1080
+ else if (holds && !granted) mismatches.push(`${label}: stream ${stream.name} syncs a row the policy denies`);
1081
+ else if (!holds && granted) mismatches.push(`${label}: granted, but stream ${stream.name} does not sync the row`);
1082
+ }
1083
+ //#endregion
1010
1084
  //#region src/cli/powersync/verify-gaps.ts
1011
1085
  function heldRoles(subject) {
1012
1086
  return /* @__PURE__ */ new Set([...subject.roles ?? [], ...(subject.memberships ?? []).flatMap((membership) => membership.roles)]);
@@ -1161,6 +1235,19 @@ async function verifyStreams(input) {
1161
1235
  for (const stream of [...plan.subject, ...plan.tables]) if (stream.user !== void 0 && await leaks(query, stream, stream.user, fixture)) mismatches.push(`stream ${stream.name} syncs another user's ${stream.table} rows to ${fixture.subject.id}`);
1162
1236
  }
1163
1237
  for (const fixture of input.fixtures) {
1238
+ if ("table" in fixture) {
1239
+ await verifyFollowedRow({
1240
+ policy: input.policy,
1241
+ config: input.config,
1242
+ customRoles: input.customRoles,
1243
+ plan,
1244
+ fixture,
1245
+ query,
1246
+ mismatches,
1247
+ synced: (stream, row) => synced(query, stream, row)
1248
+ });
1249
+ continue;
1250
+ }
1164
1251
  const permission = findPermission(input.policy.permissions, fixture.action);
1165
1252
  const stream = plan.streams.find((item) => permission !== void 0 && item.resource === permission.resource && permission.action === (input.config.powersync?.action ?? "read"));
1166
1253
  if (permission === void 0 || stream === void 0) continue;
@@ -1241,7 +1328,7 @@ async function runPowerSync(input) {
1241
1328
  const notes = [...warnings];
1242
1329
  if (input.db !== void 0) try {
1243
1330
  const fixturesPath = input.fixtures ?? input.config.rls?.fixtures ?? "rls.fixtures.json";
1244
- const { fixtures, customRoles } = await loadFixtures(input.cwd, fixturesPath);
1331
+ const { fixtures, customRoles } = await loadFixtures(input.cwd, fixturesPath, true);
1245
1332
  const database = await verifyStreams({
1246
1333
  policy,
1247
1334
  config: input.config,
@@ -0,0 +1 @@
1
+ export { n as runPowerSync } from "./powersync-DVbskWD9.js";
@@ -0,0 +1 @@
1
+ export { n as runCloudPull } from "./pull-Er4aBHlL.js";
@@ -7,7 +7,7 @@ import { o as formatCatalogJson } from "./catalog-doc-DjMKzJp9.js";
7
7
  import { t as catalogPath } from "./catalog-path-CnI0XTFK.js";
8
8
  import { n as parseSupabaseManifest } from "./manifest-BXTbHNIV.js";
9
9
  import { t as parseCatalog } from "./parse-1_ZheTW5.js";
10
- import { a as nonEmptyOr } from "./output-CtyBwTNB.js";
10
+ import { a as nonEmptyOr } from "./output-3iB3_Hfg.js";
11
11
  import { t as POLICY_NOT_PUBLISHED } from "./problem-types-DSsRfONQ.js";
12
12
  import { n as httpRequest, t as authFailure } from "./http-DOL33cO0.js";
13
13
  import { l as cloudUrl, n as cliRuntime, o as readAuth } from "./auth--yPyZNgr.js";
@@ -115,13 +115,13 @@ type PowerSyncSourceOptions<Q extends string> = {
115
115
  readonly read?: undefined;
116
116
  };
117
117
  /**
118
- * A `localSnapshot` source over a PowerSync database: `get` runs every query
119
- * with `db.getAll`, and a `db.watch` per query reports changes. Without
120
- * `queries` and `read`, it reads the tables `manifest.rows` names: the
121
- * memberships of every source with null roles dropped, the global roles and the
122
- * custom roles with their levels. A watch result with unchanged content
123
- * re-renders nothing (`connectSource`). The source takes the database as an
124
- * argument; `permdock/react-native` never imports `@powersync/*`.
118
+ * A `localSnapshot` source over a PowerSync database: `get` initially runs
119
+ * every query with `db.getAll`; watch-triggered reads refresh only fired
120
+ * queries. Without `queries` and `read`, it reads the tables `manifest.rows`
121
+ * names: the memberships of every source with null roles dropped, the global
122
+ * roles and the custom roles with their levels. A watch result with unchanged
123
+ * content re-renders nothing (`connectSource`). The source takes the database
124
+ * as an argument; `permdock/react-native` never imports `@powersync/*`.
125
125
  */
126
126
  export declare function powerSyncSource<Q extends string>(db: PowerSyncWatchable, options: PowerSyncSourceOptions<Q>): SnapshotSource;
127
127
  //#endregion
@@ -535,32 +535,73 @@ function planOf(options) {
535
535
  };
536
536
  }
537
537
  /**
538
- * A `localSnapshot` source over a PowerSync database: `get` runs every query
539
- * with `db.getAll`, and a `db.watch` per query reports changes. Without
540
- * `queries` and `read`, it reads the tables `manifest.rows` names: the
541
- * memberships of every source with null roles dropped, the global roles and the
542
- * custom roles with their levels. A watch result with unchanged content
543
- * re-renders nothing (`connectSource`). The source takes the database as an
544
- * argument; `permdock/react-native` never imports `@powersync/*`.
538
+ * A `localSnapshot` source over a PowerSync database: `get` initially runs
539
+ * every query with `db.getAll`; watch-triggered reads refresh only fired
540
+ * queries. Without `queries` and `read`, it reads the tables `manifest.rows`
541
+ * names: the memberships of every source with null roles dropped, the global
542
+ * roles and the custom roles with their levels. A watch result with unchanged
543
+ * content re-renders nothing (`connectSource`). The source takes the database
544
+ * as an argument; `permdock/react-native` never imports `@powersync/*`.
545
545
  */
546
546
  function powerSyncSource(db, options) {
547
547
  const plan = planOf(options);
548
- return localSnapshot({
549
- manifest: options.manifest,
550
- read: async () => {
551
- const results = await Promise.all(Object.entries(plan.queries).map(async ([name, query]) => {
552
- return [name, await db.getAll(query.sql, [...query.parameters ?? []])];
553
- }));
554
- return plan.read(Object.fromEntries(results));
548
+ const queries = Object.entries(plan.queries);
549
+ const rows = /* @__PURE__ */ new Map();
550
+ const dirty = /* @__PURE__ */ new Set();
551
+ const listeners = /* @__PURE__ */ new Set();
552
+ let scheduled = false;
553
+ let pending;
554
+ let snapshot;
555
+ let content;
556
+ const invalidate = (name) => {
557
+ dirty.add(name);
558
+ if (scheduled) return;
559
+ scheduled = true;
560
+ queueMicrotask(() => {
561
+ scheduled = false;
562
+ for (const listener of listeners) listener();
563
+ });
564
+ };
565
+ const read = async () => {
566
+ if (dirty.size === 0) for (const [name] of queries) dirty.add(name);
567
+ do {
568
+ const selected = queries.filter(([name]) => dirty.has(name) || !rows.has(name));
569
+ for (const [name] of selected) dirty.delete(name);
570
+ try {
571
+ const results = await Promise.all(selected.map(async ([name, query]) => [name, await db.getAll(query.sql, [...query.parameters ?? []])]));
572
+ for (const [name, result] of results) rows.set(name, result);
573
+ } catch (error) {
574
+ for (const [name] of selected) dirty.add(name);
575
+ throw error;
576
+ }
577
+ } while (dirty.size > 0);
578
+ const data = plan.read(Object.fromEntries(rows));
579
+ const next = JSON.stringify([data, data.principal?.memberships?.map((membership) => isMembershipExpired(membership))]);
580
+ if (snapshot === void 0 || next !== content) {
581
+ snapshot = buildLocalSnapshot(options.manifest, data);
582
+ content = next;
583
+ }
584
+ return snapshot;
585
+ };
586
+ return {
587
+ get: () => {
588
+ pending ??= read().finally(() => {
589
+ pending = void 0;
590
+ });
591
+ return pending;
555
592
  },
556
593
  subscribe: (listener) => {
594
+ listeners.add(listener);
557
595
  const controller = new AbortController();
558
- for (const query of Object.values(plan.queries)) db.watch(query.sql, [...query.parameters ?? []], { onResult: listener }, { signal: controller.signal });
596
+ for (const [name, query] of queries) db.watch(query.sql, [...query.parameters ?? []], { onResult: () => {
597
+ invalidate(name);
598
+ } }, { signal: controller.signal });
559
599
  return () => {
600
+ listeners.delete(listener);
560
601
  controller.abort();
561
602
  };
562
603
  }
563
- });
604
+ };
564
605
  }
565
606
  //#endregion
566
607
  //#region src/react-native/storage.ts