permdock 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/README.md +268 -1
  2. package/dist/a2a/index.d.ts +178 -0
  3. package/dist/a2a/index.js +271 -0
  4. package/dist/ai-sdk/index.d.ts +74 -0
  5. package/dist/ai-sdk/index.js +137 -0
  6. package/dist/approval-required-TzNB9oQA.js +6 -0
  7. package/dist/approvals/index.d.ts +3 -0
  8. package/dist/approvals/index.js +4 -0
  9. package/dist/approvals-DaxY-Bdg.js +218 -0
  10. package/dist/approvers-aH0PJkmf.js +20 -0
  11. package/dist/arazzo-CTeVuXr5.js +70 -0
  12. package/dist/arazzo-aoTkqYsJ.js +54 -0
  13. package/dist/authzen/index.d.ts +48 -0
  14. package/dist/authzen/index.js +336 -0
  15. package/dist/better-auth/index.d.ts +92 -0
  16. package/dist/better-auth/index.js +324 -0
  17. package/dist/budget-JqcRqyhK.d.ts +11 -0
  18. package/dist/budget-NbEtbJC_.js +11 -0
  19. package/dist/capability-B68OFnZx.js +171 -0
  20. package/dist/capability-BDG_nkxt.d.ts +99 -0
  21. package/dist/catalog/index.d.ts +12 -0
  22. package/dist/catalog/index.js +2 -0
  23. package/dist/catalog-B0E6bggc.js +48 -0
  24. package/dist/catalog-H-vFGwWK.js +55 -0
  25. package/dist/catalog-doc-BSo_Q_-6.js +298 -0
  26. package/dist/catalog-idsHc0vo.js +2 -0
  27. package/dist/catalog-path-CnI0XTFK.js +12 -0
  28. package/dist/claude-agent/index.d.ts +81 -0
  29. package/dist/claude-agent/index.js +83 -0
  30. package/dist/clerk/index.d.ts +56 -0
  31. package/dist/clerk/index.js +280 -0
  32. package/dist/cli/bin.d.ts +1 -0
  33. package/dist/cli/bin.js +20 -0
  34. package/dist/cli/index.d.ts +46 -0
  35. package/dist/cli/index.js +5 -0
  36. package/dist/cloud/index.d.ts +91 -0
  37. package/dist/cloud/index.js +390 -0
  38. package/dist/cloud-BNQTrmrr.js +50 -0
  39. package/dist/cloud-Cuxr6Czw.js +117 -0
  40. package/dist/collect-B7_TIfmg.js +49 -0
  41. package/dist/collect-O-tOtbnk.js +350 -0
  42. package/dist/collect-fRPAQIWb.js +2 -0
  43. package/dist/commands-BMh_Etm2.js +93 -0
  44. package/dist/compact-y6vfu2_f.js +19 -0
  45. package/dist/compare-pfyVnBjE.js +12 -0
  46. package/dist/compile-CQbvyIBr.d.ts +30 -0
  47. package/dist/compile-D22iwvuV.js +558 -0
  48. package/dist/config-BPYmjTOp.js +26 -0
  49. package/dist/connection-JvqwHODC.js +194 -0
  50. package/dist/context-CKEAabd7.js +73 -0
  51. package/dist/context-gtAwAILx.js +6 -0
  52. package/dist/convex/index.d.ts +40 -0
  53. package/dist/convex/index.js +55 -0
  54. package/dist/create-BKF3dgFk.d.ts +162 -0
  55. package/dist/create-CSkhDU9T.js +222 -0
  56. package/dist/credential-B_rkUNlY.js +326 -0
  57. package/dist/credentials-BgRX3WEg.js +268 -0
  58. package/dist/describe-Bt1YD5uS.js +99 -0
  59. package/dist/describe-DernyItf.d.ts +37 -0
  60. package/dist/diff-BUzf5Poi.js +48 -0
  61. package/dist/diff-x5Il8LVa.js +418 -0
  62. package/dist/digest-B3kc_PgQ.d.ts +19 -0
  63. package/dist/digest-BI-6aa6d.js +27 -0
  64. package/dist/doctor-BGle8Gqi.js +1314 -0
  65. package/dist/doctor-Bqo6FwUa.js +53 -0
  66. package/dist/drizzle/index.d.ts +72 -0
  67. package/dist/drizzle/index.js +160 -0
  68. package/dist/duration-xK2jGFVc.js +25 -0
  69. package/dist/elysia/index.d.ts +71 -0
  70. package/dist/elysia/index.js +103 -0
  71. package/dist/errors-DSMnAVNa.js +43 -0
  72. package/dist/errors-DbZ1YOnb.d.ts +138 -0
  73. package/dist/errors-Iy71s7Oj.js +177 -0
  74. package/dist/errors-m3u9TmDu.js +27 -0
  75. package/dist/evaluations-DsAU9ppl.js +479 -0
  76. package/dist/eve/index.d.ts +95 -0
  77. package/dist/eve/index.js +181 -0
  78. package/dist/express/index.d.ts +46 -0
  79. package/dist/express/index.js +108 -0
  80. package/dist/fastify/index.d.ts +56 -0
  81. package/dist/fastify/index.js +141 -0
  82. package/dist/files-B_wAGYmt.js +121 -0
  83. package/dist/fixtures-Cvaw0gDG.js +64 -0
  84. package/dist/freeze-y1_jQL4v.js +31 -0
  85. package/dist/from-snapshot-BMGnhmgv.js +552 -0
  86. package/dist/generate-CH7ThJ14.js +173 -0
  87. package/dist/grantee-Bv04_Vzf.js +521 -0
  88. package/dist/headers-B5RRv3Xm.js +6 -0
  89. package/dist/headers-C8SHQccD.d.ts +6 -0
  90. package/dist/helpers-DnSOrhB9.js +682 -0
  91. package/dist/hono/index.d.ts +70 -0
  92. package/dist/hono/index.js +131 -0
  93. package/dist/hosted-BwKQ1BTA.js +265 -0
  94. package/dist/http-DPuDaChb.d.ts +13 -0
  95. package/dist/http-GgVApX1k.js +111 -0
  96. package/dist/index-CltcQ4Bf.d.ts +138 -0
  97. package/dist/index-LF_hS-yn.d.ts +177 -0
  98. package/dist/index-fG6NSXlc.d.ts +9 -0
  99. package/dist/index-xIxJLwOQ.d.ts +415 -0
  100. package/dist/index.d.ts +11 -0
  101. package/dist/index.js +21 -0
  102. package/dist/jwt/index.d.ts +87 -0
  103. package/dist/jwt/index.js +571 -0
  104. package/dist/kernel-YOGCWNLf.js +211 -0
  105. package/dist/kysely/index.d.ts +77 -0
  106. package/dist/kysely/index.js +129 -0
  107. package/dist/load-BZ24CpZP.js +69 -0
  108. package/dist/load-QOOm1z70.js +2 -0
  109. package/dist/load-jose-BL6LH4bL.js +11 -0
  110. package/dist/lru-8q4WZjBV.js +28 -0
  111. package/dist/manifest-DDQbC6K0.d.ts +118 -0
  112. package/dist/map-DowSO96j.js +158 -0
  113. package/dist/map-claims-B_6I1Rn-.js +360 -0
  114. package/dist/map-error-RJ3ZsUyA.js +16 -0
  115. package/dist/markers-B0hYLvMD.js +60 -0
  116. package/dist/may-use-CnFEjVEp.js +37 -0
  117. package/dist/mcp/index.d.ts +127 -0
  118. package/dist/mcp/index.js +563 -0
  119. package/dist/nest/index.d.ts +80 -0
  120. package/dist/nest/index.js +270 -0
  121. package/dist/next/client.d.ts +24 -0
  122. package/dist/next/client.js +31 -0
  123. package/dist/next/index.d.ts +110 -0
  124. package/dist/next/index.js +195 -0
  125. package/dist/next/plugin.d.ts +14 -0
  126. package/dist/next/plugin.js +2 -0
  127. package/dist/node/index.d.ts +43 -0
  128. package/dist/node/index.js +66 -0
  129. package/dist/openai/index.d.ts +71 -0
  130. package/dist/openai/index.js +105 -0
  131. package/dist/openapi/index.d.ts +93 -0
  132. package/dist/openapi/index.js +3 -0
  133. package/dist/openapi-CwePaF7C.js +164 -0
  134. package/dist/openapi-DWD0Uc_4.js +250 -0
  135. package/dist/openapi-DcqM8nU7.js +306 -0
  136. package/dist/openapi-import-BWRUNNeW.js +5959 -0
  137. package/dist/openapi-schema-D8j_T9tO.js +6902 -0
  138. package/dist/orpc/index.d.ts +64 -0
  139. package/dist/orpc/index.js +188 -0
  140. package/dist/otel/index.d.ts +13 -0
  141. package/dist/otel/index.js +202 -0
  142. package/dist/package-root-zGkxO3oN.js +30 -0
  143. package/dist/parse-DPfIvFMU.js +135 -0
  144. package/dist/pdp/index.d.ts +22 -0
  145. package/dist/pdp/index.js +795 -0
  146. package/dist/peer-BOr_kJE1.js +31 -0
  147. package/dist/permdock-yoZ7EjxX.js +2956 -0
  148. package/dist/permissions-B4apnahQ.d.ts +185 -0
  149. package/dist/permissions-B8vCg_bI.d.ts +52 -0
  150. package/dist/permissions-CofRqesP.js +142 -0
  151. package/dist/permissions-DC7GROPA.js +600 -0
  152. package/dist/pg-4GJWDUxG.js +953 -0
  153. package/dist/pins-CqE1jZuF.js +12 -0
  154. package/dist/plugin-4gr7IxFJ.js +65 -0
  155. package/dist/policy-ANp36PnI.js +971 -0
  156. package/dist/policy-CInmNGkM.d.ts +1959 -0
  157. package/dist/policy-Cq2fB8Q5.d.ts +20 -0
  158. package/dist/prisma/index.d.ts +135 -0
  159. package/dist/prisma/index.js +430 -0
  160. package/dist/problem-Cgp9-KZA.js +226 -0
  161. package/dist/prompt-CXYhLfsS.js +32 -0
  162. package/dist/protected-B4K3BKoX.js +138 -0
  163. package/dist/protected-BuPAycJT.d.ts +26 -0
  164. package/dist/protected-wY7nXhY9.d.ts +23 -0
  165. package/dist/provider-_wx46HpB.js +48 -0
  166. package/dist/provider-wTAUda2v.d.ts +6 -0
  167. package/dist/react/index.d.ts +6 -0
  168. package/dist/react/index.js +6 -0
  169. package/dist/react/provider-client.d.ts +2 -0
  170. package/dist/react/provider-client.js +3 -0
  171. package/dist/react-native/index.d.ts +42 -0
  172. package/dist/react-native/index.js +174 -0
  173. package/dist/refs-ChLSxbST.js +90 -0
  174. package/dist/relations-r4soIFZB.js +426 -0
  175. package/dist/revocations-Cy06JNx2.d.ts +21 -0
  176. package/dist/rls-BOFU3chG.js +233 -0
  177. package/dist/rls-CJVja9Ww.js +2119 -0
  178. package/dist/rls-graph-DxQ39PJj.js +371 -0
  179. package/dist/rls-helpers-BxRuif0l.js +1346 -0
  180. package/dist/rls-import-CXV6LBlS.js +942 -0
  181. package/dist/rls-migrate-DrFPbf8p.js +366 -0
  182. package/dist/row-check-BajTgQZ-.d.ts +43 -0
  183. package/dist/run-CJ6UvS3a.js +155 -0
  184. package/dist/runtime-DP9ttFh1.js +27 -0
  185. package/dist/saas-BRrZzJkM.js +1193 -0
  186. package/dist/schema-P8yoma__.js +296 -0
  187. package/dist/scim/index.d.ts +2 -0
  188. package/dist/scim/index.js +2 -0
  189. package/dist/scim-9NFa0XVZ.js +1445 -0
  190. package/dist/scopes-cKVp_WTw.js +212 -0
  191. package/dist/server/index.d.ts +123 -0
  192. package/dist/server/index.js +8 -0
  193. package/dist/sha256-BO0swRXL.js +150 -0
  194. package/dist/sink-Bd9PSyfq.js +141 -0
  195. package/dist/sink-Cfr9-7YC.d.ts +104 -0
  196. package/dist/skills-C_lpV4Jx.js +50 -0
  197. package/dist/skills-Dx3QLnMY.js +110 -0
  198. package/dist/snapshot-DE8dE_hz.js +1051 -0
  199. package/dist/solid/index.d.ts +63 -0
  200. package/dist/solid/index.js +197 -0
  201. package/dist/source-DoBBYJi2.js +6 -0
  202. package/dist/sources-DOpW5Ila.d.ts +347 -0
  203. package/dist/sources-DVHM3Mwq.js +284 -0
  204. package/dist/sql-Bll1d7y0.js +22 -0
  205. package/dist/src-CTNT-PBW.js +430 -0
  206. package/dist/ssf/index.d.ts +3 -0
  207. package/dist/ssf/index.js +622 -0
  208. package/dist/store-Bq90395i.d.ts +27 -0
  209. package/dist/store-C6hFW6ta.js +508 -0
  210. package/dist/stores-CkG2HrSg.js +140 -0
  211. package/dist/stores-CvOnu_2T.d.ts +41 -0
  212. package/dist/stream-BhMfZOwr.d.ts +12 -0
  213. package/dist/stream-Q_xlB6Ko.js +52 -0
  214. package/dist/style-CTm1AXDT.js +22 -0
  215. package/dist/subject-C6dLxqHU.js +24 -0
  216. package/dist/subject-settings-xt7V845g.js +82 -0
  217. package/dist/supabase/index.d.ts +67 -0
  218. package/dist/supabase/index.js +4 -0
  219. package/dist/supabase/middleware.d.ts +77 -0
  220. package/dist/supabase/middleware.js +68 -0
  221. package/dist/supabase-D8UlBA7Z.js +83 -0
  222. package/dist/supabase-DTxGtYdi.js +796 -0
  223. package/dist/supabase-hook-DW-xP4hU.js +2 -0
  224. package/dist/supabase-hook-D_OqGb_V.js +1528 -0
  225. package/dist/svelte/Protected.svelte +34 -0
  226. package/dist/svelte/index.d.ts +10 -0
  227. package/dist/svelte/index.js +53 -0
  228. package/dist/svelte/runtime.d.ts +6 -0
  229. package/dist/svelte/runtime.js +3 -0
  230. package/dist/svelte/source.d.ts +10 -0
  231. package/dist/svelte/source.js +8 -0
  232. package/dist/terminal/index.d.ts +171 -0
  233. package/dist/terminal/index.js +821 -0
  234. package/dist/testing/index.d.ts +656 -0
  235. package/dist/testing/index.js +3208 -0
  236. package/dist/testing/saas/index.d.ts +194 -0
  237. package/dist/testing/saas/index.js +3 -0
  238. package/dist/testing/saas/permissions.d.ts +2 -0
  239. package/dist/testing/saas/permissions.js +2 -0
  240. package/dist/text-diff-BdkkYSbi.js +15 -0
  241. package/dist/thenable-okc2xDsj.js +10 -0
  242. package/dist/timeout-D51niuQP.js +8 -0
  243. package/dist/trpc/index.d.ts +80 -0
  244. package/dist/trpc/index.js +205 -0
  245. package/dist/types-B8w5PFtS.d.ts +141 -0
  246. package/dist/types-B9dmiiab.d.ts +66 -0
  247. package/dist/types-BBK-exvJ.d.ts +177 -0
  248. package/dist/types-BvBk1wgn.d.ts +118 -0
  249. package/dist/types-CxquUByI.d.ts +9 -0
  250. package/dist/types-D6Z6-rKJ.d.ts +312 -0
  251. package/dist/types-DbWdEfcm.d.ts +87 -0
  252. package/dist/types-bva3i4Gf.d.ts +98 -0
  253. package/dist/types-i4BAOvdb.d.ts +97 -0
  254. package/dist/unplugin/index.d.ts +7 -0
  255. package/dist/unplugin/index.js +30 -0
  256. package/dist/usage-1ozMubLY.js +42 -0
  257. package/dist/usage-CUnK6Ou9.js +530 -0
  258. package/dist/usage-PyEor6GA.js +2 -0
  259. package/dist/validity-D0uRmJKJ.js +224 -0
  260. package/dist/verifier-B90agnhl.js +461 -0
  261. package/dist/version-CnSh33Fj.js +16 -0
  262. package/dist/vocabulary-Dk0fs-Jk.d.ts +56 -0
  263. package/dist/vue/index.d.ts +57 -0
  264. package/dist/vue/index.js +203 -0
  265. package/dist/webmcp/index.d.ts +75 -0
  266. package/dist/webmcp/index.js +313 -0
  267. package/package.json +423 -4
  268. package/rulesets/permdock.yaml +29 -0
  269. package/schemas/catalog-v1.json +507 -0
  270. package/schemas/openapi/oas-3.1.json +1298 -0
  271. package/schemas/openapi/oas-3.2.json +1516 -0
  272. package/schemas/openapi/overlay-1.1.json +79 -0
  273. package/schemas/supabase-claims-v1.json +139 -0
  274. package/schemas/supabase-manifest-v1.json +217 -0
  275. package/skills/permdock/SKILL.md +66 -0
  276. package/skills/permdock-agents/SKILL.md +77 -0
  277. package/skills/permdock-agents/references/runtimes.md +211 -0
  278. package/skills/permdock-approvals/SKILL.md +80 -0
  279. package/skills/permdock-approvals/references/stores-and-resume.md +83 -0
  280. package/skills/permdock-audit/SKILL.md +67 -0
  281. package/skills/permdock-credentials/SKILL.md +55 -0
  282. package/skills/permdock-credentials/references/tokens-keys-links.md +127 -0
  283. package/skills/permdock-data/SKILL.md +57 -0
  284. package/skills/permdock-data/references/queries-and-rls.md +54 -0
  285. package/skills/permdock-data/references/relationships.md +65 -0
  286. package/skills/permdock-tenancy/SKILL.md +77 -0
  287. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +83 -0
  288. package/skills/permdock-wire/SKILL.md +116 -0
  289. package/skills/permdock-wire/references/adapters.md +480 -0
  290. package/skills/permdock-wire/references/policy-options.md +43 -0
