better-supabase 0.2.0 → 0.4.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 (190) hide show
  1. package/README.md +37 -33
  2. package/dist/base64-D2gsioCY.js +42 -0
  3. package/dist/claims-BAf6c7is.js +48 -0
  4. package/dist/cli/_format-Cax5EsM9.js +101 -0
  5. package/dist/cli/bin.js +39 -6
  6. package/dist/cli/command-CK14fP91.js +502 -0
  7. package/dist/cli/compare-BmIWVEui.js +9 -0
  8. package/dist/cli/config-D9R0o1LH.js +1895 -0
  9. package/dist/cli/config-SSKSTUl2.js +2 -0
  10. package/dist/cli/db-DOBPt660.js +52 -0
  11. package/dist/cli/dist-B1MvLv-M.js +129 -0
  12. package/dist/cli/dist-CFgGknmZ.js +1112 -0
  13. package/dist/cli/dist-Du7VnEUG.js +621 -0
  14. package/dist/cli/doctor-BOym3mbL.js +2382 -0
  15. package/dist/cli/env-BZ0uy8XK.js +108 -0
  16. package/dist/cli/env-jIFUADLY.js +511 -0
  17. package/dist/cli/errors-4v4DTtda.js +56 -0
  18. package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
  19. package/dist/cli/gen-Ccn_p90L.js +2008 -0
  20. package/dist/cli/gen-D5cLCULx.js +2 -0
  21. package/dist/cli/index.d.ts +295 -28
  22. package/dist/cli/index.js +184 -3
  23. package/dist/cli/init-ClnxRYBF.js +550 -0
  24. package/dist/cli/introspect-DxqfkVIv.js +81 -0
  25. package/dist/cli/io-k41_z-jT.js +24 -0
  26. package/dist/cli/json5-ChRycquj.js +787 -0
  27. package/dist/cli/jsonc-BUniUpOk.js +2 -0
  28. package/dist/cli/jsonc-D5Fu67jw.js +45 -0
  29. package/dist/cli/keys-ByKHnvP9.js +83 -0
  30. package/dist/cli/ohash-D7bG_EOb.js +277 -0
  31. package/dist/cli/openapi-8WHjxiTJ.js +66 -0
  32. package/dist/cli/project-B_GeXyHh.js +70 -0
  33. package/dist/cli/prompts-BNcNfAve.js +1142 -0
  34. package/dist/cli/prompts-BTYQGunc.js +2 -0
  35. package/dist/cli/read-sets-CMa29t9a.js +971 -0
  36. package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
  37. package/dist/cli/run-CP20LKk_.js +347 -0
  38. package/dist/cli/seed-5J6eu_Xg.js +98 -0
  39. package/dist/cli/skills-LJJIhLFR.js +137 -0
  40. package/dist/cli/sql-D4VbqmXi.js +199 -0
  41. package/dist/cli/style-C0NFdJRL.js +29 -0
  42. package/dist/cli/toml-DB-MNnFs.js +424 -0
  43. package/dist/{version-Cqq1m3UY.js → cli/version-5mW_8sY1.js} +1 -1
  44. package/dist/cli/yaml-CYCbJ6et.js +2252 -0
  45. package/dist/client/index.d.ts +2 -2
  46. package/dist/client/index.js +15 -25
  47. package/dist/{compiler-CZ9nIuqy.d.ts → compiler-DB3aSFOA.d.ts} +2 -2
  48. package/dist/config/index.d.ts +4 -3
  49. package/dist/config/index.js +5 -3
  50. package/dist/config-BpxDfnbg.js +129 -0
  51. package/dist/{index-DOsjFvMf.d.ts → config-C1CYcm6o.d.ts} +49 -49
  52. package/dist/{config-ClDXmVtY.js → config-CbvFmGD8.js} +14 -137
  53. package/dist/{define-BXMYD9AY.js → define-Bh0AqTik.js} +221 -120
  54. package/dist/{define-e3zI4_4g.d.ts → define-CRS2MRV3.d.ts} +36 -31
  55. package/dist/edge/index.d.ts +9 -13
  56. package/dist/edge/index.js +51 -25
  57. package/dist/{entitlements-Dng-ctuT.d.ts → entitlements-Dm-FHpNH.d.ts} +2 -2
  58. package/dist/env/index.js +203 -1
  59. package/dist/{errors-Bl3rkOzo.js → errors-L0xK41y_.js} +1 -1
  60. package/dist/events/index.d.ts +1 -1
  61. package/dist/events/index.js +139 -1
  62. package/dist/{events-DOt3nSO-.d.ts → events-B21UOhvP.d.ts} +4 -4
  63. package/dist/{executor-CGdNQ27g.d.ts → executor-B5IoUxLE.d.ts} +2 -2
  64. package/dist/{executor-DeppvirK.js → executor-CVt6dD0B.js} +9 -4
  65. package/dist/{executor-BYXQ9AGn.d.ts → executor-CZ7ImRiD.d.ts} +2 -2
  66. package/dist/hono/index.d.ts +17 -24
  67. package/dist/hono/index.js +11 -11
  68. package/dist/{index-DuP14VHo.d.ts → index-528Ugmyp.d.ts} +30 -10
  69. package/dist/{index-Bw1bwtC0.d.ts → index-BAy4tsAP.d.ts} +28 -15
  70. package/dist/{index-CBDufH1M.d.ts → index-BBdvo-5d.d.ts} +24 -26
  71. package/dist/{index-DbxFr7XG.d.ts → index-BhHEC04r.d.ts} +9 -4
  72. package/dist/index-Cf-D2jk1.d.ts +50 -0
  73. package/dist/{index-Ciu5Xlml.d.ts → index-D5IfUIDh.d.ts} +6 -5
  74. package/dist/{index-K-3WM2Hp.d.ts → index-DFmOpBGC.d.ts} +13 -13
  75. package/dist/{index-D4v7sncu.d.ts → index-RlLXd8dL.d.ts} +13 -13
  76. package/dist/index.d.ts +17 -17
  77. package/dist/index.js +23 -14
  78. package/dist/{invalidate-DbxuL2jL.js → invalidate-CgxmRamI.js} +7 -1
  79. package/dist/jobs/index.d.ts +33 -26
  80. package/dist/jobs/index.js +44 -22
  81. package/dist/{json-schema-BpYqkCKf.js → json-schema-DP6V7cbr.js} +8 -3
  82. package/dist/lint/index.js +1 -1
  83. package/dist/list/index.d.ts +2 -2
  84. package/dist/list/index.js +131 -96
  85. package/dist/{live-DJyZMD6w.d.ts → live-BHakSWWo.d.ts} +26 -16
  86. package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
  87. package/dist/lookup-BaP5bI27.js +18 -0
  88. package/dist/mcp/index.d.ts +34 -27
  89. package/dist/mcp/index.js +59 -25
  90. package/dist/{mfa-TVia554c.d.ts → mfa-C7MNQ1Ob.d.ts} +1 -1
  91. package/dist/{mfa-CpLfJSde.js → mfa-byfjruR9.js} +190 -46
  92. package/dist/next/image/index.js +6 -3
  93. package/dist/next/index.d.ts +64 -48
  94. package/dist/next/index.js +63 -50
  95. package/dist/openapi/index.d.ts +1 -1
  96. package/dist/openapi/index.js +61 -38
  97. package/dist/orpc/index.d.ts +18 -21
  98. package/dist/orpc/index.js +12 -12
  99. package/dist/otel/index.d.ts +5 -5
  100. package/dist/otel/index.js +84 -63
  101. package/dist/{permdock-sql-CghlHPuH.js → permdock-sql-C4KFzLc9.js} +12 -7
  102. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  103. package/dist/{plugin-C4TR9J0Z.d.ts → plugin-bNhvJwRh.d.ts} +5 -4
  104. package/dist/plugins/actor/index.d.ts +1 -1
  105. package/dist/plugins/actor/index.js +1 -1
  106. package/dist/plugins/rules/index.d.ts +2 -2
  107. package/dist/plugins/rules/index.js +46 -16
  108. package/dist/plugins/soft-delete/index.d.ts +3 -3
  109. package/dist/plugins/soft-delete/index.js +4 -4
  110. package/dist/plugins/tenant/index.d.ts +3 -3
  111. package/dist/plugins/tenant/index.js +10 -4
  112. package/dist/plugins/timestamps/index.d.ts +1 -1
  113. package/dist/plugins/timestamps/index.js +2 -2
  114. package/dist/plugins/validation/index.d.ts +2 -2
  115. package/dist/plugins/validation/index.js +3 -3
  116. package/dist/pool-DFwEQwmN.d.ts +78 -0
  117. package/dist/postgres/index.d.ts +10 -5
  118. package/dist/postgres/index.js +37 -16
  119. package/dist/{postgrest-cAEQ_IQN.d.ts → postgrest-Bc-0Xw62.d.ts} +1 -1
  120. package/dist/{problem-BTkTtRV9.js → problem-BihjPR0t.js} +11 -5
  121. package/dist/{problem-DlgB0lYl.d.ts → problem-U8UEKccv.d.ts} +6 -4
  122. package/dist/query/index.d.ts +2 -2
  123. package/dist/query/index.js +2 -2
  124. package/dist/{query-ChvDbsDE.js → query-BMMSWbI7.js} +19 -19
  125. package/dist/react/index.d.ts +7 -7
  126. package/dist/react/index.js +2 -2
  127. package/dist/react/server.d.ts +6 -6
  128. package/dist/react/server.js +3 -3
  129. package/dist/react/session.d.ts +21 -2
  130. package/dist/react/session.js +19 -1
  131. package/dist/{react-C83BdBGt.js → react-CQHMaESR.js} +59 -55
  132. package/dist/{read-set-4lI3W_gZ.js → read-set-FWK8qQ58.js} +51 -16
  133. package/dist/realtime/index.d.ts +4 -3
  134. package/dist/realtime/index.js +19 -17
  135. package/dist/{index-CG69s15v.d.ts → refresh-BwYkyNOQ.d.ts} +7 -3
  136. package/dist/{resolve-CjHILk2w.d.ts → resolve-BAqQcwMW.d.ts} +17 -11
  137. package/dist/{resource-CgZa3YG_.d.ts → resource-BJcaIfxi.d.ts} +17 -8
  138. package/dist/{resource-BHEWmQJK.js → resource-My_y-Syh.js} +43 -21
  139. package/dist/{respond-Dne1ARSa.d.ts → respond-DgIbdrt8.d.ts} +39 -14
  140. package/dist/{result-DWXatkd6.js → result-Bj6yt33D.js} +2 -2
  141. package/dist/{result-CTkCZ6JA.d.ts → result-CqEsbgYs.d.ts} +16 -11
  142. package/dist/server/index.d.ts +38 -13
  143. package/dist/server/index.js +43 -14
  144. package/dist/{respond-DMcbQwfN.js → server-BA75B0mT.js} +151 -96
  145. package/dist/{shared-_6-uPL7E.js → shared-DnoCdMDr.js} +4 -3
  146. package/dist/{simplify-bwJpLnaS.js → simplify-Cqvfr9dj.js} +76 -39
  147. package/dist/sql/index.d.ts +139 -0
  148. package/dist/sql/index.js +1983 -0
  149. package/dist/{sql-DTjA1LPj.js → sql-B34HU2DI.js} +74 -17
  150. package/dist/ssr/index.d.ts +4 -4
  151. package/dist/ssr/index.js +2 -2
  152. package/dist/{standard-FUi3Z3-7.js → standard-BRyBGTtp.js} +2 -2
  153. package/dist/{standard-CyhRzctb.d.ts → standard-BsaCYQJc.d.ts} +1 -1
  154. package/dist/{stats-C5W-rBfR.js → stats-C_YxA2r1.js} +40 -14
  155. package/dist/storage/index.d.ts +4 -3
  156. package/dist/storage/index.js +548 -1
  157. package/dist/{tables-rZxRxNPW.js → tables-Cb-3_VWc.js} +19 -5
  158. package/dist/temporal-BwshQk0W.js +27 -0
  159. package/dist/temporal-required-CBNHT6ni.js +19 -0
  160. package/dist/testing/index.d.ts +25 -24
  161. package/dist/testing/index.js +165 -66
  162. package/dist/{types-C83v5SQz.d.ts → types-DONv9SHN.d.ts} +14 -11
  163. package/dist/version-Bmf7_FzM.js +4 -0
  164. package/dist/{view-D1s0wa0T.d.ts → view-B64o81wp.d.ts} +36 -6
  165. package/dist/{view-aOYizTeE.js → view-BrJI2LKp.js} +11 -2
  166. package/dist/webhooks/index.d.ts +19 -17
  167. package/dist/webhooks/index.js +1 -1
  168. package/dist/{webhooks-DMipjhSP.js → webhooks-Bh_Jf_-l.js} +45 -24
  169. package/package.json +50 -22
  170. package/schemas/config-v1.json +31 -1
  171. package/schemas/doctor-report-v1.json +17 -1
  172. package/schemas/snapshot-v2.json +35 -0
  173. package/skills/better-supabase/SKILL.md +77 -18
  174. package/skills/better-supabase/references/plugins.md +21 -22
  175. package/skills/better-supabase/references/troubleshooting.md +38 -18
  176. package/skills/better-supabase-api/SKILL.md +66 -18
  177. package/skills/better-supabase-api/references/adapters.md +64 -41
  178. package/skills/better-supabase-auth/SKILL.md +146 -0
  179. package/skills/better-supabase-auth/references/permdock.md +55 -0
  180. package/skills/better-supabase-testing/SKILL.md +68 -11
  181. package/dist/casing-da0uRTqt.js +0 -14
  182. package/dist/claims-CoTobNC-.js +0 -25
  183. package/dist/cli-DrSgDOQC.js +0 -6646
  184. package/dist/env-BEToab0I.js +0 -204
  185. package/dist/events-BuzTgn2D.js +0 -135
  186. package/dist/pool-R2m_DkXi.d.ts +0 -37
  187. package/dist/seed-CzecODcJ.js +0 -101
  188. package/dist/session-C3LxnQ0e.d.ts +0 -22
  189. package/dist/session-CqEwzVup.js +0 -20
  190. package/dist/storage-DuGn38fr.js +0 -527
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: better-supabase-auth
3
+ description: Read and check the caller in better-supabase apps, including sessions, typed claims, user profiles, OAuth clients and agents acting for a user, scopes, ended sessions and PermDock. Use when code reads auth, session or claims, when adding a custom access token hook, an OAuth or agent integration, sign-out, account deletion or role changes, or when the project has a permdock.config.ts.
4
+ ---
5
+
6
+ # Auth with better-supabase
7
+
8
+ Every adapter resolves the caller once. It verifies the access token locally
9
+ against the project's JWKS, so a valid token costs no network call, and only
10
+ the Next.js proxy (`bs.proxy`) refreshes sessions. Don't call
11
+ `supabase.auth.getUser()` per request, and don't read cookies yourself.
12
+
13
+ ## Workflow: read the caller
14
+
15
+ 1. Take `auth` from the handler context (`ctx.auth`, `c.var.auth`,
16
+ `context.auth`), or a token-free `AuthSession` from `await bs.session()`
17
+ or `toSession(ctx.auth)`.
18
+ 2. Branch on `kind` with an exhaustive `switch` that ends in a `never` check:
19
+
20
+ ```ts
21
+ switch (auth.kind) {
22
+ case "user": // auth.user.id, auth.claims
23
+ break;
24
+ case "service": // a secret key
25
+ break;
26
+ case "anon": // auth.reason: 'none' | 'expired' | 'signed_out' | 'refresh_failed'
27
+ break;
28
+ case "invalid": // auth.reason: 'token' | 'claims' | 'actor', answer 401
29
+ break;
30
+ default: {
31
+ const unreachable: never = auth;
32
+ return unreachable;
33
+ }
34
+ }
35
+ ```
36
+
37
+ 3. Prefer the adapter's `allow` option (`['user']` by default) over checking
38
+ `kind` by hand; it answers 401 or 403 as Problem Details.
39
+
40
+ Done when every `invalid` reason, `actor` included, ends in a 401 and no code
41
+ path treats `invalid` as `anon`.
42
+
43
+ ## Workflow: type the claims
44
+
45
+ 1. Describe the claims your access token hook adds with any Standard Schema
46
+ and pass it to `betterSupabase.claims(schema)`. Use a loose object (`v.looseObject`,
47
+ `z.looseObject`) so claims the schema doesn't list stay on the session.
48
+ Make anything the hook doesn't always set optional or give it a default.
49
+ 2. For display data users edit themselves (name, avatar), use
50
+ `betterSupabase.userMetadata(schema)` and read `session.profile`.
51
+ 3. Read roles, memberships, the tenant and entitlements from the verified
52
+ claims only. Never from `user_metadata`, `session.profile`, a URL or a
53
+ request body.
54
+
55
+ Done when `session.claims` is typed without casts, and a token whose claims
56
+ fail the schema gets a 401 with code `CLAIMS_INVALID`.
57
+
58
+ ## Bearer callers: OAuth clients and agents
59
+
60
+ A token from the Supabase OAuth server carries `client_id` and `scope`; a
61
+ token exchanged for an agent carries an RFC 8693 `act` chain. The session
62
+ names who acts for the user:
63
+
64
+ | Token | `session.actor` | `session.delegation` |
65
+ | ----------------------------------------------- | ------------------------------------ | -------------------- |
66
+ | The user's own | not set | not set |
67
+ | `client_id: "c1"`, `scope: "openid crm.read"` | `{ id: "c1", kind: "oauth-client" }` | `{ scopes: [...] }` |
68
+ | `act: { sub: "agent", act: { sub: "mcp-42" } }` | `{ id: "agent", chain, ... }` | `{ scopes, chain }` |
69
+
70
+ - Limit what delegated tokens may do with the `scopes` guard option on
71
+ `bs.route`, `bs.action`, `bs.handler` and `bs.middleware`. A missing
72
+ scope answers 403 with an `insufficient_scope` challenge. The user's own
73
+ token is never limited by scopes.
74
+ - In MCP servers, refuse calls in `authorize` by reading
75
+ `toSession(ctx.auth).delegation?.scopes` (the `better-supabase-api` skill).
76
+ - RLS still decides the rows: `sub` is the user, so a client sees at most
77
+ what the user sees. Record `session.actor` (and `chain`) in audit data when
78
+ a change was made by a client or an agent.
79
+
80
+ ## Workflow: irreversible actions
81
+
82
+ Tokens stay valid until they expire (an hour by default), even after the user
83
+ signs out everywhere or an admin ends the session. Before an action you can't
84
+ undo (deleting the account, transferring ownership, rotating keys), check
85
+ that the session still exists:
86
+
87
+ ```ts
88
+ import { err } from "better-supabase";
89
+ import { checkSession } from "better-supabase/server";
90
+
91
+ const ended = await checkSession(postgres.admin, auth);
92
+ if (ended) return err(ended); // unauthorized, code SESSION_REVOKED
93
+ ```
94
+
95
+ It needs a connection that can read `auth.sessions` (`createPostgres().admin`).
96
+ Nothing calls it by default; keep it to the actions that need it.
97
+
98
+ Done when the irreversible action returns `SESSION_REVOKED` for a signed-out
99
+ session in a test.
100
+
101
+ ## When claims change
102
+
103
+ A token carries the claims from when Auth issued it, so a revoked role or
104
+ membership lasts until the next refresh. When that window matters:
105
+
106
+ - Write policies that read the source of truth, for example
107
+ `organization_id in (select better_supabase.member_org_ids())`, instead of
108
+ the claim.
109
+ - After a change the user made themselves (joining an organization), call
110
+ `supabase.auth.refreshSession()` in the browser.
111
+ - In Next.js, drop the user's cached views with
112
+ `bs.invalidateSession(userId, { tags })`.
113
+
114
+ ## Next.js caching
115
+
116
+ - Per-user data goes through `bs.cached()` inside your own
117
+ `'use cache: private'` function. It tags the entry `bs:session:<user id>`
118
+ and caps its lifetime at the token's.
119
+ - `bs.cached({ tags, life: { stale } })` adds tags and caps the stale time
120
+ further, for example for a PermDock snapshot.
121
+ - Keep layouts synchronous: pass the `bs.session()` promise to
122
+ `<SessionProvider>` and read it with `useSession()`.
123
+
124
+ ## PermDock
125
+
126
+ When the project has a `permdock.config.ts`, PermDock owns the access token
127
+ hook, roles, memberships and the tenant claim. Read
128
+ [references/permdock.md](references/permdock.md) before touching the hook,
129
+ claims, tenant or entitlements.
130
+
131
+ ## Don't
132
+
133
+ - Don't refresh sessions outside the proxy, and don't verify tokens by
134
+ calling the Auth server.
135
+ - Don't use `bs.admin()` or the service role for a user's request.
136
+ - Don't downgrade an `invalid` caller to `anon`.
137
+ - Don't build 401 or 403 JSON by hand; return the `DbError` or use `allow`
138
+ and `scopes`.
139
+
140
+ ## Docs
141
+
142
+ https://bettersupabase.com/docs/auth.md,
143
+ https://bettersupabase.com/docs/frameworks/next.md (bearer callers),
144
+ https://bettersupabase.com/docs/auth/account-deletion.md,
145
+ https://bettersupabase.com/docs/frameworks/next-cache-components.md and
146
+ https://bettersupabase.com/docs/auth/permdock.md.
@@ -0,0 +1,55 @@
1
+ # better-supabase next to PermDock
2
+
3
+ PermDock handles authorization and better-supabase handles tokens and data.
4
+ They share one access token hook and one claim contract, and neither imports
5
+ the other.
6
+
7
+ ## Who owns what
8
+
9
+ | Area | better-supabase | PermDock |
10
+ | -------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
11
+ | Access token hook | none | `permdock supabase hook generate`, the only hook |
12
+ | Claims | `features`, through `supabase.hook.claims` | `user_role`, `roles`, `memberships`, the tenant claim, `attrs`, `authz_ver` |
13
+ | SQL helpers | the SQL kit (`better_supabase.*`) | `permdock rls generate`: `permdock_has`, `permitted_<scope>_ids`, `member_<scope>_ids` |
14
+ | Storage and Realtime | `defineBucket`, `defineTopic` with a `permdock` policy mode | the helpers those policies call |
15
+ | MCP | `createMcp` with `authorize` and `visible` | the permissions in each tool's `meta` |
16
+
17
+ ## Rules
18
+
19
+ - Don't run `better-supabase sql add tenant` (it stops without `--force`),
20
+ don't call `membership_claims()` from a hook, and don't write `roles`,
21
+ `user_role`, `memberships` or the tenant claim yourself. Doctor reports
22
+ BS407 when a hook does.
23
+ - `sql add entitlements` is fine: `features` is not a PermDock claim. Add
24
+ `claims: { features: 'better_supabase.feature_claims' }` to `supabase.hook`
25
+ in `permdock.config.ts`, run `permdock supabase hook generate`, and
26
+ `hasEntitlement(session, ...)` works.
27
+ - Run `permdock supabase inspect --out` before `sql add entitlements`. With
28
+ `permdock.manifest.json` present, the module reads PermDock's
29
+ `member_<scope>_ids` helpers and doesn't add `tenant`. Set
30
+ `entitlements.permdock: { scope }` in `better-supabase.config.ts` for a
31
+ scope other than `organization`.
32
+ - Name the same tenant claim on both sides. A non-default `claims.tenant` in
33
+ `better-supabase.config.ts` must also be PermDock's `rls.tenantClaim` and
34
+ go to `subjectFromSupabase` or `subjectFromSupabaseSession` as `{ tenant }`.
35
+ - Validate claims with PermDock's schema: `betterSupabase.claims(supabaseClaims().extend(appClaims))`.
36
+ - The helpers live in PermDock's `rls.schema`, which must equal the `schema`
37
+ option of buckets and topics in `permdock` mode.
38
+ - The `permdock` policy mode checks role and scope only. Use it only for
39
+ permissions whose `rowConditions` is `false` in `permissions.catalog.json`.
40
+ Leave permissions with row conditions to the policies
41
+ `permdock rls generate` writes; doctor reports BS214 and `permdock doctor`
42
+ PD037 otherwise.
43
+ - `permdock` mode compares ids as text, so a path or topic segment must be
44
+ the id's lowercase form.
45
+ - In MCP servers, narrow `tool.meta` with PermDock's `isPermission`, answer
46
+ `visible` with `mayUse` and `authorize` with `can`. `can` decides without
47
+ a row, so check row-conditioned permissions inside `run` or rely on RLS.
48
+ - In Next.js, cache the permission snapshot with
49
+ `bs.cached({ tags: [snapshotTag(userId)], life: cacheLifeFor(snapshot) })`
50
+ and drop it with `bs.invalidateSession(userId, { tags: [snapshotTag(userId)] })`
51
+ when a role or plan changes.
52
+ - Doctor BS405 measures `memberships` plus `attrs` against PermDock's 1 KB
53
+ budget and the whole token against 2 KB; run `doctor --as <user id>`.
54
+
55
+ Docs: https://bettersupabase.com/docs/auth/permdock.md
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: better-supabase-testing
3
- description: Test Supabase RLS policies, APIs and SQL against the local stack with better-supabase/testing, typed seeds and pgTAP. Use when writing or fixing tests that touch the database, policies or auth.
3
+ description: Test Supabase RLS policies, APIs and SQL against the local stack with better-supabase/testing, typed seeds and pgTAP. Use when writing or fixing tests that touch the database, policies, auth, OAuth or agent scopes, or Temporal time values.
4
4
  ---
