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,796 @@
|
|
|
1
|
+
import { t as compact } from "./compact-y6vfu2_f.js";
|
|
2
|
+
import { n as freezeDeep } from "./freeze-y1_jQL4v.js";
|
|
3
|
+
import { t as ignoreRejection } from "./thenable-okc2xDsj.js";
|
|
4
|
+
import { t as anonymousSubject } from "./subject-C6dLxqHU.js";
|
|
5
|
+
import { o as parseCapability } from "./capability-B68OFnZx.js";
|
|
6
|
+
import { t as loadJose } from "./load-jose-BL6LH4bL.js";
|
|
7
|
+
import "./budget-NbEtbJC_.js";
|
|
8
|
+
import { a as quoteSqlTable, i as quoteSqlLiteral, r as quoteSqlIdent } from "./sql-Bll1d7y0.js";
|
|
9
|
+
import "./sources-DVHM3Mwq.js";
|
|
10
|
+
//#region src/supabase/capability.ts
|
|
11
|
+
const DEFAULT_TTL = 300;
|
|
12
|
+
const MAX_TTL = 3600;
|
|
13
|
+
function isSecret(key) {
|
|
14
|
+
return typeof key.secret === "string";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Exchanges a verified link subject (from `subjectFromCapability`) for a
|
|
18
|
+
* short-lived Supabase access token with role `anon` and the capability in a
|
|
19
|
+
* `capability` claim, which `permdock rls generate --capabilities` policies
|
|
20
|
+
* read. `undefined` for any subject that is not a live link.
|
|
21
|
+
*/
|
|
22
|
+
async function exchangeCapability(subject, options) {
|
|
23
|
+
const alg = options.alg ?? "ES256";
|
|
24
|
+
const secret = isSecret(options.key);
|
|
25
|
+
if (secret !== (alg === "HS256")) throw new Error("PermDock: exchangeCapability signs HS256 with { secret } only, and ES256 / RS256 with a private JWK only.");
|
|
26
|
+
if (!secret && (options.kid === void 0 || options.kid.length === 0)) throw new Error("PermDock: exchangeCapability needs the kid of the imported signing key.");
|
|
27
|
+
const principal = subject.principal;
|
|
28
|
+
if (principal === null || principal.kind !== "link") return;
|
|
29
|
+
const capability = parseCapability(principal["capability"]);
|
|
30
|
+
if (capability?.holder !== "link" || capability.id !== principal.id) return;
|
|
31
|
+
const now = Math.floor(options.now ?? Date.now() / 1e3);
|
|
32
|
+
const ttl = Math.min(Math.max(1, options.ttl ?? DEFAULT_TTL), MAX_TTL);
|
|
33
|
+
const exp = Math.min(now + ttl, Math.floor(capability.expiresAt));
|
|
34
|
+
if (exp <= now) return;
|
|
35
|
+
const jose = await loadJose();
|
|
36
|
+
const key = isSecret(options.key) ? new TextEncoder().encode(options.key.secret) : await jose.importJWK(options.key, alg);
|
|
37
|
+
const jwt = new jose.SignJWT({
|
|
38
|
+
role: "anon",
|
|
39
|
+
capability
|
|
40
|
+
}).setProtectedHeader(secret ? {
|
|
41
|
+
alg: "HS256",
|
|
42
|
+
typ: "JWT"
|
|
43
|
+
} : {
|
|
44
|
+
alg,
|
|
45
|
+
...options.kid === void 0 ? {} : { kid: options.kid },
|
|
46
|
+
typ: "JWT"
|
|
47
|
+
}).setIssuedAt(now).setExpirationTime(exp);
|
|
48
|
+
if (options.issuer !== void 0) jwt.setIssuer(options.issuer);
|
|
49
|
+
return jwt.sign(key);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/supabase/claims.ts
|
|
53
|
+
const KNOWN = /* @__PURE__ */ new Set([
|
|
54
|
+
"user_role",
|
|
55
|
+
"roles",
|
|
56
|
+
"memberships",
|
|
57
|
+
"memberships_truncated",
|
|
58
|
+
"attrs",
|
|
59
|
+
"authz_ver",
|
|
60
|
+
"client_id",
|
|
61
|
+
"scope",
|
|
62
|
+
"act",
|
|
63
|
+
"app_metadata"
|
|
64
|
+
]);
|
|
65
|
+
function isRecord$1(value) {
|
|
66
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
function own(record, key) {
|
|
69
|
+
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
70
|
+
}
|
|
71
|
+
function issue(issues, path, message) {
|
|
72
|
+
issues.push({
|
|
73
|
+
message,
|
|
74
|
+
path: [...path]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function isStrings(value) {
|
|
78
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
79
|
+
}
|
|
80
|
+
function optional(record, key, path, issues, check, expected) {
|
|
81
|
+
const value = own(record, key);
|
|
82
|
+
if (value !== void 0 && !check(value)) issue(issues, [...path, key], `Expected ${expected}`);
|
|
83
|
+
}
|
|
84
|
+
const isString = (value) => typeof value === "string";
|
|
85
|
+
function checkMembership(item, path, issues) {
|
|
86
|
+
if (!isRecord$1(item)) {
|
|
87
|
+
issue(issues, path, "Expected a membership object");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const roles = own(item, "roles");
|
|
91
|
+
if (!isStrings(roles) || roles.length === 0) issue(issues, [...path, "roles"], "Expected a non-empty array of strings");
|
|
92
|
+
for (const key of [
|
|
93
|
+
"scope",
|
|
94
|
+
"id",
|
|
95
|
+
"tenant",
|
|
96
|
+
"team",
|
|
97
|
+
"via",
|
|
98
|
+
"grantedBy",
|
|
99
|
+
"reason"
|
|
100
|
+
]) optional(item, key, path, issues, isString, "a string");
|
|
101
|
+
optional(item, "within", path, issues, (value) => isRecord$1(value) && Object.values(value).every((id) => typeof id === "string"), "an object of string ids");
|
|
102
|
+
optional(item, "on", path, issues, (value) => isRecord$1(value) && typeof own(value, "resource") === "string" && typeof own(value, "id") === "string", "an object with string resource and id");
|
|
103
|
+
optional(item, "expiresAt", path, issues, (value) => typeof value === "number" && Number.isFinite(value), "a number of seconds since the epoch");
|
|
104
|
+
optional(item, "member", path, issues, (value) => isRecord$1(value) && typeof own(value, "group") === "string" && own(value, "group") !== "", "an object with a non-empty string group");
|
|
105
|
+
optional(item, "managedBy", path, issues, (value) => value === "idp", "'idp'");
|
|
106
|
+
optional(item, "entitlements", path, issues, isStrings, "an array of strings");
|
|
107
|
+
optional(item, "grants", path, issues, isRecord$1, "an object");
|
|
108
|
+
if (!(own(item, "scope") !== void 0 && own(item, "id") !== void 0 || own(item, "tenant") !== void 0 || own(item, "team") !== void 0 || own(item, "on") !== void 0)) issue(issues, path, "Expected scope and id, tenant, team or on");
|
|
109
|
+
}
|
|
110
|
+
function checkPermDockClaims(record, tenantClaim, path, issues) {
|
|
111
|
+
optional(record, "user_role", path, issues, (value) => value === null || typeof value === "string" || isStrings(value), "a string, an array of strings or null");
|
|
112
|
+
optional(record, "roles", path, issues, isStrings, "an array of strings");
|
|
113
|
+
const memberships = own(record, "memberships");
|
|
114
|
+
if (memberships !== void 0) {
|
|
115
|
+
if (Array.isArray(memberships)) for (const [index, item] of memberships.entries()) checkMembership(item, [
|
|
116
|
+
...path,
|
|
117
|
+
"memberships",
|
|
118
|
+
index
|
|
119
|
+
], issues);
|
|
120
|
+
else issue(issues, [...path, "memberships"], "Expected an array");
|
|
121
|
+
}
|
|
122
|
+
optional(record, "memberships_truncated", path, issues, (value) => typeof value === "boolean", "a boolean");
|
|
123
|
+
optional(record, tenantClaim, path, issues, isString, "a string");
|
|
124
|
+
optional(record, "attrs", path, issues, isRecord$1, "an object");
|
|
125
|
+
optional(record, "authz_ver", path, issues, (value) => Number.isInteger(value), "an integer");
|
|
126
|
+
}
|
|
127
|
+
/** The outer level's `kind`: none, `support` with a `session_id`, or `impersonation`. */
|
|
128
|
+
function checkActKind(level, path, issues) {
|
|
129
|
+
const marked = own(level, "kind");
|
|
130
|
+
const kind = marked === void 0 && own(level, "session_id") !== void 0 ? "support" : marked;
|
|
131
|
+
optional(level, "reason", path, issues, isString, "a string");
|
|
132
|
+
if (kind === void 0 || kind === "impersonation") return;
|
|
133
|
+
if (kind !== "support") {
|
|
134
|
+
issue(issues, [...path, "kind"], "Expected 'support' or 'impersonation'");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const session = own(level, "session_id");
|
|
138
|
+
if (typeof session !== "string" || session === "") issue(issues, [...path, "session_id"], "Expected a non-empty string");
|
|
139
|
+
optional(level, "read_only", path, issues, (value) => typeof value === "boolean", "a boolean");
|
|
140
|
+
}
|
|
141
|
+
function checkAct(value, issues) {
|
|
142
|
+
let current = value;
|
|
143
|
+
const path = ["act"];
|
|
144
|
+
while (current !== void 0) {
|
|
145
|
+
if (!isRecord$1(current)) {
|
|
146
|
+
issue(issues, path, "Expected an object");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sub = own(current, "sub");
|
|
150
|
+
if (typeof sub !== "string" || sub === "") issue(issues, [...path, "sub"], "Expected a non-empty string");
|
|
151
|
+
if (path.length === 1) checkActKind(current, path, issues);
|
|
152
|
+
current = own(current, "act");
|
|
153
|
+
path.push("act");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function checkClaims(value, tenantClaim) {
|
|
157
|
+
const issues = [];
|
|
158
|
+
if (!isRecord$1(value)) {
|
|
159
|
+
issue(issues, [], "Expected a claims object");
|
|
160
|
+
return issues;
|
|
161
|
+
}
|
|
162
|
+
checkPermDockClaims(value, tenantClaim, [], issues);
|
|
163
|
+
optional(value, "client_id", [], issues, isString, "a string");
|
|
164
|
+
optional(value, "scope", [], issues, (scope) => typeof scope === "string" || isStrings(scope), "a string or an array of strings");
|
|
165
|
+
const act = own(value, "act");
|
|
166
|
+
if (act !== void 0) checkAct(act, issues);
|
|
167
|
+
const meta = own(value, "app_metadata");
|
|
168
|
+
if (meta !== void 0) {
|
|
169
|
+
if (isRecord$1(meta)) checkPermDockClaims(meta, tenantClaim, ["app_metadata"], issues);
|
|
170
|
+
else issue(issues, ["app_metadata"], "Expected an object");
|
|
171
|
+
}
|
|
172
|
+
return issues;
|
|
173
|
+
}
|
|
174
|
+
function merge(base, app) {
|
|
175
|
+
const issues = [...base.issues ?? [], ...app.issues ?? []];
|
|
176
|
+
if (issues.length > 0) return { issues };
|
|
177
|
+
const baseValue = base.value;
|
|
178
|
+
const appValue = app.value;
|
|
179
|
+
if (!isRecord$1(appValue)) return { issues: [{ message: "Expected the extension to output an object" }] };
|
|
180
|
+
return { value: isRecord$1(baseValue) ? {
|
|
181
|
+
...baseValue,
|
|
182
|
+
...appValue
|
|
183
|
+
} : appValue };
|
|
184
|
+
}
|
|
185
|
+
function schemaOf(validate) {
|
|
186
|
+
const schema = {
|
|
187
|
+
"~standard": Object.freeze({
|
|
188
|
+
version: 1,
|
|
189
|
+
vendor: "permdock",
|
|
190
|
+
validate
|
|
191
|
+
}),
|
|
192
|
+
extend(app) {
|
|
193
|
+
if (!isRecord$1(app) && typeof app !== "function") throw new TypeError("PermDock: supabaseClaims().extend needs a Standard Schema");
|
|
194
|
+
const props = Reflect.get(app, "~standard");
|
|
195
|
+
if (!isRecord$1(props) || typeof props["validate"] !== "function") throw new TypeError("PermDock: supabaseClaims().extend needs a Standard Schema");
|
|
196
|
+
const run = app["~standard"].validate.bind(app["~standard"]);
|
|
197
|
+
return schemaOf((value) => {
|
|
198
|
+
const base = validate(value);
|
|
199
|
+
const extra = run(value);
|
|
200
|
+
if (base instanceof Promise || extra instanceof Promise) return Promise.all([base, extra]).then(([b, e]) => merge(b, e));
|
|
201
|
+
return merge(base, extra);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
return Object.freeze(schema);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* A Standard Schema v1 for PermDock's Supabase claims. Unknown claims pass
|
|
209
|
+
* through unchanged; a known claim with the wrong shape is an issue, so the
|
|
210
|
+
* whole token is rejected as `schemas/supabase-claims-v1.json` rejects it.
|
|
211
|
+
* Needs no validation library.
|
|
212
|
+
*
|
|
213
|
+
* ```ts
|
|
214
|
+
* sb.claims(supabaseClaims().extend(z.object({ datetime_preferences: Prefs })))
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
function supabaseClaims(options = {}) {
|
|
218
|
+
const tenantClaim = options.tenantClaim ?? "tenant_id";
|
|
219
|
+
if (typeof tenantClaim !== "string" || tenantClaim === "" || KNOWN.has(tenantClaim)) throw new TypeError(`PermDock: supabaseClaims tenantClaim must be a claim name other than ${[...KNOWN].join(", ")}`);
|
|
220
|
+
return schemaOf((value) => {
|
|
221
|
+
const issues = checkClaims(value, tenantClaim);
|
|
222
|
+
return issues.length > 0 ? { issues } : { value };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/supabase/rls.ts
|
|
227
|
+
function isTable(value) {
|
|
228
|
+
return value !== null && typeof value === "object" && "table" in value && typeof value.table === "string";
|
|
229
|
+
}
|
|
230
|
+
function supabaseRls(options = {}) {
|
|
231
|
+
const raw = options.memberships;
|
|
232
|
+
const memberships = raw === void 0 ? void 0 : isTable(raw) ? { tenant: raw } : raw;
|
|
233
|
+
return compact({
|
|
234
|
+
dialect: "supabase",
|
|
235
|
+
roleClaim: options.roleClaim ?? "user_role",
|
|
236
|
+
tenantClaim: options.tenantClaim ?? "tenant_id",
|
|
237
|
+
tenantType: options.tenantType,
|
|
238
|
+
memberships,
|
|
239
|
+
suspension: options.suspension
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const ident = (name) => quoteSqlIdent(name);
|
|
243
|
+
const table = (name) => quoteSqlTable(name);
|
|
244
|
+
const literal = quoteSqlLiteral;
|
|
245
|
+
function qualifiedTable(name) {
|
|
246
|
+
return table(name.includes(".") ? name : `public.${name}`);
|
|
247
|
+
}
|
|
248
|
+
function textArray(values) {
|
|
249
|
+
return values.length === 0 ? `'{}'::text[]` : `array[${values.map(literal).join(", ")}]::text[]`;
|
|
250
|
+
}
|
|
251
|
+
function activeRow(row, id, text = false) {
|
|
252
|
+
const parts = [`s.${ident(row.id)}${text ? "::text" : ""} = ${id}`];
|
|
253
|
+
if (row.disabledAt !== void 0) parts.push(`s.${ident(row.disabledAt)} is null`);
|
|
254
|
+
if (row.status !== void 0) {
|
|
255
|
+
if (row.active === void 0 || row.active.length === 0) throw new TypeError("PermDock: a suspension status column needs its active values");
|
|
256
|
+
parts.push(`s.${ident(row.status)}::text = any(${textArray(row.active)})`);
|
|
257
|
+
}
|
|
258
|
+
if (row.disabledAt === void 0 && row.status === void 0) throw new TypeError("PermDock: a suspension table needs disabledAt or status");
|
|
259
|
+
return `exists (select 1 from ${qualifiedTable(row.table)} s where ${parts.join(" and ")})`;
|
|
260
|
+
}
|
|
261
|
+
/** Early `return false` for a suspended user, or a tenant request for a suspended instance. */
|
|
262
|
+
function suspendedGuards(suspension, scope, uid) {
|
|
263
|
+
const lines = [];
|
|
264
|
+
const users = suspension?.users;
|
|
265
|
+
if (users !== void 0) lines.push(` if not ${activeRow(users, uid)} then
|
|
266
|
+
return false; -- suspended user
|
|
267
|
+
end if;`);
|
|
268
|
+
const tenant = suspension?.scopes?.[scope];
|
|
269
|
+
if (tenant !== void 0) lines.push(` if requested_tenant is not null and not ${activeRow(tenant, "requested_tenant", true)} then
|
|
270
|
+
return false; -- suspended ${scope}
|
|
271
|
+
end if;`);
|
|
272
|
+
return lines.map((line) => `\n${line}`).join("");
|
|
273
|
+
}
|
|
274
|
+
function claimEntries(where, value) {
|
|
275
|
+
return `array(select ${value} from jsonb_array_elements_text(cg.g) e where ${where})`;
|
|
276
|
+
}
|
|
277
|
+
/** A custom role's keys hold `requested_permission` in the first scope. */
|
|
278
|
+
function customHolds(q, scope, allows, denies, includes) {
|
|
279
|
+
return `exists (
|
|
280
|
+
select 1 from ${q("role_permissions")} rp
|
|
281
|
+
where rp.permission = requested_permission::text
|
|
282
|
+
and rp.scope = ${literal(scope)}
|
|
283
|
+
and rp.effect = 'allow'
|
|
284
|
+
and rp.grant_key in (select ${q("permdock_custom_keys")}(
|
|
285
|
+
${allows},
|
|
286
|
+
${denies},
|
|
287
|
+
${includes},
|
|
288
|
+
${literal(scope)}
|
|
289
|
+
))
|
|
290
|
+
)`;
|
|
291
|
+
}
|
|
292
|
+
function customDatabaseBranch(q, scope, memberships, declared) {
|
|
293
|
+
const match = `c.tenant_id::text = requested_tenant and c.scope = ${literal(scope)} and c.scope_id is null and c.role = m.${ident(memberships.role)}::text`;
|
|
294
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${q(source)} c where ${match}${extra})`;
|
|
295
|
+
return ` or exists (
|
|
296
|
+
select 1
|
|
297
|
+
from ${qualifiedTable(memberships.table)} m
|
|
298
|
+
where m.${ident(memberships.user)} = v_member_user
|
|
299
|
+
and m.${ident(memberships.tenant)} = v_member_tenant
|
|
300
|
+
and not (m.${ident(memberships.role)}::text = any(${textArray(declared)}))${memberships.expiresAt === void 0 ? "" : `\n and (m.${ident(memberships.expiresAt)} is null or m.${ident(memberships.expiresAt)} > now())`}
|
|
301
|
+
and ${customHolds(q, scope, rows("custom_role_permissions", "permission", " and c.effect = 'allow'"), rows("custom_role_permissions", "permission", " and c.effect = 'deny'"), rows("custom_role_includes", "include_role", ""))}
|
|
302
|
+
)`;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The `role_permissions` match for one effect. An allow counts only without
|
|
306
|
+
* row conditions (`grant_key = permission`): `authorize()` never sees the row.
|
|
307
|
+
* A deny counts with or without them, so a conditional deny still denies.
|
|
308
|
+
*/
|
|
309
|
+
function effectMatch(scope, effect) {
|
|
310
|
+
return `rp.permission = requested_permission::text
|
|
311
|
+
and rp.scope = ${scope}
|
|
312
|
+
and rp.effect = '${effect}'${effect === "allow" ? "\n and rp.grant_key = rp.permission" : ""}`;
|
|
313
|
+
}
|
|
314
|
+
function databaseBody(q, scope, memberships, custom, suspension) {
|
|
315
|
+
const global = (effect) => `exists (
|
|
316
|
+
select 1
|
|
317
|
+
from ${q("user_roles")} ur
|
|
318
|
+
join ${q("role_permissions")} rp on rp.role = ur.role::text
|
|
319
|
+
where ur.user_id = uid
|
|
320
|
+
and ${effectMatch(`'global'`, effect)}
|
|
321
|
+
)`;
|
|
322
|
+
const tenantColumn = memberships?.tenant;
|
|
323
|
+
let tenantBranch = ` if requested_tenant is not null then
|
|
324
|
+
return false; -- no memberships table configured
|
|
325
|
+
end if;`;
|
|
326
|
+
if (memberships !== void 0 && tenantColumn !== void 0) {
|
|
327
|
+
const held = (effect) => `exists (
|
|
328
|
+
select 1
|
|
329
|
+
from ${qualifiedTable(memberships.table)} m
|
|
330
|
+
join ${q("role_permissions")} rp on rp.role = m.${ident(memberships.role)}::text
|
|
331
|
+
where m.${ident(memberships.user)} = v_member_user
|
|
332
|
+
and m.${ident(tenantColumn)} = v_member_tenant
|
|
333
|
+
and ${effectMatch(literal(scope), effect)}${memberships.expiresAt === void 0 ? "" : `\n and (m.${ident(memberships.expiresAt)} is null or m.${ident(memberships.expiresAt)} > now())`}
|
|
334
|
+
)`;
|
|
335
|
+
tenantBranch = ` if requested_tenant is not null then
|
|
336
|
+
begin
|
|
337
|
+
v_member_user := uid;
|
|
338
|
+
v_member_tenant := requested_tenant;
|
|
339
|
+
exception when invalid_text_representation or numeric_value_out_of_range then
|
|
340
|
+
return false; -- not an id of the memberships table
|
|
341
|
+
end;
|
|
342
|
+
return (${held("allow")}${custom === void 0 ? "" : customDatabaseBranch(q, scope, {
|
|
343
|
+
...memberships,
|
|
344
|
+
tenant: tenantColumn
|
|
345
|
+
}, custom.declared)})
|
|
346
|
+
and not ${held("deny")}
|
|
347
|
+
and not ${global("deny")};
|
|
348
|
+
end if;`;
|
|
349
|
+
}
|
|
350
|
+
return `declare
|
|
351
|
+
uid uuid := (select auth.uid());${memberships !== void 0 && tenantColumn !== void 0 ? `
|
|
352
|
+
v_member_user ${qualifiedTable(memberships.table)}.${ident(memberships.user)}%type;
|
|
353
|
+
v_member_tenant ${qualifiedTable(memberships.table)}.${ident(tenantColumn)}%type;` : ""}
|
|
354
|
+
begin
|
|
355
|
+
if uid is null then
|
|
356
|
+
return false;
|
|
357
|
+
end if;${suspendedGuards(suspension, scope, "uid")}
|
|
358
|
+
${tenantBranch}
|
|
359
|
+
return ${global("allow")}
|
|
360
|
+
and not ${global("deny")};
|
|
361
|
+
end;`;
|
|
362
|
+
}
|
|
363
|
+
/** A claim membership whose `expiresAt` (epoch seconds) has passed no longer holds its roles. */
|
|
364
|
+
const CLAIM_UNEXPIRED = `case jsonb_typeof(m -> 'expiresAt')
|
|
365
|
+
when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
|
|
366
|
+
else true
|
|
367
|
+
end`;
|
|
368
|
+
function jwtBody(q, scope, custom, suspension) {
|
|
369
|
+
const memberships = `jsonb_array_elements(
|
|
370
|
+
case jsonb_typeof(coalesce(claims -> 'memberships', claims -> 'app_metadata' -> 'memberships'))
|
|
371
|
+
when 'array' then coalesce(claims -> 'memberships', claims -> 'app_metadata' -> 'memberships')
|
|
372
|
+
else '[]'::jsonb
|
|
373
|
+
end
|
|
374
|
+
) m
|
|
375
|
+
cross join lateral jsonb_array_elements_text(
|
|
376
|
+
case jsonb_typeof(m -> 'roles') when 'array' then m -> 'roles' else '[]'::jsonb end
|
|
377
|
+
) r(role)`;
|
|
378
|
+
const customBranch = custom === void 0 ? "" : ` or exists (
|
|
379
|
+
select 1
|
|
380
|
+
from ${memberships}
|
|
381
|
+
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
382
|
+
where m ->> 'scope' = ${literal(scope)}
|
|
383
|
+
and m ->> 'id' = requested_tenant
|
|
384
|
+
and ${CLAIM_UNEXPIRED}
|
|
385
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
386
|
+
and not (r.role = any(${textArray(custom.declared)}))
|
|
387
|
+
and ${customHolds(q, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}
|
|
388
|
+
)`;
|
|
389
|
+
const held = (effect) => `exists (
|
|
390
|
+
select 1
|
|
391
|
+
from ${memberships}
|
|
392
|
+
join ${q("role_permissions")} rp on rp.role = r.role
|
|
393
|
+
where m ->> 'scope' = ${literal(scope)}
|
|
394
|
+
and m ->> 'id' = requested_tenant
|
|
395
|
+
and ${CLAIM_UNEXPIRED}
|
|
396
|
+
and ${effectMatch(literal(scope), effect)}
|
|
397
|
+
)`;
|
|
398
|
+
const global = (effect) => `exists (
|
|
399
|
+
select 1
|
|
400
|
+
from jsonb_array_elements_text(
|
|
401
|
+
case jsonb_typeof(role_claim)
|
|
402
|
+
when 'array' then role_claim
|
|
403
|
+
when 'string' then jsonb_build_array(role_claim)
|
|
404
|
+
else '[]'::jsonb
|
|
405
|
+
end
|
|
406
|
+
) r(role)
|
|
407
|
+
join ${q("role_permissions")} rp on rp.role = r.role
|
|
408
|
+
where ${effectMatch(`'global'`, effect)}
|
|
409
|
+
)`;
|
|
410
|
+
return `declare
|
|
411
|
+
claims jsonb := (select auth.jwt());
|
|
412
|
+
role_claim jsonb;
|
|
413
|
+
begin
|
|
414
|
+
if claims is null or (select auth.uid()) is null then
|
|
415
|
+
return false;
|
|
416
|
+
end if;${suspendedGuards(suspension, scope, "(select auth.uid())")}
|
|
417
|
+
-- a top-level null (no role row) falls back to app_metadata, like subjectFromSupabase
|
|
418
|
+
role_claim := coalesce(nullif(claims -> 'user_role', 'null'::jsonb), claims -> 'app_metadata' -> 'user_role');
|
|
419
|
+
if requested_tenant is not null then
|
|
420
|
+
return (${held("allow")}${customBranch})
|
|
421
|
+
and not ${held("deny")}
|
|
422
|
+
and not ${global("deny")};
|
|
423
|
+
end if;
|
|
424
|
+
return ${global("allow")}
|
|
425
|
+
and not ${global("deny")};
|
|
426
|
+
end;`;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* `authorize(requested_permission, requested_tenant text default null)` for Supabase's RBAC
|
|
430
|
+
* scaffold, over the `role_permissions (role, permission, grant_key, scope, effect)` table that
|
|
431
|
+
* `permdock rls generate` seeds. `database` reads `user_roles` (and the memberships table for a
|
|
432
|
+
* tenant) on every call; `jwt` reads the hook-injected `user_role` and `memberships` claims. A
|
|
433
|
+
* tenant request with no memberships source is denied, never answered from global roles. It
|
|
434
|
+
* answers "does a role hold this permission" without seeing a row: an allow with row conditions
|
|
435
|
+
* does not count, and a deny the user's tenant or global roles hold overrides every allow.
|
|
436
|
+
* Generated policies call the per-statement RLS helpers (`permdock_has`, `permitted_<scope>_ids`)
|
|
437
|
+
* instead. Only `authenticated` may execute it.
|
|
438
|
+
*/
|
|
439
|
+
function authorizeSql(options = {}) {
|
|
440
|
+
const schema = options.schema ?? "permdock";
|
|
441
|
+
const q = (name) => table(`${schema}.${name}`);
|
|
442
|
+
const memberships = typeof options.tenant === "object" ? options.tenant : void 0;
|
|
443
|
+
const scope = options.scope ?? "tenant";
|
|
444
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(scope)) throw new TypeError(`PermDock: unsafe scope name '${scope}'`);
|
|
445
|
+
const body = options.authorize === "jwt" ? jwtBody(q, scope, options.customRoles, options.suspension) : databaseBody(q, scope, memberships, options.customRoles, options.suspension);
|
|
446
|
+
const signature = `${q("authorize")}(${q("app_permission")}, text)`;
|
|
447
|
+
return `create or replace function ${q("authorize")}(
|
|
448
|
+
requested_permission ${q("app_permission")},
|
|
449
|
+
requested_tenant text default null
|
|
450
|
+
)
|
|
451
|
+
returns boolean
|
|
452
|
+
language plpgsql
|
|
453
|
+
stable
|
|
454
|
+
security definer
|
|
455
|
+
set search_path = ''
|
|
456
|
+
as $$
|
|
457
|
+
${body}
|
|
458
|
+
$$;
|
|
459
|
+
revoke execute on function ${signature} from public, anon;
|
|
460
|
+
grant execute on function ${signature} to authenticated;
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/supabase/subject.ts
|
|
465
|
+
const REGISTERED = /* @__PURE__ */ new Set([
|
|
466
|
+
"sub",
|
|
467
|
+
"role",
|
|
468
|
+
"iss",
|
|
469
|
+
"aud",
|
|
470
|
+
"exp",
|
|
471
|
+
"iat",
|
|
472
|
+
"nbf",
|
|
473
|
+
"aal",
|
|
474
|
+
"amr",
|
|
475
|
+
"acr",
|
|
476
|
+
"session_id",
|
|
477
|
+
"email",
|
|
478
|
+
"phone",
|
|
479
|
+
"is_anonymous",
|
|
480
|
+
"app_metadata",
|
|
481
|
+
"user_metadata",
|
|
482
|
+
"act",
|
|
483
|
+
"client_id",
|
|
484
|
+
"scope"
|
|
485
|
+
]);
|
|
486
|
+
function isRecord(value) {
|
|
487
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
488
|
+
}
|
|
489
|
+
/** Top-level first, then `app_metadata`. `null` counts as absent: the RBAC hook writes `null` for a user with no role row. */
|
|
490
|
+
function readClaim(claims, name) {
|
|
491
|
+
const top = Object.hasOwn(claims, name) ? claims[name] : void 0;
|
|
492
|
+
if (top !== void 0 && top !== null) return top;
|
|
493
|
+
const meta = claims["app_metadata"];
|
|
494
|
+
if (isRecord(meta) && Object.hasOwn(meta, name)) return meta[name] ?? void 0;
|
|
495
|
+
}
|
|
496
|
+
function asRoles(value, declared) {
|
|
497
|
+
const raw = typeof value === "string" ? value === "" ? [] : [value] : Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
498
|
+
if (declared === void 0) return raw;
|
|
499
|
+
const allowed = new Set(declared);
|
|
500
|
+
return raw.filter((role) => allowed.has(role));
|
|
501
|
+
}
|
|
502
|
+
/** Reads the `memberships` claim; an entry it cannot read is dropped and its index reported. */
|
|
503
|
+
function readMemberships(value) {
|
|
504
|
+
if (!Array.isArray(value)) return {
|
|
505
|
+
memberships: [],
|
|
506
|
+
dropped: []
|
|
507
|
+
};
|
|
508
|
+
const out = [];
|
|
509
|
+
const dropped = [];
|
|
510
|
+
for (const [index, item] of value.entries()) {
|
|
511
|
+
const membership = asMembership(item);
|
|
512
|
+
if (membership === void 0) dropped.push(index);
|
|
513
|
+
else out.push(membership);
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
memberships: out,
|
|
517
|
+
dropped
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function asMembership(item) {
|
|
521
|
+
if (!isRecord(item) || !Array.isArray(item["roles"])) return;
|
|
522
|
+
const roles = item["roles"].filter((role) => typeof role === "string");
|
|
523
|
+
if (roles.length === 0) return;
|
|
524
|
+
const scope = typeof item["scope"] === "string" ? item["scope"] : void 0;
|
|
525
|
+
const id = typeof item["id"] === "string" ? item["id"] : void 0;
|
|
526
|
+
const within = isRecord(item["within"]) ? Object.fromEntries(Object.entries(item["within"]).filter((entry) => typeof entry[1] === "string")) : void 0;
|
|
527
|
+
const tenant = typeof item["tenant"] === "string" ? item["tenant"] : void 0;
|
|
528
|
+
const team = typeof item["team"] === "string" ? item["team"] : void 0;
|
|
529
|
+
const onRecord = isRecord(item["on"]) ? item["on"] : void 0;
|
|
530
|
+
const on = onRecord !== void 0 && typeof onRecord["resource"] === "string" && typeof onRecord["id"] === "string" ? {
|
|
531
|
+
resource: onRecord["resource"],
|
|
532
|
+
id: onRecord["id"]
|
|
533
|
+
} : void 0;
|
|
534
|
+
if ((scope === void 0 || id === void 0) && tenant === void 0 && team === void 0 && on === void 0) return;
|
|
535
|
+
return compact({
|
|
536
|
+
roles,
|
|
537
|
+
scope,
|
|
538
|
+
id,
|
|
539
|
+
within,
|
|
540
|
+
tenant,
|
|
541
|
+
team,
|
|
542
|
+
on,
|
|
543
|
+
via: typeof item["via"] === "string" ? item["via"] : void 0,
|
|
544
|
+
expiresAt: typeof item["expiresAt"] === "number" ? item["expiresAt"] : void 0,
|
|
545
|
+
grantedBy: typeof item["grantedBy"] === "string" ? item["grantedBy"] : void 0,
|
|
546
|
+
reason: typeof item["reason"] === "string" ? item["reason"] : void 0,
|
|
547
|
+
member: memberOf(item["member"]),
|
|
548
|
+
managedBy: item["managedBy"] === "idp" ? "idp" : void 0,
|
|
549
|
+
entitlements: Array.isArray(item["entitlements"]) ? item["entitlements"].filter((seat) => typeof seat === "string") : void 0
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function memberOf(value) {
|
|
553
|
+
if (value === null || typeof value !== "object" || !Object.hasOwn(value, "group")) return;
|
|
554
|
+
const group = value.group;
|
|
555
|
+
return typeof group === "string" && group !== "" ? { group } : void 0;
|
|
556
|
+
}
|
|
557
|
+
function asStrings(value) {
|
|
558
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
559
|
+
}
|
|
560
|
+
/** `claims[plans][tenant]`: only the active tenant's entry, never another tenant's. */
|
|
561
|
+
function plansFor(claims, name, tenant) {
|
|
562
|
+
if (name === void 0 || tenant === void 0) return [];
|
|
563
|
+
const byTenant = readClaim(claims, name);
|
|
564
|
+
if (!isRecord(byTenant) || !Object.hasOwn(byTenant, tenant)) return [];
|
|
565
|
+
return asStrings(byTenant[tenant]);
|
|
566
|
+
}
|
|
567
|
+
const INVALID_CHAIN = Object.freeze({
|
|
568
|
+
ok: false,
|
|
569
|
+
reason: "invalid-chain"
|
|
570
|
+
});
|
|
571
|
+
const NO_ACTOR = Object.freeze({ ok: true });
|
|
572
|
+
/** Own entries of one `act` level, with the nested `act` copied the same way. */
|
|
573
|
+
function copyAct(level) {
|
|
574
|
+
const entries = Object.entries(level).map(([key, value]) => [key, key === "act" && isRecord(value) ? copyAct(value) : value]);
|
|
575
|
+
return Object.fromEntries(entries);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The outer `act` level by its `kind`, the marker better-supabase writes: none is an OAuth client
|
|
579
|
+
* or agent chain, `support` a support session (with `session_id`), `impersonation` an admin acting
|
|
580
|
+
* as the user. Any other `kind`, or a support level without `session_id`, is invalid.
|
|
581
|
+
*/
|
|
582
|
+
function actingParty(outer, id) {
|
|
583
|
+
const marked = Object.hasOwn(outer, "kind") ? outer["kind"] : void 0;
|
|
584
|
+
const kind = marked === void 0 && Object.hasOwn(outer, "session_id") ? "support" : marked;
|
|
585
|
+
const chain = copyAct(outer);
|
|
586
|
+
const reason = typeof outer["reason"] === "string" ? outer["reason"] : void 0;
|
|
587
|
+
switch (kind) {
|
|
588
|
+
case void 0: return freezeDeep({
|
|
589
|
+
ok: true,
|
|
590
|
+
actor: {
|
|
591
|
+
id,
|
|
592
|
+
kind: "oauth-client",
|
|
593
|
+
chain
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
case "support": {
|
|
597
|
+
const session = outer["session_id"];
|
|
598
|
+
const readOnly = Object.hasOwn(outer, "read_only") ? outer["read_only"] : false;
|
|
599
|
+
if (typeof session !== "string" || session === "" || typeof readOnly !== "boolean") return INVALID_CHAIN;
|
|
600
|
+
return freezeDeep({
|
|
601
|
+
ok: true,
|
|
602
|
+
actor: compact({
|
|
603
|
+
id,
|
|
604
|
+
kind: "support",
|
|
605
|
+
sessionId: session,
|
|
606
|
+
readOnly,
|
|
607
|
+
reason,
|
|
608
|
+
chain
|
|
609
|
+
})
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
case "impersonation": return freezeDeep({
|
|
613
|
+
ok: true,
|
|
614
|
+
actor: compact({
|
|
615
|
+
id,
|
|
616
|
+
kind: "impersonation",
|
|
617
|
+
reason,
|
|
618
|
+
chain
|
|
619
|
+
})
|
|
620
|
+
});
|
|
621
|
+
default: return INVALID_CHAIN;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function readActor(claims) {
|
|
625
|
+
if (Object.hasOwn(claims, "act") && claims["act"] !== void 0) {
|
|
626
|
+
let current = claims["act"];
|
|
627
|
+
while (current !== void 0) {
|
|
628
|
+
const level = isRecord(current) ? current["sub"] : void 0;
|
|
629
|
+
if (!isRecord(current) || typeof level !== "string" || level === "") return INVALID_CHAIN;
|
|
630
|
+
current = Object.hasOwn(current, "act") ? current["act"] : void 0;
|
|
631
|
+
}
|
|
632
|
+
const outer = claims["act"];
|
|
633
|
+
const sub = isRecord(outer) ? outer["sub"] : void 0;
|
|
634
|
+
if (!isRecord(outer) || typeof sub !== "string") return INVALID_CHAIN;
|
|
635
|
+
return actingParty(outer, sub);
|
|
636
|
+
}
|
|
637
|
+
const client = Object.hasOwn(claims, "client_id") ? claims["client_id"] : void 0;
|
|
638
|
+
if (typeof client === "string" && client !== "") return freezeDeep({
|
|
639
|
+
ok: true,
|
|
640
|
+
actor: {
|
|
641
|
+
id: client,
|
|
642
|
+
kind: "oauth-client"
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
return NO_ACTOR;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Who acts for the user: the outermost `sub` of an RFC 8693 `act` chain (the current
|
|
649
|
+
* actor; nested levels are prior actors, kept on `chain` for audit), else the
|
|
650
|
+
* OAuth `client_id` of a third-party app. The outer level's `kind` names the actor:
|
|
651
|
+
* none is `oauth-client`, `support` a support session, `impersonation` an admin acting as the
|
|
652
|
+
* user. Reads only `act` and `client_id`; the caller applies the role rule first (`anon` and
|
|
653
|
+
* `service_role` are anonymous and carry no actor). An `act` that is not a chain of objects each
|
|
654
|
+
* with a non-empty `sub`, that names another `kind`, or a support level without `session_id`,
|
|
655
|
+
* is `{ ok: false }` and must deny.
|
|
656
|
+
*/
|
|
657
|
+
function actorOf(claims) {
|
|
658
|
+
try {
|
|
659
|
+
return isRecord(claims) ? readActor(claims) : NO_ACTOR;
|
|
660
|
+
} catch {
|
|
661
|
+
return INVALID_CHAIN;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/** The OAuth `scope` claim (a space-separated string or a list) as `scopes`; `undefined` when empty. */
|
|
665
|
+
function delegationOf(claims) {
|
|
666
|
+
try {
|
|
667
|
+
if (!isRecord(claims) || !Object.hasOwn(claims, "scope")) return;
|
|
668
|
+
const scope = claims["scope"];
|
|
669
|
+
const scopes = typeof scope === "string" ? scope.split(/\s+/u).filter(Boolean) : asStrings(scope);
|
|
670
|
+
return scopes.length > 0 ? freezeDeep({ scopes }) : void 0;
|
|
671
|
+
} catch {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function subjectDelegation(claims, chain) {
|
|
676
|
+
const delegation = compact({
|
|
677
|
+
scopes: delegationOf(claims)?.scopes,
|
|
678
|
+
chain
|
|
679
|
+
});
|
|
680
|
+
return Object.keys(delegation).length === 0 ? void 0 : delegation;
|
|
681
|
+
}
|
|
682
|
+
function emit(options, reason, cause) {
|
|
683
|
+
try {
|
|
684
|
+
options.onAuth?.({
|
|
685
|
+
reason,
|
|
686
|
+
cause,
|
|
687
|
+
source: "supabase"
|
|
688
|
+
});
|
|
689
|
+
} catch {}
|
|
690
|
+
}
|
|
691
|
+
function validateClaims(extra, schema) {
|
|
692
|
+
const result = schema["~standard"].validate(extra);
|
|
693
|
+
if (result instanceof Promise) {
|
|
694
|
+
ignoreRejection(result);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if ("issues" in result && result.issues !== void 0) return;
|
|
698
|
+
const value = result.value;
|
|
699
|
+
return isRecord(value) ? value : void 0;
|
|
700
|
+
}
|
|
701
|
+
function extraClaims(claims) {
|
|
702
|
+
const extra = {};
|
|
703
|
+
const meta = claims["app_metadata"];
|
|
704
|
+
if (isRecord(meta)) for (const [key, value] of Object.entries(meta)) extra[key] = value;
|
|
705
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
706
|
+
if (REGISTERED.has(key) || key === "user_metadata") continue;
|
|
707
|
+
extra[key] = value;
|
|
708
|
+
}
|
|
709
|
+
return extra;
|
|
710
|
+
}
|
|
711
|
+
function mapClaims(claims, options) {
|
|
712
|
+
const role = claims["role"];
|
|
713
|
+
if (role === "anon" || role === "service_role") return anonymousSubject();
|
|
714
|
+
const id = claims["sub"];
|
|
715
|
+
if (typeof id !== "string" || id === "") return anonymousSubject();
|
|
716
|
+
const act = readActor(claims);
|
|
717
|
+
if (!act.ok) {
|
|
718
|
+
emit(options, "invalid-token", act.reason);
|
|
719
|
+
return anonymousSubject();
|
|
720
|
+
}
|
|
721
|
+
if (options.anonymousSignIns === "deny" && claims["is_anonymous"] === true) return anonymousSubject();
|
|
722
|
+
const actor = act.actor === void 0 ? void 0 : act.actor.kind === "support" ? {
|
|
723
|
+
id: act.actor.id,
|
|
724
|
+
kind: act.actor.kind,
|
|
725
|
+
sessionId: act.actor.sessionId,
|
|
726
|
+
readOnly: act.actor.readOnly
|
|
727
|
+
} : {
|
|
728
|
+
id: act.actor.id,
|
|
729
|
+
kind: act.actor.kind
|
|
730
|
+
};
|
|
731
|
+
const roleClaim = options.roles ?? "user_role";
|
|
732
|
+
const tenantClaim = options.tenant ?? "tenant_id";
|
|
733
|
+
const membershipsClaim = options.memberships ?? "memberships";
|
|
734
|
+
let extra = extraClaims(claims);
|
|
735
|
+
if (options.schema !== void 0) extra = validateClaims(extra, options.schema) ?? {};
|
|
736
|
+
const include = new Set(options.include ?? []);
|
|
737
|
+
const tenantValue = readClaim(claims, tenantClaim);
|
|
738
|
+
const version = readClaim(claims, "authz_ver");
|
|
739
|
+
const tenant = typeof tenantValue === "string" ? tenantValue : void 0;
|
|
740
|
+
const { memberships, dropped } = readMemberships(readClaim(claims, membershipsClaim));
|
|
741
|
+
if (dropped.length > 0) emit(options, "schema", "membership-dropped");
|
|
742
|
+
const plans = plansFor(claims, options.plans, tenant);
|
|
743
|
+
const principal = compact({
|
|
744
|
+
id,
|
|
745
|
+
kind: "user",
|
|
746
|
+
roles: asRoles(readClaim(claims, roleClaim), options.declared),
|
|
747
|
+
tenant,
|
|
748
|
+
plans: plans.length > 0 ? plans : void 0,
|
|
749
|
+
memberships,
|
|
750
|
+
membershipsTruncated: readClaim(claims, "memberships_truncated") === true ? true : void 0,
|
|
751
|
+
authzVersion: typeof version === "number" && Number.isInteger(version) ? version : void 0,
|
|
752
|
+
issuer: typeof claims["iss"] === "string" ? claims["iss"] : void 0,
|
|
753
|
+
assurance: typeof claims["aal"] === "string" ? { acr: claims["aal"] } : void 0,
|
|
754
|
+
claims: Object.keys(extra).length === 0 ? void 0 : extra,
|
|
755
|
+
email: include.has("email") && typeof claims["email"] === "string" ? claims["email"] : void 0,
|
|
756
|
+
phone: include.has("phone") && typeof claims["phone"] === "string" ? claims["phone"] : void 0,
|
|
757
|
+
is_anonymous: include.has("is_anonymous") && typeof claims["is_anonymous"] === "boolean" ? claims["is_anonymous"] : void 0
|
|
758
|
+
});
|
|
759
|
+
const session = typeof claims["session_id"] === "string" ? claims["session_id"] : void 0;
|
|
760
|
+
const expiresAt = typeof claims["exp"] === "number" ? claims["exp"] : void 0;
|
|
761
|
+
return freezeDeep(compact({
|
|
762
|
+
principal,
|
|
763
|
+
actor,
|
|
764
|
+
delegation: act.actor?.kind === "oauth-client" ? subjectDelegation(claims, act.actor.chain) : void 0,
|
|
765
|
+
context: {},
|
|
766
|
+
session,
|
|
767
|
+
expiresAt
|
|
768
|
+
}));
|
|
769
|
+
}
|
|
770
|
+
function subjectFromSupabase(claims, options = {}) {
|
|
771
|
+
try {
|
|
772
|
+
if (!isRecord(claims)) return anonymousSubject();
|
|
773
|
+
const rest = {};
|
|
774
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
775
|
+
if (key === "user_metadata") continue;
|
|
776
|
+
rest[key] = value;
|
|
777
|
+
}
|
|
778
|
+
return mapClaims(rest, options);
|
|
779
|
+
} catch {
|
|
780
|
+
return anonymousSubject();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* A verified session object (for example better-supabase's `AuthSession`) in, `Subject` out.
|
|
785
|
+
* Only `kind: 'user'` sessions map; `anon`, `service`, `invalid` and anything else are anonymous.
|
|
786
|
+
*/
|
|
787
|
+
function subjectFromSupabaseSession(session, options = {}) {
|
|
788
|
+
try {
|
|
789
|
+
if (session === null || session === void 0 || session.kind !== "user") return anonymousSubject();
|
|
790
|
+
return subjectFromSupabase(session.claims, options);
|
|
791
|
+
} catch {
|
|
792
|
+
return anonymousSubject();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
//#endregion
|
|
796
|
+
export { subjectFromSupabaseSession as a, supabaseClaims as c, subjectFromSupabase as i, exchangeCapability as l, delegationOf as n, authorizeSql as o, readMemberships as r, supabaseRls as s, actorOf as t };
|