permdock 0.13.0 → 0.15.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-oks22j9J.js → adoption-scan-DLel53Oa.js} +1 -1
  3. package/dist/agent/index.js +1 -1
  4. package/dist/ai-sdk/index.js +2 -2
  5. package/dist/{arazzo-BzVfXbhf.js → arazzo-CHK9NT6u.js} +1 -1
  6. package/dist/{arazzo-CBBIhC40.js → arazzo-CL8CZnpL.js} +1 -1
  7. package/dist/better-supabase/index.d.ts +20 -22
  8. package/dist/better-supabase/index.js +4 -16
  9. package/dist/better-supabase-Dirf_8Ec.js +72 -0
  10. package/dist/{casbin-C8r5trwy.js → casbin-DRFwgVIP.js} +1 -1
  11. package/dist/{casbin-BunKq48W.js → casbin-iAEXjmca.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-C_Mau1q2.js → cloud-eOtmuonF.js} +1 -1
  16. package/dist/{codemod-BqYPToUJ.js → codemod-D6NYu4AK.js} +2 -2
  17. package/dist/{codemod-CpMCbp0G.js → codemod-KEDj_3zl.js} +1 -1
  18. package/dist/{collect-D9Tt0Bcx.js → collect-DnXL4B8c.js} +1 -1
  19. package/dist/{completion-Cq4fbT2S.js → completion-DCza8aQY.js} +1 -1
  20. package/dist/{config-0xxCmoD8.js → config-BAM3a3z8.js} +1 -1
  21. package/dist/{config-BR23fL8m.js → config-CcLlvEYW.js} +4 -1
  22. package/dist/{config-report-C1-r59VY.js → config-report-VZcQ0ZpS.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-CDfzrMQB.js → doctor-CMqPKuN_.js} +16 -49
  27. package/dist/{doctor-Dea6IqRw.js → doctor-gIO9WTYo.js} +2 -2
  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-PP7uH8rC.js → import-DK_J8tVE.js} +1 -1
  31. package/dist/{init-n1gkKHvc.js → init-Buibrl9h.js} +3 -3
  32. package/dist/{init-BuUt4bLJ.js → init-jLSyPMM7.js} +1 -1
  33. package/dist/{kernel-qv4cr9w8.js → kernel-BD8QuMnv.js} +9 -2
  34. package/dist/{login-YsrgkAQo.js → login-CB55O4Gv.js} +1 -1
  35. package/dist/{login-DyQNWJok.js → login-VVpkkund.js} +3 -3
  36. package/dist/{manifest-Cl6yHiVK.js → manifest-CbEyvPN2.js} +33 -33
  37. package/dist/mcp/index.d.ts +21 -3
  38. package/dist/mcp/index.js +87 -16
  39. package/dist/next/plugin.js +1 -1
  40. package/dist/openai/index.js +2 -2
  41. package/dist/{openapi-D8JQtO8k.js → openapi-CleDZYVf.js} +1 -1
  42. package/dist/{openapi-BFyBIe9J.js → openapi-eHZ5hwUJ.js} +2 -2
  43. package/dist/{output-DpCUAST3.js → output-BVZpQDad.js} +1 -1
  44. package/dist/{powersync-Bm3ZjHwD.js → powersync-CFWILHjb.js} +2 -2
  45. package/dist/powersync-CnGzxvEl.js +1 -0
  46. package/dist/{powersync-BTLk8GSl.js → powersync-uqYWxTjq.js} +126 -39
  47. package/dist/{pull-iVPQZsnI.js → pull-BBmCxnqX.js} +1 -1
  48. package/dist/pull-D6dloyrc.js +1 -0
  49. package/dist/react-native/index.d.ts +21 -8
  50. package/dist/react-native/index.js +103 -17
  51. package/dist/{rls-BItB4vkC.js → rls-BwKXdVEW.js} +5 -5
  52. package/dist/{rls-C8nxX0RV.js → rls-yK2nZUcm.js} +1 -1
  53. package/dist/{run-CjFdAzqE.js → run-D5WIJfs7.js} +3 -3
  54. package/dist/run-DlQYPtPv.js +1 -0
  55. package/dist/{scan-COIQvTMg.js → scan-CzdMtHnq.js} +1 -1
  56. package/dist/{schema-CeQZY6bO.js → schema-BDGEVO4g.js} +1 -1
  57. package/dist/{skills-CfkYP7GG.js → skills-BIsm1n26.js} +1 -1
  58. package/dist/{skills-5XWpNzCi.js → skills-BZxPQ_09.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-PRhfO-bL.js → supautils-CGk4YpvC.js} +16 -5
  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-D3KJyaNS.js +1 -0
  69. package/dist/{usage-BfzSfOpD.js → usage-DBNrpwoT.js} +1 -1
  70. package/dist/{usage-hMYVFsN5.js → usage-e6M1Mls1.js} +2 -2
  71. package/dist/{verify-CmTiQ-yM.js → verify-CgIpJRcG.js} +3 -3
  72. package/dist/watch-CuPz0D2M.js +1 -0
  73. package/dist/{watch-Bpci3hBY.js → watch-DAkjm75Y.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-CObULVRk.js +0 -1
  78. package/dist/pull-3OQCgxUf.js +0 -1
  79. package/dist/run-BqW8S5TM.js +0 -1
  80. package/dist/supabase-hook-CWvEpdCh.js +0 -1
  81. package/dist/usage-BDiTP763.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
package/dist/mcp/index.js CHANGED
@@ -12,7 +12,7 @@ import { l as problemResponse, o as methodNotAllowed, r as bearerChallenge, u as
12
12
  import { t as clientNameOf } from "../clients-DYr3U86j.js";
13
13
  import { r as mapClaimsToSubject } from "../map-claims-C-nv08_n.js";
14
14
  import { r as instanceOptions } from "../build-instance-BWXqwQYU.js";
15
- import { i as refusalText, n as createAgentKernel } from "../kernel-qv4cr9w8.js";
15
+ import { i as refusalText, n as createAgentKernel } from "../kernel-BD8QuMnv.js";
16
16
  import { t as boundedMap } from "../lru-8q4WZjBV.js";
17
17
  import { t as delegationFrom } from "../delegation-input-FTxMK3ji.js";
18
18
  import { n as deniedToolRefusal, r as toolErrorResult, t as approvalToolRefusal } from "../tool-refusal-Qpjs_W6M.js";
@@ -211,6 +211,27 @@ function stepUpRefusal(stepUpAt, decision, permission, data, context, capabiliti
211
211
  });
212
212
  }
