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.
- package/dist/a2a/index.js +1 -1
- package/dist/{adoption-scan-oks22j9J.js → adoption-scan-DLel53Oa.js} +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.js +2 -2
- package/dist/{arazzo-BzVfXbhf.js → arazzo-CHK9NT6u.js} +1 -1
- package/dist/{arazzo-CBBIhC40.js → arazzo-CL8CZnpL.js} +1 -1
- package/dist/better-supabase/index.d.ts +20 -22
- package/dist/better-supabase/index.js +4 -16
- package/dist/better-supabase-Dirf_8Ec.js +72 -0
- package/dist/{casbin-C8r5trwy.js → casbin-DRFwgVIP.js} +1 -1
- package/dist/{casbin-BunKq48W.js → casbin-iAEXjmca.js} +1 -1
- package/dist/claude-agent/index.js +1 -1
- package/dist/cli/bin.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/{cloud-C_Mau1q2.js → cloud-eOtmuonF.js} +1 -1
- package/dist/{codemod-BqYPToUJ.js → codemod-D6NYu4AK.js} +2 -2
- package/dist/{codemod-CpMCbp0G.js → codemod-KEDj_3zl.js} +1 -1
- package/dist/{collect-D9Tt0Bcx.js → collect-DnXL4B8c.js} +1 -1
- package/dist/{completion-Cq4fbT2S.js → completion-DCza8aQY.js} +1 -1
- package/dist/{config-0xxCmoD8.js → config-BAM3a3z8.js} +1 -1
- package/dist/{config-BR23fL8m.js → config-CcLlvEYW.js} +4 -1
- package/dist/{config-report-C1-r59VY.js → config-report-VZcQ0ZpS.js} +1 -1
- package/dist/{create-6EwRQlig.js → create-Coq5fIrq.js} +1 -1
- package/dist/{diff-BwR1JdP2.js → diff-CxtbOi2l.js} +1 -1
- package/dist/{diff-CQegtBAo.js → diff-DfmYaIhB.js} +1 -1
- package/dist/{doctor-CDfzrMQB.js → doctor-CMqPKuN_.js} +16 -49
- package/dist/{doctor-Dea6IqRw.js → doctor-gIO9WTYo.js} +2 -2
- package/dist/eve/index.js +1 -1
- package/dist/{fixtures-A1_sp_tR.js → fixtures-Bm4_K1ke.js} +7 -9
- package/dist/{import-PP7uH8rC.js → import-DK_J8tVE.js} +1 -1
- package/dist/{init-n1gkKHvc.js → init-Buibrl9h.js} +3 -3
- package/dist/{init-BuUt4bLJ.js → init-jLSyPMM7.js} +1 -1
- package/dist/{kernel-qv4cr9w8.js → kernel-BD8QuMnv.js} +9 -2
- package/dist/{login-YsrgkAQo.js → login-CB55O4Gv.js} +1 -1
- package/dist/{login-DyQNWJok.js → login-VVpkkund.js} +3 -3
- package/dist/{manifest-Cl6yHiVK.js → manifest-CbEyvPN2.js} +33 -33
- package/dist/mcp/index.d.ts +21 -3
- package/dist/mcp/index.js +87 -16
- package/dist/next/plugin.js +1 -1
- package/dist/openai/index.js +2 -2
- package/dist/{openapi-D8JQtO8k.js → openapi-CleDZYVf.js} +1 -1
- package/dist/{openapi-BFyBIe9J.js → openapi-eHZ5hwUJ.js} +2 -2
- package/dist/{output-DpCUAST3.js → output-BVZpQDad.js} +1 -1
- package/dist/{powersync-Bm3ZjHwD.js → powersync-CFWILHjb.js} +2 -2
- package/dist/powersync-CnGzxvEl.js +1 -0
- package/dist/{powersync-BTLk8GSl.js → powersync-uqYWxTjq.js} +126 -39
- package/dist/{pull-iVPQZsnI.js → pull-BBmCxnqX.js} +1 -1
- package/dist/pull-D6dloyrc.js +1 -0
- package/dist/react-native/index.d.ts +21 -8
- package/dist/react-native/index.js +103 -17
- package/dist/{rls-BItB4vkC.js → rls-BwKXdVEW.js} +5 -5
- package/dist/{rls-C8nxX0RV.js → rls-yK2nZUcm.js} +1 -1
- package/dist/{run-CjFdAzqE.js → run-D5WIJfs7.js} +3 -3
- package/dist/run-DlQYPtPv.js +1 -0
- package/dist/{scan-COIQvTMg.js → scan-CzdMtHnq.js} +1 -1
- package/dist/{schema-CeQZY6bO.js → schema-BDGEVO4g.js} +1 -1
- package/dist/{skills-CfkYP7GG.js → skills-BIsm1n26.js} +1 -1
- package/dist/{skills-5XWpNzCi.js → skills-BZxPQ_09.js} +1 -1
- package/dist/supabase/index.js +1 -1
- package/dist/{supabase-CBEK6KEo.js → supabase-DYWnT800.js} +2 -2
- package/dist/{supabase-hook-CyuD9Wau.js → supabase-hook-HPgwYb9V.js} +2 -2
- package/dist/supabase-hook-qsewAVZY.js +1 -0
- package/dist/{supautils-PRhfO-bL.js → supautils-CGk4YpvC.js} +16 -5
- package/dist/testing/core.js +1 -1
- package/dist/testing/fixtures/index.js +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/unplugin/index.js +1 -1
- package/dist/usage-D3KJyaNS.js +1 -0
- package/dist/{usage-BfzSfOpD.js → usage-DBNrpwoT.js} +1 -1
- package/dist/{usage-hMYVFsN5.js → usage-e6M1Mls1.js} +2 -2
- package/dist/{verify-CmTiQ-yM.js → verify-CgIpJRcG.js} +3 -3
- package/dist/watch-CuPz0D2M.js +1 -0
- package/dist/{watch-Bpci3hBY.js → watch-DAkjm75Y.js} +1 -1
- package/package.json +16 -16
- package/skills/permdock-credentials/references/tokens-keys-links.md +1 -1
- package/skills/permdock-wire/references/adapters.md +1 -1
- package/dist/powersync-CObULVRk.js +0 -1
- package/dist/pull-3OQCgxUf.js +0 -1
- package/dist/run-BqW8S5TM.js +0 -1
- package/dist/supabase-hook-CWvEpdCh.js +0 -1
- package/dist/usage-BDiTP763.js +0 -1
- package/dist/watch-COuvly20.js +0 -1
- package/dist/{core-DzVINFbU.js → core-lxoANyai.js} +1 -1
- 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-
|
|
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
|
-
|
|
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
|
|
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, {
|
|
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, {
|
|
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) =>
|
|
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
|
package/dist/next/plugin.js
CHANGED
|
@@ -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-
|
|
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";
|
package/dist/openai/index.js
CHANGED
|
@@ -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-
|
|
4
|
-
import { t as agentRuntime } from "../create-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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: [
|
|
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: [
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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";
|