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,693 @@
|
|
|
1
|
+
import { c as getResource } from "./permissions-DVoF6dMt.js";
|
|
2
|
+
import { o as usageResult } from "./errors-CWfoNg9N.js";
|
|
3
|
+
import { a as USAGE_REPORT_SCHEMA } from "./version-BvacVTZR.js";
|
|
4
|
+
import { o as jsonSchemaOf } from "./catalog-doc-Dsc9y7sd.js";
|
|
5
|
+
import { i as rel, r as listSourceFiles } from "./files-DWUN9PzI.js";
|
|
6
|
+
import { a as loadModule, n as asPolicy, o as pickNamed } from "./load-DhWEB9P1.js";
|
|
7
|
+
import { r as runCollect } from "./collect-DDDv_ESs.js";
|
|
8
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
9
|
+
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import { createJiti } from "jiti";
|
|
12
|
+
import { Visitor, parseSync } from "oxc-parser";
|
|
13
|
+
import { isBuiltin } from "node:module";
|
|
14
|
+
//#region src/cli/doctor-source.ts
|
|
15
|
+
/** Entries a client file must not import; `tests/cli/doctor-entries.test.ts` keeps it in step with `exports`. */
|
|
16
|
+
const SERVER_SPECIFIERS = [
|
|
17
|
+
"permdock/a2a",
|
|
18
|
+
"permdock/ai-sdk",
|
|
19
|
+
"permdock/approvals",
|
|
20
|
+
"permdock/authzen",
|
|
21
|
+
"permdock/better-auth",
|
|
22
|
+
"permdock/better-supabase",
|
|
23
|
+
"permdock/claude-agent",
|
|
24
|
+
"permdock/clerk",
|
|
25
|
+
"permdock/cloud",
|
|
26
|
+
"permdock/convex",
|
|
27
|
+
"permdock/drizzle",
|
|
28
|
+
"permdock/elysia",
|
|
29
|
+
"permdock/eve",
|
|
30
|
+
"permdock/express",
|
|
31
|
+
"permdock/fastify",
|
|
32
|
+
"permdock/hono",
|
|
33
|
+
"permdock/jwt",
|
|
34
|
+
"permdock/kysely",
|
|
35
|
+
"permdock/mcp",
|
|
36
|
+
"permdock/nest",
|
|
37
|
+
"permdock/next",
|
|
38
|
+
"permdock/node",
|
|
39
|
+
"permdock/openai",
|
|
40
|
+
"permdock/openapi",
|
|
41
|
+
"permdock/orpc",
|
|
42
|
+
"permdock/otel",
|
|
43
|
+
"permdock/pdp",
|
|
44
|
+
"permdock/prisma",
|
|
45
|
+
"permdock/scim",
|
|
46
|
+
"permdock/server",
|
|
47
|
+
"permdock/ssf",
|
|
48
|
+
"permdock/supabase",
|
|
49
|
+
"permdock/supabase/middleware",
|
|
50
|
+
"permdock/terminal",
|
|
51
|
+
"permdock/trpc"
|
|
52
|
+
];
|
|
53
|
+
/** Entries that feed request, tool or model data into a check. */
|
|
54
|
+
const ADAPTER_SPECIFIERS = [
|
|
55
|
+
"permdock/a2a",
|
|
56
|
+
"permdock/ai-sdk",
|
|
57
|
+
"permdock/authzen",
|
|
58
|
+
"permdock/claude-agent",
|
|
59
|
+
"permdock/elysia",
|
|
60
|
+
"permdock/eve",
|
|
61
|
+
"permdock/express",
|
|
62
|
+
"permdock/fastify",
|
|
63
|
+
"permdock/hono",
|
|
64
|
+
"permdock/mcp",
|
|
65
|
+
"permdock/nest",
|
|
66
|
+
"permdock/next",
|
|
67
|
+
"permdock/node",
|
|
68
|
+
"permdock/openai",
|
|
69
|
+
"permdock/orpc",
|
|
70
|
+
"permdock/server",
|
|
71
|
+
"permdock/supabase/middleware",
|
|
72
|
+
"permdock/terminal",
|
|
73
|
+
"permdock/trpc",
|
|
74
|
+
"permdock/webmcp"
|
|
75
|
+
];
|
|
76
|
+
const UNTRUSTED_CLAIMS = [
|
|
77
|
+
"user_metadata",
|
|
78
|
+
"unsafeMetadata",
|
|
79
|
+
"untrusted_metadata",
|
|
80
|
+
"clientMetadata",
|
|
81
|
+
"preferred_username"
|
|
82
|
+
];
|
|
83
|
+
function withoutComments(source) {
|
|
84
|
+
const { comments } = parseSync(source.file, source.text);
|
|
85
|
+
let text = source.text;
|
|
86
|
+
for (const comment of comments) text = text.slice(0, comment.start) + " ".repeat(comment.end - comment.start) + text.slice(comment.end);
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
function isClientSource(source, clientEntries) {
|
|
90
|
+
return clientEntries.has(source.file) || source.text.includes("'use client'") || source.text.includes("\"use client\"") || source.file.includes(".client.");
|
|
91
|
+
}
|
|
92
|
+
/** Specifiers a module loads at runtime: value imports, re-exports and literal dynamic imports. */
|
|
93
|
+
function runtimeSpecifiers(source) {
|
|
94
|
+
const { module } = parseSync(source.file, source.text);
|
|
95
|
+
const out = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const item of module.staticImports) if (item.entries.length === 0 || item.entries.some((entry) => !entry.isType)) out.add(item.moduleRequest.value);
|
|
97
|
+
for (const item of module.staticExports) for (const entry of item.entries) if (entry.moduleRequest !== null && !entry.isType) out.add(entry.moduleRequest.value);
|
|
98
|
+
for (const item of module.dynamicImports) {
|
|
99
|
+
const literal = /^\s*(['"`])([^'"`$]+)\1\s*$/u.exec(source.text.slice(item.moduleRequest.start, item.moduleRequest.end));
|
|
100
|
+
if (literal?.[2] !== void 0) out.add(literal[2]);
|
|
101
|
+
}
|
|
102
|
+
return [...out];
|
|
103
|
+
}
|
|
104
|
+
function realFile(path) {
|
|
105
|
+
try {
|
|
106
|
+
return realpathSync(path);
|
|
107
|
+
} catch {
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A resolver for imports of `file`, the way the bundler resolves them:
|
|
113
|
+
* relative paths with or without an extension, the `paths` of the nearest
|
|
114
|
+
* `tsconfig.json`, and package names through `node_modules` and each
|
|
115
|
+
* package's `exports`. An unresolvable specifier gives `undefined`.
|
|
116
|
+
*/
|
|
117
|
+
function resolverFor(file) {
|
|
118
|
+
const jiti = createJiti(file, { tsconfigPaths: dirname(file) });
|
|
119
|
+
const parentURL = pathToFileURL(file);
|
|
120
|
+
return (specifier) => {
|
|
121
|
+
if (specifier === "" || isBuiltin(specifier)) return;
|
|
122
|
+
try {
|
|
123
|
+
const url = jiti.esmResolve(specifier, {
|
|
124
|
+
parentURL,
|
|
125
|
+
try: true
|
|
126
|
+
});
|
|
127
|
+
const path = url?.startsWith("file:") === true ? fileURLToPath(url) : void 0;
|
|
128
|
+
return path !== void 0 && existsSync(path) ? realFile(path) : void 0;
|
|
129
|
+
} catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function pd001(sources, clientEntries = /* @__PURE__ */ new Set(), policy) {
|
|
135
|
+
const findings = [];
|
|
136
|
+
const policyFile = policy === void 0 ? void 0 : realFile(resolve(policy.cwd, policy.path));
|
|
137
|
+
for (const source of sources) {
|
|
138
|
+
if (!isClientSource(source, clientEntries)) continue;
|
|
139
|
+
if (policy !== void 0 && policyFile !== void 0) {
|
|
140
|
+
const resolveImport = resolverFor(resolve(policy.cwd, source.file));
|
|
141
|
+
for (const specifier of runtimeSpecifiers(source)) if (specifier !== "permdock" && !specifier.startsWith("permdock/") && resolveImport(specifier) === policyFile) findings.push({
|
|
142
|
+
code: "PD001",
|
|
143
|
+
severity: "error",
|
|
144
|
+
message: `${source.file} imports the policy module through ${specifier}`,
|
|
145
|
+
fix: "decide on the server and pass a snapshot to the client ('permdock/react'); never import the policy into a client entry"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
for (const spec of SERVER_SPECIFIERS) if (source.text.includes(`'${spec}'`) || source.text.includes(`"${spec}"`)) findings.push({
|
|
149
|
+
code: "PD001",
|
|
150
|
+
severity: "error",
|
|
151
|
+
message: `${source.file} imports ${spec}`,
|
|
152
|
+
fix: "move the check into a Server Component or import from 'permdock/react'"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return findings;
|
|
156
|
+
}
|
|
157
|
+
function pd007(sources) {
|
|
158
|
+
const hasNever = sources.some((source) => /validate\s*:\s*['"]never['"]/u.test(source.text));
|
|
159
|
+
const hasAdapter = sources.some((source) => ADAPTER_SPECIFIERS.some((spec) => source.text.includes(spec)));
|
|
160
|
+
if (hasNever && hasAdapter) return [{
|
|
161
|
+
code: "PD007",
|
|
162
|
+
severity: "warning",
|
|
163
|
+
message: "policy sets validate: 'never' while an HTTP, MCP or agent adapter is imported",
|
|
164
|
+
fix: "use validate: 'boundary' for untrusted input"
|
|
165
|
+
}];
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
function pd008(sources) {
|
|
169
|
+
const findings = [];
|
|
170
|
+
for (const source of sources) {
|
|
171
|
+
if (!source.text.includes("permdock")) continue;
|
|
172
|
+
if (/export\s+(?:const|function|class)\s+(?:dock|ability)\b/u.test(source.text)) findings.push({
|
|
173
|
+
code: "PD008",
|
|
174
|
+
severity: "warning",
|
|
175
|
+
message: `${source.file} exports a reserved name (dock or ability)`,
|
|
176
|
+
fix: "use createPermDock and permdock"
|
|
177
|
+
});
|
|
178
|
+
if (/export\s+(?:const|function|type|interface)\s+\$/u.test(source.text)) findings.push({
|
|
179
|
+
code: "PD008",
|
|
180
|
+
severity: "warning",
|
|
181
|
+
message: `${source.file} exports a $ prefixed member`,
|
|
182
|
+
fix: "drop the $ prefix"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return findings;
|
|
186
|
+
}
|
|
187
|
+
function pd010(sources) {
|
|
188
|
+
const findings = [];
|
|
189
|
+
for (const source of sources) {
|
|
190
|
+
const text = withoutComments(source);
|
|
191
|
+
for (const claim of UNTRUSTED_CLAIMS) if (text.includes(claim) && /roles|tenant/u.test(text)) findings.push({
|
|
192
|
+
code: "PD010",
|
|
193
|
+
severity: "error",
|
|
194
|
+
message: `${source.file} reads roles or tenant from ${claim}`,
|
|
195
|
+
fix: "use a server-set claim or a MembershipSource"
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return findings;
|
|
199
|
+
}
|
|
200
|
+
function pd011(sources) {
|
|
201
|
+
const findings = [];
|
|
202
|
+
for (const source of sources) if (/tenant['"]?\s*:\s*['"]hd['"]/u.test(source.text) || /accounts\.google\.com/u.test(source.text) && /tenant/.test(source.text)) findings.push({
|
|
203
|
+
code: "PD011",
|
|
204
|
+
severity: "warning",
|
|
205
|
+
message: `${source.file} reads tenant from an optional issuer claim`,
|
|
206
|
+
fix: "compare the claim to onboarded tenants; do not default a tenant"
|
|
207
|
+
});
|
|
208
|
+
return findings;
|
|
209
|
+
}
|
|
210
|
+
function pd013(sources) {
|
|
211
|
+
const findings = [];
|
|
212
|
+
for (const source of sources) {
|
|
213
|
+
if (/algorithms[\s\S]{0,120}EdDSA/u.test(source.text)) findings.push({
|
|
214
|
+
code: "PD013",
|
|
215
|
+
severity: "warning",
|
|
216
|
+
message: `${source.file} lists polymorphic EdDSA`,
|
|
217
|
+
fix: "write Ed25519"
|
|
218
|
+
});
|
|
219
|
+
if (/algorithms[\s\S]{0,120}['"]none['"]/u.test(source.text)) findings.push({
|
|
220
|
+
code: "PD013",
|
|
221
|
+
severity: "error",
|
|
222
|
+
message: `${source.file} allows alg none`,
|
|
223
|
+
fix: "remove none and RSA1_5 from algorithms"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return findings;
|
|
227
|
+
}
|
|
228
|
+
function unwrapped(node) {
|
|
229
|
+
let current = node ?? void 0;
|
|
230
|
+
while (current?.type === "TSAsExpression" || current?.type === "TSSatisfiesExpression" || current?.type === "ParenthesizedExpression") current = current.expression;
|
|
231
|
+
return current;
|
|
232
|
+
}
|
|
233
|
+
function propertyName(property) {
|
|
234
|
+
if (property.computed) return;
|
|
235
|
+
const key = property.key;
|
|
236
|
+
if (key.type === "Identifier") return key.name;
|
|
237
|
+
return key.type === "Literal" && typeof key.value === "string" ? key.value : void 0;
|
|
238
|
+
}
|
|
239
|
+
function stringValue(node) {
|
|
240
|
+
const value = unwrapped(node);
|
|
241
|
+
if (value?.type === "Literal") return typeof value.value === "string" ? value.value : void 0;
|
|
242
|
+
return value?.type === "TemplateLiteral" ? value.quasis[0]?.value.cooked ?? void 0 : void 0;
|
|
243
|
+
}
|
|
244
|
+
function isPermDockSpecifier(value) {
|
|
245
|
+
return value === "permdock" || value.startsWith("permdock/");
|
|
246
|
+
}
|
|
247
|
+
function permdockOptionObjects(source) {
|
|
248
|
+
const { program } = parseSync(source.file, source.text);
|
|
249
|
+
const functions = /* @__PURE__ */ new Set();
|
|
250
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
251
|
+
const constants = /* @__PURE__ */ new Map();
|
|
252
|
+
const calls = [];
|
|
253
|
+
new Visitor({
|
|
254
|
+
ImportDeclaration(node) {
|
|
255
|
+
if (node.importKind === "type" || !isPermDockSpecifier(node.source.value)) return;
|
|
256
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportNamespaceSpecifier") namespaces.add(specifier.local.name);
|
|
257
|
+
else if (specifier.type === "ImportDefaultSpecifier" || specifier.importKind !== "type") functions.add(specifier.local.name);
|
|
258
|
+
},
|
|
259
|
+
VariableDeclarator(node) {
|
|
260
|
+
const init = unwrapped(node.init);
|
|
261
|
+
if (node.id.type === "Identifier" && init?.type === "ObjectExpression") constants.set(node.id.name, init);
|
|
262
|
+
},
|
|
263
|
+
CallExpression(node) {
|
|
264
|
+
calls.push(node);
|
|
265
|
+
}
|
|
266
|
+
}).visit(program);
|
|
267
|
+
const objects = [];
|
|
268
|
+
const collect = (node) => {
|
|
269
|
+
const value = unwrapped(node);
|
|
270
|
+
const target = value?.type === "Identifier" ? constants.get(value.name) : value;
|
|
271
|
+
if (target?.type !== "ObjectExpression" || objects.includes(target)) return;
|
|
272
|
+
objects.push(target);
|
|
273
|
+
for (const property of target.properties) if (property.type === "Property") collect(property.value);
|
|
274
|
+
};
|
|
275
|
+
for (const call of calls) {
|
|
276
|
+
const callee = call.callee;
|
|
277
|
+
if (callee.type === "Identifier" && functions.has(callee.name) || callee.type === "MemberExpression" && callee.object.type === "Identifier" && namespaces.has(callee.object.name)) for (const argument of call.arguments) collect(argument);
|
|
278
|
+
}
|
|
279
|
+
return objects;
|
|
280
|
+
}
|
|
281
|
+
function pd014(sources) {
|
|
282
|
+
const findings = [];
|
|
283
|
+
for (const source of sources) {
|
|
284
|
+
if (!source.text.includes("permdock")) continue;
|
|
285
|
+
for (const object of permdockOptionObjects(source)) {
|
|
286
|
+
const keys = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const property of object.properties) {
|
|
288
|
+
if (property.type !== "Property") continue;
|
|
289
|
+
const name = propertyName(property);
|
|
290
|
+
if (name !== void 0) keys.set(name, property.value);
|
|
291
|
+
}
|
|
292
|
+
const discovery = keys.get("discovery");
|
|
293
|
+
const setsDiscovery = keys.has("discovery");
|
|
294
|
+
const setsJwks = keys.has("jwks");
|
|
295
|
+
if (stringValue(discovery)?.startsWith("http:") === true) findings.push({
|
|
296
|
+
code: "PD014",
|
|
297
|
+
severity: "error",
|
|
298
|
+
message: `${source.file} uses a plain-HTTP discovery URL`,
|
|
299
|
+
fix: "use an https: issuer"
|
|
300
|
+
});
|
|
301
|
+
if (setsDiscovery && (setsJwks || keys.has("issuer"))) findings.push({
|
|
302
|
+
code: "PD014",
|
|
303
|
+
severity: "error",
|
|
304
|
+
message: `${source.file} sets discovery together with jwks or issuer`,
|
|
305
|
+
fix: "use discovery alone, or jwks plus issuer"
|
|
306
|
+
});
|
|
307
|
+
if (setsJwks && !setsDiscovery && !keys.has("issuer")) findings.push({
|
|
308
|
+
code: "PD014",
|
|
309
|
+
severity: "error",
|
|
310
|
+
message: `${source.file} sets jwks without issuer`,
|
|
311
|
+
fix: "set issuer with jwks, or switch to discovery"
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return findings;
|
|
316
|
+
}
|
|
317
|
+
function pd015(sources) {
|
|
318
|
+
const findings = [];
|
|
319
|
+
for (const source of sources) if (/accept:\s*['"]id-token['"]/u.test(source.text)) findings.push({
|
|
320
|
+
code: "PD015",
|
|
321
|
+
severity: "warning",
|
|
322
|
+
message: `${source.file} accepts id-token on what looks like an API resolver`,
|
|
323
|
+
fix: "leave accept as 'access-token' for API routes"
|
|
324
|
+
});
|
|
325
|
+
return findings;
|
|
326
|
+
}
|
|
327
|
+
const SUPABASE_SUBJECT_CALL = /\bsubjectFromSupabase(?:Session)?\s*\(/gu;
|
|
328
|
+
/** Top-level arguments of the call whose `(` ends at `open`, or undefined when unbalanced. */
|
|
329
|
+
function callArguments(text, open) {
|
|
330
|
+
const args = [];
|
|
331
|
+
let depth = 0;
|
|
332
|
+
let quote;
|
|
333
|
+
let start = open + 1;
|
|
334
|
+
for (let index = open + 1; index < text.length; index += 1) {
|
|
335
|
+
const char = text[index];
|
|
336
|
+
if (quote !== void 0) {
|
|
337
|
+
if (char === "\\") index += 1;
|
|
338
|
+
else if (char === quote) quote = void 0;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (char === "'" || char === "\"" || char === "`") quote = char;
|
|
342
|
+
else if (char === "(" || char === "{" || char === "[") depth += 1;
|
|
343
|
+
else if (char === ")" || char === "}" || char === "]") {
|
|
344
|
+
if (depth === 0) {
|
|
345
|
+
args.push(text.slice(start, index).trim());
|
|
346
|
+
return args.filter((arg) => arg !== "");
|
|
347
|
+
}
|
|
348
|
+
depth -= 1;
|
|
349
|
+
} else if (char === "," && depth === 0) {
|
|
350
|
+
args.push(text.slice(start, index).trim());
|
|
351
|
+
start = index + 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** The claim a `subjectFromSupabase` call reads the tenant from, or undefined when the options are not a literal. */
|
|
356
|
+
function readTenantClaim(options, fallback) {
|
|
357
|
+
if (options === void 0) return fallback;
|
|
358
|
+
if (!options.startsWith("{")) return;
|
|
359
|
+
const tenant = /(?:^|[{,\s])tenant\s*:\s*(?:'([^']*)'|"([^"]*)"|([^,}\s]+))/u.exec(options);
|
|
360
|
+
if (tenant === null) return fallback;
|
|
361
|
+
return tenant[1] ?? tenant[2];
|
|
362
|
+
}
|
|
363
|
+
function pd038(sources, tenantClaim, fallback) {
|
|
364
|
+
const findings = [];
|
|
365
|
+
for (const source of sources) for (const match of source.text.matchAll(SUPABASE_SUBJECT_CALL)) {
|
|
366
|
+
const open = match.index + match[0].length - 1;
|
|
367
|
+
const args = callArguments(source.text, open);
|
|
368
|
+
if (args === void 0) continue;
|
|
369
|
+
const read = readTenantClaim(args[1], fallback);
|
|
370
|
+
if (read === void 0 || read === tenantClaim) continue;
|
|
371
|
+
const line = source.text.slice(0, match.index).split("\n").length;
|
|
372
|
+
findings.push({
|
|
373
|
+
code: "PD038",
|
|
374
|
+
severity: "warning",
|
|
375
|
+
message: `${source.file}:${line} reads the tenant from '${read}', but rls.tenantClaim is '${tenantClaim}'`,
|
|
376
|
+
fix: `pass tenant: '${tenantClaim}' or set rls.tenantClaim to '${read}'`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return findings;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* PD057: `rls.anonymousSignIns` is `'deny'`, so RLS refuses `signInAnonymously()` users, but a
|
|
383
|
+
* `subjectFromSupabase(Session)` call maps them as signed-in users. Options that are not a
|
|
384
|
+
* literal object are not read.
|
|
385
|
+
*/
|
|
386
|
+
function pd057(sources) {
|
|
387
|
+
const findings = [];
|
|
388
|
+
for (const source of sources) for (const match of source.text.matchAll(SUPABASE_SUBJECT_CALL)) {
|
|
389
|
+
const open = match.index + match[0].length - 1;
|
|
390
|
+
const args = callArguments(source.text, open);
|
|
391
|
+
if (args === void 0) continue;
|
|
392
|
+
const options = args[1];
|
|
393
|
+
if (options !== void 0 && !options.startsWith("{")) continue;
|
|
394
|
+
if (options !== void 0 && /(?:^|[{,\s])anonymousSignIns\s*:\s*['"]deny['"]/u.test(options)) continue;
|
|
395
|
+
const line = source.text.slice(0, match.index).split("\n").length;
|
|
396
|
+
findings.push({
|
|
397
|
+
code: "PD057",
|
|
398
|
+
severity: "warning",
|
|
399
|
+
message: `${source.file}:${line} maps anonymous sign-ins as users, but rls.anonymousSignIns is 'deny'`,
|
|
400
|
+
fix: "pass anonymousSignIns: 'deny' so the subject agrees with RLS"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return findings;
|
|
404
|
+
}
|
|
405
|
+
const EXCHANGE_CALL = /\bexchangeCapability\s*\(/gu;
|
|
406
|
+
/** PD041: `exchangeCapability` signing link tokens with the project's shared JWT secret. */
|
|
407
|
+
function pd041(sources) {
|
|
408
|
+
const findings = [];
|
|
409
|
+
for (const source of sources) for (const match of source.text.matchAll(EXCHANGE_CALL)) {
|
|
410
|
+
const open = match.index + match[0].length - 1;
|
|
411
|
+
const options = callArguments(source.text, open)?.[1];
|
|
412
|
+
if (options === void 0 || !/(?:^|[{,\s])alg\s*:\s*['"]HS256['"]/u.test(options)) continue;
|
|
413
|
+
const line = source.text.slice(0, match.index).split("\n").length;
|
|
414
|
+
findings.push({
|
|
415
|
+
code: "PD041",
|
|
416
|
+
severity: "warning",
|
|
417
|
+
message: `${source.file}:${String(line)} signs capability tokens with HS256, the project's shared JWT secret: whoever holds it can mint any user's token`,
|
|
418
|
+
fix: "sign with alg: 'ES256' and the private JWK of an asymmetric Supabase signing key; keep HS256 for the local stack only"
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return findings;
|
|
422
|
+
}
|
|
423
|
+
const PROTECT_CALL = /\bprotect\s*\(/gu;
|
|
424
|
+
const ID_ROUTE = /['"`](\/[^'"`\s]*(?::[A-Za-z_$][\w$]*|\{[A-Za-z_$][\w$]*\})[^'"`\s]*)['"`]/gu;
|
|
425
|
+
const DYNAMIC_SEGMENT = /(?:^|[\\/])(\[[^\]/\\]+\])(?=[\\/])/u;
|
|
426
|
+
/** The id-bearing route a `protect` call at `index` sits in: the file's dynamic segment, or a route literal earlier in the same statement. */
|
|
427
|
+
function idRouteOf(source, index) {
|
|
428
|
+
const segment = DYNAMIC_SEGMENT.exec(source.file);
|
|
429
|
+
if (segment !== null) return segment[1];
|
|
430
|
+
const window = source.text.slice(Math.max(0, index - 400), index);
|
|
431
|
+
const start = Math.max(window.lastIndexOf(";"), window.lastIndexOf("\n\n"));
|
|
432
|
+
return [...window.slice(start + 1).matchAll(ID_ROUTE)].at(-1)?.[1];
|
|
433
|
+
}
|
|
434
|
+
/** PD036: a `protect(permission)` with no row loader on a route whose path names an object id (BOLA, OWASP API1). */
|
|
435
|
+
function pd036(sources) {
|
|
436
|
+
const findings = [];
|
|
437
|
+
for (const source of sources) {
|
|
438
|
+
if (!source.text.includes("permdock")) continue;
|
|
439
|
+
for (const match of source.text.matchAll(PROTECT_CALL)) {
|
|
440
|
+
const open = match.index + match[0].length - 1;
|
|
441
|
+
const args = callArguments(source.text, open);
|
|
442
|
+
if (args?.length !== 1 || args[0]?.trim() === "null") continue;
|
|
443
|
+
const route = idRouteOf(source, match.index);
|
|
444
|
+
if (route === void 0) continue;
|
|
445
|
+
const line = source.text.slice(0, match.index).split("\n").length;
|
|
446
|
+
findings.push({
|
|
447
|
+
code: "PD036",
|
|
448
|
+
severity: "warning",
|
|
449
|
+
message: `${source.file}:${String(line)} protects ${route} with ${args[0] ?? ""} and no row loader, so the check never sees the row the id names (BOLA, OWASP API1)`,
|
|
450
|
+
fix: "pass a loader that fetches the row by the id: protect(permission, (request) => load(request))"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return findings;
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/cli/usage.ts
|
|
458
|
+
async function runUsage(input) {
|
|
459
|
+
const collected = input.collected === void 0 ? await runCollect({
|
|
460
|
+
cwd: input.cwd,
|
|
461
|
+
config: input.config,
|
|
462
|
+
collect: input.config.collect ?? {},
|
|
463
|
+
check: true,
|
|
464
|
+
now: input.now,
|
|
465
|
+
io: input.io
|
|
466
|
+
}) : await input.collected();
|
|
467
|
+
if (collected.document === void 0 || collected.scan === void 0) return {
|
|
468
|
+
code: 2,
|
|
469
|
+
output: collected.message
|
|
470
|
+
};
|
|
471
|
+
const policyRel = input.config.policy;
|
|
472
|
+
if (policyRel === void 0) return {
|
|
473
|
+
code: 2,
|
|
474
|
+
output: "usage: set policy in permdock.config.ts"
|
|
475
|
+
};
|
|
476
|
+
const policy = await usagePolicy(input.cwd, policyRel, input.policy);
|
|
477
|
+
if (!("roles" in policy)) return policy;
|
|
478
|
+
const granted = /* @__PURE__ */ new Set();
|
|
479
|
+
const mergedRoles = /* @__PURE__ */ new Set();
|
|
480
|
+
for (const role of policy.roles) {
|
|
481
|
+
mergedRoles.add(role.name);
|
|
482
|
+
for (const grant of role.grants) if (grant.effect === "allow") granted.add(grant.permission.key);
|
|
483
|
+
}
|
|
484
|
+
for (const grant of policy.grants ?? []) if (grant.effect === "allow") granted.add(grant.permission.key);
|
|
485
|
+
const used = /* @__PURE__ */ new Set();
|
|
486
|
+
const usedAt = {};
|
|
487
|
+
for (const permission of collected.document.permissions) if (permission.usages.some((usage) => usage.call !== "allow")) {
|
|
488
|
+
used.add(permission.key);
|
|
489
|
+
usedAt[permission.key] = permission.usages;
|
|
490
|
+
}
|
|
491
|
+
const unused = [];
|
|
492
|
+
const ungranted = [];
|
|
493
|
+
const noRole = [];
|
|
494
|
+
const dynamic = [];
|
|
495
|
+
for (const permission of collected.document.permissions) {
|
|
496
|
+
const key = permission.key;
|
|
497
|
+
if (ignored(key, input.ignore)) continue;
|
|
498
|
+
if (!used.has(key) && !input.dynamicAsUsed) unused.push({
|
|
499
|
+
kind: "unused",
|
|
500
|
+
key,
|
|
501
|
+
detail: permission.usages[0]?.file ?? "defined"
|
|
502
|
+
});
|
|
503
|
+
if (used.has(key) && !granted.has(key)) {
|
|
504
|
+
const site = usedAt[key]?.[0];
|
|
505
|
+
ungranted.push({
|
|
506
|
+
kind: "ungranted",
|
|
507
|
+
key,
|
|
508
|
+
detail: site === void 0 ? "checked" : `${site.file}:${String(site.line)} (${site.call})`
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (const name of collected.scan.roleNames) if (!mergedRoles.has(name)) noRole.push({
|
|
513
|
+
kind: "no-role",
|
|
514
|
+
key: name,
|
|
515
|
+
detail: `role '${name}' not passed to definePolicy`
|
|
516
|
+
});
|
|
517
|
+
for (const site of collected.scan.dynamic) dynamic.push({
|
|
518
|
+
kind: "dynamic",
|
|
519
|
+
key: "*",
|
|
520
|
+
detail: `${site.file}:${String(site.line)} (${site.call})`
|
|
521
|
+
});
|
|
522
|
+
const undeclared = undeclaredFields(policy).filter((finding) => !ignored(finding.key, input.ignore));
|
|
523
|
+
const outsideInclude = outsideSnapshotInclude(input.cwd, input.config, collected.scan, usedAt).filter((finding) => !ignored(finding.key, input.ignore));
|
|
524
|
+
const report = {
|
|
525
|
+
$schema: USAGE_REPORT_SCHEMA,
|
|
526
|
+
unused,
|
|
527
|
+
ungranted,
|
|
528
|
+
noRole,
|
|
529
|
+
dynamic,
|
|
530
|
+
undeclared,
|
|
531
|
+
outsideInclude,
|
|
532
|
+
warnings: unused.length + ungranted.length + dynamic.length + undeclared.length + outsideInclude.length,
|
|
533
|
+
errors: noRole.length
|
|
534
|
+
};
|
|
535
|
+
return {
|
|
536
|
+
code: report.errors > 0 || input.strict && report.warnings > 0 ? 1 : 0,
|
|
537
|
+
output: input.json ? `${JSON.stringify(report, null, 2)}\n` : formatUsage(report)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function usagePolicy(cwd, policyRel, loaded) {
|
|
541
|
+
if (loaded !== void 0) return await loaded() ?? {
|
|
542
|
+
code: 2,
|
|
543
|
+
output: `PermDock CLI: policy module failed to load: ${policyRel}`
|
|
544
|
+
};
|
|
545
|
+
const policyAbs = resolve(cwd, policyRel);
|
|
546
|
+
if (!existsSync(policyAbs)) return {
|
|
547
|
+
code: 2,
|
|
548
|
+
output: `PermDock CLI: policy module not found: ${policyRel}`
|
|
549
|
+
};
|
|
550
|
+
try {
|
|
551
|
+
return asPolicy(pickNamed(await loadModule(policyAbs), ["policy"]));
|
|
552
|
+
} catch (error) {
|
|
553
|
+
return usageResult(error);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const CLIENT_CALLS = /* @__PURE__ */ new Set([
|
|
557
|
+
"usePermission",
|
|
558
|
+
"can",
|
|
559
|
+
"decide",
|
|
560
|
+
"filter",
|
|
561
|
+
"actions"
|
|
562
|
+
]);
|
|
563
|
+
function undeclaredFields(policy) {
|
|
564
|
+
const findings = [];
|
|
565
|
+
for (const grant of policy.grants) {
|
|
566
|
+
const declared = declaredFields(policy, grant.permission.resource);
|
|
567
|
+
if (declared === void 0) continue;
|
|
568
|
+
const fields = /* @__PURE__ */ new Set();
|
|
569
|
+
conditionFields(grant.where, fields);
|
|
570
|
+
conditionFields(grant.check, fields);
|
|
571
|
+
for (const field of fields) if (!declared.has(field)) findings.push({
|
|
572
|
+
kind: "undeclared-field",
|
|
573
|
+
key: grant.permission.key,
|
|
574
|
+
detail: `${grant.role === null ? "policy grant" : `role '${grant.role}'`} reads '${field}', which the ${grant.permission.resource} schema does not declare`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return findings;
|
|
578
|
+
}
|
|
579
|
+
function declaredFields(policy, resource) {
|
|
580
|
+
const node = getResource(policy.permissions, resource);
|
|
581
|
+
const schema = node === void 0 ? null : jsonSchemaOf(node);
|
|
582
|
+
if (schema === null || typeof schema !== "object") return;
|
|
583
|
+
const properties = schema.properties;
|
|
584
|
+
if (properties === null || typeof properties !== "object") return;
|
|
585
|
+
return new Set(Object.keys(properties));
|
|
586
|
+
}
|
|
587
|
+
function conditionFields(condition, out) {
|
|
588
|
+
if (condition === void 0) return;
|
|
589
|
+
const add = (field) => {
|
|
590
|
+
out.add(field.split(".")[0] ?? field);
|
|
591
|
+
};
|
|
592
|
+
switch (condition.op) {
|
|
593
|
+
case "eq":
|
|
594
|
+
case "ne":
|
|
595
|
+
case "gt":
|
|
596
|
+
case "gte":
|
|
597
|
+
case "lt":
|
|
598
|
+
case "lte":
|
|
599
|
+
case "contains":
|
|
600
|
+
case "in":
|
|
601
|
+
case "notIn":
|
|
602
|
+
case "isNull":
|
|
603
|
+
add(condition.field);
|
|
604
|
+
break;
|
|
605
|
+
case "and":
|
|
606
|
+
case "or":
|
|
607
|
+
for (const child of condition.conditions) conditionFields(child, out);
|
|
608
|
+
break;
|
|
609
|
+
case "not":
|
|
610
|
+
conditionFields(condition.condition, out);
|
|
611
|
+
break;
|
|
612
|
+
case "memberOf":
|
|
613
|
+
add(condition.field);
|
|
614
|
+
for (const parent of condition.parents ?? []) add(typeof parent === "string" ? parent : parent.field);
|
|
615
|
+
break;
|
|
616
|
+
case "sqlFunction":
|
|
617
|
+
for (const arg of condition.args) if (arg !== null && typeof arg === "object" && !Array.isArray(arg) && "field" in arg) add(arg.field);
|
|
618
|
+
conditionFields(condition.twin, out);
|
|
619
|
+
break;
|
|
620
|
+
case "related":
|
|
621
|
+
add(condition.field);
|
|
622
|
+
if (condition.restricted !== void 0) add(condition.restricted);
|
|
623
|
+
if (condition.restrictedAncestors !== void 0) {
|
|
624
|
+
add(condition.restrictedAncestors.id);
|
|
625
|
+
add(condition.restrictedAncestors.parent);
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
case "opaque":
|
|
629
|
+
case "liveSession": break;
|
|
630
|
+
default: throw new Error(`unknown condition ${String(condition)}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function outsideSnapshotInclude(cwd, config, scan, usedAt) {
|
|
634
|
+
if (scan.snapshots.length === 0 || scan.snapshots.some((site) => site.include === void 0 || site.include === null)) return [];
|
|
635
|
+
const include = scan.snapshots.flatMap((site) => site.include ?? []);
|
|
636
|
+
const clientEntries = new Set(listSourceFiles(cwd, config.doctor?.clientEntries ?? []).map((file) => rel(cwd, file)));
|
|
637
|
+
const client = /* @__PURE__ */ new Map();
|
|
638
|
+
const isClient = (file) => {
|
|
639
|
+
let known = client.get(file);
|
|
640
|
+
if (known === void 0) {
|
|
641
|
+
const path = resolve(cwd, file);
|
|
642
|
+
known = existsSync(path) && isClientSource({
|
|
643
|
+
file,
|
|
644
|
+
text: readFileSync(path, "utf8")
|
|
645
|
+
}, clientEntries);
|
|
646
|
+
client.set(file, known);
|
|
647
|
+
}
|
|
648
|
+
return known;
|
|
649
|
+
};
|
|
650
|
+
const findings = [];
|
|
651
|
+
for (const [key, sites] of Object.entries(usedAt)) {
|
|
652
|
+
if (include.some((item) => key === item || key.startsWith(`${item}.`))) continue;
|
|
653
|
+
const site = sites.find((usage) => CLIENT_CALLS.has(usage.call) && isClient(usage.file));
|
|
654
|
+
if (site !== void 0) findings.push({
|
|
655
|
+
kind: "outside-include",
|
|
656
|
+
key,
|
|
657
|
+
detail: `${site.file}:${String(site.line)} (${site.call}) is outside every snapshot include`
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return findings;
|
|
661
|
+
}
|
|
662
|
+
function ignored(key, patterns) {
|
|
663
|
+
return patterns.some((pattern) => {
|
|
664
|
+
if (pattern.endsWith(".*")) return key.startsWith(pattern.slice(0, -2));
|
|
665
|
+
return key === pattern;
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
function formatUsage(report) {
|
|
669
|
+
const lines = ["permdock usage", ""];
|
|
670
|
+
lines.push(` defined but unused (${String(report.unused.length)})`);
|
|
671
|
+
for (const finding of report.unused) lines.push(` ${finding.key.padEnd(28)} ${finding.detail}`);
|
|
672
|
+
lines.push("");
|
|
673
|
+
lines.push(` used but ungranted (${String(report.ungranted.length)})`);
|
|
674
|
+
for (const finding of report.ungranted) lines.push(` ${finding.key.padEnd(28)} ${finding.detail}`);
|
|
675
|
+
lines.push("");
|
|
676
|
+
lines.push(` granted by no role (${String(report.noRole.length)})`);
|
|
677
|
+
for (const finding of report.noRole) lines.push(` ${finding.key.padEnd(28)} ${finding.detail}`);
|
|
678
|
+
for (const [title, list] of [["conditions on undeclared fields", report.undeclared], ["client checks outside include", report.outsideInclude]]) if (list.length > 0) {
|
|
679
|
+
lines.push("");
|
|
680
|
+
lines.push(` ${title} (${String(list.length)})`);
|
|
681
|
+
for (const finding of list) lines.push(` ${finding.key.padEnd(28)} ${finding.detail}`);
|
|
682
|
+
}
|
|
683
|
+
if (report.dynamic.length > 0) {
|
|
684
|
+
lines.push("");
|
|
685
|
+
lines.push(` dynamic (${String(report.dynamic.length)})`);
|
|
686
|
+
for (const finding of report.dynamic) lines.push(` ${finding.detail}`);
|
|
687
|
+
}
|
|
688
|
+
lines.push("");
|
|
689
|
+
lines.push(` ${String(report.warnings)} warning${report.warnings === 1 ? "" : "s"}, ${String(report.errors)} error${report.errors === 1 ? "" : "s"}`);
|
|
690
|
+
return `${lines.join("\n")}\n`;
|
|
691
|
+
}
|
|
692
|
+
//#endregion
|
|
693
|
+
export { pd010 as a, pd014 as c, pd038 as d, pd041 as f, pd008 as i, pd015 as l, pd001 as n, pd011 as o, pd057 as p, pd007 as r, pd013 as s, runUsage as t, pd036 as u };
|