permdock 0.0.0 → 0.1.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/README.md +268 -1
- package/dist/a2a/index.d.ts +178 -0
- package/dist/a2a/index.js +271 -0
- package/dist/ai-sdk/index.d.ts +74 -0
- package/dist/ai-sdk/index.js +137 -0
- package/dist/approval-required-TzNB9oQA.js +6 -0
- package/dist/approvals/index.d.ts +3 -0
- package/dist/approvals/index.js +4 -0
- package/dist/approvals-DaxY-Bdg.js +218 -0
- package/dist/approvers-aH0PJkmf.js +20 -0
- package/dist/arazzo-CTeVuXr5.js +70 -0
- package/dist/arazzo-aoTkqYsJ.js +54 -0
- package/dist/authzen/index.d.ts +48 -0
- package/dist/authzen/index.js +336 -0
- package/dist/better-auth/index.d.ts +92 -0
- package/dist/better-auth/index.js +324 -0
- package/dist/budget-JqcRqyhK.d.ts +11 -0
- package/dist/budget-NbEtbJC_.js +11 -0
- package/dist/capability-B68OFnZx.js +171 -0
- package/dist/capability-BDG_nkxt.d.ts +99 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.js +2 -0
- package/dist/catalog-B0E6bggc.js +48 -0
- package/dist/catalog-H-vFGwWK.js +55 -0
- package/dist/catalog-doc-BSo_Q_-6.js +298 -0
- package/dist/catalog-idsHc0vo.js +2 -0
- package/dist/catalog-path-CnI0XTFK.js +12 -0
- package/dist/claude-agent/index.d.ts +81 -0
- package/dist/claude-agent/index.js +83 -0
- package/dist/clerk/index.d.ts +56 -0
- package/dist/clerk/index.js +280 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +20 -0
- package/dist/cli/index.d.ts +46 -0
- package/dist/cli/index.js +5 -0
- package/dist/cloud/index.d.ts +91 -0
- package/dist/cloud/index.js +390 -0
- package/dist/cloud-BNQTrmrr.js +50 -0
- package/dist/cloud-Cuxr6Czw.js +117 -0
- package/dist/collect-B7_TIfmg.js +49 -0
- package/dist/collect-O-tOtbnk.js +350 -0
- package/dist/collect-fRPAQIWb.js +2 -0
- package/dist/commands-BMh_Etm2.js +93 -0
- package/dist/compact-y6vfu2_f.js +19 -0
- package/dist/compare-pfyVnBjE.js +12 -0
- package/dist/compile-CQbvyIBr.d.ts +30 -0
- package/dist/compile-D22iwvuV.js +558 -0
- package/dist/config-BPYmjTOp.js +26 -0
- package/dist/connection-JvqwHODC.js +194 -0
- package/dist/context-CKEAabd7.js +73 -0
- package/dist/context-gtAwAILx.js +6 -0
- package/dist/convex/index.d.ts +40 -0
- package/dist/convex/index.js +55 -0
- package/dist/create-BKF3dgFk.d.ts +162 -0
- package/dist/create-CSkhDU9T.js +222 -0
- package/dist/credential-B_rkUNlY.js +326 -0
- package/dist/credentials-BgRX3WEg.js +268 -0
- package/dist/describe-Bt1YD5uS.js +99 -0
- package/dist/describe-DernyItf.d.ts +37 -0
- package/dist/diff-BUzf5Poi.js +48 -0
- package/dist/diff-x5Il8LVa.js +418 -0
- package/dist/digest-B3kc_PgQ.d.ts +19 -0
- package/dist/digest-BI-6aa6d.js +27 -0
- package/dist/doctor-BGle8Gqi.js +1314 -0
- package/dist/doctor-Bqo6FwUa.js +53 -0
- package/dist/drizzle/index.d.ts +72 -0
- package/dist/drizzle/index.js +160 -0
- package/dist/duration-xK2jGFVc.js +25 -0
- package/dist/elysia/index.d.ts +71 -0
- package/dist/elysia/index.js +103 -0
- package/dist/errors-DSMnAVNa.js +43 -0
- package/dist/errors-DbZ1YOnb.d.ts +138 -0
- package/dist/errors-Iy71s7Oj.js +177 -0
- package/dist/errors-m3u9TmDu.js +27 -0
- package/dist/evaluations-DsAU9ppl.js +479 -0
- package/dist/eve/index.d.ts +95 -0
- package/dist/eve/index.js +181 -0
- package/dist/express/index.d.ts +46 -0
- package/dist/express/index.js +108 -0
- package/dist/fastify/index.d.ts +56 -0
- package/dist/fastify/index.js +141 -0
- package/dist/files-B_wAGYmt.js +121 -0
- package/dist/fixtures-Cvaw0gDG.js +64 -0
- package/dist/freeze-y1_jQL4v.js +31 -0
- package/dist/from-snapshot-BMGnhmgv.js +552 -0
- package/dist/generate-CH7ThJ14.js +173 -0
- package/dist/grantee-Bv04_Vzf.js +521 -0
- package/dist/headers-B5RRv3Xm.js +6 -0
- package/dist/headers-C8SHQccD.d.ts +6 -0
- package/dist/helpers-DnSOrhB9.js +682 -0
- package/dist/hono/index.d.ts +70 -0
- package/dist/hono/index.js +131 -0
- package/dist/hosted-BwKQ1BTA.js +265 -0
- package/dist/http-DPuDaChb.d.ts +13 -0
- package/dist/http-GgVApX1k.js +111 -0
- package/dist/index-CltcQ4Bf.d.ts +138 -0
- package/dist/index-LF_hS-yn.d.ts +177 -0
- package/dist/index-fG6NSXlc.d.ts +9 -0
- package/dist/index-xIxJLwOQ.d.ts +415 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/jwt/index.d.ts +87 -0
- package/dist/jwt/index.js +571 -0
- package/dist/kernel-YOGCWNLf.js +211 -0
- package/dist/kysely/index.d.ts +77 -0
- package/dist/kysely/index.js +129 -0
- package/dist/load-BZ24CpZP.js +69 -0
- package/dist/load-QOOm1z70.js +2 -0
- package/dist/load-jose-BL6LH4bL.js +11 -0
- package/dist/lru-8q4WZjBV.js +28 -0
- package/dist/manifest-DDQbC6K0.d.ts +118 -0
- package/dist/map-DowSO96j.js +158 -0
- package/dist/map-claims-B_6I1Rn-.js +360 -0
- package/dist/map-error-RJ3ZsUyA.js +16 -0
- package/dist/markers-B0hYLvMD.js +60 -0
- package/dist/may-use-CnFEjVEp.js +37 -0
- package/dist/mcp/index.d.ts +127 -0
- package/dist/mcp/index.js +563 -0
- package/dist/nest/index.d.ts +80 -0
- package/dist/nest/index.js +270 -0
- package/dist/next/client.d.ts +24 -0
- package/dist/next/client.js +31 -0
- package/dist/next/index.d.ts +110 -0
- package/dist/next/index.js +195 -0
- package/dist/next/plugin.d.ts +14 -0
- package/dist/next/plugin.js +2 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +66 -0
- package/dist/openai/index.d.ts +71 -0
- package/dist/openai/index.js +105 -0
- package/dist/openapi/index.d.ts +93 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi-CwePaF7C.js +164 -0
- package/dist/openapi-DWD0Uc_4.js +250 -0
- package/dist/openapi-DcqM8nU7.js +306 -0
- package/dist/openapi-import-BWRUNNeW.js +5959 -0
- package/dist/openapi-schema-D8j_T9tO.js +6902 -0
- package/dist/orpc/index.d.ts +64 -0
- package/dist/orpc/index.js +188 -0
- package/dist/otel/index.d.ts +13 -0
- package/dist/otel/index.js +202 -0
- package/dist/package-root-zGkxO3oN.js +30 -0
- package/dist/parse-DPfIvFMU.js +135 -0
- package/dist/pdp/index.d.ts +22 -0
- package/dist/pdp/index.js +795 -0
- package/dist/peer-BOr_kJE1.js +31 -0
- package/dist/permdock-yoZ7EjxX.js +2956 -0
- package/dist/permissions-B4apnahQ.d.ts +185 -0
- package/dist/permissions-B8vCg_bI.d.ts +52 -0
- package/dist/permissions-CofRqesP.js +142 -0
- package/dist/permissions-DC7GROPA.js +600 -0
- package/dist/pg-4GJWDUxG.js +953 -0
- package/dist/pins-CqE1jZuF.js +12 -0
- package/dist/plugin-4gr7IxFJ.js +65 -0
- package/dist/policy-ANp36PnI.js +971 -0
- package/dist/policy-CInmNGkM.d.ts +1959 -0
- package/dist/policy-Cq2fB8Q5.d.ts +20 -0
- package/dist/prisma/index.d.ts +135 -0
- package/dist/prisma/index.js +430 -0
- package/dist/problem-Cgp9-KZA.js +226 -0
- package/dist/prompt-CXYhLfsS.js +32 -0
- package/dist/protected-B4K3BKoX.js +138 -0
- package/dist/protected-BuPAycJT.d.ts +26 -0
- package/dist/protected-wY7nXhY9.d.ts +23 -0
- package/dist/provider-_wx46HpB.js +48 -0
- package/dist/provider-wTAUda2v.d.ts +6 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +6 -0
- package/dist/react/provider-client.d.ts +2 -0
- package/dist/react/provider-client.js +3 -0
- package/dist/react-native/index.d.ts +42 -0
- package/dist/react-native/index.js +174 -0
- package/dist/refs-ChLSxbST.js +90 -0
- package/dist/relations-r4soIFZB.js +426 -0
- package/dist/revocations-Cy06JNx2.d.ts +21 -0
- package/dist/rls-BOFU3chG.js +233 -0
- package/dist/rls-CJVja9Ww.js +2119 -0
- package/dist/rls-graph-DxQ39PJj.js +371 -0
- package/dist/rls-helpers-BxRuif0l.js +1346 -0
- package/dist/rls-import-CXV6LBlS.js +942 -0
- package/dist/rls-migrate-DrFPbf8p.js +366 -0
- package/dist/row-check-BajTgQZ-.d.ts +43 -0
- package/dist/run-CJ6UvS3a.js +155 -0
- package/dist/runtime-DP9ttFh1.js +27 -0
- package/dist/saas-BRrZzJkM.js +1193 -0
- package/dist/schema-P8yoma__.js +296 -0
- package/dist/scim/index.d.ts +2 -0
- package/dist/scim/index.js +2 -0
- package/dist/scim-9NFa0XVZ.js +1445 -0
- package/dist/scopes-cKVp_WTw.js +212 -0
- package/dist/server/index.d.ts +123 -0
- package/dist/server/index.js +8 -0
- package/dist/sha256-BO0swRXL.js +150 -0
- package/dist/sink-Bd9PSyfq.js +141 -0
- package/dist/sink-Cfr9-7YC.d.ts +104 -0
- package/dist/skills-C_lpV4Jx.js +50 -0
- package/dist/skills-Dx3QLnMY.js +110 -0
- package/dist/snapshot-DE8dE_hz.js +1051 -0
- package/dist/solid/index.d.ts +63 -0
- package/dist/solid/index.js +197 -0
- package/dist/source-DoBBYJi2.js +6 -0
- package/dist/sources-DOpW5Ila.d.ts +347 -0
- package/dist/sources-DVHM3Mwq.js +284 -0
- package/dist/sql-Bll1d7y0.js +22 -0
- package/dist/src-CTNT-PBW.js +430 -0
- package/dist/ssf/index.d.ts +3 -0
- package/dist/ssf/index.js +622 -0
- package/dist/store-Bq90395i.d.ts +27 -0
- package/dist/store-C6hFW6ta.js +508 -0
- package/dist/stores-CkG2HrSg.js +140 -0
- package/dist/stores-CvOnu_2T.d.ts +41 -0
- package/dist/stream-BhMfZOwr.d.ts +12 -0
- package/dist/stream-Q_xlB6Ko.js +52 -0
- package/dist/style-CTm1AXDT.js +22 -0
- package/dist/subject-C6dLxqHU.js +24 -0
- package/dist/subject-settings-xt7V845g.js +82 -0
- package/dist/supabase/index.d.ts +67 -0
- package/dist/supabase/index.js +4 -0
- package/dist/supabase/middleware.d.ts +77 -0
- package/dist/supabase/middleware.js +68 -0
- package/dist/supabase-D8UlBA7Z.js +83 -0
- package/dist/supabase-DTxGtYdi.js +796 -0
- package/dist/supabase-hook-DW-xP4hU.js +2 -0
- package/dist/supabase-hook-D_OqGb_V.js +1528 -0
- package/dist/svelte/Protected.svelte +34 -0
- package/dist/svelte/index.d.ts +10 -0
- package/dist/svelte/index.js +53 -0
- package/dist/svelte/runtime.d.ts +6 -0
- package/dist/svelte/runtime.js +3 -0
- package/dist/svelte/source.d.ts +10 -0
- package/dist/svelte/source.js +8 -0
- package/dist/terminal/index.d.ts +171 -0
- package/dist/terminal/index.js +821 -0
- package/dist/testing/index.d.ts +656 -0
- package/dist/testing/index.js +3208 -0
- package/dist/testing/saas/index.d.ts +194 -0
- package/dist/testing/saas/index.js +3 -0
- package/dist/testing/saas/permissions.d.ts +2 -0
- package/dist/testing/saas/permissions.js +2 -0
- package/dist/text-diff-BdkkYSbi.js +15 -0
- package/dist/thenable-okc2xDsj.js +10 -0
- package/dist/timeout-D51niuQP.js +8 -0
- package/dist/trpc/index.d.ts +80 -0
- package/dist/trpc/index.js +205 -0
- package/dist/types-B8w5PFtS.d.ts +141 -0
- package/dist/types-B9dmiiab.d.ts +66 -0
- package/dist/types-BBK-exvJ.d.ts +177 -0
- package/dist/types-BvBk1wgn.d.ts +118 -0
- package/dist/types-CxquUByI.d.ts +9 -0
- package/dist/types-D6Z6-rKJ.d.ts +312 -0
- package/dist/types-DbWdEfcm.d.ts +87 -0
- package/dist/types-bva3i4Gf.d.ts +98 -0
- package/dist/types-i4BAOvdb.d.ts +97 -0
- package/dist/unplugin/index.d.ts +7 -0
- package/dist/unplugin/index.js +30 -0
- package/dist/usage-1ozMubLY.js +42 -0
- package/dist/usage-CUnK6Ou9.js +530 -0
- package/dist/usage-PyEor6GA.js +2 -0
- package/dist/validity-D0uRmJKJ.js +224 -0
- package/dist/verifier-B90agnhl.js +461 -0
- package/dist/version-CnSh33Fj.js +16 -0
- package/dist/vocabulary-Dk0fs-Jk.d.ts +56 -0
- package/dist/vue/index.d.ts +57 -0
- package/dist/vue/index.js +203 -0
- package/dist/webmcp/index.d.ts +75 -0
- package/dist/webmcp/index.js +313 -0
- package/package.json +423 -4
- package/rulesets/permdock.yaml +29 -0
- package/schemas/catalog-v1.json +507 -0
- package/schemas/openapi/oas-3.1.json +1298 -0
- package/schemas/openapi/oas-3.2.json +1516 -0
- package/schemas/openapi/overlay-1.1.json +79 -0
- package/schemas/supabase-claims-v1.json +139 -0
- package/schemas/supabase-manifest-v1.json +217 -0
- package/skills/permdock/SKILL.md +66 -0
- package/skills/permdock-agents/SKILL.md +77 -0
- package/skills/permdock-agents/references/runtimes.md +211 -0
- package/skills/permdock-approvals/SKILL.md +80 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +83 -0
- package/skills/permdock-audit/SKILL.md +67 -0
- package/skills/permdock-credentials/SKILL.md +55 -0
- package/skills/permdock-credentials/references/tokens-keys-links.md +127 -0
- package/skills/permdock-data/SKILL.md +57 -0
- package/skills/permdock-data/references/queries-and-rls.md +54 -0
- package/skills/permdock-data/references/relationships.md +65 -0
- package/skills/permdock-tenancy/SKILL.md +77 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +83 -0
- package/skills/permdock-wire/SKILL.md +116 -0
- package/skills/permdock-wire/references/adapters.md +480 -0
- package/skills/permdock-wire/references/policy-options.md +43 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import { n as isReadonlyArray, r as sole, t as compact } from "./compact-y6vfu2_f.js";
|
|
2
|
+
import { n as freezeDeep } from "./freeze-y1_jQL4v.js";
|
|
3
|
+
import { S as isForbiddenKey, d as isFieldRelation, f as isPermission, g as listPermissions, h as isSelfParented, i as findPermission, m as isRegistryTree, s as getRegistry, t as annotationsFor, w as ownKeys, x as assertSafeKey } from "./permissions-DC7GROPA.js";
|
|
4
|
+
import { c as isConditionRef, l as isSqlFunctionField, n as isSubjectRef, o as isCondition, s as isConditionDate } from "./refs-ChLSxbST.js";
|
|
5
|
+
import { n as isUserApprover, r as user, t as flattenApprovers } from "./approvers-aH0PJkmf.js";
|
|
6
|
+
import { a as resolveScope, n as defineScopes } from "./scopes-cKVp_WTw.js";
|
|
7
|
+
import { S as isRole, _ as roleScopeOf, a as authenticated, c as isGraphRelation, g as roleNameOf, m as relationStart, p as relationHops, r as asGrantee, s as flattenGrantee, w as listRoles } from "./grantee-Bv04_Vzf.js";
|
|
8
|
+
import { t as parseDuration } from "./duration-xK2jGFVc.js";
|
|
9
|
+
import { g as sanitizeFields, l as relatesTo, n as normalizeValidity } from "./validity-D0uRmJKJ.js";
|
|
10
|
+
import { n as sha256, t as bytesToBase64Url } from "./sha256-BO0swRXL.js";
|
|
11
|
+
//#region src/conditions/opaque.ts
|
|
12
|
+
function opaque(input) {
|
|
13
|
+
return freezeDeep({
|
|
14
|
+
op: "opaque",
|
|
15
|
+
sql: input.sql,
|
|
16
|
+
fingerprint: input.fingerprint
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/conditions/sql-function-assert.ts
|
|
21
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
22
|
+
function assertSqlFunctionName(name) {
|
|
23
|
+
const parts = name.split(".");
|
|
24
|
+
if (parts.length === 0 || parts.length > 2) throw new Error(`PermDock: sqlFunction name '${name}' is not a SQL identifier`);
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
if (!IDENT.test(part)) throw new Error(`PermDock: sqlFunction name '${name}' is not a SQL identifier`);
|
|
27
|
+
assertSafeKey(part, "sqlFunction name");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function assertSqlFunctionArg(arg) {
|
|
31
|
+
if (isSqlFunctionField(arg)) assertSafeKey(arg.field, "sqlFunction argument field");
|
|
32
|
+
}
|
|
33
|
+
function assertPortableTwin(condition, depth = 0) {
|
|
34
|
+
/* v8 ignore next 3 */
|
|
35
|
+
if (depth > 32) throw new Error("PermDock: sqlFunction twin is nested too deeply");
|
|
36
|
+
switch (condition.op) {
|
|
37
|
+
case "opaque": throw new Error("PermDock: sqlFunction twin must not be opaque");
|
|
38
|
+
case "sqlFunction": throw new Error("PermDock: sqlFunction twin must not nest sqlFunction");
|
|
39
|
+
case "related": throw new Error("PermDock: sqlFunction twin must not read the relation graph");
|
|
40
|
+
case "and":
|
|
41
|
+
case "or":
|
|
42
|
+
for (const child of condition.conditions) assertPortableTwin(child, depth + 1);
|
|
43
|
+
break;
|
|
44
|
+
case "not": assertPortableTwin(condition.condition, depth + 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/conditions/normalize.ts
|
|
49
|
+
const FIELD_OPS = /* @__PURE__ */ new Set([
|
|
50
|
+
"eq",
|
|
51
|
+
"ne",
|
|
52
|
+
"gt",
|
|
53
|
+
"gte",
|
|
54
|
+
"lt",
|
|
55
|
+
"lte",
|
|
56
|
+
"contains",
|
|
57
|
+
"in",
|
|
58
|
+
"notIn",
|
|
59
|
+
"isNull"
|
|
60
|
+
]);
|
|
61
|
+
function toValue(raw) {
|
|
62
|
+
if (isSubjectRef(raw) || isConditionRef(raw)) return freezeDeep({ ref: raw.ref });
|
|
63
|
+
if (raw instanceof Date) return freezeDeep({ date: raw.toISOString() });
|
|
64
|
+
if (isConditionDate(raw)) return freezeDeep({ date: raw.date });
|
|
65
|
+
if (Array.isArray(raw)) return raw.map((item) => toValue(item));
|
|
66
|
+
if (raw === null || typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") return raw;
|
|
67
|
+
throw new Error("PermDock: unsupported condition value");
|
|
68
|
+
}
|
|
69
|
+
function flatten(op, conditions) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const condition of conditions) if (condition.op === op) out.push(...condition.conditions);
|
|
72
|
+
else out.push(condition);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function collapse(condition) {
|
|
76
|
+
if (condition.op === "and" || condition.op === "or") {
|
|
77
|
+
const flat = flatten(condition.op, condition.conditions).map(collapse);
|
|
78
|
+
if (flat.length === 0) throw new Error(`PermDock: empty ${condition.op} condition`);
|
|
79
|
+
const single = sole(flat);
|
|
80
|
+
if (single !== void 0) return single;
|
|
81
|
+
return freezeDeep({
|
|
82
|
+
op: condition.op,
|
|
83
|
+
conditions: flat
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (condition.op === "not") return freezeDeep({
|
|
87
|
+
op: "not",
|
|
88
|
+
condition: collapse(condition.condition)
|
|
89
|
+
});
|
|
90
|
+
if (condition.op === "sqlFunction") return freezeDeep({
|
|
91
|
+
op: "sqlFunction",
|
|
92
|
+
name: condition.name,
|
|
93
|
+
args: condition.args,
|
|
94
|
+
twin: collapse(condition.twin)
|
|
95
|
+
});
|
|
96
|
+
return freezeDeep(condition);
|
|
97
|
+
}
|
|
98
|
+
function fieldCondition(field, raw) {
|
|
99
|
+
assertSafeKey(field, "condition field");
|
|
100
|
+
if (isCondition(raw) && (raw.op === "opaque" || raw.op === "sqlFunction")) return raw.op === "sqlFunction" ? normalizeSqlFunction(raw) : raw;
|
|
101
|
+
if (raw !== null && typeof raw === "object" && !isSubjectRef(raw) && !isConditionRef(raw) && !isConditionDate(raw) && !Array.isArray(raw) && !(raw instanceof Date)) {
|
|
102
|
+
const keys = ownKeys(raw);
|
|
103
|
+
if (keys.length === 1 && keys[0] !== void 0 && FIELD_OPS.has(keys[0])) {
|
|
104
|
+
const op = keys[0];
|
|
105
|
+
const value = raw[op];
|
|
106
|
+
if (op === "isNull") {
|
|
107
|
+
if (typeof value !== "boolean") throw new TypeError("PermDock: isNull requires a boolean");
|
|
108
|
+
return collapse({
|
|
109
|
+
op: "isNull",
|
|
110
|
+
field,
|
|
111
|
+
value
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (op === "in" || op === "notIn") {
|
|
115
|
+
if (isSubjectRef(value) || isConditionRef(value)) return collapse({
|
|
116
|
+
op,
|
|
117
|
+
field,
|
|
118
|
+
value: { ref: value.ref }
|
|
119
|
+
});
|
|
120
|
+
if (!Array.isArray(value)) throw new TypeError(`PermDock: ${op} requires an array or subject reference`);
|
|
121
|
+
return collapse({
|
|
122
|
+
op,
|
|
123
|
+
field,
|
|
124
|
+
value: value.map((item) => toValue(item))
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return collapse({
|
|
128
|
+
op,
|
|
129
|
+
field,
|
|
130
|
+
value: toValue(value)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return collapse({
|
|
135
|
+
op: "eq",
|
|
136
|
+
field,
|
|
137
|
+
value: toValue(raw)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function normalizeSqlFunction(input) {
|
|
141
|
+
assertSqlFunctionName(input.name);
|
|
142
|
+
const args = input.args ?? [];
|
|
143
|
+
for (const arg of args) assertSqlFunctionArg(arg);
|
|
144
|
+
if (isCondition(input.twin) && input.twin.op === "sqlFunction") throw new Error("PermDock: sqlFunction twin must not nest sqlFunction");
|
|
145
|
+
const twin = isCondition(input.twin) ? collapse(input.twin) : normalizeWhere(input.twin);
|
|
146
|
+
assertPortableTwin(twin);
|
|
147
|
+
return freezeDeep({
|
|
148
|
+
op: "sqlFunction",
|
|
149
|
+
name: input.name,
|
|
150
|
+
args,
|
|
151
|
+
twin
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function normalizeWhere(input) {
|
|
155
|
+
if (isCondition(input)) {
|
|
156
|
+
if (input.op === "sqlFunction") return collapse(normalizeSqlFunction(input));
|
|
157
|
+
return collapse(input);
|
|
158
|
+
}
|
|
159
|
+
if (input !== null && typeof input === "object" && "sql" in input && "fingerprint" in input && typeof input.sql === "string") return opaque({
|
|
160
|
+
sql: input.sql,
|
|
161
|
+
fingerprint: input.fingerprint
|
|
162
|
+
});
|
|
163
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("PermDock: condition must be an object");
|
|
164
|
+
const parts = [];
|
|
165
|
+
for (const key of Object.keys(input)) {
|
|
166
|
+
const value = input[key];
|
|
167
|
+
if (key === "and") {
|
|
168
|
+
if (!Array.isArray(value)) throw new TypeError("PermDock: and requires an array");
|
|
169
|
+
parts.push(collapse({
|
|
170
|
+
op: "and",
|
|
171
|
+
conditions: value.map(normalizeWhere)
|
|
172
|
+
}));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (key === "or") {
|
|
176
|
+
if (!Array.isArray(value)) throw new TypeError("PermDock: or requires an array");
|
|
177
|
+
parts.push(collapse({
|
|
178
|
+
op: "or",
|
|
179
|
+
conditions: value.map(normalizeWhere)
|
|
180
|
+
}));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (key === "not") {
|
|
184
|
+
parts.push(collapse({
|
|
185
|
+
op: "not",
|
|
186
|
+
condition: normalizeWhere(value)
|
|
187
|
+
}));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
parts.push(fieldCondition(key, value));
|
|
191
|
+
}
|
|
192
|
+
if (parts.length === 0) throw new Error("PermDock: empty condition");
|
|
193
|
+
const single = sole(parts);
|
|
194
|
+
if (single !== void 0) return single;
|
|
195
|
+
return collapse({
|
|
196
|
+
op: "and",
|
|
197
|
+
conditions: parts
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/core/limits.ts
|
|
202
|
+
const UNIT_SECONDS = {
|
|
203
|
+
s: 1,
|
|
204
|
+
sec: 1,
|
|
205
|
+
secs: 1,
|
|
206
|
+
second: 1,
|
|
207
|
+
seconds: 1,
|
|
208
|
+
m: 60,
|
|
209
|
+
min: 60,
|
|
210
|
+
mins: 60,
|
|
211
|
+
minute: 60,
|
|
212
|
+
minutes: 60,
|
|
213
|
+
h: 3600,
|
|
214
|
+
hr: 3600,
|
|
215
|
+
hrs: 3600,
|
|
216
|
+
hour: 3600,
|
|
217
|
+
hours: 3600,
|
|
218
|
+
d: 86400,
|
|
219
|
+
day: 86400,
|
|
220
|
+
days: 86400
|
|
221
|
+
};
|
|
222
|
+
function isThenable(value) {
|
|
223
|
+
return value !== null && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
224
|
+
}
|
|
225
|
+
const PER_PATTERN = /^(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/u;
|
|
226
|
+
/** The window length of `per` in seconds, or `undefined` when unrecognised. */
|
|
227
|
+
function limitWindowSeconds(per) {
|
|
228
|
+
const trimmed = per.trim().toLowerCase();
|
|
229
|
+
const named = UNIT_SECONDS[trimmed];
|
|
230
|
+
if (named !== void 0) return named;
|
|
231
|
+
const match = PER_PATTERN.exec(trimmed);
|
|
232
|
+
if (match === null) return;
|
|
233
|
+
const amount = Number(match[1]);
|
|
234
|
+
const unit = UNIT_SECONDS[match[2] ?? ""];
|
|
235
|
+
return unit !== void 0 && Number.isSafeInteger(amount) && amount > 0 ? amount * unit : void 0;
|
|
236
|
+
}
|
|
237
|
+
/** Throws at definition time for a `limit` no store could count. */
|
|
238
|
+
function assertLimit(limit, permissionKey) {
|
|
239
|
+
if (limit === void 0) return;
|
|
240
|
+
if (!Number.isSafeInteger(limit.count) || limit.count <= 0) throw new Error(`PermDock: limit count on '${permissionKey}' must be a positive integer`);
|
|
241
|
+
if (limitWindowSeconds(limit.per) === void 0) throw new Error(`PermDock: limit per '${limit.per}' on '${permissionKey}' is not a duration such as 'hour' or '15 min'`);
|
|
242
|
+
if (limit.mode !== void 0 && limit.mode !== "hard" && limit.mode !== "soft") throw new Error(`PermDock: limit mode on '${permissionKey}' must be 'hard' or 'soft'`);
|
|
243
|
+
if (limit.alertAt !== void 0 && (typeof limit.alertAt !== "number" || !(limit.alertAt > 0 && limit.alertAt <= 1))) throw new Error(`PermDock: limit alertAt on '${permissionKey}' must be a fraction above 0 and at most 1`);
|
|
244
|
+
}
|
|
245
|
+
/** The declared fields of a validated `limit`, so extra keys never reach the grant. */
|
|
246
|
+
function normalizeLimit(limit) {
|
|
247
|
+
if (limit === void 0) return;
|
|
248
|
+
return {
|
|
249
|
+
count: limit.count,
|
|
250
|
+
per: limit.per,
|
|
251
|
+
...limit.mode === void 0 ? {} : { mode: limit.mode },
|
|
252
|
+
...limit.alertAt === void 0 ? {} : { alertAt: limit.alertAt }
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function limitWindowId(per, now) {
|
|
256
|
+
const seconds = limitWindowSeconds(per);
|
|
257
|
+
return seconds === void 0 ? void 0 : String(Math.floor(now / seconds));
|
|
258
|
+
}
|
|
259
|
+
function limitCacheKey(input, now) {
|
|
260
|
+
const window = limitWindowId(input.per, now);
|
|
261
|
+
return window === void 0 ? void 0 : JSON.stringify([
|
|
262
|
+
input.tenant ?? null,
|
|
263
|
+
input.subjectId,
|
|
264
|
+
input.key,
|
|
265
|
+
input.per,
|
|
266
|
+
window
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
function locate(input) {
|
|
270
|
+
const now = input.now ?? Date.now() / 1e3;
|
|
271
|
+
const seconds = limitWindowSeconds(input.per);
|
|
272
|
+
const id = limitCacheKey(input, now);
|
|
273
|
+
if (seconds === void 0 || id === void 0) return;
|
|
274
|
+
return {
|
|
275
|
+
id,
|
|
276
|
+
until: (Math.floor(now / seconds) + 1) * seconds,
|
|
277
|
+
now
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function memoryLimitStore() {
|
|
281
|
+
const used = /* @__PURE__ */ new Map();
|
|
282
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
283
|
+
const sweep = (now) => {
|
|
284
|
+
if (now < earliest) return;
|
|
285
|
+
earliest = Number.POSITIVE_INFINITY;
|
|
286
|
+
for (const [id, entry] of used) if (entry.until <= now) used.delete(id);
|
|
287
|
+
else earliest = Math.min(earliest, entry.until);
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
remaining(input) {
|
|
291
|
+
const cap = input.count;
|
|
292
|
+
const slot = locate(input);
|
|
293
|
+
if (!Number.isFinite(cap) || cap <= 0 || slot === void 0) return { remaining: -1 };
|
|
294
|
+
return { remaining: cap - (used.get(slot.id)?.count ?? 0) };
|
|
295
|
+
},
|
|
296
|
+
consume(input) {
|
|
297
|
+
const cap = input.count;
|
|
298
|
+
const slot = locate(input);
|
|
299
|
+
if (!Number.isFinite(cap) || cap <= 0 || slot === void 0) return { remaining: -1 };
|
|
300
|
+
sweep(slot.now);
|
|
301
|
+
const seen = used.get(slot.id)?.count ?? 0;
|
|
302
|
+
if (seen >= cap) return { remaining: -1 };
|
|
303
|
+
used.set(slot.id, {
|
|
304
|
+
count: seen + 1,
|
|
305
|
+
until: slot.until
|
|
306
|
+
});
|
|
307
|
+
earliest = Math.min(earliest, slot.until);
|
|
308
|
+
return { remaining: cap - seen - 1 };
|
|
309
|
+
},
|
|
310
|
+
size() {
|
|
311
|
+
return used.size;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* `left` is what remains once this call counts: the store's answer after
|
|
317
|
+
* `consume`, or one less than `remaining` when only peeking. Below zero the
|
|
318
|
+
* call is past the count.
|
|
319
|
+
*/
|
|
320
|
+
function verdictFor(limit, left, now) {
|
|
321
|
+
const seconds = limitWindowSeconds(limit.per);
|
|
322
|
+
if (seconds === void 0) return {
|
|
323
|
+
ok: false,
|
|
324
|
+
reason: "limit-unavailable"
|
|
325
|
+
};
|
|
326
|
+
const resetsAt = (Math.floor(now / seconds) + 1) * seconds;
|
|
327
|
+
if (left < 0) return limit.mode === "soft" ? {
|
|
328
|
+
ok: true,
|
|
329
|
+
quota: {
|
|
330
|
+
remaining: 0,
|
|
331
|
+
resetsAt
|
|
332
|
+
},
|
|
333
|
+
obligations: [{ kind: "over-limit" }]
|
|
334
|
+
} : {
|
|
335
|
+
ok: false,
|
|
336
|
+
reason: "limit",
|
|
337
|
+
detail: {
|
|
338
|
+
count: limit.count,
|
|
339
|
+
window: seconds,
|
|
340
|
+
resetsAt
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
return limit.alertAt !== void 0 && (limit.count - left) / limit.count >= limit.alertAt ? {
|
|
344
|
+
ok: true,
|
|
345
|
+
quota: {
|
|
346
|
+
remaining: left,
|
|
347
|
+
resetsAt
|
|
348
|
+
},
|
|
349
|
+
obligations: [{ kind: "near-limit" }]
|
|
350
|
+
} : {
|
|
351
|
+
ok: true,
|
|
352
|
+
quota: {
|
|
353
|
+
remaining: left,
|
|
354
|
+
resetsAt
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function applyQuota(input) {
|
|
359
|
+
const limit = input.grant.limit;
|
|
360
|
+
if (limit === void 0) return { ok: true };
|
|
361
|
+
if (input.store === void 0) return {
|
|
362
|
+
ok: false,
|
|
363
|
+
reason: "limit-unavailable"
|
|
364
|
+
};
|
|
365
|
+
const payload = {
|
|
366
|
+
key: input.permissionKey,
|
|
367
|
+
subjectId: input.subjectId,
|
|
368
|
+
count: limit.count,
|
|
369
|
+
per: limit.per,
|
|
370
|
+
now: input.now,
|
|
371
|
+
...input.tenant === void 0 ? {} : { tenant: input.tenant }
|
|
372
|
+
};
|
|
373
|
+
const cacheKey = limitCacheKey(payload, input.now);
|
|
374
|
+
if (cacheKey === void 0) return {
|
|
375
|
+
ok: false,
|
|
376
|
+
reason: "limit-unavailable"
|
|
377
|
+
};
|
|
378
|
+
if (!input.consume) {
|
|
379
|
+
try {
|
|
380
|
+
const peeked = input.store.remaining(payload);
|
|
381
|
+
if (isThenable(peeked)) return {
|
|
382
|
+
ok: false,
|
|
383
|
+
reason: "limit-unavailable"
|
|
384
|
+
};
|
|
385
|
+
if (peeked !== void 0) return verdictFor(limit, peeked.remaining - 1, input.now);
|
|
386
|
+
} catch {
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
reason: "limit-unavailable"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const cached = input.cache.get(cacheKey);
|
|
393
|
+
if (cached === void 0) return {
|
|
394
|
+
ok: false,
|
|
395
|
+
reason: "limit-unavailable"
|
|
396
|
+
};
|
|
397
|
+
return verdictFor(limit, cached - 1, input.now);
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const consumed = input.store.consume(payload);
|
|
401
|
+
if (isThenable(consumed)) return {
|
|
402
|
+
ok: false,
|
|
403
|
+
reason: "limit-unavailable"
|
|
404
|
+
};
|
|
405
|
+
input.cache.set(cacheKey, consumed.remaining);
|
|
406
|
+
return verdictFor(limit, consumed.remaining, input.now);
|
|
407
|
+
} catch {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
reason: "limit-unavailable"
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/core/policy.ts
|
|
416
|
+
/** Every approver a requirement names: `by`, each stage and `escalation.to`. */
|
|
417
|
+
function approversOf(requirement) {
|
|
418
|
+
return [
|
|
419
|
+
...flattenApprovers(requirement.by),
|
|
420
|
+
...(requirement.stages ?? []).flatMap((stage) => flattenApprovers(stage.by)),
|
|
421
|
+
...flattenApprovers(requirement.escalation?.to)
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
function asApprover(input) {
|
|
425
|
+
if (isReadonlyArray(input)) {
|
|
426
|
+
const items = [];
|
|
427
|
+
for (const item of input) items.push(...flattenApprovers(asApprover(item)));
|
|
428
|
+
return items;
|
|
429
|
+
}
|
|
430
|
+
if (isUserApprover(input)) return user(input.id);
|
|
431
|
+
return asGrantee(input);
|
|
432
|
+
}
|
|
433
|
+
function indexPolicy(roles, grants, vocabulary) {
|
|
434
|
+
const declaredRoles = new Set(roles.map((item) => item.name));
|
|
435
|
+
for (const leaf of listRoles(vocabulary.roles)) declaredRoles.add(leaf.key);
|
|
436
|
+
const supports = roles.flatMap((binding) => binding.support === void 0 ? [] : [binding.support]);
|
|
437
|
+
const listed = grants.length > 0 ? grants : roles.flatMap((binding) => binding.grants);
|
|
438
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const grant of listed) {
|
|
440
|
+
const bucket = byKey.get(grant.permission.key);
|
|
441
|
+
if (bucket === void 0) byKey.set(grant.permission.key, [grant]);
|
|
442
|
+
else bucket.push(grant);
|
|
443
|
+
}
|
|
444
|
+
for (const bucket of byKey.values()) Object.freeze(bucket);
|
|
445
|
+
return freezeDeep({
|
|
446
|
+
declaredRoles,
|
|
447
|
+
supports,
|
|
448
|
+
grantsByKey: byKey
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function normalizeApproval(approval, permission) {
|
|
452
|
+
if (approval === void 0) return;
|
|
453
|
+
if (approval === "human") return "human";
|
|
454
|
+
if (approval.staleOn !== void 0 && approval.staleOn !== "resource-change") throw new Error(`PermDock: approval staleOn must be 'resource-change', got '${String(approval.staleOn)}'`);
|
|
455
|
+
const label = permission ?? "a grant";
|
|
456
|
+
assertQuorum(approval.quorum, "approval.quorum", label);
|
|
457
|
+
if (approval.ttl !== void 0 && parseDuration(approval.ttl) === void 0) throw new Error(`PermDock: approval.ttl on '${label}' must be a duration such as '30m', got '${approval.ttl}'`);
|
|
458
|
+
const escalation = normalizeEscalation(approval.escalation, label);
|
|
459
|
+
const mode = approval.mode ?? "any";
|
|
460
|
+
if (mode !== "any") return compact({
|
|
461
|
+
mode,
|
|
462
|
+
stages: normalizeStages(approval, mode, label),
|
|
463
|
+
distinct: approval.distinct,
|
|
464
|
+
staleOn: approval.staleOn,
|
|
465
|
+
ttl: approval.ttl,
|
|
466
|
+
escalation
|
|
467
|
+
});
|
|
468
|
+
if (approval.stages !== void 0) throw new Error(`PermDock: approval.stages on '${label}' needs mode: 'all' or 'sequential'`);
|
|
469
|
+
const by = approval.by === void 0 ? void 0 : asApprover(approval.by);
|
|
470
|
+
if (by === void 0 && approval.distinct === void 0 && approval.staleOn === void 0 && approval.quorum === void 0 && approval.ttl === void 0 && escalation === void 0) return "human";
|
|
471
|
+
return compact({
|
|
472
|
+
by: by ?? authenticated(),
|
|
473
|
+
distinct: approval.distinct,
|
|
474
|
+
staleOn: approval.staleOn,
|
|
475
|
+
quorum: approval.quorum,
|
|
476
|
+
ttl: approval.ttl,
|
|
477
|
+
escalation
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function assertQuorum(quorum, name, label) {
|
|
481
|
+
if (quorum !== void 0 && (!Number.isInteger(quorum) || quorum < 1)) throw new Error(`PermDock: ${name} on '${label}' must be an integer of at least 1, got ${String(quorum)}`);
|
|
482
|
+
}
|
|
483
|
+
function normalizeStages(approval, mode, label) {
|
|
484
|
+
if (mode !== "all" && mode !== "sequential") throw new Error(`PermDock: approval.mode on '${label}' must be 'any', 'all' or 'sequential', got '${mode}'`);
|
|
485
|
+
if (approval.by !== void 0 || approval.quorum !== void 0) throw new Error(`PermDock: approval on '${label}' sets mode '${mode}': put by and quorum on each stage`);
|
|
486
|
+
const stages = approval.stages ?? [];
|
|
487
|
+
if (stages.length === 0) throw new Error(`PermDock: approval mode '${mode}' on '${label}' needs at least one stage`);
|
|
488
|
+
return stages.map((stage, index) => {
|
|
489
|
+
assertQuorum(stage.quorum, `approval.stages[${index}].quorum`, label);
|
|
490
|
+
const by = asApprover(stage.by);
|
|
491
|
+
if (flattenApprovers(by).length === 0) throw new Error(`PermDock: approval.stages[${index}].by on '${label}' names no approver`);
|
|
492
|
+
return compact({
|
|
493
|
+
by,
|
|
494
|
+
quorum: stage.quorum
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function normalizeEscalation(escalation, label) {
|
|
499
|
+
if (escalation === void 0) return;
|
|
500
|
+
if (parseDuration(escalation.after) === void 0) throw new Error(`PermDock: approval.escalation.after on '${label}' must be a duration such as '4h', got '${escalation.after}'`);
|
|
501
|
+
return {
|
|
502
|
+
after: escalation.after,
|
|
503
|
+
to: asApprover(escalation.to)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function isClosure(value) {
|
|
507
|
+
return typeof value === "function";
|
|
508
|
+
}
|
|
509
|
+
function flattenPermissions(input) {
|
|
510
|
+
if (isPermission(input)) return [input];
|
|
511
|
+
if (isReadonlyArray(input)) return input.flatMap((item) => flattenPermissions(item));
|
|
512
|
+
return [...listPermissions(input)];
|
|
513
|
+
}
|
|
514
|
+
function delegationTarget(to, index) {
|
|
515
|
+
if (typeof to === "string") return { kind: to };
|
|
516
|
+
if ("actor" in to) return { kind: to.actor };
|
|
517
|
+
const target = compact({
|
|
518
|
+
kind: to.kind,
|
|
519
|
+
id: to.id
|
|
520
|
+
});
|
|
521
|
+
if (typeof target.kind !== "string" || target.kind === "") throw new Error(`PermDock: delegations[${index}].to needs an actor kind, such as actor('eve')`);
|
|
522
|
+
if (target.id !== void 0 && (typeof target.id !== "string" || target.id === "")) throw new Error(`PermDock: delegations[${index}].to.id must be a non-empty string`);
|
|
523
|
+
return target;
|
|
524
|
+
}
|
|
525
|
+
function normalizeDelegation(input, index, tree) {
|
|
526
|
+
const label = `delegations[${index}]`;
|
|
527
|
+
const from = asGrantee(input.from);
|
|
528
|
+
const items = flattenGrantee(from);
|
|
529
|
+
if (items.length === 0) throw new Error(`PermDock: ${label}.from names nobody`);
|
|
530
|
+
if (items.some((item) => item.kind === "relation")) throw new Error(`PermDock: ${label}.from names a relation; a delegation is matched without a row, so name a role or another subject-only grantee`);
|
|
531
|
+
if (items.some((item) => item.kind === "actor")) throw new Error(`PermDock: ${label}.from names an actor; the principal hands over, the actor is \`to\``);
|
|
532
|
+
const leaves = /* @__PURE__ */ new Map();
|
|
533
|
+
for (const leaf of input.permissions.flatMap((item) => flattenPermissions(item))) {
|
|
534
|
+
if (findPermission(tree, leaf.key) === void 0) throw new Error(`PermDock: ${label} names unknown permission '${leaf.key}'`);
|
|
535
|
+
leaves.set(leaf.key, leaf);
|
|
536
|
+
}
|
|
537
|
+
const keys = [...leaves.keys()].toSorted();
|
|
538
|
+
if (keys.length === 0) throw new Error(`PermDock: ${label}.permissions is empty`);
|
|
539
|
+
return compact({
|
|
540
|
+
from,
|
|
541
|
+
to: delegationTarget(input.to, index),
|
|
542
|
+
permissions: keys,
|
|
543
|
+
readOnly: [...leaves.values()].filter((leaf) => annotationsFor(leaf).readOnlyHint).map((leaf) => leaf.key).toSorted(),
|
|
544
|
+
validity: normalizeValidity(input, label)
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function resolveRoleScope(on) {
|
|
548
|
+
if (on === void 0) return "global";
|
|
549
|
+
if (typeof on === "string") {
|
|
550
|
+
if (on === "global" || on === "resource") throw new Error(`PermDock: role on: '${on}' is not a scope name`);
|
|
551
|
+
return on;
|
|
552
|
+
}
|
|
553
|
+
const permissions = flattenPermissions(on);
|
|
554
|
+
const names = new Set(permissions.map((permission) => permission.resource));
|
|
555
|
+
const [resource] = names;
|
|
556
|
+
if (names.size !== 1 || resource === void 0) throw new Error("PermDock: role on: resource must name exactly one resource");
|
|
557
|
+
return { resource };
|
|
558
|
+
}
|
|
559
|
+
function makeGrant(permission, effect, condition) {
|
|
560
|
+
const toInput = isClosure(condition) ? void 0 : condition?.to;
|
|
561
|
+
const to = toInput === void 0 ? {
|
|
562
|
+
kind: "role",
|
|
563
|
+
role: "",
|
|
564
|
+
scope: "global"
|
|
565
|
+
} : asGrantee(toInput);
|
|
566
|
+
if (isClosure(condition)) return {
|
|
567
|
+
permission,
|
|
568
|
+
effect,
|
|
569
|
+
to,
|
|
570
|
+
portable: false,
|
|
571
|
+
closure: condition
|
|
572
|
+
};
|
|
573
|
+
const whereInput = condition?.where;
|
|
574
|
+
const checkInput = condition?.check;
|
|
575
|
+
if (whereInput !== void 0 && permission.kind === "collection") throw new Error(`PermDock: where is not allowed on collection action '${permission.key}'`);
|
|
576
|
+
const where = whereInput === void 0 ? void 0 : normalizeWhere(whereInput);
|
|
577
|
+
const check = checkInput === void 0 ? void 0 : normalizeWhere(checkInput);
|
|
578
|
+
assertLimit(condition?.limit, permission.key);
|
|
579
|
+
const purpose = condition?.purpose === void 0 || condition.purpose.length === 0 ? void 0 : Object.freeze([...new Set(condition.purpose)]);
|
|
580
|
+
const portable = condition?.limit === void 0 && purpose === void 0;
|
|
581
|
+
return compact({
|
|
582
|
+
permission,
|
|
583
|
+
effect,
|
|
584
|
+
to,
|
|
585
|
+
where,
|
|
586
|
+
check,
|
|
587
|
+
approval: normalizeApproval(condition?.approval, permission.key),
|
|
588
|
+
portable,
|
|
589
|
+
limit: normalizeLimit(condition?.limit),
|
|
590
|
+
fields: sanitizeFields(condition?.fields),
|
|
591
|
+
name: condition?.name,
|
|
592
|
+
purpose,
|
|
593
|
+
validity: normalizeValidity(condition ?? {}, permission.key)
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
function allow(permission, condition) {
|
|
597
|
+
const grants = flattenPermissions(permission).map((leaf) => makeGrant(leaf, "allow", condition));
|
|
598
|
+
return sole(grants) ?? grants;
|
|
599
|
+
}
|
|
600
|
+
function deny(permission, condition) {
|
|
601
|
+
const grants = flattenPermissions(permission).map((leaf) => makeGrant(leaf, "deny", condition));
|
|
602
|
+
return sole(grants) ?? grants;
|
|
603
|
+
}
|
|
604
|
+
function flattenGrants(grants) {
|
|
605
|
+
const out = [];
|
|
606
|
+
for (const grant of grants) if (Array.isArray(grant)) out.push(...grant);
|
|
607
|
+
else out.push(grant);
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
function role(name, grants, options) {
|
|
611
|
+
if (options !== void 0 && Object.hasOwn(options, "restricted")) throw new Error(`PermDock: role option 'restricted' is reserved`);
|
|
612
|
+
const leaf = isRole(name) ? name : void 0;
|
|
613
|
+
const roleName = leaf?.key ?? name;
|
|
614
|
+
const scope = resolveRoleScope(options?.on ?? leaf?.on);
|
|
615
|
+
const assignable = options?.assignable ?? leaf?.assignable ?? scope !== "global";
|
|
616
|
+
const rules = roleRules(roleName, scope, options);
|
|
617
|
+
const activation = normalizeActivation(roleName, options?.activation);
|
|
618
|
+
const roleGrantee = asGrantee(freezeDeep({
|
|
619
|
+
kind: "role",
|
|
620
|
+
role: roleName,
|
|
621
|
+
scope
|
|
622
|
+
}));
|
|
623
|
+
const normalised = flattenGrants(grants).map((grant) => {
|
|
624
|
+
const items = flattenGrantee(grant.to);
|
|
625
|
+
const first = items[0];
|
|
626
|
+
const to = items.length === 1 && first?.kind === "role" ? roleGrantee : grant.to;
|
|
627
|
+
return freezeDeep({
|
|
628
|
+
...grant,
|
|
629
|
+
to,
|
|
630
|
+
role: roleName,
|
|
631
|
+
scope: roleScopeOf(to, scope)
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
return freezeDeep(compact({
|
|
635
|
+
name: roleName,
|
|
636
|
+
grants: normalised,
|
|
637
|
+
on: options?.on ?? leaf?.on,
|
|
638
|
+
assignable,
|
|
639
|
+
exclusiveWith: options?.exclusiveWith,
|
|
640
|
+
...rules,
|
|
641
|
+
meta: options?.meta ?? leaf?.meta,
|
|
642
|
+
activation
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* A role with `activation` is eligible-only: it needs a positive `maxDuration`
|
|
647
|
+
* (Doctor PD033 warns without one). `justification` defaults to `'optional'`.
|
|
648
|
+
*/
|
|
649
|
+
function activationApproval(option, roleName) {
|
|
650
|
+
const label = `activation of '${roleName}'`;
|
|
651
|
+
const approval = normalizeApproval(option, label);
|
|
652
|
+
if (approval !== void 0 && approval !== "human" && approversOf(approval).some((item) => item.kind === "relation")) throw new Error(`PermDock: approval on ${label} names a relation; an activation has no row to read it on`);
|
|
653
|
+
return approval;
|
|
654
|
+
}
|
|
655
|
+
function normalizeActivation(roleName, option) {
|
|
656
|
+
if (option === void 0) return;
|
|
657
|
+
const justification = option.justification ?? "optional";
|
|
658
|
+
if (justification !== "required" && justification !== "optional") throw new Error(`PermDock: role '${roleName}' activation justification must be 'required' or 'optional'`);
|
|
659
|
+
return compact({
|
|
660
|
+
maxDuration: option.maxDuration,
|
|
661
|
+
justification,
|
|
662
|
+
approval: activationApproval(option.approval, roleName),
|
|
663
|
+
assurance: normalizeAssurance(option.assurance)
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function normalizeAssurance(input) {
|
|
667
|
+
if (input === void 0) return;
|
|
668
|
+
const acr = input.acr === void 0 || input.acr.length === 0 ? void 0 : Object.freeze([...input.acr]);
|
|
669
|
+
const amr = input.amr === void 0 || input.amr.length === 0 ? void 0 : Object.freeze([...input.amr]);
|
|
670
|
+
const maxAge = typeof input.maxAge === "number" && input.maxAge >= 0 ? input.maxAge : void 0;
|
|
671
|
+
if (acr === void 0 && amr === void 0 && maxAge === void 0) return;
|
|
672
|
+
return compact({
|
|
673
|
+
maxAge,
|
|
674
|
+
acr,
|
|
675
|
+
amr
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function nameList(roleName, option, value) {
|
|
679
|
+
if (value === void 0) return;
|
|
680
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "" || isForbiddenKey(item))) throw new Error(`PermDock: role '${roleName}' ${option} must be a list of names`);
|
|
681
|
+
return [...new Set(value)];
|
|
682
|
+
}
|
|
683
|
+
function holderCount(roleName, option, value) {
|
|
684
|
+
if (value === void 0) return;
|
|
685
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`PermDock: role '${roleName}' ${option} must be a whole number of holders`);
|
|
686
|
+
return value;
|
|
687
|
+
}
|
|
688
|
+
/** Ownership rules count holders per scope instance, so they need a named-scope role. */
|
|
689
|
+
function roleRules(roleName, scope, options) {
|
|
690
|
+
const min = holderCount(roleName, "min", options?.min);
|
|
691
|
+
const max = holderCount(roleName, "max", options?.max);
|
|
692
|
+
const transferOnly = options?.transferOnly;
|
|
693
|
+
if (transferOnly !== void 0 && typeof transferOnly !== "boolean") throw new Error(`PermDock: role '${roleName}' transferOnly must be a boolean`);
|
|
694
|
+
if (max !== void 0 && max < 1) throw new Error(`PermDock: role '${roleName}' max must be at least 1`);
|
|
695
|
+
if (min !== void 0 && max !== void 0 && min > max) throw new Error(`PermDock: role '${roleName}' min must not exceed max`);
|
|
696
|
+
if ((min !== void 0 && min > 0 || max !== void 0 || transferOnly === true) && (typeof scope !== "string" || scope === "global")) throw new Error(`PermDock: role '${roleName}' min, max and transferOnly need on: '<scope>'`);
|
|
697
|
+
return compact({
|
|
698
|
+
min,
|
|
699
|
+
max,
|
|
700
|
+
transferOnly,
|
|
701
|
+
assigns: nameList(roleName, "assigns", options?.assigns),
|
|
702
|
+
for: nameList(roleName, "for", options?.for)
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function canonicalPolicy(grants, delegations) {
|
|
706
|
+
const payload = canonicalGrants(grants);
|
|
707
|
+
if (delegations.length === 0) return payload;
|
|
708
|
+
return `${payload}\n${JSON.stringify(delegations)}`;
|
|
709
|
+
}
|
|
710
|
+
function canonicalGrants(grants) {
|
|
711
|
+
const payload = grants.map((grant) => ({
|
|
712
|
+
permission: grant.permission.key,
|
|
713
|
+
effect: grant.effect,
|
|
714
|
+
to: grant.to,
|
|
715
|
+
role: grant.role,
|
|
716
|
+
where: grant.where,
|
|
717
|
+
check: grant.check,
|
|
718
|
+
approval: grant.approval,
|
|
719
|
+
portable: grant.portable,
|
|
720
|
+
fields: grant.fields,
|
|
721
|
+
scope: grant.scope,
|
|
722
|
+
name: grant.name,
|
|
723
|
+
purpose: grant.purpose,
|
|
724
|
+
breakGlass: grant.breakGlass,
|
|
725
|
+
viaOnly: grant.viaOnly,
|
|
726
|
+
validity: grant.validity
|
|
727
|
+
}));
|
|
728
|
+
return JSON.stringify(payload);
|
|
729
|
+
}
|
|
730
|
+
function assertParentGraph(resources) {
|
|
731
|
+
for (const node of resources.values()) {
|
|
732
|
+
if (node.parent === void 0) continue;
|
|
733
|
+
if (!resources.has(node.parent.resource)) throw new Error(`PermDock: resource '${node.name}' parents unknown resource '${node.parent.resource}'`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Graph relation grantees must reach their resource: `through: 'parent'` from
|
|
738
|
+
* the row to its parent resource or along its own self-parent, and never over
|
|
739
|
+
* a `memberOf` relation, which is tenancy, not the object graph.
|
|
740
|
+
*/
|
|
741
|
+
function assertRelationGrants(grants, resources) {
|
|
742
|
+
for (const grant of grants) for (const item of flattenGrantee(grant.to)) {
|
|
743
|
+
if (item.kind !== "relation" || !isGraphRelation(item, resources)) continue;
|
|
744
|
+
const label = `grant ${grant.permission.key} relation '${item.relation}'`;
|
|
745
|
+
const target = resources.get(item.resource);
|
|
746
|
+
const spec = target?.relations[item.relation];
|
|
747
|
+
if (target === void 0 || spec === void 0) throw new Error(`PermDock: ${label}: ${item.resource} does not declare it`);
|
|
748
|
+
if (isFieldRelation(spec) && spec.memberOf !== void 0) throw new Error(`PermDock: ${label} is a memberOf relation; through walks the object graph, not scopes`);
|
|
749
|
+
const row = resources.get(grant.permission.resource);
|
|
750
|
+
if (Array.isArray(item.through)) {
|
|
751
|
+
if (relationHops(item, row, resources) === void 0) throw new Error(`PermDock: ${label}: the links [${item.through.join(", ")}] from ${grant.permission.resource} do not end on ${target.name}${(item.depth ?? 0) > 0 ? ` (or ${target.name} does not parent itself for depth)` : ""}`);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (item.through === "parent" && target.name === row?.name && !isSelfParented(target)) throw new Error(`PermDock: ${label}: through: 'parent' needs ${target.name} to parent itself`);
|
|
755
|
+
if (relationStart(item, row, target) === void 0) throw new Error(item.through === void 0 ? `PermDock: ${label} is on ${target.name}, not ${grant.permission.resource}; add through: 'parent'` : `PermDock: ${label}: ${grant.permission.resource} has no parent on ${target.name}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function scopeOfGrant(scope, declared) {
|
|
759
|
+
if (typeof scope !== "string" || scope === "global") return scope;
|
|
760
|
+
const resolved = resolveScope(declared, scope);
|
|
761
|
+
if (resolved === void 0) throw new Error(`PermDock: definePolicy({ scopes }) must declare '${scope}' for on: '${scope}' roles`);
|
|
762
|
+
return resolved;
|
|
763
|
+
}
|
|
764
|
+
function rescopeGrantee(to, declared) {
|
|
765
|
+
const items = flattenGrantee(to);
|
|
766
|
+
if (!items.some((item) => item.kind === "role")) return to;
|
|
767
|
+
const mapped = items.map((item) => item.kind === "role" ? freezeDeep({
|
|
768
|
+
...item,
|
|
769
|
+
scope: scopeOfGrant(item.scope, declared)
|
|
770
|
+
}) : item);
|
|
771
|
+
if (Array.isArray(to)) return freezeDeep(mapped);
|
|
772
|
+
const [single] = mapped;
|
|
773
|
+
return single ?? to;
|
|
774
|
+
}
|
|
775
|
+
/** Resolves `tenant` / `team` aliases to declared names; an undeclared scope throws. */
|
|
776
|
+
function rescopeGrant(grant, declared) {
|
|
777
|
+
const scope = scopeOfGrant(grant.scope, declared);
|
|
778
|
+
const to = rescopeGrantee(grant.to, declared);
|
|
779
|
+
if (scope === grant.scope && to === grant.to) return grant;
|
|
780
|
+
return freezeDeep({
|
|
781
|
+
...grant,
|
|
782
|
+
scope,
|
|
783
|
+
to
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function rescopeBinding(binding, declared) {
|
|
787
|
+
const grants = binding.grants.map((grant) => rescopeGrant(grant, declared));
|
|
788
|
+
const on = typeof binding.on === "string" ? scopeOfGrant(binding.on, declared) : binding.on;
|
|
789
|
+
return freezeDeep(compact({
|
|
790
|
+
...binding,
|
|
791
|
+
grants,
|
|
792
|
+
on
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* No implicit cascade: a grant on scope S reaches a row only through the row's
|
|
797
|
+
* own S key, so every resource an instance grant on S touches declares a
|
|
798
|
+
* `memberOf: S` relation on that key. Without it `where()` and RLS could not
|
|
799
|
+
* narrow the rows.
|
|
800
|
+
*/
|
|
801
|
+
function assertScopeKeys(grants, scopes, resources) {
|
|
802
|
+
for (const grant of grants) {
|
|
803
|
+
if (typeof grant.scope !== "string" || grant.scope === "global" || grant.permission.kind !== "instance") continue;
|
|
804
|
+
const key = scopes.find((scope) => scope.name === grant.scope)?.key;
|
|
805
|
+
const node = resources.get(grant.permission.resource);
|
|
806
|
+
if (key === void 0 || node === void 0 || relatesTo(node, key, grant.scope, scopes)) continue;
|
|
807
|
+
throw new Error(`PermDock: resource '${node.name}' needs relations: { <name>: { field: '${key}', memberOf: '${grant.scope}' } } for ${grant.permission.key} on '${grant.scope}' roles`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* A stale-on-change approval needs a row to version: an instance permission
|
|
812
|
+
* whose resource declares `version`.
|
|
813
|
+
*/
|
|
814
|
+
function assertApprovalVersions(grants, resources) {
|
|
815
|
+
for (const grant of grants) {
|
|
816
|
+
const approval = grant.approval;
|
|
817
|
+
if (approval === void 0 || approval === "human" || approval.staleOn === void 0) continue;
|
|
818
|
+
if (grant.permission.kind !== "instance") throw new Error(`PermDock: approval staleOn on '${grant.permission.key}' needs an instance action; a collection action has no row to version`);
|
|
819
|
+
if (resources.get(grant.permission.resource)?.version === void 0) throw new Error(`PermDock: approval staleOn on '${grant.permission.key}' needs resource(..., { version: '<field>' }) on '${grant.permission.resource}'`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* A relation approver is checked by id, from the requested row: the
|
|
824
|
+
* relation lives on the permission's own resource (optionally up its parent
|
|
825
|
+
* chain), or at the end of a list of links from it.
|
|
826
|
+
*/
|
|
827
|
+
function assertApprovalRelations(grants, resources) {
|
|
828
|
+
for (const grant of grants) {
|
|
829
|
+
const approval = grant.approval;
|
|
830
|
+
if (approval === void 0 || approval === "human") continue;
|
|
831
|
+
for (const item of approversOf(approval)) {
|
|
832
|
+
if (item.kind !== "relation") continue;
|
|
833
|
+
const label = `${grant.permission.key}: approver relation '${item.relation}'`;
|
|
834
|
+
if (grant.permission.kind !== "instance") throw new Error(`PermDock: ${label} needs an instance action; a collection action has no row to read it on`);
|
|
835
|
+
if (approverRelationResource(grant.permission.resource, item.through, resources) !== item.resource) throw new Error(`PermDock: ${label} must live on '${grant.permission.resource}' or at the end of its through links`);
|
|
836
|
+
if (resources.get(item.resource)?.relations[item.relation] === void 0) throw new Error(`PermDock: ${label} is not declared on '${item.resource}'`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/** The resource a relation approver's `through` reaches from `start`, or `undefined`. */
|
|
841
|
+
function approverRelationResource(start, through, resources) {
|
|
842
|
+
if (through === void 0) return start;
|
|
843
|
+
if (through === "parent") return isSelfParented(resources.get(start)) ? start : void 0;
|
|
844
|
+
let current = start;
|
|
845
|
+
for (const name of through) {
|
|
846
|
+
const node = current === void 0 ? void 0 : resources.get(current);
|
|
847
|
+
current = node !== void 0 && Object.hasOwn(node.links, name) ? node.links[name]?.resource : void 0;
|
|
848
|
+
}
|
|
849
|
+
return current;
|
|
850
|
+
}
|
|
851
|
+
function isVocabularyInput(value) {
|
|
852
|
+
return value !== null && typeof value === "object" && "permissions" in value && isRegistryTree(value.permissions);
|
|
853
|
+
}
|
|
854
|
+
function completeGrant(grant) {
|
|
855
|
+
const to = flattenGrantee(grant.to);
|
|
856
|
+
const first = to[0];
|
|
857
|
+
if (to.length === 1 && first?.kind === "role" && first.role === "") throw new Error("PermDock: grant is missing to");
|
|
858
|
+
return freezeDeep({
|
|
859
|
+
...grant,
|
|
860
|
+
role: roleNameOf(grant.to),
|
|
861
|
+
scope: roleScopeOf(grant.to)
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
function mergeBindings(items) {
|
|
865
|
+
const merged = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const item of items) {
|
|
867
|
+
const existing = merged.get(item.name);
|
|
868
|
+
if (existing === void 0) {
|
|
869
|
+
merged.set(item.name, item);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
merged.set(item.name, freezeDeep(compact({
|
|
873
|
+
name: item.name,
|
|
874
|
+
grants: [...existing.grants, ...item.grants],
|
|
875
|
+
on: existing.on,
|
|
876
|
+
assignable: existing.assignable,
|
|
877
|
+
exclusiveWith: existing.exclusiveWith ?? item.exclusiveWith,
|
|
878
|
+
min: existing.min ?? item.min,
|
|
879
|
+
max: existing.max ?? item.max,
|
|
880
|
+
transferOnly: existing.transferOnly ?? item.transferOnly,
|
|
881
|
+
assigns: existing.assigns ?? item.assigns,
|
|
882
|
+
for: existing.for ?? item.for,
|
|
883
|
+
meta: existing.meta ?? item.meta,
|
|
884
|
+
activation: existing.activation ?? item.activation,
|
|
885
|
+
support: existing.support ?? item.support
|
|
886
|
+
})));
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
roles: [...merged.values()],
|
|
890
|
+
rolesByName: merged
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function definePolicy(permissions, options) {
|
|
894
|
+
const vocabulary = isVocabularyInput(permissions) ? permissions : { permissions };
|
|
895
|
+
const tree = vocabulary.permissions;
|
|
896
|
+
const mapper = options.principal ?? options.subject;
|
|
897
|
+
if (mapper === void 0) throw new Error("PermDock: definePolicy requires principal or subject");
|
|
898
|
+
const resources = getRegistry(tree);
|
|
899
|
+
assertParentGraph(resources);
|
|
900
|
+
const scopes = defineScopes(options.scopes);
|
|
901
|
+
const { roles, rolesByName } = mergeBindings((options.roles ?? []).map((binding) => rescopeBinding(binding, scopes)));
|
|
902
|
+
const declared = new Set(roles.map((item) => item.name));
|
|
903
|
+
for (const leaf of listRoles(vocabulary.roles)) declared.add(leaf.key);
|
|
904
|
+
for (const binding of roles) for (const name of binding.assigns ?? []) if (!declared.has(name)) throw new Error(`PermDock: role '${binding.name}' names undeclared role '${name}'`);
|
|
905
|
+
const fromBindings = roles.flatMap((item) => item.grants);
|
|
906
|
+
const fromGrants = flattenGrants(options.grants ?? []).map(completeGrant).map((grant) => rescopeGrant(grant, scopes));
|
|
907
|
+
const grants = [...fromBindings, ...fromGrants];
|
|
908
|
+
assertScopeKeys(grants, scopes, resources);
|
|
909
|
+
assertApprovalVersions(grants, resources);
|
|
910
|
+
assertApprovalRelations(grants, resources);
|
|
911
|
+
assertRelationGrants(grants, resources);
|
|
912
|
+
const delegations = (options.delegations ?? []).map((item, index) => normalizeDelegation(item, index, tree));
|
|
913
|
+
const fingerprint = bytesToBase64Url(sha256(canonicalPolicy(grants, delegations)));
|
|
914
|
+
const hostable = [...new Set((options.hostable ?? []).flatMap((item) => flattenPermissions(item).map((leaf) => leaf.key)))].toSorted();
|
|
915
|
+
const fresh = [...new Set((options.fresh ?? []).flatMap((item) => flattenPermissions(item).map((leaf) => leaf.key)))].toSorted();
|
|
916
|
+
return freezeDeep({
|
|
917
|
+
permissions: tree,
|
|
918
|
+
roles,
|
|
919
|
+
rolesByName,
|
|
920
|
+
grants,
|
|
921
|
+
vocabulary,
|
|
922
|
+
scopes,
|
|
923
|
+
principal: mapper,
|
|
924
|
+
subject: mapper,
|
|
925
|
+
context: options.context,
|
|
926
|
+
validate: options.validate ?? "boundary",
|
|
927
|
+
onDenied: options.onDenied,
|
|
928
|
+
fingerprint,
|
|
929
|
+
resources,
|
|
930
|
+
providers: options.providers,
|
|
931
|
+
hostable,
|
|
932
|
+
fresh,
|
|
933
|
+
delegations: delegations.length === 0 ? void 0 : delegations,
|
|
934
|
+
index: indexPolicy(roles, grants, vocabulary)
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
function exclusivePairs(policy) {
|
|
938
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const binding of policy.roles) if (binding.exclusiveWith !== void 0) pairs.set(binding.name, binding.exclusiveWith);
|
|
940
|
+
return pairs;
|
|
941
|
+
}
|
|
942
|
+
function grantList(policy) {
|
|
943
|
+
if (policy.grants !== void 0 && policy.grants.length > 0) return policy.grants;
|
|
944
|
+
return policy.roles.flatMap((binding) => binding.grants);
|
|
945
|
+
}
|
|
946
|
+
function declaredRoleNames(policy) {
|
|
947
|
+
return policy.index.declaredRoles;
|
|
948
|
+
}
|
|
949
|
+
function separationConflicts(policy, memberships) {
|
|
950
|
+
const exclusive = exclusivePairs(policy);
|
|
951
|
+
const seen = /* @__PURE__ */ new Set();
|
|
952
|
+
const conflicts = [];
|
|
953
|
+
for (const membership of memberships) {
|
|
954
|
+
const held = new Set(membership.roles);
|
|
955
|
+
for (const name of held) for (const other of exclusive.get(name) ?? []) {
|
|
956
|
+
if (!held.has(other)) continue;
|
|
957
|
+
const pair = [name, other].toSorted();
|
|
958
|
+
const key = `${membership.principal ?? ""}:${membership.tenant ?? ""}:${pair.join("+")}`;
|
|
959
|
+
if (seen.has(key)) continue;
|
|
960
|
+
seen.add(key);
|
|
961
|
+
conflicts.push(compact({
|
|
962
|
+
principal: membership.principal ?? "",
|
|
963
|
+
tenant: membership.tenant,
|
|
964
|
+
roles: pair
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return conflicts;
|
|
969
|
+
}
|
|
970
|
+
//#endregion
|
|
971
|
+
export { assertSqlFunctionArg as _, definePolicy as a, indexPolicy as c, role as d, separationConflicts as f, assertPortableTwin as g, normalizeWhere as h, declaredRoleNames as i, normalizeApproval as l, memoryLimitStore as m, assertApprovalRelations as n, deny as o, applyQuota as p, completeGrant as r, grantList as s, allow as t, normalizeAssurance as u, assertSqlFunctionName as v, opaque as y };
|