5
5
 
6
6
  # Testing with better-supabase
@@ -11,7 +11,8 @@ supabase-js, and don't use the service role for anything a user does.
11
11
  ## Workflow: test a table or policy
12
12
 
13
13
  1. Start the stack (`supabase start`) and write `.env.local` with
14
- `better-supabase env`.
14
+ `better-supabase env`. In a sandbox without Docker, start it with
15
+ `SUPABASE_EXPERIMENTAL_STACK=1` (the Supabase CLI's native stack).
15
16
  2. Add the rows the test needs to `supabase/seed.ts`, including one row
16
17
  owned by another tenant, then `supabase db reset`.
17
18
  3. Write the RLS test with `asUser` for each role that matters.
@@ -29,24 +30,30 @@ again once you restore it.
29
30
  - Typed fixtures live in `supabase/seed.ts`:
30
31
 
31
32
  ```ts
32
- import { defineSeed } from 'better-supabase/testing';
33
- import { sb } from '../src/lib/supabase.ts';
33
+ import { defineSeed } from "better-supabase/testing";
34
+ import { betterSupabase } from "../src/lib/supabase/index.ts";
34
35
 
35
- export const seed = defineSeed(sb, {
36
- organizations: { acme: { id: ACME, name: 'Acme' } },
37
- customers: { first: { id: FIRST, organizationId: ACME, name: 'First' } },
36
+ export const seed = defineSeed(betterSupabase, {
37
+ organizations: { acme: { id: ACME, name: "Acme" } },
38
+ customers: { first: { id: FIRST, organizationId: ACME, name: "First" } },
38
39
  });
39
40
  ```
40
41
 
41
- `better-supabase seed` renders them to SQL for `supabase db reset`, and
42
- tests import the same rows (`seed.rows.customers.first.id`).
42
+ `better-supabase seed` renders them to SQL for `supabase db reset`, and
43
+ tests import the same rows (`seed.rows.customers.first.id`).
43
44
 
44
45
  ## RLS tests
45
46
 
46
47
  ```ts
47
- const alice = await asUser(sb, { sub: aliceId, tenant_id: ACME }, { postgres });
48
+ const alice = await asUser(
49
+ betterSupabase,
50
+ { sub: aliceId, tenant_id: ACME },
51
+ { postgres },
52
+ );
48
53
  expect(await alice.db.customers.count().orThrow()).toBe(1);
49
- expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok: false });
54
+ expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({
55
+ ok: false,
56
+ });
50
57
  ```
51
58
 
52
59
  - Check both directions: the user sees their own rows and never sees another tenant's.
@@ -58,6 +65,56 @@ expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok
58
65
  Send `authorization: Bearer ${alice.token}` (or a token from `signLocalJwt`).
59
66
  The adapter verifies it against the local JWKS; no test-only resolver is needed.
60
67
 
68
+ For a route with `scopes`, sign a delegated token the way the Supabase OAuth
69
+ server or an agent token exchange would, and assert both sides:
70
+
71
+ ```ts
72
+ const client = await signLocalJwt({
73
+ sub: aliceId,
74
+ tenant_id: ACME,
75
+ client_id: "c1",
76
+ scope: "openid", // add "customers:read" to pass
77
+ });
78
+ const response = await app.request("/api/customers", {
79
+ headers: { authorization: `Bearer ${client}` },
80
+ });
81
+ expect(response.status).toBe(403);
82
+ expect(response.headers.get("www-authenticate")).toContain(
83
+ 'error="insufficient_scope"',
84
+ );
85
+ ```
86
+
87
+ The user's own token (no `client_id`, no `act`) must still pass, and a
88
+ malformed `act` claim (`act: { sub: "" }`) must get a 401.
89
+
90
+ ## Time values
91
+
92
+ With `codecs.timestamptz: 'instant'`, rows hold `Temporal` values. Import
93
+ `temporal-polyfill/global` in the test setup on Node 24, and compare with
94
+ `.equals()` or an equality tester, because `toEqual` treats any two instants
95
+ as equal:
96
+
97
+ ```ts title="tests/setup.ts"
98
+ import "temporal-polyfill/global";
99
+ import { expect } from "vitest";
100
+
101
+ expect.addEqualityTesters([
102
+ (a, b) =>
103
+ a instanceof Temporal.Instant && b instanceof Temporal.Instant
104
+ ? a.equals(b)
105
+ : undefined,
106
+ ]);
107
+ ```
108
+
109
+ With the polyfill, `vi.useFakeTimers({ now })` drives `Temporal.Now` too.
110
+
111
+ ## Doctor in CI
112
+
113
+ Run `pnpm better-supabase doctor` against the reset stack and fail the job on
114
+ a nonzero exit. `--json` prints the report with each finding's code,
115
+ `--format github` annotates the pull request, and
116
+ `--format sarif --out doctor.sarif` feeds code scanning.
117
+
61
118
  ## pgTAP
62
119
 
63
120
  `better-supabase sql add pgtap` installs `tests.create_user`,
@@ -1,14 +0,0 @@
1
- //#region src/casing/index.ts
2
- /** `first_name` to `firstName`. Leaves already camel-cased names untouched. */
3
- function toCamel(name) {
4
- return name.replace(/_+([a-z0-9])/g, (_, char) => char.toUpperCase());
5
- }
6
- /** `firstName` to `first_name`. */
7
- function toSnake(name) {
8
- return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
9
- }
10
- function applyCasing(name, casing) {
11
- return casing === "camel" ? toCamel(name) : name;
12
- }
13
- //#endregion
14
- export { toCamel as n, toSnake as r, applyCasing as t };
@@ -1,25 +0,0 @@
1
- //#region src/core/claims.ts
2
- const DEFAULT_CLAIMS = {
3
- tenant: "tenant_id",
4
- scope: "tenant",
5
- features: "features"
6
- };
7
- /** The configured claim names, with defaults for the ones codegen left out. */
8
- function claimsOf(meta) {
9
- return {
10
- ...DEFAULT_CLAIMS,
11
- ...meta?.claims
12
- };
13
- }
14
- /** Where the active tenant is read from: the top-level claim, then `app_metadata`. */
15
- function tenantClaimPaths(tenant = DEFAULT_CLAIMS.tenant) {
16
- return [tenant, `app_metadata.${tenant}`];
17
- }
18
- /** The string at a dotted claim path, if any. */
19
- function claimAt(claims, path) {
20
- let value = claims;
21
- for (const segment of path.split(".")) value = typeof value === "object" && value !== null ? value[segment] : void 0;
22
- return typeof value === "string" && value.length > 0 ? value : void 0;
23
- }
24
- //#endregion
25
- export { tenantClaimPaths as i, claimAt as n, claimsOf as r, DEFAULT_CLAIMS as t };