@@ -0,0 +1,1528 @@
1
+ import { a as resolveScope, c as scopeChain, s as rootScope, u as scopeList } from "./scopes-cKVp_WTw.js";
2
+ import { r as SUPABASE_MANIFEST_SCHEMA } from "./version-CnSh33Fj.js";
3
+ import { i as sqlFiles, r as rel } from "./files-B_wAGYmt.js";
4
+ import { a as loadModule, n as asPolicy, o as pickNamed } from "./load-BZ24CpZP.js";
5
+ import { t as shortDiff } from "./text-diff-BdkkYSbi.js";
6
+ import { r as scopeColumn } from "./compile-D22iwvuV.js";
7
+ import { o as authorizeSql, r as readMemberships } from "./supabase-DTxGtYdi.js";
8
+ import "./budget-NbEtbJC_.js";
9
+ import { t as AUTHZ_VERSION_TABLE } from "./sources-DVHM3Mwq.js";
10
+ import { a as hookMarkerFields, n as HOOK_MARKER, t as GRANTS_MARKER } from "./markers-B0hYLvMD.js";
11
+ import { A as memberForSources, E as hasMemberFor, H as scopeTypeOf, I as quoteIdent, L as quoteLiteral, M as memberIdsHelper, R as quoteTable, h as activeRowSql, j as memberForTable, k as memberForHelper, p as globalRoleSource, y as checkSuspension } from "./rls-helpers-BxRuif0l.js";
12
+ import { r as FIELD_VIEWS, t as connectPg, y as roleNames } from "./pg-4GJWDUxG.js";
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
14
+ import { dirname, join, posix, resolve } from "node:path";
15
+ import { createRequire } from "node:module";
16
+ import { parse } from "smol-toml";
17
+ //#region src/cli/deciding-columns.ts
18
+ /** `schema.table`, unquoted and lower case; `public` when unqualified. */
19
+ function tableKey(name) {
20
+ const parts = name.split(".").map((part) => part.replaceAll("\"", ""));
21
+ return (parts.length === 1 ? ["public", ...parts] : parts).join(".").toLowerCase();
22
+ }
23
+ /**
24
+ * The columns of each membership source's table that decide who holds which
25
+ * membership (user, scope, id, `within`, role, `via`, expiry), keyed by
26
+ * {@link tableKey}. A client that can write one can give itself a membership.
27
+ */
28
+ function membershipColumns(config) {
29
+ const sources = config.supabase?.hook?.memberships ?? config.rls?.membershipSources ?? [];
30
+ const byTable = /* @__PURE__ */ new Map();
31
+ for (const source of sources) {
32
+ const table = tableKey(source.sql.table);
33
+ const columns = byTable.get(table) ?? /* @__PURE__ */ new Set();
34
+ for (const column of source.sql.columns) columns.add(column);
35
+ byTable.set(table, columns);
36
+ }
37
+ return byTable;
38
+ }
39
+ /**
40
+ * Every `schema.table.column` a claim or a membership is computed from: the
41
+ * membership columns and the `attrs` table columns. Sorted.
42
+ */
43
+ function decidingColumns(config, attrs) {
44
+ const entries = /* @__PURE__ */ new Set();
45
+ for (const [table, columns] of membershipColumns(config)) for (const column of columns) entries.add(`${table}.${column}`);
46
+ if (attrs?.table !== void 0) {
47
+ const table = tableKey(attrs.table);
48
+ for (const column of attrs.columns) entries.add(`${table}.${column}`);
49
+ }
50
+ return [...entries].toSorted();
51
+ }
52
+ //#endregion
53
+ //#region src/cli/sql-statements.ts
54
+ function blankComments(sql) {
55
+ return sql.replaceAll(/--[^\n]*/gu, (comment) => " ".repeat(comment.length)).replaceAll(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replaceAll(/[^\n]/gu, " "));
56
+ }
57
+ /** Split on `;` outside string literals and dollar-quoted bodies. */
58
+ function sqlStatements(sql) {
59
+ const text = blankComments(sql);
60
+ const statements = [];
61
+ const push = (start, end) => {
62
+ const body = text.slice(start, end);
63
+ const lead = body.length - body.trimStart().length;
64
+ const trimmed = body.trim();
65
+ if (trimmed !== "") statements.push({
66
+ line: text.slice(0, start + lead).split("\n").length,
67
+ text: trimmed
68
+ });
69
+ };
70
+ let start = 0;
71
+ let index = 0;
72
+ while (index < text.length) {
73
+ const char = text[index];
74
+ if (char === "'") {
75
+ const end = text.indexOf("'", index + 1);
76
+ index = end === -1 ? text.length : end + 1;
77
+ continue;
78
+ }
79
+ if (char === "$") {
80
+ const tag = /^\$[A-Za-z_]*\$/u.exec(text.slice(index, index + 64));
81
+ if (tag !== null) {
82
+ const end = text.indexOf(tag[0], index + tag[0].length);
83
+ index = end === -1 ? text.length : end + tag[0].length;
84
+ continue;
85
+ }
86
+ }
87
+ if (char === ";") {
88
+ push(start, index);
89
+ start = index + 1;
90
+ }
91
+ index += 1;
92
+ }
93
+ push(start, text.length);
94
+ return statements;
95
+ }
96
+ /** Capture group `index` of a match, or '' when an optional group took no part. */
97
+ function group(match, index) {
98
+ return match[index] ?? "";
99
+ }
100
+ //#endregion
101
+ //#region src/cli/doctor-project.ts
102
+ function pd005(cwd) {
103
+ const lockPath = join(cwd, ".permdock/skills-lock.json");
104
+ if (![
105
+ join(cwd, ".agents/skills"),
106
+ join(cwd, ".claude/skills"),
107
+ join(cwd, ".cursor/skills")
108
+ ].some((folder) => existsSync(join(folder, "permdock/SKILL.md")))) return [{
109
+ code: "PD005",
110
+ severity: "warning",
111
+ message: "Agent Skills are not installed",
112
+ fix: "pnpm exec permdock skills install"
113
+ }];
114
+ if (!existsSync(lockPath)) return [{
115
+ code: "PD005",
116
+ severity: "warning",
117
+ message: "skills lock is missing",
118
+ fix: "pnpm exec permdock skills install"
119
+ }];
120
+ return [];
121
+ }
122
+ function pd006(cwd) {
123
+ try {
124
+ const pkg = createRequire(resolve(cwd, "package.json"))("typescript/package.json");
125
+ const major = Number(pkg.version.split(".")[0]);
126
+ if (major < 5 || major === 5 && Number(pkg.version.split(".")[1]) < 9) return [{
127
+ code: "PD006",
128
+ severity: "error",
129
+ message: `TypeScript ${pkg.version} is below the supported matrix (5.9, 6, 7)`,
130
+ fix: "upgrade typescript to 5.9 or later"
131
+ }];
132
+ if (major > 7) return [{
133
+ code: "PD006",
134
+ severity: "error",
135
+ message: `TypeScript ${pkg.version} is not in the supported matrix (5.9, 6, 7)`,
136
+ fix: "use TypeScript 5.9, 6 or 7"
137
+ }];
138
+ return [];
139
+ } catch {
140
+ return [{
141
+ code: "PD006",
142
+ severity: "error",
143
+ message: "typescript is not installed",
144
+ fix: "add typescript 5.9, 6 or 7"
145
+ }];
146
+ }
147
+ }
148
+ function pd009(cwd) {
149
+ const copies = [];
150
+ function walk(dir, depth) {
151
+ if (depth > 6 || !existsSync(dir)) return;
152
+ const pkg = join(dir, "node_modules/permdock/package.json");
153
+ if (existsSync(pkg)) copies.push(pkg);
154
+ if (!existsSync(join(dir, "node_modules"))) return;
155
+ for (const name of readdirSync(join(dir, "node_modules"))) {
156
+ if (name.startsWith(".")) continue;
157
+ const nested = join(dir, "node_modules", name);
158
+ try {
159
+ if (statSync(nested).isDirectory()) walk(nested, depth + 1);
160
+ } catch {}
161
+ }
162
+ }
163
+ walk(cwd, 0);
164
+ if (copies.length > 1) return [{
165
+ code: "PD009",
166
+ severity: "error",
167
+ message: `duplicate permdock copies: ${copies.map((item) => rel(cwd, dirname(item))).join(", ")}`,
168
+ fix: "dedupe so only one permdock version is installed"
169
+ }];
170
+ return [];
171
+ }
172
+ function pd012(cwd, config) {
173
+ const docs = config.openapi?.doc ?? [];
174
+ const findings = [];
175
+ for (const doc of docs) {
176
+ const abs = resolve(cwd, doc);
177
+ if (!existsSync(abs)) continue;
178
+ const text = readFileSync(abs, "utf8");
179
+ if (text.includes("\"drafts\"") && !text.includes("overlay")) findings.push({
180
+ code: "PD012",
181
+ severity: "warning",
182
+ message: `${doc} carries a draft pin the CLI no longer emits`,
183
+ fix: "regenerate with permdock openapi"
184
+ });
185
+ }
186
+ return findings;
187
+ }
188
+ const MIGRATION_DIRS = [
189
+ "supabase/migrations",
190
+ "supabase/schemas",
191
+ "migrations",
192
+ "drizzle",
193
+ "prisma/migrations",
194
+ "db/migrations"
195
+ ];
196
+ const VIEW_NAME = String.raw`((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)`;
197
+ const CREATE_VIEW = new RegExp(String.raw`\bcreate\s+(?:or\s+replace\s+)?(?:temp(?:orary)?\s+)?(?:recursive\s+)?view\s+${VIEW_NAME}([\s\S]*?)\bas\b`, "giu");
198
+ const ALTER_VIEW = new RegExp(String.raw`\balter\s+view\s+(?:if\s+exists\s+)?${VIEW_NAME}\s+set\s*\(([^)]*)\)`, "giu");
199
+ const INVOKER = /\bsecurity_invoker\s*(?:=\s*(?:true|on|'true'|'on'|1)\b|[,)]|$)/iu;
200
+ const COMPANION = new RegExp(String.raw`\bcomment\s+on\s+view\s+${VIEW_NAME}\s+is\s+'${FIELD_VIEWS.comment}\b`, "giu");
201
+ /**
202
+ * Views run as their owner unless `security_invoker` is set, so they read
203
+ * past RLS on the tables beneath them. The `<table>_visible_fields`
204
+ * companion `rls generate --revoke-columns` writes reads as its owner on
205
+ * purpose and carries a comment that says so.
206
+ */
207
+ function pd022(cwd, config) {
208
+ if (config.rls === void 0 && config.doctor?.migrations === void 0) return [];
209
+ const created = /* @__PURE__ */ new Map();
210
+ const invoker = /* @__PURE__ */ new Set();
211
+ const companions = /* @__PURE__ */ new Set();
212
+ for (const file of sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS)) {
213
+ const raw = readFileSync(file, "utf8");
214
+ const text = raw.replaceAll(/--[^\n]*/gu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
215
+ for (const [, name = "", options = ""] of text.matchAll(CREATE_VIEW)) {
216
+ const key = tableKey(name);
217
+ created.set(key, rel(cwd, file));
218
+ if (INVOKER.test(options)) invoker.add(key);
219
+ else invoker.delete(key);
220
+ }
221
+ for (const [, name = "", options = ""] of text.matchAll(ALTER_VIEW)) if (INVOKER.test(options)) invoker.add(tableKey(name));
222
+ for (const [, name = ""] of raw.matchAll(COMPANION)) companions.add(tableKey(name));
223
+ }
224
+ return [...created].filter(([key]) => !invoker.has(key) && !companions.has(key)).map(([key, file]) => ({
225
+ code: "PD022",
226
+ severity: "warning",
227
+ message: `view ${key} in ${file} is not security_invoker, so it reads past row level security`,
228
+ fix: `create the view with (security_invoker = true), or alter view ${key} set (security_invoker = true); Postgres 15 or later. For column-level reads, generate field views with permdock rls generate --fields views`
229
+ }));
230
+ }
231
+ const AUTH_ROLE = /\bauth\.role\s*\(\s*\)/giu;
232
+ /**
233
+ * PD040: `auth.role()` in a migration. Supabase deprecated it; a policy
234
+ * names its Postgres roles with `to authenticated` instead, and the role
235
+ * claim it reads is the one PostgREST already switched to.
236
+ */
237
+ function pd040(cwd, config) {
238
+ const findings = [];
239
+ for (const file of sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS)) {
240
+ const text = readFileSync(file, "utf8").replaceAll(/--[^\n]*/gu, (comment) => " ".repeat(comment.length)).replaceAll(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replaceAll(/[^\n]/gu, " "));
241
+ for (const match of text.matchAll(AUTH_ROLE)) {
242
+ const line = text.slice(0, match.index).split("\n").length;
243
+ findings.push({
244
+ code: "PD040",
245
+ severity: "warning",
246
+ message: `${rel(cwd, file)}:${String(line)} calls auth.role(), which Supabase deprecated`,
247
+ fix: "name the roles on the policy (create policy ... to authenticated) and drop the auth.role() = 'authenticated' test; call permdock_has or permitted_<scope>_ids for permissions"
248
+ });
249
+ }
250
+ }
251
+ return findings;
252
+ }
253
+ const GRANT = /\b(grant|revoke)\s+([\s\S]+?)\s+on\s+(?:table\s+)?([\w."]+)\s+(?:to|from)\s+([\w\s,"]+?)(?:\s+with\s+grant\s+option|\s+cascade|\s+restrict)?\s*;/giu;
254
+ const CLIENT_ROLES = /* @__PURE__ */ new Set([
255
+ "anon",
256
+ "authenticated",
257
+ "public"
258
+ ]);
259
+ function applyPrivilege(writable, verb, part) {
260
+ const match = /^\s*(all(?:\s+privileges)?|insert|update)\s*(?:\(([^)]*)\))?\s*$/iu.exec(part);
261
+ if (match === null) return;
262
+ const kind = (match[1] ?? "").toLowerCase();
263
+ const sets = kind === "insert" ? [writable.insert] : kind === "update" ? [writable.update] : [writable.insert, writable.update];
264
+ const columns = match[2] === void 0 ? ["*"] : match[2].split(",").map((column) => column.trim().replaceAll("\"", ""));
265
+ for (const set of sets) for (const column of columns) if (verb.toLowerCase() === "grant") set.add(column);
266
+ else if (column === "*") set.clear();
267
+ else set.delete(column);
268
+ }
269
+ const CREATE_TABLE = /^create\s+(?:(?:global|local)\s+)?(?:(?:temp|temporary|unlogged)\s+)?table\s+(?:if\s+not\s+exists\s+)?((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)\s*\(/iu;
270
+ const REVOKE_DEFAULT_TABLES = /^alter\s+default\s+privileges\s+(?:for\s+(?:role|user)\s+[\w\s,"]+?\s+)?in\s+schema\s+"?public"?\s+revoke\s+[\s\S]+?\s+on\s+tables\s+from\s+[\s\S]*\b(?:anon|authenticated)\b/iu;
271
+ /**
272
+ * Columns of `table` a client role may insert or update, per the migrations
273
+ * in order; `*` for every column. On Supabase a table created in `public`
274
+ * starts writable: the default privileges grant `anon` and `authenticated`
275
+ * everything, until a migration revokes them.
276
+ */
277
+ function clientWritable(cwd, config, target) {
278
+ const writable = {
279
+ insert: /* @__PURE__ */ new Set(),
280
+ update: /* @__PURE__ */ new Set()
281
+ };
282
+ let defaults = config.supabase !== void 0 && target.startsWith("public.");
283
+ for (const file of sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS)) for (const { text } of sqlStatements(readFileSync(file, "utf8"))) {
284
+ if (REVOKE_DEFAULT_TABLES.test(text)) {
285
+ defaults = false;
286
+ continue;
287
+ }
288
+ const created = CREATE_TABLE.exec(text);
289
+ if (created !== null) {
290
+ if (defaults && tableKey(created[1] ?? "") === target) {
291
+ writable.insert.add("*");
292
+ writable.update.add("*");
293
+ }
294
+ continue;
295
+ }
296
+ for (const [, verb = "", privileges = "", name = "", roles = ""] of `${text};`.matchAll(GRANT)) {
297
+ if (tableKey(name) !== target) continue;
298
+ if (!roles.split(",").map((role) => role.trim().replaceAll("\"", "").toLowerCase()).some((role) => CLIENT_ROLES.has(role))) continue;
299
+ for (const part of privileges.split(/,(?![^(]*\))/u)) applyPrivilege(writable, verb, part);
300
+ }
301
+ }
302
+ return /* @__PURE__ */ new Set([...writable.insert, ...writable.update]);
303
+ }
304
+ /** Membership columns a client can write: a user who sets `user_id` or `role` grants themselves access. */
305
+ function membershipFindings(cwd, config) {
306
+ const findings = [];
307
+ for (const [table, columns] of membershipColumns(config)) {
308
+ const writable = clientWritable(cwd, config, table);
309
+ const exposed = [...columns].filter((column) => writable.has("*") || writable.has(column));
310
+ if (exposed.length > 0) findings.push({
311
+ code: "PD028",
312
+ severity: "warning",
313
+ message: `${table}.${exposed.join(", ")} decide${exposed.length === 1 ? "s" : ""} memberships, and the migrations let anon or authenticated insert or update ${exposed.length === 1 ? "it" : "them"}: a user could give themselves a membership`,
314
+ fix: `revoke insert, update on ${table} from anon, authenticated, then grant update (<columns clients edit>) on ${table} to authenticated; a column-level revoke alone leaves a table-level grant in place`
315
+ });
316
+ }
317
+ return findings;
318
+ }
319
+ /**
320
+ * `attrs` claims must come from server-owned columns: never `user_metadata`,
321
+ * never a column clients can write. Membership sources' deciding columns are
322
+ * held to the same rule.
323
+ */
324
+ function pd028(cwd, config, plan) {
325
+ const memberships = membershipFindings(cwd, config);
326
+ const attrs = config.supabase?.hook?.attrs;
327
+ if (attrs === void 0) return memberships;
328
+ const planned = plan(attrs);
329
+ const findings = planned.errors.map((message) => ({
330
+ code: "PD028",
331
+ severity: "error",
332
+ message,
333
+ fix: "list server-owned columns or app_metadata.<key> entries; user_metadata is user-editable"
334
+ }));
335
+ if (planned.table !== void 0 && planned.columns.length > 0) {
336
+ const writable = clientWritable(cwd, config, tableKey(planned.table));
337
+ const exposed = planned.columns.filter((column) => writable.has("*") || writable.has(column));
338
+ if (exposed.length > 0) findings.push({
339
+ code: "PD028",
340
+ severity: "warning",
341
+ message: `attrs reads ${exposed.join(", ")} from ${tableKey(planned.table)}, which the migrations let anon or authenticated insert or update: a user could set their own attribute`,
342
+ fix: `revoke insert, update on ${tableKey(planned.table)} from anon, authenticated, and grant column-level update only on columns that are not attributes; the generated hook migration refuses to install otherwise`
343
+ });
344
+ }
345
+ return [...findings, ...memberships];
346
+ }
347
+ //#endregion
348
+ //#region src/cli/supabase-config.ts
349
+ const SUPABASE_CONFIG = "supabase/config.toml";
350
+ function table$1(value) {
351
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
352
+ return value;
353
+ }
354
+ function strings(value) {
355
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : void 0;
356
+ }
357
+ /** `supabase/config.toml`, parsed; `undefined` when the project has none. */
358
+ function readSupabaseConfig(cwd) {
359
+ const file = join(cwd, SUPABASE_CONFIG);
360
+ if (!existsSync(file)) return;
361
+ let root;
362
+ try {
363
+ root = parse(readFileSync(file, "utf8"));
364
+ } catch (error) {
365
+ return {
366
+ ok: false,
367
+ error: error instanceof Error ? error.message : String(error)
368
+ };
369
+ }
370
+ const expiry = table$1(root["auth"])?.["jwt_expiry"];
371
+ const paths = table$1(table$1(root["db"])?.["migrations"])?.["schema_paths"];
372
+ const schemaPaths = strings(paths);
373
+ const apiSchemas = strings(table$1(root["api"])?.["schemas"]);
374
+ const pgDelta = table$1(table$1(root["experimental"])?.["pgdelta"]);
375
+ const schemaPath = pgDelta?.["declarative_schema_path"];
376
+ return {
377
+ ok: true,
378
+ config: {
379
+ ...typeof expiry === "number" ? { jwtExpiry: expiry } : {},
380
+ ...schemaPaths === void 0 ? {} : { schemaPaths },
381
+ ...apiSchemas === void 0 ? {} : { apiSchemas },
382
+ ...pgDelta?.["enabled"] === true ? { pgDelta: { schemaDir: posix.join("supabase", typeof schemaPath === "string" ? schemaPath : "schemas").replace(/\/$/u, "") } } : {}
383
+ }
384
+ };
385
+ }
386
+ /** The parsed config, or `{}` when it is missing or does not parse (PD045 reports that). */
387
+ function supabaseConfig(cwd) {
388
+ const read = readSupabaseConfig(cwd);
389
+ return read?.ok === true ? read.config : {};
390
+ }
391
+ /** PD045: a `supabase/config.toml` the CLI cannot parse, so every check that reads it falls back to defaults. */
392
+ function pd045(cwd) {
393
+ const read = readSupabaseConfig(cwd);
394
+ if (read === void 0 || read.ok) return [];
395
+ return [{
396
+ code: "PD045",
397
+ severity: "warning",
398
+ message: `${SUPABASE_CONFIG} does not parse: ${read.error.replace(/\n[\s\S]*/u, "")}`,
399
+ fix: `fix the TOML syntax in ${SUPABASE_CONFIG}; until then doctor assumes Supabase's defaults for jwt_expiry and schema_paths`
400
+ }];
401
+ }
402
+ //#endregion
403
+ //#region src/cli/rls-rbac.ts
404
+ /**
405
+ * Where the helpers read roles and memberships: the flag, `rls.authorize`,
406
+ * `rls.rbac.authorize`, then `database` with `rls.membershipSources`, with
407
+ * `--rbac` or with a mapped memberships table, else `jwt`.
408
+ */
409
+ function resolveAuthorize(config, flags = {}) {
410
+ const rls = config.rls;
411
+ const explicit = flags.authorize ?? rls?.authorize ?? rls?.rbac?.authorize;
412
+ if (explicit !== void 0) return explicit;
413
+ if (rls?.membershipSources !== void 0 || flags.rbac === true) return "database";
414
+ const memberships = flags.memberships ?? rls?.memberships;
415
+ return memberships?.tenant !== void 0 || memberships?.team !== void 0 || Object.keys(memberships?.scopes ?? {}).length > 0 ? "database" : "jwt";
416
+ }
417
+ function parseRbacAuthorize(raw) {
418
+ if (raw === void 0) return;
419
+ if (raw === "database" || raw === "jwt") return raw;
420
+ throw new Error(`PermDock CLI: --authorize must be database or jwt, got '${raw}'`);
421
+ }
422
+ function hookUri(schema) {
423
+ return `pg-functions://postgres/${schema}/custom_access_token_hook`;
424
+ }
425
+ function createEnum(schema, name, values) {
426
+ const list = values.map((value) => quoteLiteral(value)).join(", ");
427
+ return `do $$ begin
428
+ create type ${quoteTable(`${schema}.${name}`)} as enum (${list});
429
+ exception when duplicate_object then null;
430
+ end $$;`;
431
+ }
432
+ function hookTable(name) {
433
+ return quoteTable(name.includes(".") ? name : `public.${name}`);
434
+ }
435
+ /** A read policy and `select` grant for `supabase_auth_admin` on a table the hook reads. */
436
+ function authAdminRead(table, label) {
437
+ const t = hookTable(table);
438
+ const policy = quoteIdent(`permdock_auth_admin_read_${label}`);
439
+ return `grant select on table ${t} to supabase_auth_admin;
440
+ drop policy if exists ${policy} on ${t};
441
+ create policy ${policy} on ${t}
442
+ as permissive for select
443
+ to supabase_auth_admin
444
+ using (true);`;
445
+ }
446
+ /**
447
+ * Supabase's Custom Claims and RBAC scaffold on top of the PermDock helpers:
448
+ * enums, `user_roles` and `authorize()` over the shared `role_permissions`.
449
+ * The token hook comes only from `permdock supabase hook generate`, so one
450
+ * function writes the claims. Policies never call `authorize()` per row; they
451
+ * call the helpers. Never grants anything to `service_role`.
452
+ */
453
+ function rbacScaffold(policy, options) {
454
+ const schema = options.schema;
455
+ const q = (name) => quoteTable(`${schema}.${name}`);
456
+ const s = quoteIdent(schema);
457
+ const permissions = [...new Set(policy.grants.map((grant) => grant.permission.key))];
458
+ const authorizeFn = authorizeSql({
459
+ schema,
460
+ authorize: options.authorize,
461
+ ...options.scope === void 0 ? {} : { scope: options.scope },
462
+ ...options.memberships === void 0 ? {} : { tenant: options.memberships },
463
+ ...options.customRoles === void 0 ? {} : { customRoles: options.customRoles },
464
+ ...options.context?.suspension === void 0 ? {} : { suspension: options.context.suspension }
465
+ });
466
+ return {
467
+ head: `-- rbac scaffold (Supabase Custom Claims and RBAC)
468
+ -- authorize: ${options.authorize}${options.authorize === "jwt" ? " (reads the hook claims; stale until the token refreshes)" : " (reads user_roles on every statement)"}
469
+ -- the custom access token hook that writes user_role${options.authorize === "jwt" ? " and memberships" : ""}: permdock supabase hook generate
470
+ ${schema === "public" ? "" : `create schema if not exists ${s};\n`}${createEnum(schema, "app_role", roleNames(policy))}
471
+ ${createEnum(schema, "app_permission", permissions)}
472
+
473
+ create table if not exists ${q("user_roles")} (
474
+ user_id uuid not null references auth.users on delete cascade,
475
+ role ${options.customRoles === void 0 ? q("app_role") : "text"} not null,
476
+ primary key (user_id, role)
477
+ );
478
+
479
+ alter table ${q("user_roles")} enable row level security;
480
+ revoke all on table ${q("user_roles")} from authenticated, anon, public;
481
+ `,
482
+ tail: authorizeFn,
483
+ warnings: []
484
+ };
485
+ }
486
+ /** `--check`: one entry per file that is missing or differs, naming its part; a differing file carries a short diff. */
487
+ function driftOf(cwd, files) {
488
+ const drift = [];
489
+ for (const file of files) {
490
+ if (file.rel === "-") continue;
491
+ const path = resolve(cwd, file.rel);
492
+ if (!existsSync(path)) drift.push(`${file.part}: missing ${file.rel}`);
493
+ else {
494
+ const onDisk = readFileSync(path, "utf8");
495
+ if (onDisk !== file.text) drift.push(`${file.part}: ${file.rel}\n${shortDiff(file.rel, onDisk, file.text)}`);
496
+ }
497
+ }
498
+ return drift;
499
+ }
500
+ /** Writes every file but the stdout ones, whose text it returns for printing. */
501
+ function writeSqlFiles(cwd, files) {
502
+ const wrote = [];
503
+ const printed = [];
504
+ for (const file of files) {
505
+ if (file.rel === "-") {
506
+ printed.push(file.text.trimEnd());
507
+ continue;
508
+ }
509
+ const path = resolve(cwd, file.rel);
510
+ mkdirSync(dirname(path), { recursive: true });
511
+ writeFileSync(path, file.text);
512
+ wrote.push(file.rel);
513
+ }
514
+ return {
515
+ wrote,
516
+ printed: printed.join("\n")
517
+ };
518
+ }
519
+ const PARTS = [
520
+ "helpers",
521
+ "seeds",
522
+ "indexes",
523
+ "policies",
524
+ "hook"
525
+ ];
526
+ /** `--split helpers,seeds,indexes,policies,hook`: the parts in that fixed order, or an error. */
527
+ function parseSplit(raw) {
528
+ if (raw === void 0) return;
529
+ const names = raw.split(",").map((name) => name.trim()).filter((name) => name !== "");
530
+ if (names.filter((name) => !PARTS.some((part) => part === name)).length > 0 || names.length === 0) return `rls generate --split takes helpers, seeds, indexes, policies and hook, got '${raw}'`;
531
+ return PARTS.filter((part) => names.includes(part));
532
+ }
533
+ /** The path of one split part: `{part}` in `out` replaced by the part name. */
534
+ function partPath(out, part) {
535
+ return out.replaceAll("{part}", part);
536
+ }
537
+ /**
538
+ * pg-delta's per-schema, unnumbered layout: pg-delta orders statements by
539
+ * their dependencies, so no file needs a number to apply first.
540
+ */
541
+ function pgDeltaPath(root, part, schema) {
542
+ switch (part) {
543
+ case "helpers":
544
+ case "indexes":
545
+ case "seeds": return `${root}/${schema}/${part}.sql`;
546
+ case "policies": return `${root}/public/policies/permdock.sql`;
547
+ case "hook": return `${root}/${schema}/functions/custom_access_token_hook.sql`;
548
+ default: return part;
549
+ }
550
+ }
551
+ //#endregion
552
+ //#region src/cli/supabase-setup.ts
553
+ const HELPER_NAME = /^(?:permdock_has|permitted_[a-z][a-z0-9_]*_ids|member_[a-z][a-z0-9_]*_ids(?:_for)?)$/u;
554
+ function escape(text) {
555
+ return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
556
+ }
557
+ /** The helper functions `sql` creates in `schema`; unqualified names count for `public`. */
558
+ function helpersDefined(sql, schema) {
559
+ const qualifier = schema === "public" ? String.raw`(?:(?:"public"|public)\.)?` : String.raw`(?:"${escape(schema)}"|${escape(schema)})\.`;
560
+ const create = new RegExp(String.raw`\bcreate\s+(?:or\s+replace\s+)?function\s+${qualifier}"?([a-z_][a-z0-9_]*)"?\s*\(`, "giu");
561
+ const names = /* @__PURE__ */ new Set();
562
+ for (const match of sql.matchAll(create)) {
563
+ const name = (match[1] ?? "").toLowerCase();
564
+ if (HELPER_NAME.test(name)) names.add(name);
565
+ }
566
+ return names;
567
+ }
568
+ /** SQL files that may hold the generated helpers: `rls.out` (else `rls.sql`) and the migration folders, not the hook itself. */
569
+ function helperSources(cwd, config, hookOut) {
570
+ const hook = resolve(cwd, hookOut);
571
+ const files = new Set(sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS));
572
+ const rlsOut = resolve(cwd, config.rls?.out ?? "rls.sql");
573
+ if (rlsOut.endsWith(".sql") && existsSync(rlsOut)) files.add(rlsOut);
574
+ files.delete(hook);
575
+ return [...files];
576
+ }
577
+ function missingHelpersInFiles(cwd, config, manifest) {
578
+ const found = /* @__PURE__ */ new Set();
579
+ for (const file of helperSources(cwd, config, manifest.hook.out)) for (const name of helpersDefined(readFileSync(file, "utf8"), manifest.helpers.schema)) found.add(name);
580
+ return manifest.helpers.functions.filter((name) => !found.has(name));
581
+ }
582
+ async function missingHelpersInDb(db, manifest, connect = (url) => connectPg(url, "permdock supabase hook generate --db")) {
583
+ const client = await connect(db);
584
+ try {
585
+ const result = await client.query(`select p.proname from pg_proc p join pg_namespace n on n.oid = p.pronamespace
586
+ where n.nspname = $1 and p.proname = any($2::text[])`, [manifest.helpers.schema, [...manifest.helpers.functions]]);
587
+ const found = new Set(result.rows.map((row) => row["proname"]));
588
+ return manifest.helpers.functions.filter((name) => !found.has(name));
589
+ } finally {
590
+ await client.end();
591
+ }
592
+ }
593
+ function missingHelpersMessage(manifest, missing) {
594
+ return `PD039 schema ${manifest.helpers.schema} has no ${missing.join(", ")}: the hook's claims are read by these helpers; run permdock rls generate and apply its migration`;
595
+ }
596
+ /** Bytes of JSON of each extra claim in the largest sample, for claims that pass the budget. */
597
+ function oversizedClaims(samples, manifest) {
598
+ const encoder = new TextEncoder();
599
+ const oversized = [];
600
+ for (const claim of manifest.claims) {
601
+ if (claim.source === "permdock") continue;
602
+ let bytes = 0;
603
+ for (const sample of samples) {
604
+ if (sample === null || typeof sample !== "object") continue;
605
+ const value = Object.hasOwn(sample, claim.name) ? sample[claim.name] : void 0;
606
+ if (value !== void 0) bytes = Math.max(bytes, encoder.encode(JSON.stringify(value)).byteLength);
607
+ }
608
+ if (bytes > manifest.budget.bytes) oversized.push({
609
+ name: claim.name,
610
+ bytes
611
+ });
612
+ }
613
+ return oversized;
614
+ }
615
+ /** `memberships[i]` entries `subjectFromSupabase` drops, per sample index. */
616
+ function droppedMemberships(samples) {
617
+ const out = [];
618
+ for (const [index, sample] of samples.entries()) {
619
+ if (sample === null || typeof sample !== "object") continue;
620
+ const claim = Object.hasOwn(sample, "memberships") ? sample["memberships"] : void 0;
621
+ const { dropped } = readMemberships(claim);
622
+ if (dropped.length > 0) out.push({
623
+ sample: index,
624
+ entries: dropped
625
+ });
626
+ }
627
+ return out;
628
+ }
629
+ function pd039(input) {
630
+ const findings = [];
631
+ const missing = missingHelpersInFiles(input.cwd, input.config, input.manifest);
632
+ if (missing.length > 0) findings.push({
633
+ code: "PD039",
634
+ severity: "warning",
635
+ message: missingHelpersMessage(input.manifest, missing).slice(6),
636
+ fix: "run permdock rls generate, or set rls.out to the migration that holds the helpers"
637
+ });
638
+ const fixture = input.config.doctor?.claims;
639
+ if (fixture !== void 0) {
640
+ const parsed = JSON.parse(readFileSync(resolve(input.cwd, fixture), "utf8"));
641
+ const samples = Array.isArray(parsed) ? parsed : [];
642
+ for (const claim of oversizedClaims(samples, input.manifest)) findings.push({
643
+ code: "PD039",
644
+ severity: "warning",
645
+ message: `claim ${claim.name} is ${String(claim.bytes)} bytes of JSON in ${fixture}, more than the ${String(input.manifest.budget.bytes)}-byte memberships budget`,
646
+ fix: "return less from the claim function, or read the data per request instead of from the token"
647
+ });
648
+ for (const { sample, entries } of droppedMemberships(samples)) findings.push({
649
+ code: "PD039",
650
+ severity: "warning",
651
+ message: `sample ${String(sample)} in ${fixture} has memberships ${entries.map((entry) => `[${String(entry)}]`).join(", ")} that subjectFromSupabase drops (membership-dropped)`,
652
+ fix: "emit { scope, id, roles } (or tenant, team or on) with a non-empty roles array for each entry"
653
+ });
654
+ }
655
+ return findings;
656
+ }
657
+ //#endregion
658
+ //#region src/cli/supabase-hook.ts
659
+ const SUPABASE_HELP = `permdock supabase hook generate | inspect
660
+
661
+ hook generate [--out supabase/permdock-hook.sql] [--check] [--db <url>]
662
+ [--active-from app_metadata.active_<scope>|<table>.<column>]
663
+ [--budget 1024] [--schema public] [--grants-out <file>|-]
664
+ inspect [--json] [--out [permdock.manifest.json]] [--check]
665
+
666
+ Reads supabase.hook from permdock.config.ts: the fromTable / fromJunction sources the app
667
+ passes as memberships. hook generate emits custom_access_token_hook(jsonb), the grants it
668
+ needs (in --grants-out instead, for declarative schemas), the permdock_authz_version table
669
+ and its triggers. Never grants anything to
670
+ service_role. inspect prints the manifest: helper schema, names and signatures, tenant claim,
671
+ budget, the claims the hook writes, the membership sources and the columns that decide them.
672
+ --out writes it as JSON, to permdock.manifest.json without a path; --check exits 1 when that
673
+ file differs.
674
+ `;
675
+ const MANIFEST_FILE = "permdock.manifest.json";
676
+ const MANAGED_TRIGGER = "permdock_protect_managed";
677
+ const CLAIM_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/u;
678
+ const PROTOTYPE_KEYS = /* @__PURE__ */ new Set([
679
+ "__proto__",
680
+ "constructor",
681
+ "prototype"
682
+ ]);
683
+ const USER_EDITABLE = /^(raw_)?user_meta(_)?data$/iu;
684
+ /**
685
+ * Splits `supabase.hook.attrs` into table columns and `app_metadata` keys and
686
+ * lists what cannot be compiled: `user_metadata` (the user can edit it), a
687
+ * direct `auth.users` column, an unsafe or prototype key, a duplicate key,
688
+ * or a table column without a table.
689
+ */
690
+ function attrsPlan(attrs) {
691
+ const errors = [];
692
+ const columns = [];
693
+ const meta = [];
694
+ const seen = /* @__PURE__ */ new Set();
695
+ if (attrs.table?.replaceAll("\"", "").toLowerCase() === "auth.users") errors.push("supabase.hook.attrs.table cannot be auth.users: list app_metadata.<key> entries instead");
696
+ for (const entry of attrs.columns) {
697
+ const [head = "", ...rest] = entry.split(".");
698
+ const isMeta = head === "app_metadata" || head === "raw_app_meta_data";
699
+ const key = isMeta ? rest.join(".") : entry;
700
+ if (USER_EDITABLE.test(head) || USER_EDITABLE.test(key) || /user_?meta/iu.test(entry)) {
701
+ errors.push(`supabase.hook.attrs lists ${entry}: user_metadata is user-editable and never becomes a claim`);
702
+ continue;
703
+ }
704
+ if (!CLAIM_KEY.test(key) || PROTOTYPE_KEYS.has(key)) {
705
+ errors.push(`supabase.hook.attrs lists ${entry}: a key must match ${CLAIM_KEY.source} and not be a prototype key`);
706
+ continue;
707
+ }
708
+ if (seen.has(key)) {
709
+ errors.push(`supabase.hook.attrs names the key ${key} twice`);
710
+ continue;
711
+ }
712
+ seen.add(key);
713
+ if (isMeta) meta.push(key);
714
+ else columns.push(key);
715
+ }
716
+ if (columns.length > 0 && attrs.table === void 0) errors.push(`supabase.hook.attrs lists ${columns.join(", ")} without a table`);
717
+ return {
718
+ ...attrs.table === void 0 ? {} : { table: attrs.table },
719
+ id: attrs.id ?? "id",
720
+ columns,
721
+ meta,
722
+ errors
723
+ };
724
+ }
725
+ /**
726
+ * Claim names `supabase.hook.claims` cannot write: the ones this hook owns
727
+ * (plus the tenant claim) and the ones Supabase Auth issues.
728
+ */
729
+ const RESERVED_CLAIMS = /* @__PURE__ */ new Set([
730
+ "roles",
731
+ "user_role",
732
+ "memberships",
733
+ "memberships_truncated",
734
+ "attrs",
735
+ "authz_ver",
736
+ "sub",
737
+ "aud",
738
+ "role",
739
+ "exp",
740
+ "iat",
741
+ "iss",
742
+ "aal",
743
+ "amr",
744
+ "session_id",
745
+ "is_anonymous",
746
+ "email",
747
+ "phone",
748
+ "app_metadata",
749
+ "user_metadata"
750
+ ]);
751
+ const CLAIM_FUNCTION = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/u;
752
+ /**
753
+ * Checks `supabase.hook.claims`: each name is a safe claim key outside
754
+ * {@link RESERVED_CLAIMS} and the tenant claim, each function is
755
+ * schema-qualified (the hook runs with an empty `search_path`).
756
+ */
757
+ function extraClaimsPlan(claims, tenantClaim) {
758
+ const errors = [];
759
+ const planned = [];
760
+ for (const [claim, fn] of Object.entries(claims ?? {})) {
761
+ if (!CLAIM_KEY.test(claim) || PROTOTYPE_KEYS.has(claim)) {
762
+ errors.push(`supabase.hook.claims names ${claim}: a claim must match ${CLAIM_KEY.source} and not be a prototype key`);
763
+ continue;
764
+ }
765
+ if (RESERVED_CLAIMS.has(claim) || claim === tenantClaim) {
766
+ errors.push(`supabase.hook.claims names ${claim}, which PermDock or Supabase Auth writes`);
767
+ continue;
768
+ }
769
+ if (typeof fn !== "string" || !CLAIM_FUNCTION.test(fn)) {
770
+ errors.push(`supabase.hook.claims.${claim} must be a schema-qualified function name such as better_supabase.feature_claims`);
771
+ continue;
772
+ }
773
+ planned.push({
774
+ claim,
775
+ fn
776
+ });
777
+ }
778
+ return {
779
+ claims: planned,
780
+ errors
781
+ };
782
+ }
783
+ const VERSION_TRIGGER = "permdock_authz_version";
784
+ const AUTHZ_VERSION_BUMP = "permdock_bump_authz_version_for";
785
+ function memberForPlan(input) {
786
+ const memberFor = input.scopes.map((scope) => scope.name).filter((name) => hasMemberFor({
787
+ dialect: "supabase",
788
+ ...input
789
+ }, name));
790
+ const entries = /* @__PURE__ */ new Map();
791
+ for (const name of memberFor) {
792
+ const mapped = memberForTable(input, name);
793
+ const found = mapped === void 0 ? memberForSources(input, name).map((source) => source.sql.manifest) : [mappedMembership(mapped, input.scopes, name)];
794
+ for (const entry of found) entries.set(JSON.stringify(entry), entry);
795
+ }
796
+ return {
797
+ memberFor,
798
+ memberships: [...entries.values()]
799
+ };
800
+ }
801
+ /** An `rls.memberships` table in the manifest's `fromTable` / `fromJunction` shape, for scope `name`. */
802
+ function mappedMembership(mapped, scopes, name) {
803
+ const id = scopeColumn(mapped, scopes, name) ?? "";
804
+ const within = {};
805
+ for (const ancestor of scopeChain(scopes, name).slice(1)) {
806
+ const column = scopeColumn(mapped, scopes, ancestor);
807
+ if (column !== void 0) within[ancestor] = column;
808
+ }
809
+ const columns = [
810
+ mapped.user,
811
+ id,
812
+ mapped.role,
813
+ ...Object.values(within),
814
+ ...mapped.via === void 0 ? [] : [mapped.via],
815
+ ...mapped.expiresAt === void 0 ? [] : [mapped.expiresAt]
816
+ ];
817
+ return {
818
+ table: mapped.table.includes(".") ? mapped.table : `public.${mapped.table}`,
819
+ user: { column: mapped.user },
820
+ scope: { value: name },
821
+ id: { column: id },
822
+ role: { column: mapped.role },
823
+ ...Object.keys(within).length === 0 ? {} : { within: { columns: within } },
824
+ ...mapped.via === void 0 ? {} : { via: { column: mapped.via } },
825
+ ...mapped.expiresAt === void 0 ? {} : { expiresAt: { column: mapped.expiresAt } },
826
+ columns: [...new Set(columns)]
827
+ };
828
+ }
829
+ function table(name) {
830
+ return quoteTable(name.includes(".") ? name : `public.${name}`);
831
+ }
832
+ function positiveInteger(value, label) {
833
+ const number = typeof value === "string" ? Number(value) : value;
834
+ if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) throw new Error(`PermDock CLI: ${label} must be a positive integer`);
835
+ return number;
836
+ }
837
+ /**
838
+ * SQL for the active first-scope id, as text, for user `uid`; a table source
839
+ * compares its id column with `v_active_user`, declared as `userType`.
840
+ */
841
+ function activeFromSql(input, root) {
842
+ const spec = input ?? `app_metadata.active_${root}`;
843
+ if (typeof spec === "string" && spec.startsWith("app_metadata.")) {
844
+ const key = spec.slice(13);
845
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`PermDock CLI: unsafe app_metadata key '${key}'`);
846
+ return { sql: `claims -> 'app_metadata' ->> ${quoteLiteral(key)}` };
847
+ }
848
+ const parsed = typeof spec === "string" ? (() => {
849
+ const dot = spec.lastIndexOf(".");
850
+ if (dot <= 0) throw new Error(`PermDock CLI: --active-from must be app_metadata.<key> or <table>.<column>, got '${spec}'`);
851
+ return {
852
+ table: spec.slice(0, dot),
853
+ column: spec.slice(dot + 1)
854
+ };
855
+ })() : spec;
856
+ const id = "id" in parsed && parsed.id !== void 0 ? parsed.id : "id";
857
+ return {
858
+ sql: `(select a.${quoteIdent(parsed.column)}::text from ${table(parsed.table)} a where a.${quoteIdent(id)} = v_active_user)`,
859
+ userType: `${table(parsed.table)}.${quoteIdent(id)}%type`
860
+ };
861
+ }
862
+ /**
863
+ * One variable per user-keyed table the hook reads, holding `uid` (a uuid, as
864
+ * Supabase Auth issues) as that column's type, so each lookup compares the
865
+ * column uncast and its index applies.
866
+ */
867
+ function typedUsers(parts) {
868
+ return [
869
+ ...parts.sources.map((source, index) => `v_user_${String(index)} ${source.sql.userType} := uid;`),
870
+ ...parts.roles === void 0 ? [] : [`v_roles_user ${table(parts.roles.table)}.${quoteIdent(parts.roles.user)}%type := uid;`],
871
+ ...parts.attrs?.table === void 0 || parts.attrs.columns.length === 0 ? [] : [`v_attrs_user ${table(parts.attrs.table)}.${quoteIdent(parts.attrs.id)}%type := uid;`],
872
+ ...parts.active.userType === void 0 ? [] : [`v_active_user ${parts.active.userType} := uid;`]
873
+ ].map((line) => `\n ${line}`).join("");
874
+ }
875
+ function checkSources(parts) {
876
+ if (parts.sources.length === 0) throw new Error("PermDock CLI: supabase.hook.memberships needs at least one fromTable or fromJunction source");
877
+ for (const source of parts.sources) {
878
+ if (source.sql === void 0 || typeof source.sql.select !== "function") throw new Error("PermDock CLI: supabase.hook.memberships takes fromTable / fromJunction sources from permdock/supabase");
879
+ const scope = source.sql.scope;
880
+ if (scope === void 0) continue;
881
+ if (resolveScope(parts.scopes, scope) !== scope) throw new Error(`PermDock CLI: the ${source.sql.table} source names scope '${scope}', which the policy does not declare by that name`);
882
+ const chain = scopeChain(parts.scopes, scope);
883
+ const holds = new Set(source.sql.holds ?? [scope]);
884
+ const missing = chain.filter((ancestor) => !holds.has(ancestor));
885
+ if (missing.length > 0) throw new Error(`PermDock CLI: the ${source.sql.table} source needs within columns for ${missing.join(", ")}: a ${scope} membership without every ancestor grants nothing`);
886
+ }
887
+ }
888
+ function entriesSql(parts) {
889
+ const tenant = `case when s.scope = ${quoteLiteral(parts.root)} then s.id else s.within ->> ${quoteLiteral(parts.root)} end`;
890
+ return parts.sources.map((source, index) => `select ${String(index)} as ord,
891
+ (${tenant}) as tenant,
892
+ jsonb_strip_nulls(jsonb_build_object(
893
+ 'scope', s.scope, 'id', s.id, 'within', s.within, 'roles', s.roles, 'via', s.via,
894
+ 'expiresAt', s.expires_at, 'grantedBy', s.granted_by, 'reason', s.reason,
895
+ 'member', case when s.member_group is not null then jsonb_build_object('group', s.member_group) end,
896
+ 'managedBy', s.managed_by, 'entitlements', s.seats
897
+ )) as entry
898
+ from (
899
+ ${source.sql.select(`v_user_${String(index)}`).replaceAll(/^/gmu, " ")}
900
+ ) s`).join("\nunion all\n");
901
+ }
902
+ function hookSql(parts) {
903
+ const schema = quoteIdent(parts.schema);
904
+ const fn = `${schema}.custom_access_token_hook`;
905
+ const rows = parts.roles;
906
+ const roles = rows === void 0 ? ` held := '[]'::jsonb;` : ` select coalesce(jsonb_agg(distinct ${rows.roleSql} order by ${rows.roleSql}), '[]'::jsonb)
907
+ into held
908
+ from ${rows.from}
909
+ where ${rows.userSql} = v_roles_user;`;
910
+ const plan = parts.attrs;
911
+ const fromTable = plan === void 0 || plan.columns.length === 0 || plan.table === void 0 ? "" : `
912
+ select jsonb_strip_nulls(jsonb_build_object(${plan.columns.map((column) => `${quoteLiteral(column)}, to_jsonb(p.${quoteIdent(column)})`).join(", ")}))
913
+ into attrs
914
+ from ${table(plan.table)} p
915
+ where p.${quoteIdent(plan.id)} = v_attrs_user;`;
916
+ const fromMeta = plan === void 0 || plan.meta.length === 0 ? "" : `
917
+ attrs := coalesce(attrs, '{}'::jsonb) || jsonb_strip_nulls(jsonb_build_object(${plan.meta.map((key) => `${quoteLiteral(key)}, claims -> 'app_metadata' -> ${quoteLiteral(key)}`).join(", ")}));`;
918
+ const attrs = plan === void 0 ? "" : `${fromTable}${fromMeta}
919
+ if attrs is not null and attrs <> '{}'::jsonb then
920
+ used := octet_length(attrs::text);
921
+ if used > budget then
922
+ truncated := true;
923
+ used := 0;
924
+ else
925
+ claims := jsonb_set(claims, '{attrs}', attrs);
926
+ end if;
927
+ end if;`;
928
+ const versionTable = `${schema}.${quoteIdent(AUTHZ_VERSION_TABLE)}`;
929
+ const version = parts.version ? `
930
+ select v.version into ver from ${versionTable} v where v.user_id = uid;
931
+ claims := jsonb_set(claims, '{authz_ver}', to_jsonb(coalesce(ver, 0)));` : "";
932
+ const dropExtra = parts.extra.map((entry) => ` - ${quoteLiteral(entry.claim)}`).join("");
933
+ const extra = parts.extra.map((entry) => `
934
+ extra := ${quoteTable(entry.fn)}(uid);
935
+ if extra is not null then
936
+ claims := jsonb_set(claims, ${quoteLiteral(`{${entry.claim}}`)}, extra);
937
+ end if;`).join("");
938
+ const suspended = parts.users === void 0 ? "" : `
939
+ if not ${activeRowSql(parts.users, "uid")} then
940
+ claims := claims - 'memberships_truncated' - 'attrs' - ${quoteLiteral(parts.tenantClaim)}${dropExtra};
941
+ claims := claims || jsonb_build_object('user_role', '[]'::jsonb, 'roles', '[]'::jsonb, 'memberships', '[]'::jsonb);${version}
942
+ return jsonb_set(event, '{claims}', claims);
943
+ end if;`;
944
+ return `${parts.schema === "public" ? "" : `create schema if not exists ${schema};\nrevoke all on schema ${schema} from public;\n\n`}create or replace function ${fn}(event jsonb)
945
+ returns jsonb
946
+ language plpgsql
947
+ stable
948
+ set search_path = ''
949
+ as $$
950
+ declare
951
+ claims jsonb := event -> 'claims';
952
+ uid uuid := (event ->> 'user_id')::uuid;
953
+ active text;
954
+ held jsonb;
955
+ kept jsonb := '[]'::jsonb;
956
+ truncated boolean := false;
957
+ in_active boolean := false;
958
+ budget integer := ${String(parts.budget)};
959
+ used integer := 0;
960
+ item record;${typedUsers(parts)}${plan === void 0 ? "" : "\n attrs jsonb;"}${parts.extra.length === 0 ? "" : "\n extra jsonb;"}${parts.version ? "\n ver bigint;" : ""}
961
+ begin${suspended}
962
+ claims := claims - 'attrs'${dropExtra};
963
+ ${roles}
964
+ claims := jsonb_set(claims, '{roles}', held);
965
+ if jsonb_array_length(held) = 1 then
966
+ claims := jsonb_set(claims, '{user_role}', held -> 0);
967
+ elsif jsonb_array_length(held) > 1 then
968
+ claims := jsonb_set(claims, '{user_role}', held);
969
+ end if;
970
+ active := ${parts.active.sql};${attrs}
971
+ for item in
972
+ select x.entry, x.tenant is not distinct from active as current
973
+ from (
974
+ ${entriesSql(parts).replaceAll(/^/gmu, " ")}
975
+ ) x
976
+ order by (x.tenant is not distinct from active) desc, x.ord, x.entry ->> 'scope', x.entry ->> 'id', x.entry::text
977
+ loop
978
+ if octet_length((kept || jsonb_build_array(item.entry))::text) + used > budget then
979
+ truncated := true;
980
+ exit;
981
+ end if;
982
+ kept := kept || jsonb_build_array(item.entry);
983
+ in_active := in_active or (active is not null and item.current);
984
+ end loop;
985
+ claims := jsonb_set(claims, '{memberships}', kept);
986
+ if truncated then
987
+ claims := jsonb_set(claims, '{memberships_truncated}', 'true'::jsonb);
988
+ else
989
+ claims := claims - 'memberships_truncated';
990
+ end if;
991
+ if in_active then
992
+ claims := jsonb_set(claims, ${quoteLiteral(`{${parts.tenantClaim}}`)}, to_jsonb(active));
993
+ end if;${extra}${version}
994
+ return jsonb_set(event, '{claims}', claims);
995
+ end;
996
+ $$;`;
997
+ }
998
+ /**
999
+ * Everything the hook grants `supabase_auth_admin`, and the execute revoke:
1000
+ * the statements `supabase db diff` drops, so `--grants-out` moves them to
1001
+ * their own file.
1002
+ */
1003
+ function grantsSql(parts) {
1004
+ const schema = quoteIdent(parts.schema);
1005
+ const fn = `${schema}.custom_access_token_hook`;
1006
+ return [
1007
+ `-- supabase_auth_admin: the grants and read policies the hook needs
1008
+ grant usage on schema ${schema} to supabase_auth_admin;
1009
+ grant execute on function ${fn}(jsonb) to supabase_auth_admin;
1010
+ revoke execute on function ${fn}(jsonb) from authenticated, anon, public;${extraGrantsSql(parts.extra)}${memberForGrantsSql(parts)}`,
1011
+ readsSql(parts),
1012
+ parts.version ? authAdminRead(`${parts.schema}.${AUTHZ_VERSION_TABLE}`, "version") : ""
1013
+ ].filter((chunk) => chunk !== "").join("\n");
1014
+ }
1015
+ function extraGrantsSql(extra) {
1016
+ return [...[...new Set(extra.map((entry) => entry.fn.split(".")[0]))].map((name) => `\ngrant usage on schema ${quoteIdent(name ?? "")} to supabase_auth_admin;`), ...extra.map((entry) => `\ngrant execute on function ${quoteTable(entry.fn)}(uuid) to supabase_auth_admin;`)].join("");
1017
+ }
1018
+ /**
1019
+ * Execute on each `member_<scope>_ids_for(uuid)` for the `hook.claims`
1020
+ * functions, which run as `supabase_auth_admin` and may call them. Only with
1021
+ * `hook.claims`: the grant needs the helpers migration applied first, and a
1022
+ * hook without extra claims does not.
1023
+ */
1024
+ function memberForGrantsSql(parts) {
1025
+ const { schema, memberFor } = parts.helpers;
1026
+ if (memberFor.length === 0 || parts.extra.length === 0) return "";
1027
+ return [...schema === parts.schema ? [] : [`\ngrant usage on schema ${quoteIdent(schema)} to supabase_auth_admin;`], ...memberFor.map((scope) => `\ngrant execute on function ${quoteIdent(schema)}.${memberForHelper(scope)}(uuid) to supabase_auth_admin;`)].join("");
1028
+ }
1029
+ function readsSql(parts) {
1030
+ const reads = /* @__PURE__ */ new Map();
1031
+ for (const source of parts.sources) reads.set(source.sql.table, "memberships");
1032
+ for (const source of parts.sources) for (const name of source.sql.reads) reads.set(name, reads.get(name) ?? "status");
1033
+ if (parts.roles !== void 0) {
1034
+ const { table: rolesTable, through } = parts.roles;
1035
+ reads.set(rolesTable, reads.get(rolesTable) ?? "roles");
1036
+ if (through !== void 0) reads.set(through.table, reads.get(through.table) ?? "role_keys");
1037
+ }
1038
+ if (parts.attrs?.table !== void 0 && parts.attrs.columns.length > 0) reads.set(parts.attrs.table, reads.get(parts.attrs.table) ?? "attrs");
1039
+ if (parts.users !== void 0) reads.set(parts.users.table, reads.get(parts.users.table) ?? "status_users");
1040
+ return [...[...new Set([...reads.keys()].map((name) => name.includes(".") ? name.split(".")[0] : "public").filter((schema) => schema !== void 0 && schema !== parts.schema && schema !== "auth"))].map((schema) => `grant usage on schema ${quoteIdent(schema)} to supabase_auth_admin;`), ...[...reads].map(([name, label]) => authAdminRead(name, label))].join("\n");
1041
+ }
1042
+ function versionSql(parts) {
1043
+ if (!parts.version) return "";
1044
+ const schema = quoteIdent(parts.schema);
1045
+ const versionTable = `${schema}.${quoteIdent(AUTHZ_VERSION_TABLE)}`;
1046
+ const bump = `${schema}.permdock_bump_authz_version`;
1047
+ const bumpFor = `${schema}.${AUTHZ_VERSION_BUMP}`;
1048
+ const tables = [...new Map(parts.sources.map((source) => [source.sql.table, source.sql.user]))];
1049
+ if (parts.roles !== void 0) tables.push([parts.roles.table, parts.roles.user]);
1050
+ return `-- the authorization version: bumped on every membership change, written to authz_ver
1051
+ create table if not exists ${versionTable} (
1052
+ user_id uuid primary key references auth.users on delete cascade,
1053
+ version bigint not null default 0
1054
+ );
1055
+ alter table ${versionTable} enable row level security;
1056
+ revoke all on table ${versionTable} from anon, authenticated, public;
1057
+
1058
+ -- bumps each listed user once, for a membership source outside the hook; no client role may call it
1059
+ create or replace function ${bumpFor}(p_users uuid[])
1060
+ returns void
1061
+ language sql
1062
+ security definer
1063
+ set search_path = ''
1064
+ as $$
1065
+ insert into ${versionTable} as v (user_id, version)
1066
+ select distinct u, 1 from unnest(p_users) u where u is not null
1067
+ on conflict (user_id) do update set version = v.version + 1
1068
+ $$;
1069
+ revoke execute on function ${bumpFor}(uuid[]) from public, anon, authenticated;
1070
+
1071
+ create or replace function ${bump}()
1072
+ returns trigger
1073
+ language plpgsql
1074
+ security definer
1075
+ set search_path = ''
1076
+ as $$
1077
+ declare
1078
+ column_name text := tg_argv[0];
1079
+ begin
1080
+ perform ${bumpFor}(array[
1081
+ case when tg_op <> 'INSERT' then to_jsonb(old) ->> column_name end,
1082
+ case when tg_op <> 'DELETE' then to_jsonb(new) ->> column_name end
1083
+ ]::uuid[]);
1084
+ return null;
1085
+ end;
1086
+ $$;
1087
+ revoke execute on function ${bump}() from public, anon, authenticated;
1088
+ ${tables.map(([name, user]) => `drop trigger if exists ${quoteIdent(VERSION_TRIGGER)} on ${table(name)};
1089
+ create trigger ${quoteIdent(VERSION_TRIGGER)}
1090
+ after insert or update or delete on ${table(name)}
1091
+ for each row execute function ${bump}(${quoteLiteral(user)});`).join("\n")}${roleKeysVersionSql(parts, versionTable)}`;
1092
+ }
1093
+ /** A renamed role key changes every holder's `roles` claim, so updates to the roles table bump each holder. */
1094
+ function roleKeysVersionSql(parts, versionTable) {
1095
+ const through = parts.roles?.through;
1096
+ if (parts.roles === void 0 || through === void 0) return "";
1097
+ const bump = `${quoteIdent(parts.schema)}.permdock_bump_authz_version_role_keys`;
1098
+ const id = quoteIdent(through.id);
1099
+ const key = quoteIdent(through.key);
1100
+ return `
1101
+
1102
+ -- a renamed role key changes the roles claim of everyone who holds it
1103
+ create or replace function ${bump}()
1104
+ returns trigger
1105
+ language plpgsql
1106
+ security definer
1107
+ set search_path = ''
1108
+ as $$
1109
+ begin
1110
+ if tg_op = 'UPDATE'
1111
+ and new.${key} is not distinct from old.${key}
1112
+ and new.${id} is not distinct from old.${id} then
1113
+ return null;
1114
+ end if;
1115
+ insert into ${versionTable} as v (user_id, version)
1116
+ select distinct h.${quoteIdent(parts.roles.user)}::uuid, 1
1117
+ from ${table(parts.roles.table)} h
1118
+ where h.${quoteIdent(through.ref)} = old.${id}
1119
+ on conflict (user_id) do update set version = v.version + 1;
1120
+ return null;
1121
+ end;
1122
+ $$;
1123
+ revoke execute on function ${bump}() from public, anon, authenticated;
1124
+ drop trigger if exists ${quoteIdent(VERSION_TRIGGER)} on ${table(through.table)};
1125
+ create trigger ${quoteIdent(VERSION_TRIGGER)}
1126
+ after update or delete on ${table(through.table)}
1127
+ for each row execute function ${bump}();`;
1128
+ }
1129
+ /**
1130
+ * Fails the migration when a client role can write an `attrs` column: a
1131
+ * claim the user can set is not a server-owned attribute.
1132
+ */
1133
+ function attrsGuardSql(plan) {
1134
+ if (plan?.table === void 0 || plan.columns.length === 0) return "";
1135
+ const target = quoteLiteral(table(plan.table));
1136
+ return `-- attrs must be server-owned: refuse columns anon or authenticated can insert or update
1137
+ do $$
1138
+ begin
1139
+ if exists (
1140
+ select 1
1141
+ from (values ${plan.columns.map((column) => `(${quoteLiteral(column)})`).join(", ")}) c(name)
1142
+ cross join (values ('anon'), ('authenticated')) r(role)
1143
+ where has_column_privilege(r.role, ${target}, c.name, 'INSERT')
1144
+ or has_column_privilege(r.role, ${target}, c.name, 'UPDATE')
1145
+ ) then
1146
+ raise exception 'PermDock: an attrs column of % is writable by anon or authenticated; revoke insert and update on it before it becomes a claim', ${target}
1147
+ using errcode = '42501';
1148
+ end if;
1149
+ end
1150
+ $$;`;
1151
+ }
1152
+ function managedSql(parts) {
1153
+ const managed = parts.sources.filter((source) => source.sql.managed !== void 0);
1154
+ if (managed.length === 0) return "";
1155
+ const guard = `${quoteIdent(parts.schema)}.permdock_protect_managed`;
1156
+ return `-- memberships the identity provider owns: clients (anon, authenticated) cannot write them
1157
+ create or replace function ${guard}()
1158
+ returns trigger
1159
+ language plpgsql
1160
+ set search_path = ''
1161
+ as $$
1162
+ declare
1163
+ column_name text := tg_argv[0];
1164
+ begin
1165
+ if current_user in ('anon', 'authenticated') and (
1166
+ column_name = ''
1167
+ or (tg_op <> 'INSERT' and to_jsonb(old) ->> column_name = 'idp')
1168
+ or (tg_op <> 'DELETE' and to_jsonb(new) ->> column_name = 'idp')
1169
+ ) then
1170
+ raise exception 'PermDock: % rows managed by the identity provider cannot be changed here', tg_table_name
1171
+ using errcode = '42501';
1172
+ end if;
1173
+ return coalesce(new, old);
1174
+ end;
1175
+ $$;
1176
+ ${managed.map((source) => `drop trigger if exists ${quoteIdent(MANAGED_TRIGGER)} on ${table(source.sql.table)};
1177
+ create trigger ${quoteIdent(MANAGED_TRIGGER)}
1178
+ before insert or update or delete on ${table(source.sql.table)}
1179
+ for each row execute function ${guard}(${quoteLiteral(source.sql.managed ?? "")});`).join("\n")}`;
1180
+ }
1181
+ /** The `config.toml` block that enables the hook and bounds token staleness. */
1182
+ function configToml(schema, jwtExpiry) {
1183
+ return `[auth]
1184
+ jwt_expiry = ${String(jwtExpiry)}
1185
+
1186
+ [auth.hook.custom_access_token]
1187
+ enabled = true
1188
+ uri = "${hookUri(schema)}"`;
1189
+ }
1190
+ const BUDGET_MEASURE = "octet_length(memberships::text) + octet_length(attrs::text)";
1191
+ function hookParts(scopes, config, overrides) {
1192
+ const hook = config.supabase?.hook;
1193
+ if (hook === void 0) throw new Error("PermDock CLI: supabase hook generate needs supabase.hook in permdock.config.ts");
1194
+ const root = rootScope(scopes) ?? "tenant";
1195
+ const suspension = checkSuspension(hook.suspension ?? config.rls?.suspension, scopes);
1196
+ const schema = overrides.schema ?? hook.schema ?? config.rls?.schema ?? "permdock";
1197
+ quoteIdent(schema);
1198
+ const tenantClaim = config.rls?.tenantClaim ?? "tenant_id";
1199
+ const extraPlan = extraClaimsPlan(hook.claims, tenantClaim);
1200
+ checkSources({
1201
+ sources: hook.memberships,
1202
+ scopes
1203
+ });
1204
+ const parts = {
1205
+ schema,
1206
+ scopes,
1207
+ root,
1208
+ sources: hook.memberships,
1209
+ budget: positiveInteger(overrides.budget ?? hook.budget ?? 1024, "budget"),
1210
+ version: hook.version !== false,
1211
+ jwtExpiry: positiveInteger(hook.jwtExpiry ?? 900, "jwtExpiry"),
1212
+ tenantClaim,
1213
+ users: suspension?.users,
1214
+ hook,
1215
+ roles: hook.roles === false ? void 0 : globalRoleSource(hook.roles ?? config.rls?.roles ?? { table: `${schema}.user_roles` }, "public", "r"),
1216
+ active: activeFromSql(overrides.activeFrom ?? hook.activeFrom, root),
1217
+ attrs: hook.attrs === void 0 ? void 0 : attrsPlan(hook.attrs),
1218
+ extra: extraPlan.claims,
1219
+ helpers: {
1220
+ schema: config.rls?.schema ?? "permdock",
1221
+ ...memberForPlan({
1222
+ scopes,
1223
+ ...config.rls?.memberships === void 0 ? {} : { memberships: config.rls.memberships },
1224
+ memberSources: config.rls?.membershipSources ?? hook.memberships
1225
+ })
1226
+ }
1227
+ };
1228
+ if (parts.attrs !== void 0 && parts.attrs.errors.length > 0) throw new Error(`PermDock CLI: ${parts.attrs.errors.join("; ")}`);
1229
+ if (extraPlan.errors.length > 0) throw new Error(`PermDock CLI: ${extraPlan.errors.join("; ")}`);
1230
+ quoteIdent(parts.tenantClaim);
1231
+ return parts;
1232
+ }
1233
+ function permdockClaim(name, budget = false) {
1234
+ return {
1235
+ name,
1236
+ source: "permdock",
1237
+ budget
1238
+ };
1239
+ }
1240
+ function hookClaims(parts) {
1241
+ return [
1242
+ permdockClaim("user_role"),
1243
+ permdockClaim("roles"),
1244
+ permdockClaim("memberships", true),
1245
+ permdockClaim("memberships_truncated"),
1246
+ permdockClaim(parts.tenantClaim),
1247
+ ...parts.attrs === void 0 ? [] : [permdockClaim("attrs", true)],
1248
+ ...parts.version ? [permdockClaim("authz_ver")] : [],
1249
+ ...parts.extra.map((entry) => ({
1250
+ name: entry.claim,
1251
+ source: entry.fn,
1252
+ budget: false
1253
+ }))
1254
+ ];
1255
+ }
1256
+ function helperList(parts, types) {
1257
+ const client = ["authenticated"];
1258
+ return [{
1259
+ name: "permdock_has",
1260
+ args: "p_grant text",
1261
+ returns: "boolean",
1262
+ execute: client
1263
+ }, ...parts.scopes.flatMap((scope) => {
1264
+ const returns = `setof ${types.get(scope.name) ?? "uuid"}`;
1265
+ return [
1266
+ {
1267
+ name: `permitted_${scope.name}_ids`,
1268
+ args: "p_grant text",
1269
+ returns,
1270
+ execute: client
1271
+ },
1272
+ {
1273
+ name: memberIdsHelper(scope.name),
1274
+ args: "",
1275
+ returns,
1276
+ execute: client
1277
+ },
1278
+ ...parts.helpers.memberFor.includes(scope.name) ? [{
1279
+ name: memberForHelper(scope.name),
1280
+ args: "p_user uuid",
1281
+ returns,
1282
+ execute: parts.extra.length === 0 ? [] : ["supabase_auth_admin"]
1283
+ }] : []
1284
+ ];
1285
+ })];
1286
+ }
1287
+ function manifestOf(parts, out, config) {
1288
+ const rls = config.rls;
1289
+ const ctx = {
1290
+ dialect: "supabase",
1291
+ scopes: parts.scopes,
1292
+ tenantClaim: parts.tenantClaim,
1293
+ gucPrefix: rls?.gucPrefix ?? "app",
1294
+ tenantType: rls?.tenantType ?? "uuid",
1295
+ ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
1296
+ ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes }
1297
+ };
1298
+ const types = new Map(parts.scopes.map((scope) => [scope.name, scopeTypeOf(ctx, scope.name)]));
1299
+ const helpers = helperList(parts, types);
1300
+ return {
1301
+ $schema: SUPABASE_MANIFEST_SCHEMA,
1302
+ version: 1,
1303
+ hook: {
1304
+ schema: parts.schema,
1305
+ function: "custom_access_token_hook",
1306
+ out
1307
+ },
1308
+ helpers: {
1309
+ schema: parts.helpers.schema,
1310
+ functions: helpers.map((helper) => helper.name)
1311
+ },
1312
+ tenantClaim: parts.tenantClaim,
1313
+ budget: {
1314
+ bytes: parts.budget,
1315
+ measure: BUDGET_MEASURE
1316
+ },
1317
+ claims: hookClaims(parts),
1318
+ authzVersion: parts.version,
1319
+ ...parts.version ? { authzVersionBump: {
1320
+ schema: parts.schema,
1321
+ function: AUTHZ_VERSION_BUMP,
1322
+ args: "p_users uuid[]"
1323
+ } } : {},
1324
+ memberships: parts.sources.map((source) => source.sql.manifest),
1325
+ rls: {
1326
+ schema: parts.helpers.schema,
1327
+ mode: resolveAuthorize(config),
1328
+ tenantClaim: parts.tenantClaim,
1329
+ scopes: parts.scopes.map((scope) => ({
1330
+ name: scope.name,
1331
+ type: types.get(scope.name) ?? "uuid",
1332
+ ...scope.within === void 0 ? {} : { within: scope.within }
1333
+ })),
1334
+ helpers,
1335
+ memberships: parts.helpers.memberships
1336
+ },
1337
+ decidingColumns: decidingColumns(config, parts.attrs === void 0 ? void 0 : {
1338
+ ...parts.attrs.table === void 0 ? {} : { table: parts.attrs.table },
1339
+ columns: parts.attrs.columns
1340
+ }),
1341
+ markers: {
1342
+ hook: "v1",
1343
+ grants: "v1"
1344
+ }
1345
+ };
1346
+ }
1347
+ /** The first line of the generated hook: fields `--check` compares before the full text. */
1348
+ function hookMarker(manifest) {
1349
+ return `${HOOK_MARKER} schema=${manifest.hook.schema} tenant=${manifest.tenantClaim} budget=${String(manifest.budget.bytes)} claims=${manifest.claims.map((claim) => claim.name).join(",")}`;
1350
+ }
1351
+ function defaultOut(config) {
1352
+ return config.supabase?.hook?.out ?? "supabase/permdock-hook.sql";
1353
+ }
1354
+ /** `supabase.hook.out`, else pg-delta's per-schema path, else `supabase/permdock-hook.sql`. */
1355
+ function hookOut(cwd, config, schema) {
1356
+ const pgDelta = supabaseConfig(cwd).pgDelta;
1357
+ return config.supabase?.hook?.out !== void 0 || pgDelta === void 0 ? defaultOut(config) : pgDeltaPath(pgDelta.schemaDir, "hook", schema ?? config.supabase?.hook?.schema ?? config.rls?.schema ?? "permdock");
1358
+ }
1359
+ function supabaseHookManifest(scopes, config, overrides = {}) {
1360
+ return manifestOf(hookParts(scopes, config, overrides), overrides.out ?? defaultOut(config), config);
1361
+ }
1362
+ /**
1363
+ * The hook migration. With `grantsOut`, the `supabase_auth_admin` grants are
1364
+ * left out of `sql` and returned in `grants` for that file.
1365
+ */
1366
+ function supabaseHookSql(scopes, config, overrides = {}, grantsOut) {
1367
+ const parts = hookParts(scopes, config, overrides);
1368
+ const manifest = manifestOf(parts, defaultOut(config), config);
1369
+ const toml = configToml(parts.schema, parts.jwtExpiry).split("\n").map((line) => line === "" ? "--" : `-- ${line}`).join("\n");
1370
+ const sql = [
1371
+ `${hookMarker(manifest)}
1372
+ -- custom_access_token_hook(jsonb): memberships go active ${parts.root} first and stop at the budget
1373
+ -- supabase/config.toml:
1374
+ ${toml}${grantsOut === void 0 ? "" : `\n-- the supabase_auth_admin grants are in ${grantsOut}`}`,
1375
+ attrsGuardSql(parts.attrs),
1376
+ hookSql(parts),
1377
+ versionSql(parts),
1378
+ grantsOut === void 0 ? grantsSql(parts) : "",
1379
+ managedSql(parts)
1380
+ ].filter((chunk) => chunk !== "").join("\n\n");
1381
+ const grants = `${GRANTS_MARKER} schema=${parts.schema}
1382
+ ${grantsSql(parts)}
1383
+ `;
1384
+ return {
1385
+ sql: `${sql}\n`,
1386
+ grants,
1387
+ manifest
1388
+ };
1389
+ }
1390
+ /** What the hook file's header says about where its grants went. */
1391
+ function grantsLabel(grantsOut) {
1392
+ if (grantsOut === void 0) return;
1393
+ return grantsOut === "-" ? "a separate migration" : grantsOut;
1394
+ }
1395
+ function markerDrift(onDisk, expected, outRel) {
1396
+ const found = hookMarkerFields(onDisk);
1397
+ if (found === void 0) return `supabase hook drift: ${outRel} has no ${HOOK_MARKER} line`;
1398
+ const want = hookMarkerFields(hookMarker(expected)) ?? {};
1399
+ const changed = Object.keys(want).filter((key) => found[key] !== want[key]).map((key) => `${key} ${found[key] ?? "(none)"} -> ${want[key] ?? ""}`);
1400
+ return changed.length === 0 ? "supabase hook drift" : `supabase hook drift: ${changed.join("; ")}`;
1401
+ }
1402
+ function inspectText(manifest) {
1403
+ return [
1404
+ `hook ${manifest.hook.schema}.${manifest.hook.function} (${manifest.hook.out})`,
1405
+ `helpers ${manifest.helpers.functions.map((fn) => `${manifest.helpers.schema}.${fn}`).join(", ")}`,
1406
+ `tenant claim ${manifest.tenantClaim}`,
1407
+ `budget ${String(manifest.budget.bytes)} bytes of ${manifest.budget.measure}`,
1408
+ `claims ${manifest.claims.map((claim) => `${claim.name}${claim.source === "permdock" ? "" : ` (${claim.source})`}${claim.budget ? " [budget]" : ""}`).join(", ")}`,
1409
+ `authz_ver ${manifest.authzVersion ? "on" : "off"}`
1410
+ ].join("\n");
1411
+ }
1412
+ /**
1413
+ * Writes the manifest to `rel`, or with `check` compares it as JSON, so a
1414
+ * formatter that reflows the file is not drift.
1415
+ */
1416
+ function manifestFile(cwd, rel, manifest, check) {
1417
+ const path = resolve(cwd, rel);
1418
+ if (!check) {
1419
+ mkdirSync(dirname(path), { recursive: true });
1420
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
1421
+ return {
1422
+ code: 0,
1423
+ output: `wrote ${rel}`
1424
+ };
1425
+ }
1426
+ if (!existsSync(path)) return {
1427
+ code: 1,
1428
+ output: `supabase manifest drift: missing ${rel}`
1429
+ };
1430
+ let onDisk;
1431
+ try {
1432
+ onDisk = JSON.parse(readFileSync(path, "utf8"));
1433
+ } catch {
1434
+ return {
1435
+ code: 1,
1436
+ output: `supabase manifest drift: ${rel} is not JSON`
1437
+ };
1438
+ }
1439
+ const found = typeof onDisk === "object" && onDisk !== null && !Array.isArray(onDisk) ? Object.fromEntries(Object.entries(onDisk)) : {};
1440
+ const changed = [.../* @__PURE__ */ new Set([...Object.keys(manifest), ...Object.keys(found)])].filter((key) => JSON.stringify(Reflect.get(manifest, key)) !== JSON.stringify(found[key]));
1441
+ return changed.length === 0 ? {
1442
+ code: 0,
1443
+ output: "supabase manifest up to date"
1444
+ } : {
1445
+ code: 1,
1446
+ output: `supabase manifest drift: ${rel} differs in ${changed.join(", ")}; run permdock supabase inspect --out ${rel}`
1447
+ };
1448
+ }
1449
+ async function loadScopes(cwd, config) {
1450
+ if (config.policy === void 0) throw new Error("PermDock CLI: supabase hook generate needs policy in permdock.config.ts");
1451
+ const policy = asPolicy(pickNamed(await loadModule(resolve(cwd, config.policy)), ["policy"]));
1452
+ return scopeList(policy.scopes);
1453
+ }
1454
+ async function runSupabase(input) {
1455
+ const [area, action] = input.rest;
1456
+ const overrides = Object.fromEntries(Object.entries({
1457
+ activeFrom: input.activeFrom,
1458
+ budget: input.budget,
1459
+ schema: input.schema
1460
+ }).filter(([, value]) => value !== void 0));
1461
+ if (area === "inspect" && action === void 0) {
1462
+ const manifest = supabaseHookManifest(await loadScopes(input.cwd, input.config), input.config, {
1463
+ ...overrides,
1464
+ out: hookOut(input.cwd, input.config, input.schema)
1465
+ });
1466
+ if (input.out !== void 0 || input.check) return manifestFile(input.cwd, typeof input.out === "string" ? input.out : MANIFEST_FILE, manifest, input.check);
1467
+ return {
1468
+ code: 0,
1469
+ output: input.json === true ? JSON.stringify(manifest, null, 2) : inspectText(manifest)
1470
+ };
1471
+ }
1472
+ if (area !== "hook" || action !== "generate") return {
1473
+ code: 2,
1474
+ output: SUPABASE_HELP
1475
+ };
1476
+ const { sql, grants, manifest } = supabaseHookSql(await loadScopes(input.cwd, input.config), input.config, overrides, grantsLabel(input.grantsOut));
1477
+ const outRel = typeof input.out === "string" ? input.out : hookOut(input.cwd, input.config, input.schema);
1478
+ const outPath = resolve(input.cwd, outRel);
1479
+ const hook = input.config.supabase?.hook;
1480
+ const grantsFile = input.grantsOut === void 0 ? [] : [{
1481
+ part: "grants",
1482
+ rel: input.grantsOut,
1483
+ text: grants
1484
+ }];
1485
+ const toml = configToml(input.schema ?? hook?.schema ?? input.config.rls?.schema ?? "permdock", hook?.jwtExpiry ?? 900);
1486
+ if (input.check) {
1487
+ if (!existsSync(outPath)) return {
1488
+ code: 1,
1489
+ output: `supabase hook drift: missing ${outRel}`
1490
+ };
1491
+ const onDisk = readFileSync(outPath, "utf8");
1492
+ if (onDisk !== sql) return {
1493
+ code: 1,
1494
+ output: markerDrift(onDisk, manifest, outRel)
1495
+ };
1496
+ const drift = driftOf(input.cwd, grantsFile);
1497
+ return drift.length === 0 ? {
1498
+ code: 0,
1499
+ output: "supabase hook up to date"
1500
+ } : {
1501
+ code: 1,
1502
+ output: `supabase hook drift: ${drift.join("; ")}`
1503
+ };
1504
+ }
1505
+ mkdirSync(dirname(outPath), { recursive: true });
1506
+ writeFileSync(outPath, sql);
1507
+ const grantsWritten = writeSqlFiles(input.cwd, grantsFile);
1508
+ const placed = {
1509
+ ...manifest,
1510
+ hook: {
1511
+ ...manifest.hook,
1512
+ out: outRel
1513
+ }
1514
+ };
1515
+ const missing = input.db === void 0 ? missingHelpersInFiles(input.cwd, input.config, placed) : await missingHelpersInDb(input.db, placed);
1516
+ return {
1517
+ code: 0,
1518
+ output: [
1519
+ `wrote ${[outRel, ...grantsWritten.wrote].join(", ")}`,
1520
+ ...grantsWritten.printed === "" ? [] : [grantsWritten.printed],
1521
+ "add to supabase/config.toml:",
1522
+ toml,
1523
+ ...missing.length === 0 ? [] : [missingHelpersMessage(placed, missing)]
1524
+ ].join("\n")
1525
+ };
1526
+ }
1527
+ //#endregion
1528
+ export { tableKey as A, pd009 as C, pd040 as D, pd028 as E, group as O, pd006 as S, pd022 as T, resolveAuthorize as _, loadScopes as a, MIGRATION_DIRS as b, supabaseHookSql as c, parseSplit as d, partPath as f, rbacScaffold as g, parseRbacAuthorize as h, hookOut as i, sqlStatements as k, pd039 as l, writeSqlFiles as m, attrsPlan as n, runSupabase as o, pgDeltaPath as p, grantsLabel as r, supabaseHookManifest as s, activeFromSql as t, driftOf as u, pd045 as v, pd012 as w, pd005 as x, supabaseConfig as y };