213
213
  //#endregion
214
+ //#region src/mcp/report.ts
215
+ /**
216
+ * Sends `error` to the `onError` sink. The check still refuses; a sink that
217
+ * throws is ignored (invariant 1).
218
+ */
219
+ function report(sink, error, context) {
220
+ if (sink === void 0) return;
221
+ try {
222
+ sink(error, context);
223
+ } catch {}
224
+ }
225
+ /** The call's tenant for an error report; `null` when it has none or throws. */
226
+ async function reportedTenant(tenant, authInfo) {
227
+ if (typeof tenant !== "function") return tenant ?? null;
228
+ try {
229
+ return await tenant(authInfo ?? {}) ?? null;
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+ //#endregion
214
235
  //#region src/mcp/call-check.ts
215
236
  function reachOf(policy, permission, declared) {
216
237
  return declared === void 0 ? {
@@ -252,10 +273,11 @@ function credentialRefusal(checks, permission, reach, authInfo) {
252
273
  * The completion re-check of a long-running tool: the call must still be
253
274
  * allowed, or approved with the same token.
254
275
  */
255
- async function recheck(checks, binding, args, call, permission, approved, tool) {
276
+ async function recheck(checks, binding, args, call, permission, approved, tool, onError) {
256
277
  const rechecked = await checks.kernel.check(binding, args, call, compact({
257
278
  simulate: true,
258
- tool
279
+ tool,
280
+ onError
259
281
  }));
260
282
  if (!rechecked.ok) return {
261
283
  ok: false,
@@ -271,7 +293,32 @@ async function recheck(checks, binding, args, call, permission, approved, tool)
271
293
  } : plainRefusal(permission, `Denied: ${permission.key} was revoked before the call completed.`, {})
272
294
  };
273
295
  }
274
- async function decideCall(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool) {
296
+ /**
297
+ * Sends the errors a check collected to the `onError` sink, after the check, so
298
+ * the tenant resolves only when there is something to report.
299
+ */
300
+ async function flush(checks, authInfo, errors, permission, name) {
301
+ if (errors.length === 0 || checks.onError === void 0) return;
302
+ const tenant = await reportedTenant(checks.options.tenant, authInfo);
303
+ for (const error of errors) report(checks.onError, error, {
304
+ permission,
305
+ name,
306
+ tenant
307
+ });
308
+ }
309
+ async function decideCall(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool, name = tool ?? permission.key) {
310
+ const authInfo = context.http?.authInfo;
311
+ const errors = [];
312
+ const onError = (error) => {
313
+ errors.push(error);
314
+ };
315
+ try {
316
+ return await decided(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool, onError);
317
+ } finally {
318
+ await flush(checks, authInfo, errors, permission.key, name);
319
+ }
320
+ }
321
+ async function decided(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool, onError) {
275
322
  const authInfo = context.http?.authInfo;
276
323
  const refused = credentialRefusal(checks, permission, reach, authInfo);
277
324
  if (refused !== void 0) return {
@@ -283,10 +330,11 @@ async function decideCall(checks, permission, reach, args, load, context, handle
283
330
  data: load === void 0 ? void 0 : () => load()
284
331
  });
285
332
  const call = compact({ authInfo });
286
- if (completion !== void 0) return recheck(checks, binding, args, call, permission, completion.approved, tool);
333
+ if (completion !== void 0) return recheck(checks, binding, args, call, permission, completion.approved, tool, onError);
287
334
  const checked = await checks.kernel.check(binding, args, call, compact({
288
335
  resumeToken: approvalTokenOf(context),
289
- tool
336
+ tool,
337
+ onError
290
338
  }));
291
339
  if (!checked.ok) return {
292
340
  ok: false,
@@ -316,7 +364,8 @@ async function decideCall(checks, permission, reach, args, load, context, handle
316
364
  inputRequired: await approvalInput(checks.options, decision, handlerContext, context, capabilities)
317
365
  })
318
366
  };
319
- } catch {
367
+ } catch (error) {
368
+ onError(error);
320
369
  return {
321
370
  ok: false,
322
371
  refusal: failedRefusal(permission, "failed")
@@ -326,8 +375,8 @@ async function decideCall(checks, permission, reach, args, load, context, handle
326
375
  }
327
376
  }
328
377
  /** `decideCall` with the refusal text replaced by `deniedText`'s. */
329
- async function checkCall(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool) {
330
- const checked = await decideCall(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool);
378
+ async function checkCall(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool, name = tool ?? permission.key) {
379
+ const checked = await decideCall(checks, permission, reach, args, load, context, handlerContext, capabilities, completion, tool, name);
331
380
  const { deniedText } = checks.options;
332
381
  if (checked.ok || deniedText === void 0) return checked;
333
382
  const { refusal } = checked;
@@ -366,7 +415,15 @@ async function visible(checks, context, permissionOf, keys, declaredOf = () => v
366
415
  let permdock;
367
416
  try {
368
417
  permdock = await checks.kernel.instance(compact({ authInfo }));
369
- } catch {
418
+ } catch (error) {
419
+ if (checks.onError !== void 0) {
420
+ const tenant = await reportedTenant(checks.options.tenant, authInfo);
421
+ for (const entry of guarded) report(checks.onError, error, {
422
+ permission: entry.permission.key,
423
+ name: entry.key,
424
+ tenant
425
+ });
426
+ }
370
427
  return shown;
371
428
  }
372
429
  for (const entry of guarded) if ((!tools || actorMayCallTool(permdock.subject.actor, entry.key)) && permdock.mayUse(entry.permission)) shown.add(entry.key);
@@ -381,12 +438,13 @@ function guard(checks, permission, load, handler, dataArgs, onRefusal, extra = {
381
438
  const loader = load === void 0 ? void 0 : () => load(...args);
382
439
  const reach = extra.reach ?? reachOf(checks.policy, permission, void 0);
383
440
  const tool = extra.tool?.();
384
- const checked = await checkCall(checks, permission, reach, args, loader, context, raw, extra.capabilities, void 0, tool);
441
+ const name = extra.name?.() ?? tool ?? permission.key;
442
+ const checked = await checkCall(checks, permission, reach, args, loader, context, raw, extra.capabilities, void 0, tool, name);
385
443
  let result;
386
444
  if (checked.ok) {
387
445
  result = await handler(...params);
388
446
  if (extra.longRunning === true) {
389
- const again = await checkCall(checks, permission, reach, args, loader, context, raw, extra.capabilities, { approved: checked.approved }, tool);
447
+ const again = await checkCall(checks, permission, reach, args, loader, context, raw, extra.capabilities, { approved: checked.approved }, tool, name);
390
448
  if (!again.ok) result = onRefusal(again.refusal);
391
449
  }
392
450
  } else result = onRefusal(checked.refusal);
@@ -496,7 +554,8 @@ function protectServer(checks, server, enforcement) {
496
554
  longRunning,
497
555
  capabilities,
498
556
  reach,
499
- tool
557
+ tool,
558
+ name: tool
500
559
  });
501
560
  const originalTool = server.registerTool.bind(server);
502
561
  const toolPermissionOf = (name, config) => {
@@ -555,7 +614,10 @@ function protectServer(checks, server, enforcement) {
555
614
  const permission = requirePermission("prompt", name, config);
556
615
  const { permission: _permission, data, scopeChallenge, ...passthrough } = config;
557
616
  const load = data;
558
- const wrap = (callback) => guard(checks, permission, load, callback, (params) => params.length >= 2 ? [params[0]] : [void 0], throwRefusal, { capabilities });
617
+ const wrap = (callback) => guard(checks, permission, load, callback, (params) => params.length >= 2 ? [params[0]] : [void 0], throwRefusal, {
618
+ capabilities,
619
+ name: () => name
620
+ });
559
621
  const registered = originalPrompt(name, {
560
622
  ...passthrough,
561
623
  scopeChallenge: challengeFor(scopeChallenge, scopesReaching(checks.policy, permission), challengeScope(checks.policy, permission))
@@ -578,7 +640,10 @@ function protectServer(checks, server, enforcement) {
578
640
  const wrap = (callback) => (...params) => {
579
641
  const permission = permissionNow();
580
642
  if (permission === void 0) return Promise.reject(/* @__PURE__ */ new Error(`Denied: resource ${name} is linked to no guarded tool.`));
581
- return guard(checks, permission, load, callback, (args) => args.slice(0, -1), throwRefusal, { capabilities })(...params);
643
+ return guard(checks, permission, load, callback, (args) => args.slice(0, -1), throwRefusal, {
644
+ capabilities,
645
+ name: () => name
646
+ })(...params);
582
647
  };
583
648
  const registered = originalResource(name, uriOrTemplate, {
584
649
  ...passthrough,
@@ -680,7 +745,13 @@ function createPermDock(policy, given) {
680
745
  ...instanceOptions(options)
681
746
  }))
682
747
  };
683
- return { protectServer: ((server, enforcement) => protectServer(checks, server, enforcement)) };
748
+ return { protectServer: ((server, enforcement, onError) => {
749
+ const sink = typeof enforcement === "function" ? enforcement : onError;
750
+ return protectServer(sink === void 0 ? checks : {
751
+ ...checks,
752
+ onError: sink
753
+ }, server, typeof enforcement === "function" ? void 0 : enforcement);
754
+ }) };
684
755
  }
685
756
  //#endregion
686
757
  //#region src/mcp/resource-guard.ts
@@ -1,5 +1,5 @@
1
1
  import { a as describeError } from "../errors-Vpu6rKHl.js";
2
- import { n as createCollectScheduler, r as report, t as collectOnce } from "../watch-Bpci3hBY.js";
2
+ import { n as createCollectScheduler, r as report, t as collectOnce } from "../watch-DAkjm75Y.js";
3
3
  //#region src/cli/plugin.ts
4
4
  const PHASE_BUILD = "phase-production-build";
5
5
  const PHASE_DEV = "phase-development-server";
@@ -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;
@@ -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-CeQZY6bO.js";
10
+ import { n as validateOverlay, t as validateOpenapi } from "./schema-BDGEVO4g.js";
11
11
  import { existsSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
13
  //#region src/cli/openapi/openapi.ts
@@ -80,7 +80,7 @@ const emit = defineLeaf({
80
80
  }
81
81
  },
82
82
  async run(ctx, { flags }) {
83
- return (await import("./openapi-D8JQtO8k.js")).runOpenapi({
83
+ return (await import("./openapi-CleDZYVf.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-PP7uH8rC.js")).runOpenapiImport({
125
+ return (await import("./import-DK_J8tVE.js")).runOpenapiImport({
126
126
  cwd: ctx.cwd,
127
127
  doc,
128
128
  out: fileOut("openapi import", flags.out),
@@ -1,5 +1,5 @@
1
1
  import { s as cliVersion } from "./version-DMQ0dnN5.js";
2
- import { t as DOCS_BASE } from "./manifest-Cl6yHiVK.js";
2
+ import { t as DOCS_BASE } from "./manifest-CbEyvPN2.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-CObULVRk.js");
17
+ const { runPowerSync } = await import("./powersync-CnGzxvEl.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-CObULVRk.js");
48
+ const { runPowerSync } = await import("./powersync-CnGzxvEl.js");
49
49
  return runPowerSync({
50
50
  cwd: ctx.cwd,
51
51
  config: ctx.config,
@@ -0,0 +1 @@
1
+ export { n as runPowerSync } from "./powersync-uqYWxTjq.js";
@@ -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-CmTiQ-yM.js";
22
+ import { m as tableFor, t as canFixture } from "./verify-CgIpJRcG.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,
@@ -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-DpCUAST3.js";
10
+ import { a as nonEmptyOr } from "./output-BVZpQDad.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";
@@ -0,0 +1 @@
1
+ export { n as runCloudPull } from "./pull-BBmCxnqX.js";