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.
- package/dist/a2a/index.js +1 -1
- package/dist/{adoption-scan-DKCOsfcF.js → adoption-scan-BGtwGfoH.js} +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.js +2 -2
- package/dist/{arazzo-CTH5iN--.js → arazzo-9uhbmKf5.js} +1 -1
- package/dist/{arazzo-BXaEhGHM.js → arazzo-Bol9OgF-.js} +1 -1
- package/dist/better-supabase/index.d.ts +19 -17
- package/dist/better-supabase/index.js +31 -16
- package/dist/better-supabase-83Gsetrv.js +56 -0
- package/dist/{casbin-BuTZnSPv.js → casbin-1PVgMHOR.js} +1 -1
- package/dist/{casbin-DTi6HSsl.js → casbin-CBXD5rFx.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-DiAiYFRo.js → cloud-BfJVKpcN.js} +1 -1
- package/dist/{codemod-C1U8_Zu3.js → codemod-BXLd4K3j.js} +1 -1
- package/dist/{codemod-_q_ZI01S.js → codemod-DWfH-KHk.js} +2 -2
- package/dist/{collect-D9Tt0Bcx.js → collect-lZgCKMe_.js} +1 -1
- package/dist/{completion-DLTuCf53.js → completion-fJ19i8Jk.js} +1 -1
- package/dist/{config-CmydKZr2.js → config-BYSk3iKV.js} +1 -1
- package/dist/{config-BR23fL8m.js → config-ZZdtyQZy.js} +4 -1
- package/dist/{config-report-5AwptLr2.js → config-report-BpztJ3HP.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-DxyVeUEE.js → doctor-NxCr_hba.js} +2 -2
- package/dist/{doctor-ktitU5Dl.js → doctor-eNc7A_7I.js} +13 -47
- package/dist/eve/index.js +1 -1
- package/dist/{fixtures-A1_sp_tR.js → fixtures-Bm4_K1ke.js} +7 -9
- package/dist/{import-t1yqnCyG.js → import-efm6qPgx.js} +1 -1
- package/dist/{init-CY7THM2v.js → init-BtWY1L0J.js} +3 -3
- package/dist/{init-DFwylmiN.js → init-D6rpLPwM.js} +1 -1
- package/dist/{kernel-qv4cr9w8.js → kernel-BD8QuMnv.js} +9 -2
- package/dist/{login-C5XLYZSF.js → login-B8s6VI8Z.js} +3 -3
- package/dist/{login-B92-dZMK.js → login-cLflB63Z.js} +1 -1
- package/dist/{manifest-Cav1rjNQ.js → manifest-BWIR_EEB.js} +33 -33
- package/dist/mcp/index.d.ts +12 -3
- package/dist/mcp/index.js +60 -16
- package/dist/next/plugin.js +1 -1
- package/dist/openai/index.js +2 -2
- package/dist/{openapi-gMQQSzNn.js → openapi-CT55v3WD.js} +2 -2
- package/dist/{openapi-CuePQL4z.js → openapi-DDENzRbG.js} +1 -1
- package/dist/{output-CtyBwTNB.js → output-3iB3_Hfg.js} +1 -1
- package/dist/{powersync-CBcsEvdr.js → powersync-COjQTHRD.js} +2 -2
- package/dist/{powersync-CD8-Ypao.js → powersync-DVbskWD9.js} +126 -39
- package/dist/powersync-k8lUeC37.js +1 -0
- package/dist/pull-BY1METGf.js +1 -0
- package/dist/{pull-CAeaAioC.js → pull-Er4aBHlL.js} +1 -1
- package/dist/react-native/index.d.ts +7 -7
- package/dist/react-native/index.js +57 -16
- package/dist/{rls-CtRaRrn3.js → rls-DY3Fc18U.js} +30 -15
- package/dist/{rls-BYHN5bcJ.js → rls-DjjOHlKE.js} +1 -1
- package/dist/{run-B5nyNKe8.js → run-BlxXmaDS.js} +3 -3
- package/dist/run-DGSLvGty.js +1 -0
- package/dist/{scan-CvFWW_fU.js → scan-CR_xm71k.js} +1 -1
- package/dist/{schema-CQzIJdEd.js → schema-CnrIP94n.js} +1 -1
- package/dist/{skills-Bar7ZqwM.js → skills-CY-MY_Oj.js} +1 -1
- package/dist/{skills-BZc81VLt.js → skills-uzsVqEi9.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-C-_jqylY.js → supautils-DZ95xxW-.js} +3 -3
- 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-CH7oISI6.js +1 -0
- package/dist/{usage-heV5KOiU.js → usage-Dm2hn0pS.js} +1 -1
- package/dist/{usage-Y6xZUfE-.js → usage-WYRc7Kqg.js} +2 -2
- package/dist/{verify-6EauWrkP.js → verify-CHQgLrtS.js} +3 -3
- package/dist/watch-C44FtM79.js +1 -0
- package/dist/{watch-Bpci3hBY.js → watch-CTfSlIGD.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-6vgKevSi.js +0 -1
- package/dist/pull-DfJALUbS.js +0 -1
- package/dist/run-0vOholnN.js +0 -1
- package/dist/supabase-hook-CWvEpdCh.js +0 -1
- package/dist/usage-CNca1i1W.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/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;
|
|
@@ -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-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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-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: [
|
|
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,
|
|
@@ -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-
|
|
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
|
|
119
|
-
* with `db.getAll
|
|
120
|
-
* `queries` and `read`, it reads the tables `manifest.rows`
|
|
121
|
-
* memberships of every source with null roles dropped, the global
|
|
122
|
-
* custom roles with their levels. A watch result with unchanged
|
|
123
|
-
* re-renders nothing (`connectSource`). The source takes the database
|
|
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
|
|
539
|
-
* with `db.getAll
|
|
540
|
-
* `queries` and `read`, it reads the tables `manifest.rows`
|
|
541
|
-
* memberships of every source with null roles dropped, the global
|
|
542
|
-
* custom roles with their levels. A watch result with unchanged
|
|
543
|
-
* re-renders nothing (`connectSource`). The source takes the database
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
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
|