permdock 0.0.0 → 0.2.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 +327 -1
- package/dist/a2a/index.d.ts +169 -0
- package/dist/a2a/index.js +232 -0
- package/dist/adapter-context-mlWsVGGg.js +23 -0
- package/dist/ai-sdk/index.d.ts +95 -0
- package/dist/ai-sdk/index.js +154 -0
- package/dist/approval-required-TzNB9oQA.js +6 -0
- package/dist/approvals/index.d.ts +3 -0
- package/dist/approvals/index.js +5 -0
- package/dist/approvals-DbYAuNkG.js +224 -0
- package/dist/approvers-DT4AVyIg.js +52 -0
- package/dist/arazzo-BAgw9ap6.js +70 -0
- package/dist/arazzo-cmvX2jDe.js +54 -0
- package/dist/authzen/index.d.ts +39 -0
- package/dist/authzen/index.js +302 -0
- package/dist/better-auth/index.d.ts +109 -0
- package/dist/better-auth/index.js +344 -0
- package/dist/better-supabase/index.d.ts +206 -0
- package/dist/better-supabase/index.js +620 -0
- package/dist/bind-CVy3Espv.d.ts +25 -0
- package/dist/bind-VnYn2HKq.js +97 -0
- package/dist/boundary-BM8a_P6E.js +13 -0
- package/dist/boundary-BmHxVH1J.d.ts +15 -0
- package/dist/budget-JqcRqyhK.d.ts +11 -0
- package/dist/budget-NbEtbJC_.js +11 -0
- package/dist/canonical-json-DemiK4Vp.js +26 -0
- package/dist/capability-4qPdO9gk.js +173 -0
- package/dist/capability-Dq43tT1u.d.ts +99 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.js +2 -0
- package/dist/catalog-BjZSovXQ.js +2 -0
- package/dist/catalog-doc-Dsc9y7sd.js +309 -0
- package/dist/catalog-h2tlKFkq.js +48 -0
- package/dist/catalog-path-CnI0XTFK.js +12 -0
- package/dist/catalog-z8q6bi_r.js +55 -0
- package/dist/claude-agent/index.d.ts +72 -0
- package/dist/claude-agent/index.js +77 -0
- package/dist/clerk/index.d.ts +65 -0
- package/dist/clerk/index.js +301 -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/clients-C7zLFuWT.d.ts +12 -0
- package/dist/clients-DjU5ABMy.js +23 -0
- package/dist/cloud/index.d.ts +90 -0
- package/dist/cloud/index.js +404 -0
- package/dist/cloud-BGhHS-lY.js +50 -0
- package/dist/cloud-Bo3e9Ak3.js +118 -0
- package/dist/collect-DDDv_ESs.js +502 -0
- package/dist/collect-DLlujdHC.js +83 -0
- package/dist/collect-rG2IQ3t0.js +2 -0
- package/dist/commands-DWOrSQic.js +97 -0
- package/dist/compact-y6vfu2_f.js +19 -0
- package/dist/compare-pfyVnBjE.js +12 -0
- package/dist/compile/index.d.ts +3 -0
- package/dist/compile/index.js +2 -0
- package/dist/compile-Ba4blROR.js +613 -0
- package/dist/compile-CNnl6uIt.d.ts +94 -0
- package/dist/config-BJiM-xAm.js +157 -0
- package/dist/config-Bn00Z3ks.js +36 -0
- package/dist/config-report-Dw_JcUqT.js +44 -0
- package/dist/connection-CU8MQxRi.js +194 -0
- package/dist/context-Ttp0ekMT.js +82 -0
- package/dist/convex/index.d.ts +45 -0
- package/dist/convex/index.js +65 -0
- package/dist/create-BWyx3jua.d.ts +245 -0
- package/dist/create-CPhDL6qx.js +253 -0
- package/dist/credential-8P81ff4u.js +306 -0
- package/dist/delegation-D8pUEuce.js +103 -0
- package/dist/describe-B_gVZEVi.js +94 -0
- package/dist/describe-PdoqESvg.d.ts +36 -0
- package/dist/diff-B8Tunc3o.js +451 -0
- package/dist/diff-CzqLxZ77.js +48 -0
- package/dist/digest-B3kc_PgQ.d.ts +19 -0
- package/dist/digest-BI-6aa6d.js +27 -0
- package/dist/doctor-WpvG6p4k.js +1194 -0
- package/dist/doctor-collect-ByfYlNv1.js +664 -0
- package/dist/doctor-rOr-4ZfS.js +53 -0
- package/dist/drizzle/index.d.ts +73 -0
- package/dist/drizzle/index.js +160 -0
- package/dist/duration-xK2jGFVc.js +25 -0
- package/dist/elysia/index.d.ts +49 -0
- package/dist/elysia/index.js +59 -0
- package/dist/errors-CWfoNg9N.js +47 -0
- package/dist/errors-Cz5zn0mW.js +123 -0
- package/dist/errors-DAqevpNk.d.ts +125 -0
- package/dist/errors-DRnGZHzF.js +30 -0
- package/dist/evaluation-items-CfCCejQd.js +40 -0
- package/dist/evaluations-tz6D9A2G.js +461 -0
- package/dist/eve/index.d.ts +86 -0
- package/dist/eve/index.js +176 -0
- package/dist/expiry-BoBJ8Tvc.js +7 -0
- package/dist/express/index.d.ts +23 -0
- package/dist/express/index.js +78 -0
- package/dist/fastify/index.d.ts +33 -0
- package/dist/fastify/index.js +92 -0
- package/dist/fields-BMl1RxoX.js +26 -0
- package/dist/files-DWUN9PzI.js +125 -0
- package/dist/fixtures-ht28Cplr.js +67 -0
- package/dist/freeze-CrU8nQeX.js +60 -0
- package/dist/from-snapshot-C3sHpZNS.js +569 -0
- package/dist/generate-CH7ThJ14.js +173 -0
- package/dist/grantee-Cv5Sogfv.js +602 -0
- package/dist/graph-sql-B-dsAT6j.d.ts +29 -0
- package/dist/headers-BZSDieNT.js +6 -0
- package/dist/headers-Cf-zWvsO.d.ts +6 -0
- package/dist/helpers-H_KARDVC.js +421 -0
- package/dist/hono/index.d.ts +48 -0
- package/dist/hono/index.js +99 -0
- package/dist/hosted-DJQw46hn.js +266 -0
- package/dist/http-BIhmLfCc.js +95 -0
- package/dist/http-DPuDaChb.d.ts +13 -0
- package/dist/http-SO7qGttf.js +44 -0
- package/dist/ids-DC4V4CFu.js +18 -0
- package/dist/index-A0YNL8jZ.d.ts +177 -0
- package/dist/index-CtE07CBF.d.ts +6 -0
- package/dist/index-D3vbkoSI.d.ts +483 -0
- package/dist/index-D6_cSa1z.d.ts +201 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/instance-options-BK_rNOSC.d.ts +11 -0
- package/dist/instance-options-Dhhcnm3y.js +22 -0
- package/dist/json-schema-BsAflUGW.js +106 -0
- package/dist/jwt/index.d.ts +91 -0
- package/dist/jwt/index.js +590 -0
- package/dist/kernel-GSQnMz3O.js +208 -0
- package/dist/kysely/index.d.ts +78 -0
- package/dist/kysely/index.js +129 -0
- package/dist/live-options-H2eG8dfJ.js +60 -0
- package/dist/load-DhWEB9P1.js +75 -0
- package/dist/load-DwJUafYN.js +2 -0
- package/dist/load-jose-BL6LH4bL.js +11 -0
- package/dist/lru-8q4WZjBV.js +28 -0
- package/dist/manifest-iaoBXHKc.js +783 -0
- package/dist/manifest-uDS9y5de.d.ts +206 -0
- package/dist/map-AcxK9o_D.js +147 -0
- package/dist/map-claims-Z4QZOiFF.js +369 -0
- package/dist/map-error-pgUWNNtG.js +23 -0
- package/dist/markers-B0hYLvMD.js +60 -0
- package/dist/may-use-DndQ5tQ-.js +77 -0
- package/dist/mcp/index.d.ts +159 -0
- package/dist/mcp/index.js +603 -0
- package/dist/nest/index.d.ts +75 -0
- package/dist/nest/index.js +261 -0
- package/dist/next/client.d.ts +23 -0
- package/dist/next/client.js +33 -0
- package/dist/next/index.d.ts +93 -0
- package/dist/next/index.js +180 -0
- package/dist/next/plugin.d.ts +14 -0
- package/dist/next/plugin.js +33 -0
- package/dist/node/index.d.ts +24 -0
- package/dist/node/index.js +34 -0
- package/dist/openai/index.d.ts +62 -0
- package/dist/openai/index.js +92 -0
- package/dist/openapi/index.d.ts +176 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi-Bbg4wGVL.js +164 -0
- package/dist/openapi-BqRBvAng.js +538 -0
- package/dist/openapi-CqqEbHsL.js +250 -0
- package/dist/openapi-import-cmwetbzw.js +5959 -0
- package/dist/openapi-schema--ARjyfQr.js +6902 -0
- package/dist/orpc/index.d.ts +51 -0
- package/dist/orpc/index.js +145 -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-BuyjxhZV.js +53 -0
- package/dist/parse-local-manifest-BkGQNzAh.d.ts +10 -0
- package/dist/parse-local-manifest-BrMozJCo.js +36 -0
- package/dist/pdp/index.d.ts +22 -0
- package/dist/pdp/index.js +682 -0
- package/dist/peer-BOr_kJE1.js +31 -0
- package/dist/permdock-CzqXjCpU.js +3471 -0
- package/dist/permissions-CDgvKG5L.d.ts +52 -0
- package/dist/permissions-CpZrivYv.js +142 -0
- package/dist/permissions-DVoF6dMt.js +662 -0
- package/dist/permissions-DttnqqxC.d.ts +214 -0
- package/dist/pins-8ILTftC9.js +12 -0
- package/dist/policy-CFZAr_Al.d.ts +2252 -0
- package/dist/policy-D8UFyERw.d.ts +20 -0
- package/dist/policy-xyWg7QdR.js +1122 -0
- package/dist/powersync-A8x_Yyvh.js +2 -0
- package/dist/powersync-C7kLglqX.js +540 -0
- package/dist/powersync-yW1UUFzp.js +62 -0
- package/dist/prisma/index.d.ts +136 -0
- package/dist/prisma/index.js +455 -0
- package/dist/problem-B43ztKIm.js +191 -0
- package/dist/problem-details-CkL50EEf.d.ts +7 -0
- package/dist/problem-details-_AMOYz-w.js +144 -0
- package/dist/prompt-CXYhLfsS.js +32 -0
- package/dist/protected-CNsMzrVd.d.ts +27 -0
- package/dist/protected-Cs6kTaHo.js +100 -0
- package/dist/protected-bZs6rbT1.d.ts +40 -0
- package/dist/provider-Cjr62YMV.d.ts +6 -0
- package/dist/provider-DLzpb2Xz.js +52 -0
- package/dist/provider-store-DNyC1y48.js +43 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +7 -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 +198 -0
- package/dist/react-native/index.js +696 -0
- package/dist/refs-KYb4ntaz.js +91 -0
- package/dist/relations-cjOL01hZ.js +505 -0
- package/dist/replay-BsOyTrdV.js +28 -0
- package/dist/replay-sqBGMnNs.d.ts +5 -0
- package/dist/resource-ref-CaS9yVPi.js +20 -0
- package/dist/revocations-Bhr0aMIC.js +27 -0
- package/dist/revocations-Cy06JNx2.d.ts +21 -0
- package/dist/rls-BMNydpHk.js +2521 -0
- package/dist/rls-CEvs64Kj.js +238 -0
- package/dist/rls-fields-BMasREhR.js +3502 -0
- package/dist/rls-graph-pc9fuXYm.js +435 -0
- package/dist/rls-import-D8F7j35Q.js +943 -0
- package/dist/rls-migrate-7wikfrVI.js +367 -0
- package/dist/rls-ownership-B5Lkq62W.js +843 -0
- package/dist/rls-shared-aOQ4hGTO.js +493 -0
- package/dist/rls-verify-D8B7dOTh.js +745 -0
- package/dist/row-check-Dt35czLy.d.ts +40 -0
- package/dist/rpc-DOK54_CS.js +89 -0
- package/dist/run-YXCJDNYk.js +174 -0
- package/dist/runtime-CB8gQFvS.js +11 -0
- package/dist/saas-Bj_FFkjN.js +1203 -0
- package/dist/schema-D3MnDMxa.js +300 -0
- package/dist/scim/index.d.ts +2 -0
- package/dist/scim/index.js +2 -0
- package/dist/scim-BFGBBAqR.js +1445 -0
- package/dist/scopes-CVdPrWN-.js +239 -0
- package/dist/server/index.d.ts +134 -0
- package/dist/server/index.js +11 -0
- package/dist/server-DlZ28Oz5.js +268 -0
- package/dist/sha256-BO0swRXL.js +150 -0
- package/dist/shared-DRu8VFVM.js +123 -0
- package/dist/sink-Bd9PSyfq.js +141 -0
- package/dist/sink-Gs6TTVa-.d.ts +104 -0
- package/dist/skills-Biq5uup3.js +192 -0
- package/dist/skills-JaymNCdK.js +56 -0
- package/dist/snapshot-Enxd0n-s.js +975 -0
- package/dist/snapshot-cache-D6jLelln.js +44 -0
- package/dist/snapshot-cache-DwQH8ufx.d.ts +45 -0
- package/dist/solid/index.d.ts +93 -0
- package/dist/solid/index.js +188 -0
- package/dist/sources-B0UvrJ21.js +534 -0
- package/dist/sources-BcJgw0lt.d.ts +192 -0
- package/dist/sql-xdg5f4Fi.js +27 -0
- package/dist/src-CSbUiozF.js +568 -0
- package/dist/ssf/index.d.ts +4 -0
- package/dist/ssf/index.js +596 -0
- package/dist/store-DMAWwAJV.js +371 -0
- package/dist/store-D_pjn91-.d.ts +36 -0
- package/dist/store-options-yO0V6gRn.js +716 -0
- package/dist/stores-4_sqiOA1.js +87 -0
- package/dist/stores-DVR5NHLE.d.ts +52 -0
- package/dist/stream-DDpvynSB.js +66 -0
- package/dist/stream-DlvG2hs-.d.ts +12 -0
- package/dist/style-CTm1AXDT.js +22 -0
- package/dist/subject-C6dLxqHU.js +24 -0
- package/dist/subject-settings-BwBmCdTR.js +128 -0
- package/dist/supabase/index.d.ts +183 -0
- package/dist/supabase/index.js +5 -0
- package/dist/supabase/middleware.d.ts +82 -0
- package/dist/supabase/middleware.js +102 -0
- package/dist/supabase-CwafwxxF.js +83 -0
- package/dist/supabase-DTBaC20O.js +774 -0
- package/dist/supabase-config-Bs44dare.js +472 -0
- package/dist/supabase-hook-BuxLQLpU.js +1814 -0
- package/dist/supabase-hook-D5qVd_D8.js +2 -0
- package/dist/supautils-DYZnzRWV.js +91 -0
- package/dist/svelte/PermissionBoundary.svelte +37 -0
- package/dist/svelte/Protected.svelte +40 -0
- package/dist/svelte/index.d.ts +13 -0
- package/dist/svelte/index.js +107 -0
- package/dist/svelte/runtime.d.ts +7 -0
- package/dist/svelte/runtime.js +4 -0
- package/dist/svelte/source.d.ts +13 -0
- package/dist/svelte/source.js +11 -0
- package/dist/terminal/index.d.ts +163 -0
- package/dist/terminal/index.js +833 -0
- package/dist/testing/index.d.ts +745 -0
- package/dist/testing/index.js +3672 -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/token-CIWxJOCh.js +50 -0
- package/dist/trpc/index.d.ts +61 -0
- package/dist/trpc/index.js +122 -0
- package/dist/types-9AWO9EGt.d.ts +197 -0
- package/dist/types-BPxgszlT.d.ts +550 -0
- package/dist/types-BVyImy5I.d.ts +120 -0
- package/dist/types-Bvc4oWT6.d.ts +9 -0
- package/dist/types-C3s9xVJG.d.ts +48 -0
- package/dist/types-Dnemwd77.d.ts +183 -0
- package/dist/types-kxznc7cn.d.ts +52 -0
- package/dist/types-vKyAMtcX.d.ts +157 -0
- package/dist/types-vNSDF7Dq.d.ts +66 -0
- package/dist/types-yF5LmQVD.d.ts +306 -0
- package/dist/unplugin/index.d.ts +7 -0
- package/dist/unplugin/index.js +31 -0
- package/dist/usage-D07N9QJ3.js +42 -0
- package/dist/usage-D82l1CwK.js +2 -0
- package/dist/usage-DG1oeCca.js +693 -0
- package/dist/validation-error-CnjZ4t92.js +33 -0
- package/dist/validation-error-CtF23SrC.d.ts +26 -0
- package/dist/validity-uOp8UpYn.js +245 -0
- package/dist/verifier-BxBo6I44.js +466 -0
- package/dist/version-BvacVTZR.js +17 -0
- package/dist/views-Soe7bGSC.js +95 -0
- package/dist/vocabulary-DA8a8zif.d.ts +56 -0
- package/dist/vue/index.d.ts +79 -0
- package/dist/vue/index.js +196 -0
- package/dist/watch-Etoa_Mvr.js +2 -0
- package/dist/watch-UihESpsJ.js +128 -0
- package/dist/webmcp/index.d.ts +75 -0
- package/dist/webmcp/index.js +307 -0
- package/package.json +450 -5
- package/rulesets/permdock.yaml +29 -0
- package/schemas/approval-request-v1.json +175 -0
- package/schemas/catalog-v1.json +535 -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 +144 -0
- package/schemas/supabase-manifest-v1.json +361 -0
- package/skills/permdock/SKILL.md +66 -0
- package/skills/permdock-agents/SKILL.md +77 -0
- package/skills/permdock-agents/references/runtimes.md +213 -0
- package/skills/permdock-approvals/SKILL.md +80 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +94 -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 +132 -0
- package/skills/permdock-data/SKILL.md +57 -0
- package/skills/permdock-data/references/queries-and-rls.md +62 -0
- package/skills/permdock-data/references/relationships.md +67 -0
- package/skills/permdock-tenancy/SKILL.md +80 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +89 -0
- package/skills/permdock-wire/SKILL.md +116 -0
- package/skills/permdock-wire/references/adapters.md +492 -0
- package/skills/permdock-wire/references/policy-options.md +45 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { t as compact } from "./compact-y6vfu2_f.js";
|
|
2
|
+
import { t as canonicalJson } from "./canonical-json-DemiK4Vp.js";
|
|
3
|
+
import { o as parseCapability } from "./capability-4qPdO9gk.js";
|
|
4
|
+
import { t as ApprovalError } from "./errors-DRnGZHzF.js";
|
|
5
|
+
import { a as decodeApprovalCursor, i as approvalPageSize, o as encodeApprovalCursor, t as applyApprovalVerdict } from "./store-DMAWwAJV.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-xdg5f4Fi.js";
|
|
9
|
+
import { l as keptKeys, s as roleColumn } from "./sources-B0UvrJ21.js";
|
|
10
|
+
import { i as readMemberships } from "./manifest-iaoBXHKc.js";
|
|
11
|
+
//#region src/supabase/capability.ts
|
|
12
|
+
const DEFAULT_TTL = 300;
|
|
13
|
+
const MAX_TTL = 3600;
|
|
14
|
+
function isSecret(key) {
|
|
15
|
+
return typeof key.secret === "string";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Exchanges a verified link subject (from `subjectFromCapability`) for a
|
|
19
|
+
* short-lived Supabase access token with role `anon` and the capability in a
|
|
20
|
+
* `capability` claim, which `permdock rls generate --capabilities` policies
|
|
21
|
+
* read. `undefined` for any subject that is not a live link.
|
|
22
|
+
*/
|
|
23
|
+
async function exchangeCapability(subject, options) {
|
|
24
|
+
const alg = options.alg ?? "ES256";
|
|
25
|
+
const secret = isSecret(options.key);
|
|
26
|
+
if (secret !== (alg === "HS256")) throw new Error("PermDock: exchangeCapability signs HS256 with { secret } only, and ES256 / RS256 with a private JWK only.");
|
|
27
|
+
if (!secret && (options.kid === void 0 || options.kid.length === 0)) throw new Error("PermDock: exchangeCapability needs the kid of the imported signing key.");
|
|
28
|
+
const principal = subject.principal;
|
|
29
|
+
if (principal === null || principal.kind !== "link") return;
|
|
30
|
+
const capability = parseCapability(principal["capability"]);
|
|
31
|
+
if (capability?.holder !== "link" || capability.id !== principal.id) return;
|
|
32
|
+
const now = Math.floor(options.now ?? Date.now() / 1e3);
|
|
33
|
+
const ttl = Math.min(Math.max(1, options.ttl ?? DEFAULT_TTL), MAX_TTL);
|
|
34
|
+
const exp = Math.min(now + ttl, Math.floor(capability.expiresAt));
|
|
35
|
+
if (exp <= now) return;
|
|
36
|
+
const jose = await loadJose();
|
|
37
|
+
const key = isSecret(options.key) ? new TextEncoder().encode(options.key.secret) : await jose.importJWK(options.key, alg);
|
|
38
|
+
const jwt = new jose.SignJWT({
|
|
39
|
+
role: "anon",
|
|
40
|
+
capability
|
|
41
|
+
}).setProtectedHeader(secret ? {
|
|
42
|
+
alg: "HS256",
|
|
43
|
+
typ: "JWT"
|
|
44
|
+
} : {
|
|
45
|
+
alg,
|
|
46
|
+
...options.kid === void 0 ? {} : { kid: options.kid },
|
|
47
|
+
typ: "JWT"
|
|
48
|
+
}).setIssuedAt(now).setExpirationTime(exp);
|
|
49
|
+
if (options.issuer !== void 0) jwt.setIssuer(options.issuer);
|
|
50
|
+
return jwt.sign(key);
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/supabase/claims.ts
|
|
54
|
+
const KNOWN = /* @__PURE__ */ new Set([
|
|
55
|
+
"user_role",
|
|
56
|
+
"roles",
|
|
57
|
+
"memberships",
|
|
58
|
+
"memberships_truncated",
|
|
59
|
+
"attrs",
|
|
60
|
+
"authz_ver",
|
|
61
|
+
"client_id",
|
|
62
|
+
"scope",
|
|
63
|
+
"act",
|
|
64
|
+
"app_metadata"
|
|
65
|
+
]);
|
|
66
|
+
function isRecord$2(value) {
|
|
67
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
|
+
function own(record, key) {
|
|
70
|
+
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
71
|
+
}
|
|
72
|
+
function issue(issues, path, message) {
|
|
73
|
+
issues.push({
|
|
74
|
+
message,
|
|
75
|
+
path: [...path]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function isStrings(value) {
|
|
79
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
80
|
+
}
|
|
81
|
+
function optional(record, key, path, issues, check, expected) {
|
|
82
|
+
const value = own(record, key);
|
|
83
|
+
if (value !== void 0 && !check(value)) issue(issues, [...path, key], `Expected ${expected}`);
|
|
84
|
+
}
|
|
85
|
+
const isString = (value) => typeof value === "string";
|
|
86
|
+
function checkMembership(item, path, issues) {
|
|
87
|
+
if (!isRecord$2(item)) {
|
|
88
|
+
issue(issues, path, "Expected a membership object");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const roles = own(item, "roles");
|
|
92
|
+
if (!isStrings(roles) || roles.length === 0) issue(issues, [...path, "roles"], "Expected a non-empty array of strings");
|
|
93
|
+
for (const key of [
|
|
94
|
+
"scope",
|
|
95
|
+
"id",
|
|
96
|
+
"tenant",
|
|
97
|
+
"team",
|
|
98
|
+
"via",
|
|
99
|
+
"grantedBy",
|
|
100
|
+
"reason"
|
|
101
|
+
]) optional(item, key, path, issues, isString, "a string");
|
|
102
|
+
optional(item, "within", path, issues, (value) => isRecord$2(value) && Object.values(value).every((id) => typeof id === "string"), "an object of string ids");
|
|
103
|
+
optional(item, "on", path, issues, (value) => isRecord$2(value) && typeof own(value, "resource") === "string" && typeof own(value, "id") === "string", "an object with string resource and id");
|
|
104
|
+
optional(item, "expiresAt", path, issues, (value) => typeof value === "number" && Number.isFinite(value), "a number of seconds since the epoch");
|
|
105
|
+
optional(item, "member", path, issues, (value) => isRecord$2(value) && typeof own(value, "group") === "string" && own(value, "group") !== "", "an object with a non-empty string group");
|
|
106
|
+
optional(item, "managedBy", path, issues, (value) => value === "idp", "'idp'");
|
|
107
|
+
optional(item, "entitlements", path, issues, isStrings, "an array of strings");
|
|
108
|
+
optional(item, "keep", path, issues, isStrings, "an array of strings");
|
|
109
|
+
optional(item, "grants", path, issues, isRecord$2, "an object");
|
|
110
|
+
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");
|
|
111
|
+
}
|
|
112
|
+
function checkPermDockClaims(record, tenantClaim, path, issues) {
|
|
113
|
+
optional(record, "user_role", path, issues, (value) => value === null || typeof value === "string" || isStrings(value), "a string, an array of strings or null");
|
|
114
|
+
optional(record, "roles", path, issues, isStrings, "an array of strings");
|
|
115
|
+
const memberships = own(record, "memberships");
|
|
116
|
+
if (memberships !== void 0) {
|
|
117
|
+
if (Array.isArray(memberships)) for (const [index, item] of memberships.entries()) checkMembership(item, [
|
|
118
|
+
...path,
|
|
119
|
+
"memberships",
|
|
120
|
+
index
|
|
121
|
+
], issues);
|
|
122
|
+
else issue(issues, [...path, "memberships"], "Expected an array");
|
|
123
|
+
}
|
|
124
|
+
optional(record, "memberships_truncated", path, issues, (value) => typeof value === "boolean", "a boolean");
|
|
125
|
+
optional(record, tenantClaim, path, issues, isString, "a string");
|
|
126
|
+
optional(record, "attrs", path, issues, isRecord$2, "an object");
|
|
127
|
+
optional(record, "authz_ver", path, issues, (value) => Number.isInteger(value), "an integer");
|
|
128
|
+
}
|
|
129
|
+
/** The outer level's `kind`: none, `support` with a `session_id`, or `impersonation`. */
|
|
130
|
+
function checkActKind(level, path, issues) {
|
|
131
|
+
const marked = own(level, "kind");
|
|
132
|
+
const kind = marked === void 0 && own(level, "session_id") !== void 0 ? "support" : marked;
|
|
133
|
+
optional(level, "reason", path, issues, isString, "a string");
|
|
134
|
+
if (kind === void 0 || kind === "impersonation") return;
|
|
135
|
+
if (kind !== "support") {
|
|
136
|
+
issue(issues, [...path, "kind"], "Expected 'support' or 'impersonation'");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const session = own(level, "session_id");
|
|
140
|
+
if (typeof session !== "string" || session === "") issue(issues, [...path, "session_id"], "Expected a non-empty string");
|
|
141
|
+
optional(level, "read_only", path, issues, (value) => typeof value === "boolean", "a boolean");
|
|
142
|
+
}
|
|
143
|
+
function checkAct(value, issues) {
|
|
144
|
+
let current = value;
|
|
145
|
+
const path = ["act"];
|
|
146
|
+
while (current !== void 0) {
|
|
147
|
+
if (!isRecord$2(current)) {
|
|
148
|
+
issue(issues, path, "Expected an object");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const sub = own(current, "sub");
|
|
152
|
+
if (typeof sub !== "string" || sub === "") issue(issues, [...path, "sub"], "Expected a non-empty string");
|
|
153
|
+
if (path.length === 1) checkActKind(current, path, issues);
|
|
154
|
+
current = own(current, "act");
|
|
155
|
+
path.push("act");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function checkClaims(value, tenantClaim) {
|
|
159
|
+
const issues = [];
|
|
160
|
+
if (!isRecord$2(value)) {
|
|
161
|
+
issue(issues, [], "Expected a claims object");
|
|
162
|
+
return issues;
|
|
163
|
+
}
|
|
164
|
+
checkPermDockClaims(value, tenantClaim, [], issues);
|
|
165
|
+
optional(value, "client_id", [], issues, isString, "a string");
|
|
166
|
+
optional(value, "scope", [], issues, (scope) => typeof scope === "string" || isStrings(scope), "a string or an array of strings");
|
|
167
|
+
const act = own(value, "act");
|
|
168
|
+
if (act !== void 0) checkAct(act, issues);
|
|
169
|
+
const meta = own(value, "app_metadata");
|
|
170
|
+
if (meta !== void 0) {
|
|
171
|
+
if (isRecord$2(meta)) checkPermDockClaims(meta, tenantClaim, ["app_metadata"], issues);
|
|
172
|
+
else issue(issues, ["app_metadata"], "Expected an object");
|
|
173
|
+
}
|
|
174
|
+
return issues;
|
|
175
|
+
}
|
|
176
|
+
function merge(base, app) {
|
|
177
|
+
const issues = [...base.issues ?? [], ...app.issues ?? []];
|
|
178
|
+
if (issues.length > 0) return { issues };
|
|
179
|
+
const baseValue = base.value;
|
|
180
|
+
const appValue = app.value;
|
|
181
|
+
if (!isRecord$2(appValue)) return { issues: [{ message: "Expected the extension to output an object" }] };
|
|
182
|
+
return { value: isRecord$2(baseValue) ? {
|
|
183
|
+
...baseValue,
|
|
184
|
+
...appValue
|
|
185
|
+
} : appValue };
|
|
186
|
+
}
|
|
187
|
+
function schemaOf(validate) {
|
|
188
|
+
const schema = {
|
|
189
|
+
"~standard": Object.freeze({
|
|
190
|
+
version: 1,
|
|
191
|
+
vendor: "permdock",
|
|
192
|
+
validate
|
|
193
|
+
}),
|
|
194
|
+
extend(app) {
|
|
195
|
+
if (!isRecord$2(app) && typeof app !== "function") throw new TypeError("PermDock: supabaseClaims().extend needs a Standard Schema");
|
|
196
|
+
const props = Reflect.get(app, "~standard");
|
|
197
|
+
if (!isRecord$2(props) || typeof props["validate"] !== "function") throw new TypeError("PermDock: supabaseClaims().extend needs a Standard Schema");
|
|
198
|
+
const run = app["~standard"].validate.bind(app["~standard"]);
|
|
199
|
+
return schemaOf((value) => {
|
|
200
|
+
const base = validate(value);
|
|
201
|
+
const extra = run(value);
|
|
202
|
+
if (base instanceof Promise || extra instanceof Promise) return Promise.all([base, extra]).then(([b, e]) => merge(b, e));
|
|
203
|
+
return merge(base, extra);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
return Object.freeze(schema);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A Standard Schema v1 for PermDock's Supabase claims. Unknown claims pass
|
|
211
|
+
* through unchanged; a known claim with the wrong shape is an issue, so the
|
|
212
|
+
* whole token is rejected as `schemas/supabase-claims-v1.json` rejects it.
|
|
213
|
+
* Needs no validation library.
|
|
214
|
+
*
|
|
215
|
+
* ```ts
|
|
216
|
+
* sb.claims(supabaseClaims().extend(z.object({ datetime_preferences: Prefs })))
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
function supabaseClaims(options = {}) {
|
|
220
|
+
const tenantClaim = options.tenantClaim ?? "tenant_id";
|
|
221
|
+
if (typeof tenantClaim !== "string" || tenantClaim === "" || KNOWN.has(tenantClaim)) throw new TypeError(`PermDock: supabaseClaims tenantClaim must be a claim name other than ${[...KNOWN].join(", ")}`);
|
|
222
|
+
return schemaOf((value) => {
|
|
223
|
+
const issues = checkClaims(value, tenantClaim);
|
|
224
|
+
return issues.length > 0 ? { issues } : { value };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/supabase/postgrest.ts
|
|
229
|
+
function callRpc(client, schema, fn, args) {
|
|
230
|
+
return client.schema(schema).rpc(fn, args);
|
|
231
|
+
}
|
|
232
|
+
function isRecord$1(value) {
|
|
233
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
234
|
+
}
|
|
235
|
+
function strings(value) {
|
|
236
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
237
|
+
}
|
|
238
|
+
function grantOf(value) {
|
|
239
|
+
if (!isRecord$1(value) || typeof value["permission"] !== "string") return;
|
|
240
|
+
return compact({
|
|
241
|
+
permission: value["permission"],
|
|
242
|
+
effect: value["effect"] === "deny" ? "deny" : "allow",
|
|
243
|
+
level: typeof value["level"] === "string" ? value["level"] : void 0
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function customRoleOf(value) {
|
|
247
|
+
if (!isRecord$1(value) || typeof value["name"] !== "string") return;
|
|
248
|
+
const grants = Array.isArray(value["grants"]) ? value["grants"].flatMap((item) => {
|
|
249
|
+
const grant = grantOf(item);
|
|
250
|
+
return grant === void 0 ? [] : [grant];
|
|
251
|
+
}) : [];
|
|
252
|
+
const includes = strings(value["includes"]);
|
|
253
|
+
const body = {
|
|
254
|
+
name: value["name"],
|
|
255
|
+
grants,
|
|
256
|
+
...includes.length === 0 ? {} : { includes }
|
|
257
|
+
};
|
|
258
|
+
if (value["scope"] === "global") return value["tenant"] === void 0 ? {
|
|
259
|
+
...body,
|
|
260
|
+
scope: "global"
|
|
261
|
+
} : void 0;
|
|
262
|
+
if (typeof value["tenant"] !== "string") return;
|
|
263
|
+
return compact({
|
|
264
|
+
...body,
|
|
265
|
+
tenant: value["tenant"],
|
|
266
|
+
scope: typeof value["scope"] === "string" ? value["scope"] : void 0,
|
|
267
|
+
id: typeof value["id"] === "string" ? value["id"] : void 0
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function readMemberEntries(value) {
|
|
271
|
+
if (!Array.isArray(value)) return [];
|
|
272
|
+
return value.flatMap((item) => {
|
|
273
|
+
if (!isRecord$1(item) || !isRecord$1(item["principal"])) return [];
|
|
274
|
+
const id = item["principal"]["id"];
|
|
275
|
+
const [membership] = readMemberships([item["membership"]]).memberships;
|
|
276
|
+
return typeof id === "string" && id !== "" && membership !== void 0 ? [{
|
|
277
|
+
principal: { id },
|
|
278
|
+
membership
|
|
279
|
+
}] : [];
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function versionOf(value) {
|
|
283
|
+
const version = typeof value === "string" ? Number(value) : value;
|
|
284
|
+
return typeof version === "number" && Number.isFinite(version) ? version : void 0;
|
|
285
|
+
}
|
|
286
|
+
/** Reads `subject_for`'s JSON; anything it cannot read is left out, and a malformed record is no record. */
|
|
287
|
+
function readSubjectRecord(value) {
|
|
288
|
+
if (!isRecord$1(value) || typeof value["id"] !== "string") return;
|
|
289
|
+
if (value["active"] !== true) return {
|
|
290
|
+
id: value["id"],
|
|
291
|
+
active: false,
|
|
292
|
+
roles: [],
|
|
293
|
+
memberships: [],
|
|
294
|
+
customRoles: []
|
|
295
|
+
};
|
|
296
|
+
const version = value["authzVersion"];
|
|
297
|
+
return compact({
|
|
298
|
+
id: value["id"],
|
|
299
|
+
active: true,
|
|
300
|
+
roles: strings(value["roles"]),
|
|
301
|
+
memberships: readMemberships(value["memberships"]).memberships,
|
|
302
|
+
customRoles: Array.isArray(value["customRoles"]) ? value["customRoles"].flatMap((item) => {
|
|
303
|
+
const role = customRoleOf(item);
|
|
304
|
+
return role === void 0 ? [] : [role];
|
|
305
|
+
}) : [],
|
|
306
|
+
authzVersion: typeof version === "number" && Number.isFinite(version) ? version : void 0
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Membership, custom-role and subject sources over the `subject_for(p_user)`,
|
|
311
|
+
* `authz_version_for(p_user)` and `members_of(p_scope, p_id)` functions
|
|
312
|
+
* `permdock supabase hook generate` writes, for a backend that reaches
|
|
313
|
+
* Postgres only through PostgREST. `memberships.version` reads only the
|
|
314
|
+
* version, so `claimsFirst` with `onStale: 'reread'` reads the full subject
|
|
315
|
+
* only for a stale token; when the version function is missing it falls back
|
|
316
|
+
* to `subject_for`. With `policy`, a token that claims a custom role has its
|
|
317
|
+
* subject read in one `subject_for` call instead, and custom roles are read
|
|
318
|
+
* only for a subject that holds one. Each user and each instance is read once per
|
|
319
|
+
* `postgrestSources` call, so create it per request or per job.
|
|
320
|
+
*/
|
|
321
|
+
function postgrestSources(client, options = {}) {
|
|
322
|
+
const schema = options.schema ?? "permdock";
|
|
323
|
+
const fn = options.fn ?? "subject_for";
|
|
324
|
+
const membersFn = options.membersFn ?? "members_of";
|
|
325
|
+
const versionFn = options.versionFn ?? "authz_version_for";
|
|
326
|
+
const declared = options.policy === void 0 ? void 0 : options.policy.index.declaredRoles;
|
|
327
|
+
const onlyDeclared = (held) => declared !== void 0 && held?.every((name) => declared.has(name)) === true;
|
|
328
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
329
|
+
const versions = /* @__PURE__ */ new Map();
|
|
330
|
+
const listed = /* @__PURE__ */ new Map();
|
|
331
|
+
const list = (query) => {
|
|
332
|
+
const key = JSON.stringify([query.scope, query.id]);
|
|
333
|
+
const cached = listed.get(key);
|
|
334
|
+
if (cached !== void 0) return cached.then((entries) => [...entries]);
|
|
335
|
+
const pending = Promise.resolve(callRpc(client, schema, membersFn, {
|
|
336
|
+
p_scope: query.scope,
|
|
337
|
+
p_id: query.id
|
|
338
|
+
})).then((result) => {
|
|
339
|
+
if (result.error !== null) throw new Error(`PermDock: ${schema}.${membersFn} failed: ${result.error.message}`);
|
|
340
|
+
return readMemberEntries(result.data);
|
|
341
|
+
});
|
|
342
|
+
listed.set(key, pending);
|
|
343
|
+
return pending.then((entries) => [...entries]);
|
|
344
|
+
};
|
|
345
|
+
const record = (userId) => {
|
|
346
|
+
const cached = loaded.get(userId);
|
|
347
|
+
if (cached !== void 0) return cached;
|
|
348
|
+
const pending = Promise.resolve(callRpc(client, schema, fn, { p_user: userId })).then((result) => {
|
|
349
|
+
if (result.error !== null) throw new Error(`PermDock: ${schema}.${fn} failed: ${result.error.message}`);
|
|
350
|
+
return readSubjectRecord(result.data);
|
|
351
|
+
});
|
|
352
|
+
loaded.set(userId, pending);
|
|
353
|
+
return pending;
|
|
354
|
+
};
|
|
355
|
+
const version = (userId) => {
|
|
356
|
+
const read = loaded.get(userId);
|
|
357
|
+
if (read !== void 0) return read.then((found) => found?.authzVersion);
|
|
358
|
+
const cached = versions.get(userId);
|
|
359
|
+
if (cached !== void 0) return cached;
|
|
360
|
+
const pending = Promise.resolve(callRpc(client, schema, versionFn, { p_user: userId })).then((result) => result.error === null ? versionOf(result.data) : record(userId).then((found) => found?.authzVersion));
|
|
361
|
+
versions.set(userId, pending);
|
|
362
|
+
return pending;
|
|
363
|
+
};
|
|
364
|
+
const live = async (userId) => {
|
|
365
|
+
const found = await record(userId);
|
|
366
|
+
return found?.active === true ? found : void 0;
|
|
367
|
+
};
|
|
368
|
+
return {
|
|
369
|
+
memberships: {
|
|
370
|
+
async membershipsFor(principal) {
|
|
371
|
+
return [...(await live(principal.id))?.memberships ?? []];
|
|
372
|
+
},
|
|
373
|
+
version(principal) {
|
|
374
|
+
return declared !== void 0 && !onlyDeclared([...principal.roles ?? [], ...(principal.memberships ?? []).flatMap((membership) => membership.roles)]) ? record(principal.id).then((found) => found?.authzVersion) : version(principal.id);
|
|
375
|
+
},
|
|
376
|
+
list
|
|
377
|
+
},
|
|
378
|
+
customRoles(principal) {
|
|
379
|
+
return {
|
|
380
|
+
async rolesFor(tenant, context) {
|
|
381
|
+
if (onlyDeclared(context?.held)) return [];
|
|
382
|
+
return ((await live(principal.id))?.customRoles ?? []).filter((role) => role.tenant === tenant);
|
|
383
|
+
},
|
|
384
|
+
async globalRoles(context) {
|
|
385
|
+
if (onlyDeclared(context?.held)) return [];
|
|
386
|
+
return ((await live(principal.id))?.customRoles ?? []).filter((role) => role.scope === "global");
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
async subject(userId) {
|
|
391
|
+
const found = await live(userId);
|
|
392
|
+
if (found === void 0) return {
|
|
393
|
+
principal: null,
|
|
394
|
+
context: {}
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
principal: compact({
|
|
398
|
+
id: found.id,
|
|
399
|
+
roles: found.roles,
|
|
400
|
+
memberships: found.memberships,
|
|
401
|
+
authzVersion: found.authzVersion
|
|
402
|
+
}),
|
|
403
|
+
context: {}
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
record
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/supabase/approval-store.ts
|
|
411
|
+
function isRecord(value) {
|
|
412
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
413
|
+
}
|
|
414
|
+
/** A stored body as the request it is; the store wrote it from an `ApprovalRequest`. */
|
|
415
|
+
function requestOf(value) {
|
|
416
|
+
if (!isRecord(value) || value["v"] !== 1 || typeof value["token"] !== "string") return null;
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
function filterOf(filter) {
|
|
420
|
+
return compact({
|
|
421
|
+
status: filter.status,
|
|
422
|
+
tenant: filter.tenant,
|
|
423
|
+
principalId: filter.principalId,
|
|
424
|
+
actorId: filter.actorId,
|
|
425
|
+
session: filter.session
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* An `ApprovalStore` over the table and functions `rls.approvals` generates,
|
|
430
|
+
* called through supabase-js (`client.schema(schema).rpc(...)`) with a
|
|
431
|
+
* client that may execute them, such as a `service_role` client. Each method
|
|
432
|
+
* is one atomic statement in the database: a repeated ask keeps the open
|
|
433
|
+
* request, two racing approvers both count towards a quorum, and two racing
|
|
434
|
+
* resumes never both consume an approval.
|
|
435
|
+
*/
|
|
436
|
+
function supabaseApprovalStore(client, options = {}) {
|
|
437
|
+
const schema = options.schema ?? "permdock";
|
|
438
|
+
const call = async (fn, args) => {
|
|
439
|
+
const result = await callRpc(client, schema, fn, args);
|
|
440
|
+
if (result.error !== null) throw new Error(`PermDock: ${schema}.${fn} failed: ${result.error.message}`);
|
|
441
|
+
return result.data;
|
|
442
|
+
};
|
|
443
|
+
const get = async (token) => requestOf(await call("permdock_approval_get", { p_token: token }));
|
|
444
|
+
return {
|
|
445
|
+
...options.ttl === void 0 ? {} : { ttl: options.ttl },
|
|
446
|
+
async create(request) {
|
|
447
|
+
await call("permdock_approval_open", { p_request: request });
|
|
448
|
+
if (options.onOpen !== void 0) {
|
|
449
|
+
const stored = await get(request.token);
|
|
450
|
+
if (stored !== null && canonicalJson(stored) === canonicalJson(request)) await options.onOpen(stored);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
get,
|
|
454
|
+
async resolve(token, verdict) {
|
|
455
|
+
const current = await get(token);
|
|
456
|
+
if (current === null) throw new ApprovalError("approval-not-found", "approval was not found");
|
|
457
|
+
const next = applyApprovalVerdict(current, verdict);
|
|
458
|
+
const written = requestOf(await call("permdock_approval_resolve", {
|
|
459
|
+
p_token: token,
|
|
460
|
+
p_next: next,
|
|
461
|
+
p_seen: current.approvals?.length ?? 0
|
|
462
|
+
}));
|
|
463
|
+
if (written === null) throw new ApprovalError("approval-not-pending", "approval is not pending");
|
|
464
|
+
return written;
|
|
465
|
+
},
|
|
466
|
+
async consume(token, now = /* @__PURE__ */ new Date()) {
|
|
467
|
+
return requestOf(await call("permdock_approval_consume", {
|
|
468
|
+
p_token: token,
|
|
469
|
+
p_now: now.toISOString()
|
|
470
|
+
}));
|
|
471
|
+
},
|
|
472
|
+
async list(query) {
|
|
473
|
+
const after = query.cursor === void 0 ? void 0 : decodeApprovalCursor(query.cursor);
|
|
474
|
+
if (after === null) return { items: [] };
|
|
475
|
+
const size = approvalPageSize(query.limit);
|
|
476
|
+
const data = await call("permdock_approval_list", {
|
|
477
|
+
p_filter: filterOf(query),
|
|
478
|
+
p_after_created: after?.[0] ?? null,
|
|
479
|
+
p_after_token: after?.[1] ?? null,
|
|
480
|
+
p_limit: size + 1
|
|
481
|
+
});
|
|
482
|
+
const rows = Array.isArray(data) ? data.flatMap((row) => {
|
|
483
|
+
const request = requestOf(row);
|
|
484
|
+
return request === null ? [] : [request];
|
|
485
|
+
}) : [];
|
|
486
|
+
const items = rows.slice(0, size);
|
|
487
|
+
const last = items.at(-1);
|
|
488
|
+
return rows.length > size && last !== void 0 ? {
|
|
489
|
+
items,
|
|
490
|
+
next: encodeApprovalCursor(last)
|
|
491
|
+
} : { items };
|
|
492
|
+
},
|
|
493
|
+
async expire(now = /* @__PURE__ */ new Date()) {
|
|
494
|
+
const count = await call("permdock_approval_expire", { p_now: now.toISOString() });
|
|
495
|
+
return typeof count === "number" ? count : 0;
|
|
496
|
+
},
|
|
497
|
+
async cancel(filter, meta) {
|
|
498
|
+
const count = await call("permdock_approval_cancel", {
|
|
499
|
+
p_filter: filterOf(filter),
|
|
500
|
+
p_by: meta.by,
|
|
501
|
+
p_note: meta.note ?? null,
|
|
502
|
+
p_now: (/* @__PURE__ */ new Date()).toISOString()
|
|
503
|
+
});
|
|
504
|
+
return typeof count === "number" ? count : 0;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/supabase/rls.ts
|
|
510
|
+
function isTable(value) {
|
|
511
|
+
return value !== null && typeof value === "object" && "table" in value && typeof value.table === "string";
|
|
512
|
+
}
|
|
513
|
+
function supabaseRls(options = {}) {
|
|
514
|
+
const raw = options.memberships;
|
|
515
|
+
const memberships = raw === void 0 ? void 0 : isTable(raw) ? { tenant: raw } : raw;
|
|
516
|
+
return compact({
|
|
517
|
+
dialect: "supabase",
|
|
518
|
+
roleClaim: options.roleClaim ?? "user_role",
|
|
519
|
+
tenantClaim: options.tenantClaim ?? "tenant_id",
|
|
520
|
+
tenantType: options.tenantType,
|
|
521
|
+
memberships,
|
|
522
|
+
suspension: options.suspension,
|
|
523
|
+
realtime: options.realtime,
|
|
524
|
+
storage: options.storage
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const ident = (name) => quoteSqlIdent(name);
|
|
528
|
+
const table = (name) => quoteSqlTable(name);
|
|
529
|
+
const literal = quoteSqlLiteral;
|
|
530
|
+
function qualifiedTable(name) {
|
|
531
|
+
return table(name.includes(".") ? name : `public.${name}`);
|
|
532
|
+
}
|
|
533
|
+
/** The memberships table's role key over alias `m`, joined to its roles table when it holds a reference. */
|
|
534
|
+
function tenantRole(memberships) {
|
|
535
|
+
return roleColumn(memberships.role, memberships.table.includes(".") ? memberships.table : `public.${memberships.table}`, "m", {
|
|
536
|
+
label: "authorizeSql tenant.role",
|
|
537
|
+
indent: " "
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function textArray(values) {
|
|
541
|
+
return values.length === 0 ? `'{}'::text[]` : `array[${values.map(literal).join(", ")}]::text[]`;
|
|
542
|
+
}
|
|
543
|
+
function activeRow(row, id, text = false) {
|
|
544
|
+
const parts = [`s.${ident(row.id)}${text ? "::text" : ""} = ${id}`];
|
|
545
|
+
if (row.disabledAt !== void 0) parts.push(`s.${ident(row.disabledAt)} is null`);
|
|
546
|
+
if (row.status !== void 0) {
|
|
547
|
+
if (row.active === void 0 || row.active.length === 0) throw new TypeError("PermDock: a suspension status column needs its active values");
|
|
548
|
+
parts.push(`s.${ident(row.status)}::text = any(${textArray(row.active)})`);
|
|
549
|
+
}
|
|
550
|
+
if (row.disabledAt === void 0 && row.status === void 0) throw new TypeError("PermDock: a suspension table needs disabledAt or status");
|
|
551
|
+
return `exists (select 1 from ${qualifiedTable(row.table)} s where ${parts.join(" and ")})`;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Early `return false` for a suspended user, or a tenant request for a
|
|
555
|
+
* suspended instance unless its scope keeps the requested permission.
|
|
556
|
+
*/
|
|
557
|
+
function suspendedGuards(suspension, scope, uid) {
|
|
558
|
+
const lines = [];
|
|
559
|
+
const users = suspension?.users;
|
|
560
|
+
if (users !== void 0) lines.push(` if not ${activeRow(users, uid)} then
|
|
561
|
+
return false; -- suspended user
|
|
562
|
+
end if;`);
|
|
563
|
+
const tenant = suspension?.scopes?.[scope];
|
|
564
|
+
if (tenant !== void 0) {
|
|
565
|
+
const keep = keptKeys(tenant);
|
|
566
|
+
const kept = keep.length === 0 ? "" : ` and not (requested_permission::text = any(${textArray(keep)}))`;
|
|
567
|
+
lines.push(` if requested_tenant is not null and not ${activeRow(tenant, "requested_tenant", true)}${kept} then
|
|
568
|
+
return false; -- suspended ${scope}
|
|
569
|
+
end if;`);
|
|
570
|
+
}
|
|
571
|
+
return lines.map((line) => `\n${line}`).join("");
|
|
572
|
+
}
|
|
573
|
+
function claimEntries(where, value) {
|
|
574
|
+
return `array(select ${value} from jsonb_array_elements_text(cg.g) e where ${where})`;
|
|
575
|
+
}
|
|
576
|
+
/** A custom role's keys hold `requested_permission` in the first scope. */
|
|
577
|
+
function customHolds(q, scope, allows, denies, includes) {
|
|
578
|
+
return `exists (
|
|
579
|
+
select 1 from ${q("role_permissions")} rp
|
|
580
|
+
where rp.permission = requested_permission::text
|
|
581
|
+
and rp.scope = ${literal(scope)}
|
|
582
|
+
and rp.effect = 'allow'
|
|
583
|
+
and rp.grant_key in (select ${q("permdock_custom_keys")}(
|
|
584
|
+
${allows},
|
|
585
|
+
${denies},
|
|
586
|
+
${includes},
|
|
587
|
+
${literal(scope)}
|
|
588
|
+
))
|
|
589
|
+
)`;
|
|
590
|
+
}
|
|
591
|
+
function disabledLine(memberships, suspension) {
|
|
592
|
+
if (memberships.disabledAt === void 0) return "";
|
|
593
|
+
const column = `m.${ident(memberships.disabledAt)}`;
|
|
594
|
+
const keep = keptKeys(suspension?.memberships);
|
|
595
|
+
return keep.length === 0 ? `\n and ${column} is null` : `\n and (${column} is null or requested_permission::text = any(${textArray(keep)}))`;
|
|
596
|
+
}
|
|
597
|
+
function claimKept(suspension) {
|
|
598
|
+
return keptKeys(suspension?.memberships).length === 0 ? "" : `\n and case coalesce(jsonb_typeof(m -> 'keep'), 'null') when 'null' then true when 'array' then m -> 'keep' @> jsonb_build_array(requested_permission::text) else false end`;
|
|
599
|
+
}
|
|
600
|
+
function customDatabaseBranch(q, scope, memberships, declared, levels, suspension) {
|
|
601
|
+
const role = tenantRole(memberships);
|
|
602
|
+
const match = `c.tenant_id::text = requested_tenant and c.scope = ${literal(scope)} and c.scope_id is null and c.role = ${role.sql}`;
|
|
603
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${q(source)} c where ${match}${extra})`;
|
|
604
|
+
return ` or exists (
|
|
605
|
+
select 1
|
|
606
|
+
from ${qualifiedTable(memberships.table)} m${role.join}
|
|
607
|
+
where m.${ident(memberships.user)} = v_member_user
|
|
608
|
+
and m.${ident(memberships.tenant)} = v_member_tenant
|
|
609
|
+
and not (${role.sql} = any(${textArray(declared)}))${memberships.expiresAt === void 0 ? "" : `\n and (m.${ident(memberships.expiresAt)} is null or m.${ident(memberships.expiresAt)} > now())`}${disabledLine(memberships, suspension)}
|
|
610
|
+
and ${customHolds(q, scope, rows("custom_role_permissions", levels ? "permission || coalesce('@' || c.level, '')" : "permission", " and c.effect = 'allow'"), rows("custom_role_permissions", "permission", " and c.effect = 'deny'"), rows("custom_role_includes", "include_role", ""))}
|
|
611
|
+
)`;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* The `role_permissions` match for one effect. An allow counts only without
|
|
615
|
+
* row conditions (`grant_key = permission`): `authorize()` never sees the row.
|
|
616
|
+
* A deny counts with or without them, so a conditional deny still denies.
|
|
617
|
+
*/
|
|
618
|
+
function effectMatch(scope, effect) {
|
|
619
|
+
return `rp.permission = requested_permission::text
|
|
620
|
+
and rp.scope = ${scope}
|
|
621
|
+
and rp.effect = '${effect}'${effect === "allow" ? "\n and rp.grant_key = rp.permission" : ""}`;
|
|
622
|
+
}
|
|
623
|
+
function databaseBody(q, scope, memberships, custom, suspension) {
|
|
624
|
+
const global = (effect) => `exists (
|
|
625
|
+
select 1
|
|
626
|
+
from ${q("user_roles")} ur
|
|
627
|
+
join ${q("role_permissions")} rp on rp.role = ur.role::text
|
|
628
|
+
where ur.user_id = uid
|
|
629
|
+
and ${effectMatch(`'global'`, effect)}
|
|
630
|
+
)`;
|
|
631
|
+
const tenantColumn = memberships?.tenant;
|
|
632
|
+
let tenantBranch = ` if requested_tenant is not null then
|
|
633
|
+
return false; -- no memberships table configured
|
|
634
|
+
end if;`;
|
|
635
|
+
if (memberships !== void 0 && tenantColumn !== void 0) {
|
|
636
|
+
const role = tenantRole(memberships);
|
|
637
|
+
const held = (effect) => `exists (
|
|
638
|
+
select 1
|
|
639
|
+
from ${qualifiedTable(memberships.table)} m${role.join}
|
|
640
|
+
join ${q("role_permissions")} rp on rp.role = ${role.sql}
|
|
641
|
+
where m.${ident(memberships.user)} = v_member_user
|
|
642
|
+
and m.${ident(tenantColumn)} = v_member_tenant
|
|
643
|
+
and ${effectMatch(literal(scope), effect)}${memberships.expiresAt === void 0 ? "" : `\n and (m.${ident(memberships.expiresAt)} is null or m.${ident(memberships.expiresAt)} > now())`}${disabledLine(memberships, suspension)}
|
|
644
|
+
)`;
|
|
645
|
+
tenantBranch = ` if requested_tenant is not null then
|
|
646
|
+
begin
|
|
647
|
+
v_member_user := uid;
|
|
648
|
+
v_member_tenant := requested_tenant;
|
|
649
|
+
exception when invalid_text_representation or numeric_value_out_of_range then
|
|
650
|
+
return false; -- not an id of the memberships table
|
|
651
|
+
end;
|
|
652
|
+
return (${held("allow")}${custom === void 0 ? "" : customDatabaseBranch(q, scope, {
|
|
653
|
+
...memberships,
|
|
654
|
+
tenant: tenantColumn
|
|
655
|
+
}, custom.declared, custom.levels === true, suspension)})
|
|
656
|
+
and not ${held("deny")}
|
|
657
|
+
and not ${global("deny")};
|
|
658
|
+
end if;`;
|
|
659
|
+
}
|
|
660
|
+
return `declare
|
|
661
|
+
uid uuid := (select auth.uid());${memberships !== void 0 && tenantColumn !== void 0 ? `
|
|
662
|
+
v_member_user ${qualifiedTable(memberships.table)}.${ident(memberships.user)}%type;
|
|
663
|
+
v_member_tenant ${qualifiedTable(memberships.table)}.${ident(tenantColumn)}%type;` : ""}
|
|
664
|
+
begin
|
|
665
|
+
if uid is null then
|
|
666
|
+
return false;
|
|
667
|
+
end if;${suspendedGuards(suspension, scope, "uid")}
|
|
668
|
+
${tenantBranch}
|
|
669
|
+
return ${global("allow")}
|
|
670
|
+
and not ${global("deny")};
|
|
671
|
+
end;`;
|
|
672
|
+
}
|
|
673
|
+
/** A claim membership whose `expiresAt` (epoch seconds) has passed no longer holds its roles. */
|
|
674
|
+
const CLAIM_UNEXPIRED = `case jsonb_typeof(m -> 'expiresAt')
|
|
675
|
+
when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
|
|
676
|
+
else true
|
|
677
|
+
end`;
|
|
678
|
+
function jwtBody(q, scope, custom, suspension) {
|
|
679
|
+
const memberships = `jsonb_array_elements(
|
|
680
|
+
case jsonb_typeof(coalesce(claims -> 'memberships', claims -> 'app_metadata' -> 'memberships'))
|
|
681
|
+
when 'array' then coalesce(claims -> 'memberships', claims -> 'app_metadata' -> 'memberships')
|
|
682
|
+
else '[]'::jsonb
|
|
683
|
+
end
|
|
684
|
+
) m
|
|
685
|
+
cross join lateral jsonb_array_elements_text(
|
|
686
|
+
case jsonb_typeof(m -> 'roles') when 'array' then m -> 'roles' else '[]'::jsonb end
|
|
687
|
+
) r(role)`;
|
|
688
|
+
const customBranch = custom === void 0 ? "" : ` or exists (
|
|
689
|
+
select 1
|
|
690
|
+
from ${memberships}
|
|
691
|
+
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
692
|
+
where m ->> 'scope' = ${literal(scope)}
|
|
693
|
+
and m ->> 'id' = requested_tenant
|
|
694
|
+
and ${CLAIM_UNEXPIRED}${claimKept(suspension)}
|
|
695
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
696
|
+
and not (r.role = any(${textArray(custom.declared)}))
|
|
697
|
+
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)"))}
|
|
698
|
+
)`;
|
|
699
|
+
const held = (effect) => `exists (
|
|
700
|
+
select 1
|
|
701
|
+
from ${memberships}
|
|
702
|
+
join ${q("role_permissions")} rp on rp.role = r.role
|
|
703
|
+
where m ->> 'scope' = ${literal(scope)}
|
|
704
|
+
and m ->> 'id' = requested_tenant
|
|
705
|
+
and ${CLAIM_UNEXPIRED}${claimKept(suspension)}
|
|
706
|
+
and ${effectMatch(literal(scope), effect)}
|
|
707
|
+
)`;
|
|
708
|
+
const global = (effect) => `exists (
|
|
709
|
+
select 1
|
|
710
|
+
from jsonb_array_elements_text(
|
|
711
|
+
case jsonb_typeof(role_claim)
|
|
712
|
+
when 'array' then role_claim
|
|
713
|
+
when 'string' then jsonb_build_array(role_claim)
|
|
714
|
+
else '[]'::jsonb
|
|
715
|
+
end
|
|
716
|
+
) r(role)
|
|
717
|
+
join ${q("role_permissions")} rp on rp.role = r.role
|
|
718
|
+
where ${effectMatch(`'global'`, effect)}
|
|
719
|
+
)`;
|
|
720
|
+
return `declare
|
|
721
|
+
claims jsonb := (select auth.jwt());
|
|
722
|
+
role_claim jsonb;
|
|
723
|
+
begin
|
|
724
|
+
if claims is null or (select auth.uid()) is null then
|
|
725
|
+
return false;
|
|
726
|
+
end if;${suspendedGuards(suspension, scope, "(select auth.uid())")}
|
|
727
|
+
-- a top-level null (no role row) falls back to app_metadata, like subjectFromSupabase
|
|
728
|
+
role_claim := coalesce(nullif(claims -> 'user_role', 'null'::jsonb), claims -> 'app_metadata' -> 'user_role');
|
|
729
|
+
if requested_tenant is not null then
|
|
730
|
+
return (${held("allow")}${customBranch})
|
|
731
|
+
and not ${held("deny")}
|
|
732
|
+
and not ${global("deny")};
|
|
733
|
+
end if;
|
|
734
|
+
return ${global("allow")}
|
|
735
|
+
and not ${global("deny")};
|
|
736
|
+
end;`;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* `authorize(requested_permission, requested_tenant text default null)` for Supabase's RBAC
|
|
740
|
+
* scaffold, over the `role_permissions (role, permission, grant_key, scope, effect)` table that
|
|
741
|
+
* `permdock rls generate` seeds. `database` reads `user_roles` (and the memberships table for a
|
|
742
|
+
* tenant) on every call; `jwt` reads the hook-injected `user_role` and `memberships` claims. A
|
|
743
|
+
* tenant request with no memberships source is denied, never answered from global roles. It
|
|
744
|
+
* answers "does a role hold this permission" without seeing a row: an allow with row conditions
|
|
745
|
+
* does not count, and a deny the user's tenant or global roles hold overrides every allow.
|
|
746
|
+
* Generated policies call the per-statement RLS helpers (`permdock_has`, `permitted_<scope>_ids`)
|
|
747
|
+
* instead. Only `authenticated` may execute it.
|
|
748
|
+
*/
|
|
749
|
+
function authorizeSql(options = {}) {
|
|
750
|
+
const schema = options.schema ?? "permdock";
|
|
751
|
+
const q = (name) => table(`${schema}.${name}`);
|
|
752
|
+
const memberships = typeof options.tenant === "object" ? options.tenant : void 0;
|
|
753
|
+
const scope = options.scope ?? "tenant";
|
|
754
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(scope)) throw new TypeError(`PermDock: unsafe scope name '${scope}'`);
|
|
755
|
+
const body = options.authorize === "jwt" ? jwtBody(q, scope, options.customRoles, options.suspension) : databaseBody(q, scope, memberships, options.customRoles, options.suspension);
|
|
756
|
+
const signature = `${q("authorize")}(${q("app_permission")}, text)`;
|
|
757
|
+
return `create or replace function ${q("authorize")}(
|
|
758
|
+
requested_permission ${q("app_permission")},
|
|
759
|
+
requested_tenant text default null
|
|
760
|
+
)
|
|
761
|
+
returns boolean
|
|
762
|
+
language plpgsql
|
|
763
|
+
stable
|
|
764
|
+
security definer
|
|
765
|
+
set search_path = ''
|
|
766
|
+
as $$
|
|
767
|
+
${body}
|
|
768
|
+
$$;
|
|
769
|
+
revoke execute on function ${signature} from public, anon;
|
|
770
|
+
grant execute on function ${signature} to authenticated;
|
|
771
|
+
`;
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
export { readSubjectRecord as a, postgrestSources as i, supabaseRls as n, supabaseClaims as o, supabaseApprovalStore as r, exchangeCapability as s, authorizeSql as t };
|