better-supabase 0.5.0 → 0.5.1

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 (62) hide show
  1. package/README.md +133 -16
  2. package/dist/actor-DaJjqD5R.d.ts +43 -0
  3. package/dist/cli/bin.js +1 -1
  4. package/dist/cli/{doctor-L7Z8mYTx.js → doctor-C_A-KEAz.js} +6 -4
  5. package/dist/cli/gen-CIdSQZad.js +2 -0
  6. package/dist/cli/{gen-C2osRcr5.js → gen-CjElD1Vw.js} +72 -35
  7. package/dist/cli/index.d.ts +1 -1
  8. package/dist/cli/index.js +3 -3
  9. package/dist/cli/{init-Saf5-R9T.js → init-DMa6M7hV.js} +1 -1
  10. package/dist/cli/{introspect--H7oGmcd.js → introspect-BKlaIu_M.js} +1 -1
  11. package/dist/cli/{read-sets-BfMQcKy8.js → read-sets-VQcPgxU9.js} +173 -26
  12. package/dist/cli/{run-BKR2W_79.js → run-Bo_NLT5n.js} +7 -7
  13. package/dist/cli/{sql-DzPbfngx.js → sql-CVx-1O8G.js} +30 -5
  14. package/dist/cli/{version-BDJuO0UR.js → version-BCtlgDQx.js} +1 -1
  15. package/dist/client/index.js +1 -1
  16. package/dist/config/index.d.ts +2 -2
  17. package/dist/{config-T_ejwyd3.d.ts → config-Bv7gvfzC.d.ts} +10 -1
  18. package/dist/edge/index.d.ts +2 -2
  19. package/dist/edge/index.js +2 -2
  20. package/dist/{entitlements-DUxU4oHe.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
  21. package/dist/hono/index.d.ts +3 -3
  22. package/dist/hono/index.js +2 -2
  23. package/dist/impersonation-BgSHAYLf.js +133 -0
  24. package/dist/{resolve-C15TJq77.d.ts → impersonation-BmePsuxz.d.ts} +25 -1
  25. package/dist/{index-Dxn2a5cw.d.ts → index-CWMDdEr4.d.ts} +1 -1
  26. package/dist/{index-BGdgBPdf.d.ts → index-Dp2VDwog.d.ts} +1 -1
  27. package/dist/mcp/index.d.ts +2 -2
  28. package/dist/mcp/index.js +2 -2
  29. package/dist/{mfa-CusDgPaD.d.ts → mfa-Cu96oJdw.d.ts} +1 -19
  30. package/dist/next/index.d.ts +6 -6
  31. package/dist/next/index.js +3 -3
  32. package/dist/orpc/index.d.ts +2 -2
  33. package/dist/orpc/index.js +1 -1
  34. package/dist/otel/index.js +5 -5
  35. package/dist/react/index.d.ts +5 -5
  36. package/dist/react/server.d.ts +5 -5
  37. package/dist/react/session.d.ts +2 -2
  38. package/dist/{refresh-BTrQ_ZTp.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
  39. package/dist/{resource-D8iqWMr-.d.ts → resource-DNRFP7KR.d.ts} +1 -1
  40. package/dist/{resource-CRxxwkQq.js → resource-mqfJW4YF.js} +1 -1
  41. package/dist/{respond-D2OQl62-.d.ts → respond-BR1fJWOB.d.ts} +12 -6
  42. package/dist/server/index.d.ts +9 -8
  43. package/dist/server/index.js +5 -5
  44. package/dist/{server-Csx2sRjg.js → server-B1mZ7CgQ.js} +11 -6
  45. package/dist/sql/index.d.ts +38 -3
  46. package/dist/sql/index.js +237 -118
  47. package/dist/ssr/index.d.ts +4 -4
  48. package/dist/ssr/index.js +1 -1
  49. package/dist/{support-DT-OhSxD.d.ts → support-CEPwlw2G.d.ts} +2 -1
  50. package/dist/{support-view-Da5peKFg.d.ts → support-view-B8SlToQX.d.ts} +1 -1
  51. package/dist/{support-Dk6vdmEw.js → support-wHnK6zp-.js} +4 -2
  52. package/dist/testing/index.d.ts +36 -4
  53. package/dist/testing/index.js +141 -1
  54. package/dist/{version-DM_Roffb.js → version-DOYTYwHP.js} +1 -1
  55. package/dist/{view-Bq64ki8-.js → view-Bn6HNKUe.js} +7 -65
  56. package/dist/{view-BEf0diC5.d.ts → view-CbgObAeb.d.ts} +17 -31
  57. package/package.json +13 -13
  58. package/schemas/doctor-report-v1.json +1 -0
  59. package/skills/better-supabase-auth/SKILL.md +15 -7
  60. package/skills/better-supabase-auth/references/permdock.md +22 -0
  61. package/dist/cli/gen-BJc9vdyu.js +0 -2
  62. package/dist/impersonation-VfaGc4Q_.js +0 -41
package/README.md CHANGED
@@ -1,12 +1,64 @@
1
1
  # better-supabase
2
2
 
3
- **A typed data layer, auth glue and CLI for Supabase apps, APIs, MCP servers and jobs.**
3
+ **Typed repositories, auth glue and a CLI for Supabase apps, APIs, MCP servers and jobs.**
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/better-supabase)](https://www.npmjs.com/package/better-supabase)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ScaleDockHQ/better-supabase/blob/main/LICENSE)
7
7
  ![TypeScript](https://img.shields.io/badge/TypeScript-6%20%7C%207-3178c6.svg)
8
8
 
9
- `better-supabase gen` extends `supabase gen types` with relation cardinality, unique keys, CHECK unions and typed jsonb. The generated `schema` gives every table a typed repository that compiles to one PostgREST request, returns a `Result` instead of throwing, and runs as the caller so RLS always applies.
9
+ [Docs](https://bettersupabase.com/docs) · [Quickstart](https://bettersupabase.com/docs/getting-started) · [Examples](https://bettersupabase.com/docs/examples) · [Changelog](https://bettersupabase.com/changelog) · [GitHub](https://github.com/ScaleDockHQ/better-supabase)
10
+
11
+ better-supabase sits on top of the supabase-js client you already use. You keep your database, your RLS policies, Supabase Auth and Storage. The CLI reads your schema and generates a typed repository for every table, so a query is a typed object instead of a string, a typo in a column name is a compile error, and the row type follows what you selected.
12
+
13
+ It also writes the glue every Supabase app repeats: the session cookie and token refresh for Next.js, Hono, oRPC, Edge Functions and MCP servers, pagination, soft delete, timestamps, cache invalidation, background jobs and typed Storage and Realtime. It is built on [`@supabase/supabase-js`](https://github.com/supabase/supabase-js), [`@supabase/server`](https://github.com/supabase/server), [`@supabase/middleware`](https://github.com/supabase/middleware) and [`@supabase/ssr`](https://github.com/supabase/ssr). It is an independent open-source project, not made by Supabase.
14
+
15
+ ## Before and after
16
+
17
+ With supabase-js, the query is a string and the error is a Postgres code:
18
+
19
+ ```ts
20
+ const { data, error } = await supabase
21
+ .from("customers")
22
+ .select("id, name, organization(name), notes!inner(kind)")
23
+ .eq("status", "active")
24
+ .eq("notes.kind", "call")
25
+ .order("name")
26
+ .limit(20);
27
+ if (error) throw error;
28
+
29
+ const { error: insertError } = await supabase.from("tags").insert({ name });
30
+ if (insertError?.code === "23505") return { message: "Tag already exists" };
31
+ ```
32
+
33
+ With better-supabase, every column, relation and operator is typed, and errors are values with a `kind`:
34
+
35
+ ```ts
36
+ const customers = await db.customers.findMany({
37
+ select: ["id", "name"],
38
+ where: { status: "active", notes: { some: { kind: "call" } } },
39
+ include: { organization: { select: ["name"] } },
40
+ orderBy: { name: "asc" },
41
+ limit: 20,
42
+ });
43
+ // customers.data: { id: string; name: string; organization: { name: string } }[]
44
+
45
+ const tag = await db.tags.create({ organizationId, name });
46
+ if (tag.error?.kind === "conflict") return { message: "Tag already exists" };
47
+ ```
48
+
49
+ Each call is still one PostgREST request, and it runs as the signed-in user, so your RLS policies decide what it can read and write. The [supabase-js migration guide](https://bettersupabase.com/docs/migration/supabase-js) maps every supabase-js call to its repository equivalent, and the two work side by side while you move over.
50
+
51
+ ## What you get
52
+
53
+ - **Stronger generated types.** [`better-supabase gen`](https://bettersupabase.com/docs/cli/gen) writes the same `database.types.ts` as `supabase gen types` and adds relationship cardinality, unique keys, CHECK-constraint unions, typed jsonb and camelCase column maps.
54
+ - **A typed repository per table.** [`findMany`, `findUnique`, `create`, `upsert`, `paginate`, counts and aggregates](https://bettersupabase.com/docs/repository), with nested relation filters and includes, compiled to PostgREST or to SQL on a direct Postgres connection.
55
+ - **Results instead of exceptions.** Every call returns a [`Result` with a plain, serializable `DbError`](https://bettersupabase.com/docs/concepts/results) that you can return from server actions and RPC handlers. `.orThrow()` is opt-in, and adapters turn errors into RFC 9457 Problem Details with the right status.
56
+ - **Your casing.** Keep database names with `casing: 'snake'`, or get [camelCase rows](https://bettersupabase.com/docs/concepts/casing) renamed inside the PostgREST query.
57
+ - **Auth without extra round trips.** Valid access tokens are [verified locally](https://bettersupabase.com/docs/auth) against the JWKS and never reach the Auth server. Refresh happens once, in the proxy. The service role is an explicit `bs.admin()` call.
58
+ - **Cache tags that follow writes.** Mutations [invalidate the tables they change](https://bettersupabase.com/docs/concepts/caching), with read-your-writes in Next.js server actions and table-based invalidation in TanStack Query.
59
+ - **Plugins.** [Timestamps, soft delete, tenant scoping, actor columns, validation and rules](https://bettersupabase.com/docs/plugins), each versioned and opt-in.
60
+ - **Kits for the SQL every app repeats.** [Jobs on Supabase Queues](https://bettersupabase.com/docs/kits/jobs), webhook inboxes, idempotency keys, organizations and invitations, notifications, list pages, typed Storage paths and Realtime topics, vector search and Stripe entitlements. [`better-supabase sql add`](https://bettersupabase.com/docs/kits/sql) writes the tables, functions and policies into your declarative schema.
61
+ - **Tests and CI checks.** [`asUser`](https://bettersupabase.com/docs/testing) runs RLS tests as any user against the local stack, `gen --check` fails on schema drift, and [`doctor`](https://bettersupabase.com/docs/cli/doctor) reports security and performance findings, with SARIF output for code scanning.
10
62
 
11
63
  ## Install
12
64
 
@@ -15,18 +67,21 @@ pnpm add better-supabase @supabase/supabase-js
15
67
  pnpm add -D pg
16
68
  ```
17
69
 
18
- ESM only. The package includes the `better-supabase` CLI, which needs Node 24 or later; the runtime entries run on every WinterTC runtime (Node, Deno, Bun, Workers, Supabase Edge Functions). TypeScript 6 and 7 are tested.
70
+ Before installing, `npx better-supabase init` detects your frameworks and prints the install command for your package manager. The package is ESM only. The CLI needs Node 24 or later, and the runtime entries run on every WinterTC runtime. On runtimes without a native `Temporal` (Node 24, Safari), load [`temporal-polyfill`](https://bettersupabase.com/docs/concepts/temporal) once at startup. TypeScript 6 and 7 are tested.
19
71
 
20
72
  ## Quick start
21
73
 
22
- Or start with `npx better-supabase init`, which detects your frameworks and prints the install command.
74
+ Generate the typed schema from your local stack:
23
75
 
24
76
  ```bash
25
77
  supabase start
26
78
  pnpm better-supabase init # config, src/lib/supabase/index.ts and framework glue
79
+ pnpm better-supabase env # URL and keys into .env.local
27
80
  pnpm better-supabase gen # database.types.ts and generated.ts
28
81
  ```
29
82
 
83
+ Define the client once, then connect it per request with the current user's Supabase client:
84
+
30
85
  ```ts
31
86
  import { createClient } from "@supabase/supabase-js";
32
87
  import { defineSupabase } from "better-supabase";
@@ -37,16 +92,78 @@ export const betterSupabase = defineSupabase(schema);
37
92
 
38
93
  const db = betterSupabase.connect(createClient(url, publishableKey));
39
94
 
40
- const result = await db.customers.findMany({
41
- select: ["id", "name"],
42
- where: { status: "active", notes: { some: { kind: "call" } } },
43
- include: { organization: { select: ["name"] } },
44
- orderBy: { name: "asc" },
45
- limit: 20,
46
- });
47
- if (!result.ok) return result; // DbError: kind, message, status, code
95
+ const customers = await db.customers
96
+ .findMany({ select: ["id", "name"], where: { status: "active" } })
97
+ .orThrow();
98
+ ```
99
+
100
+ `defineSupabase` holds no secrets and no connection. Because you connect with the client for the current user, every query runs under their RLS policies.
101
+
102
+ ## Use it where you run
103
+
104
+ An adapter verifies the session, refreshes it where it should and gives each handler a `db` for the caller. In Next.js:
105
+
106
+ ```tsx
107
+ // src/lib/supabase/server.ts
108
+ export const bs = createNext(betterSupabase);
109
+
110
+ // src/proxy.ts
111
+ export const proxy = (request: NextRequest) => bs.proxy(request);
112
+
113
+ // app/customers/page.tsx
114
+ export default async function Customers() {
115
+ const { db } = await bs.context();
116
+ const customers = await db.customers
117
+ .findMany({ select: ["id", "name"] })
118
+ .orThrow();
119
+ return <CustomerList customers={customers} />;
120
+ }
48
121
  ```
49
122
 
123
+ | Where you run | What the adapter gives you | Docs |
124
+ | ---------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------- |
125
+ | Next.js | Proxy, Server Components, route handlers, server actions and cache tags | [Next.js](https://bettersupabase.com/docs/frameworks/next) |
126
+ | Hono | Middleware, Result-aware handlers and REST resources that match OpenAPI | [Hono](https://bettersupabase.com/docs/frameworks/hono) |
127
+ | oRPC | A middleware that adds the caller's repositories to the oRPC context | [oRPC](https://bettersupabase.com/docs/frameworks/orpc) |
128
+ | Edge Functions, Deno, Bun, Workers | Fetch handlers with the caller's repositories | [Edge](https://bettersupabase.com/docs/frameworks/edge) |
129
+ | MCP servers | Tools from your tables and your own code, running as the signed-in user | [MCP](https://bettersupabase.com/docs/frameworks/mcp) |
130
+ | React and TanStack Query | A browser client, typed hooks, query and mutation options, live queries | [Frontend](https://bettersupabase.com/docs/frontend/query) |
131
+
132
+ ## The CLI
133
+
134
+ The `better-supabase` command ships in this package:
135
+
136
+ ```bash
137
+ pnpm better-supabase init # config, client and framework glue
138
+ pnpm better-supabase env # local URL and keys into .env.local
139
+ pnpm better-supabase gen # database.types.ts and generated.ts
140
+ pnpm better-supabase gen --check # exit 1 in CI when the schema drifted
141
+ pnpm better-supabase doctor # RLS, grants, indexes and Supabase advisors
142
+ pnpm better-supabase doctor --format sarif # the same findings for GitHub code scanning
143
+ pnpm better-supabase sql add jobs # a SQL kit module into supabase/schemas
144
+ pnpm better-supabase skills install # Agent Skills for your coding agent
145
+ ```
146
+
147
+ See the [CLI reference](https://bettersupabase.com/docs/cli) for every command and flag.
148
+
149
+ ## Works with
150
+
151
+ | Area | Supported |
152
+ | ---------- | ------------------------------------------------------------------------- |
153
+ | Frameworks | Next.js 16, Hono, oRPC, Supabase Edge Functions, MCP servers |
154
+ | Runtimes | Node.js, Deno, Bun, Cloudflare Workers, Vercel Functions, Supabase Edge |
155
+ | Frontend | React 19, TanStack Query 5, live queries over Realtime |
156
+ | Validation | Zod, Valibot and any Standard Schema |
157
+ | Standards | OpenAPI 3.1, RFC 9457, OpenTelemetry, CloudEvents, Standard Webhooks, MCP |
158
+ | Testing | Vitest, pgTAP, the Supabase local stack |
159
+
160
+ ## Good to know
161
+
162
+ - **It does not replace supabase-js.** Auth flows, Storage and Realtime stay on the supabase-js client, which is one property away as `db.$client`.
163
+ - **It does not replace RLS.** Queries run as the caller and your policies decide. Bypassing them takes an explicit `bs.admin()`.
164
+ - **A direct Postgres connection is optional.** Queries go through PostgREST by default. [`better-supabase/postgres`](https://bettersupabase.com/docs/auth/postgres) runs the same repository API over SQL for jobs, scripts and transactions.
165
+ - **PostgREST's limits still apply.** There are no transactions across requests, and a few filters only work on reads. The [limitations page](https://bettersupabase.com/docs/guides/limitations) lists each one and what to use instead.
166
+
50
167
  ## Subpaths
51
168
 
52
169
  | Import | What it gives you |
@@ -77,7 +194,7 @@ if (!result.ok) return result; // DbError: kind, message, status, code
77
194
 
78
195
  ## For AI agents
79
196
 
80
- The package ships [Agent Skills](https://agentskills.io) in `skills/`:
197
+ The package ships [Agent Skills](https://agentskills.io) for queries, APIs, auth and tests:
81
198
 
82
199
  ```bash
83
200
  npx skills add ScaleDockHQ/better-supabase
@@ -85,12 +202,12 @@ npx skills add ScaleDockHQ/better-supabase
85
202
  pnpm better-supabase skills install
86
203
  ```
87
204
 
88
- The docs are also available as [`/llms.txt`](https://bettersupabase.com/llms.txt), as Markdown at `/docs/<page>.md`, and through the docs MCP server at `https://bettersupabase.com/mcp`.
205
+ The docs are also available as [`/llms.txt`](https://bettersupabase.com/llms.txt), as Markdown at `/docs/<page>.md`, and through the read-only docs MCP server at `https://bettersupabase.com/mcp`.
89
206
 
90
207
  ## Documentation
91
208
 
92
- [bettersupabase.com/docs](https://bettersupabase.com/docs)
209
+ The full documentation is at [bettersupabase.com/docs](https://bettersupabase.com/docs), and a runnable app for every adapter is in [`apps/examples`](https://github.com/ScaleDockHQ/better-supabase/tree/main/apps/examples).
93
210
 
94
211
  ## License
95
212
 
96
- [MIT](https://github.com/ScaleDockHQ/better-supabase/blob/main/LICENSE)
213
+ [MIT](https://github.com/ScaleDockHQ/better-supabase/blob/main/LICENSE) © 2026 ScaleDock
@@ -0,0 +1,43 @@
1
+ //#region src/auth/actor.d.ts
2
+ /** One level of an RFC 8693 `act` chain: who acts, and who acted before it. */
3
+ interface ActClaim {
4
+ readonly sub: string;
5
+ readonly act?: ActClaim;
6
+ readonly [claim: string]: unknown;
7
+ }
8
+ /**
9
+ * Who acts for the user, from the outermost level of the RFC 8693 `act`
10
+ * claim, by its `kind`:
11
+ *
12
+ * - `oauth-client`: an `act` without `kind` (prior actors stay on `chain`),
13
+ * else the OAuth `client_id` of a Supabase OAuth server token. Only this
14
+ * kind is limited to the scopes the user delegated.
15
+ * - `support`: an admin in a support session (`act.kind: "support"`, from
16
+ * `supportClaims`). An `act` without `kind` but with `session_id`, as
17
+ * 0.5.0 minted it, counts as one until 0.6.
18
+ * - `impersonation`: an admin acting as the user (`act.kind:
19
+ * "impersonation"`, from `actClaim`).
20
+ */
21
+ type SessionActor = {
22
+ readonly kind: "oauth-client";
23
+ readonly id: string;
24
+ readonly chain?: ActClaim;
25
+ } | {
26
+ readonly kind: "support";
27
+ readonly id: string;
28
+ readonly sessionId: string;
29
+ /** `act.read_only`; a support token without it is read-only. */
30
+ readonly readOnly: boolean;
31
+ readonly reason?: string;
32
+ } | {
33
+ readonly kind: "impersonation";
34
+ readonly id: string;
35
+ readonly reason?: string;
36
+ };
37
+ /** What the user delegated to the actor: the token's OAuth `scope`. */
38
+ interface SessionDelegation {
39
+ readonly scopes: readonly string[];
40
+ readonly chain?: ActClaim;
41
+ }
42
+ //#endregion
43
+ export { SessionActor as n, SessionDelegation as r, ActClaim as t };
package/dist/cli/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as run } from "./run-BKR2W_79.js";
2
+ import { i as run } from "./run-Bo_NLT5n.js";
3
3
  import { t as colorEnabled } from "./style-C0NFdJRL.js";
4
4
  import { text } from "node:stream/consumers";
5
5
  //#region src/cli/bin.ts
@@ -2,11 +2,11 @@ import { a as list, r as defineCliCommand } from "./command-CK14fP91.js";
2
2
  import { n as CliError } from "./errors-4v4DTtda.js";
3
3
  import { r as writeIfChanged } from "./io-k41_z-jT.js";
4
4
  import { i as plain, r as painter } from "./style-C0NFdJRL.js";
5
- import { t as VERSION } from "./version-BDJuO0UR.js";
5
+ import { t as VERSION } from "./version-BCtlgDQx.js";
6
6
  import { c as diffEngine, d as pgFunctionHooks, f as readSupabaseToml, l as httpHooks, m as tomlGet, p as schemaPaths, s as stdinDatabaseUrl, u as migrationCommand } from "./config-CPVpXP5I.js";
7
- import { a as parseHookMarker, i as parseGrantsMarker, o as permdockSource, s as readPermdock, t as compiledReadSets } from "./read-sets-BfMQcKy8.js";
7
+ import { c as parseHookMarker, l as permdockSource, s as parseGrantsMarker, t as compiledReadSets, u as readPermdock } from "./read-sets-VQcPgxU9.js";
8
8
  import { t as byCodePoint } from "./compare-BmIWVEui.js";
9
- import { C as catalogOf, D as policyObject, E as lineOf, O as qualified, S as entitlementsKit, T as exposedSchemas, c as openSource, d as snapshotFile, h as CACHE_DIR, k as tableObject, o as loadSnapshot, r as renderFiles, s as managementTarget, w as exposed, x as PERMDOCK_RULES } from "./gen-C2osRcr5.js";
9
+ import { A as tableObject, C as entitlementsKit, D as lineOf, E as exposedSchemas, O as policyObject, S as accessKit, T as exposed, c as openSource, d as snapshotFile, h as CACHE_DIR, k as qualified, o as loadSnapshot, r as renderFiles, s as managementTarget, w as catalogOf, x as PERMDOCK_RULES } from "./gen-CjElD1Vw.js";
10
10
  import { n as withSpinner } from "./prompts-BNcNfAve.js";
11
11
  import { t as connect } from "./db-DOBPt660.js";
12
12
  import { dirname, resolve } from "node:path";
@@ -1830,9 +1830,11 @@ const isTrue = (expression) => expression !== null && /^\(*\s*true\s*\)*$/i.test
1830
1830
  /** The `sql.kit` files as this release renders them, next to what is on disk. */
1831
1831
  async function kitFiles(context) {
1832
1832
  if (context.config.sql.kit.length === 0) return [];
1833
+ const access = accessKit(context);
1834
+ if (access.kind === "invalid") return [];
1833
1835
  const readSets = context.readSets;
1834
1836
  const skipped = readSets !== void 0 && "skipped" in readSets;
1835
- const files = renderKit(context.config.sql.kit, kitLayout(context.config, context.config.sql.testsDir, skipped ? [] : readSets, entitlementsKit(context))).filter((file) => !(skipped && file.module === "read-sets"));
1837
+ const files = renderKit(context.config.sql.kit, kitLayout(context.config, context.config.sql.testsDir, skipped ? [] : readSets, entitlementsKit(context), access.kind === "permdock" ? access.access : void 0)).filter((file) => !(skipped && file.module === "read-sets"));
1836
1838
  return Promise.all(files.map(async (file) => {
1837
1839
  const current = await readFile(resolve(context.config.root, file.path), "utf8").catch(() => void 0);
1838
1840
  const version = kitFileVersion(file.contents)?.version ?? 1;
@@ -0,0 +1,2 @@
1
+ import { n as genCommand } from "./gen-CjElD1Vw.js";
2
+ export { genCommand };
@@ -2,10 +2,10 @@ import { r as defineCliCommand } from "./command-CK14fP91.js";
2
2
  import { n as CliError } from "./errors-4v4DTtda.js";
3
3
  import { n as importPath, r as writeIfChanged, t as display } from "./io-k41_z-jT.js";
4
4
  import { r as painter } from "./style-C0NFdJRL.js";
5
- import { t as VERSION } from "./version-BDJuO0UR.js";
5
+ import { t as VERSION } from "./version-BCtlgDQx.js";
6
6
  import { d as pgFunctionHooks, f as readSupabaseToml, m as tomlGet, s as stdinDatabaseUrl, t as databaseUrl } from "./config-CPVpXP5I.js";
7
7
  import { t as fileDiff } from "./diff-SjkO5rNr.js";
8
- import { a as parseHookMarker, i as parseGrantsMarker, n as entitlementRequirements, r as entitlementsMode, s as readPermdock, t as compiledReadSets } from "./read-sets-BfMQcKy8.js";
8
+ import { a as entitlementsMode, c as parseHookMarker, d as scopeMemberships, f as unsafeKey, i as entitlementRequirements, n as accessPermdockMode, o as kitKeyProblems, r as accessRequirements, s as parseGrantsMarker, t as compiledReadSets, u as readPermdock } from "./read-sets-VQcPgxU9.js";
9
9
  import { t as byCodePoint } from "./compare-BmIWVEui.js";
10
10
  import { n as withSpinner } from "./prompts-BNcNfAve.js";
11
11
  import { t as connect } from "./db-DOBPt660.js";
@@ -13,7 +13,7 @@ import { basename, dirname, resolve } from "node:path";
13
13
  import { existsSync } from "node:fs";
14
14
  import { mkdir, readFile, writeFile } from "node:fs/promises";
15
15
  import { setTimeout } from "node:timers/promises";
16
- import { PERMDOCK_SCHEMA, kitLayout, permdockKeyStatus, permdockKeys, renderKit, sameKitFile } from "better-supabase/sql";
16
+ import { PERMDOCK_SCHEMA, kitLayout, kitPermissionKeys, permdockKeys, renderKit, resolveModules, sameKitFile } from "better-supabase/sql";
17
17
  import { DEFAULT_CLAIMS, tenantClaimPaths } from "better-supabase/config";
18
18
  import { toCamel, toSnake } from "better-supabase";
19
19
  import { generateTypescript, pgTypeToTsType, sortGeneratorMetadata } from "@supabase/postgrest-typegen/generation";
@@ -318,37 +318,16 @@ function placementProblems(project, catalog, call, where) {
318
318
  if (resolved !== call.scope) problems.push(`${where} checks "${call.key}" at scope "${call.scope}", but ${project.catalogPath} declares it at "${resolved}", so PermDock's helpers never grant it there. Use scope "${resolved}".`);
319
319
  return problems;
320
320
  }
321
- const ROW_CONDITIONS_FIX = "Use the policies `permdock rls generate` writes for it, or a permission whose catalog entry has rowConditions: false.";
322
- const REGENERATE_FIX = "Regenerate it with a current `permdock catalog`, which writes rowConditions for every permission.";
323
- /**
324
- * Why the SQL helpers can't be trusted with `key`, or `undefined` when the
325
- * catalog marks it `rowConditions: false`. An entry without the flag and a
326
- * key the catalog doesn't list are unknown, so they count as unsafe.
327
- */
328
- function unsafeKey(catalog, key, catalogPath) {
329
- const status = permdockKeyStatus(catalog, key);
330
- switch (status) {
331
- case "scope-only": return;
332
- case "row-conditions": return {
333
- reason: `has row conditions in ${catalogPath}`,
334
- fix: ROW_CONDITIONS_FIX
335
- };
336
- case "no-flag": return {
337
- reason: `has no rowConditions flag in ${catalogPath}, so whether it has row conditions is unknown`,
338
- fix: REGENERATE_FIX
339
- };
340
- case "missing": return {
341
- reason: `is not in ${catalogPath}`,
342
- fix: REGENERATE_FIX
343
- };
344
- default: return status;
345
- }
346
- }
347
321
  /** PermDock's helpers for the `entitlements` kit module, when the manifest allows PermDock mode. */
348
322
  function entitlementsKit(context) {
349
323
  const mode = entitlementsMode(context.config, context.permdock);
350
324
  return mode.kind === "permdock" ? mode.permdock : void 0;
351
325
  }
326
+ /** PermDock's helpers for the `permdock` access model, when `sql.kit` installs `access`. */
327
+ function accessKit(context) {
328
+ if (!resolveModules(context.config.sql.kit, {}).some((module) => module.name === "access")) return { kind: "off" };
329
+ return accessPermdockMode(context.config, context.permdock);
330
+ }
352
331
  const PERMDOCK_RULES = [
353
332
  {
354
333
  code: "BS214",
@@ -451,8 +430,17 @@ const PERMDOCK_RULES = [
451
430
  });
452
431
  }
453
432
  const scope = mode.permdock.scope;
454
- if (!mode.permdock.memberships.some((source) => !("value" in source.scope) || source.scope.value === scope)) findings.push({
455
- message: `No membership source in ${manifest} covers scope "${scope}", so entitlement_members() finds no users and plan changes never invalidate sessions. Add a source for "${scope}" (\`supabase.hook.memberships\` in permdock.config.ts), ${regenerate}`,
433
+ const memberships = project?.manifest ? scopeMemberships(project.manifest, scope) : {
434
+ sources: [],
435
+ from: "hook"
436
+ };
437
+ if (memberships.from === "hook") findings.push({
438
+ severity: "info",
439
+ message: `${manifest} has no rls.memberships, so entitlement_members() reads the hook's membership sources, which can differ from the tables member_${scope}_ids_for reads. Run \`permdock supabase inspect --out\` with a current PermDock.`,
440
+ target: `entitlements.permdock.${scope}`
441
+ });
442
+ if (memberships.sources.length === 0) findings.push({
443
+ message: memberships.from === "rls" ? `${manifest} maps no rls.memberships table to scope "${scope}", so entitlement_members() finds no users and plan changes never invalidate sessions. Map a table for "${scope}" (\`rls.memberships\` or \`rls.membershipSources\` in permdock.config.ts), ${regenerate}` : `No membership source in ${manifest} covers scope "${scope}", so entitlement_members() finds no users and plan changes never invalidate sessions. Add a source for "${scope}" (\`supabase.hook.memberships\` in permdock.config.ts), ${regenerate}`,
456
444
  target: `entitlements.permdock.${scope}`
457
445
  });
458
446
  return findings.concat(entitlementRequirements(mode.permdock).flatMap((requirement) => {
@@ -518,6 +506,55 @@ const PERMDOCK_RULES = [
518
506
  });
519
507
  return findings;
520
508
  }
509
+ },
510
+ {
511
+ code: "BS411",
512
+ severity: "error",
513
+ title: "The permdock access model doesn't match PermDock's manifest",
514
+ description: "With `kits.access.model: 'permdock'`, the `access` module calls PermDock's `<rls.schema>.permitted_<scope>_ids(permission)` for tenant checks and `<rls.schema>.permdock_has(permission)` for platform checks, with the manifest's root scope unless `kits.access.permdock.scope` names another. Doctor reports a project without a readable manifest or `rls` block, no single root scope, a scope, schema or id type the manifest doesn't declare, a helper the manifest doesn't list, doesn't let `authenticated` execute or the database lacks, and every kit permission key (from `kitPermissionKeys`) whose catalog entry isn't `rowConditions: false`, since the helpers check role and scope only. It warns when `kits.access.functions.canAssign` is not set, because only the service role then assigns roles, and notes a `permdock_can_assign` template the manifest's helpers lack.",
515
+ check: (context) => {
516
+ const mode = accessKit(context);
517
+ if (mode.kind === "off") return [];
518
+ if (mode.kind === "invalid") return [{
519
+ message: mode.problem,
520
+ target: "kits.access.permdock"
521
+ }];
522
+ const project = context.permdock;
523
+ const manifest = context.config.permdock.manifest;
524
+ const findings = [];
525
+ const listed = new Map((project?.manifest?.rls?.helpers ?? []).map((helper) => [`${mode.access.schema}.${helper.name}`, helper]));
526
+ const read = context.snapshot.schemas.includes(mode.access.schema);
527
+ const present = new Set(catalogOf(context).functions.map((fn) => `${fn.schema}.${fn.name}`));
528
+ for (const { helper, role } of accessRequirements(mode.access)) {
529
+ const entry = listed.get(helper);
530
+ if (!entry) findings.push({
531
+ message: `${manifest} lists no ${helper}, but the permdock access model calls it from can() and the kit modules. Run \`permdock rls generate\` with a current PermDock, then \`permdock supabase inspect --out\`.`,
532
+ target: helper
533
+ });
534
+ else if (!entry.execute.includes(role)) findings.push({
535
+ message: `${manifest} says ${role} may not execute ${helper}, but the permdock access model calls it as ${role}. Run \`permdock rls generate\` with a current PermDock, then \`permdock supabase inspect --out\`.`,
536
+ target: helper
537
+ });
538
+ else if (read && !present.has(helper)) findings.push({
539
+ message: `${helper} is in ${manifest} but not in the database. Apply the migration \`permdock rls generate\` wrote before the access module.`,
540
+ target: helper
541
+ });
542
+ }
543
+ if (project) findings.push(...kitKeyProblems(project, kitPermissionKeys(context.config.kits, context.config.sql.kit), mode.access));
544
+ const canAssign = context.config.kits.access?.functions?.canAssign;
545
+ const recipe = `${mode.access.schema}.permdock_can_assign({role}, {tenant}::text)`;
546
+ if (canAssign === void 0) findings.push({
547
+ severity: "warning",
548
+ message: `kits.access.functions.canAssign is not set, so under the permdock model only the service role assigns roles: the memberships guard refuses every membership a member adds or changes${context.config.sql.kit.includes("tenant") ? "" : " (the owner-role fallback needs the tenant module, which PermDock projects don't install)"}. Set canAssign: "${recipe}", which PermDock writes when a role declares \`assigns\`.`,
549
+ target: "kits.access.functions.canAssign"
550
+ });
551
+ else if (canAssign.includes("permdock_can_assign") && !listed.has(`${mode.access.schema}.permdock_can_assign`)) findings.push({
552
+ severity: "info",
553
+ message: `kits.access.functions.canAssign calls permdock_can_assign, but ${manifest} doesn't list ${mode.access.schema}.permdock_can_assign. PermDock writes it only when a role in permdock.config.ts declares \`assigns\`; add one, then run \`permdock rls generate\` and \`permdock supabase inspect --out\`.`,
554
+ target: "kits.access.functions.canAssign"
555
+ });
556
+ return findings;
557
+ }
521
558
  }
522
559
  ];
523
560
  //#endregion
@@ -908,8 +945,8 @@ async function formatTypes(code) {
908
945
  return formatted;
909
946
  }
910
947
  /**
911
- * Renders `database.types.ts`, byte for byte what `supabase gen types` prints
912
- * when oxfmt is installed.
948
+ * Renders `database.types.ts` as `supabase gen types` prints it when oxfmt is
949
+ * installed, plus the `ComputedFields` keys Supabase CLI 2.119 doesn't write.
913
950
  */
914
951
  function generateDatabaseTypes(metadata, options) {
915
952
  return generateTypescript(sortGeneratorMetadata(restrictSchemas(metadata, options.schemas)), {
@@ -2020,7 +2057,7 @@ async function rowConditionedBuckets(config) {
2020
2057
  return problem ? [` buckets.${bucket}: "${key}" ${problem.reason}. ${problem.fix}`] : [];
2021
2058
  }));
2022
2059
  }
2023
- const UNFORMATTED = "oxfmt is not installed, so database.types.ts is not formatted like `supabase gen types` output. Install the version @supabase/postgrest-typegen pins: pnpm add -D oxfmt@0.66.0. To use a newer oxfmt, allow it in pnpm-workspace.yaml under peerDependencyRules.allowedVersions, keyed \"@supabase/postgrest-typegen>oxfmt\".";
2060
+ const UNFORMATTED = "oxfmt is not installed, so database.types.ts is not formatted like `supabase gen types` output. Install it: pnpm add -D oxfmt. @supabase/postgrest-typegen pins oxfmt 0.66.0 as its peer; to allow a newer one, add it to peerDependencyRules.allowedVersions in pnpm-workspace.yaml, keyed \"@supabase/postgrest-typegen>oxfmt\".";
2024
2061
  async function runGen(options) {
2025
2062
  const { config } = options;
2026
2063
  const refused = await rowConditionedBuckets(config);
@@ -2201,4 +2238,4 @@ const genCommand = defineCliCommand({
2201
2238
  }
2202
2239
  });
2203
2240
  //#endregion
2204
- export { toCatalog as A, catalogOf as C, policyObject as D, lineOf as E, qualified as O, entitlementsKit as S, exposedSchemas as T, restrictSchemas as _, sourceArgs as a, emitModule as b, openSource as c, snapshotFile as d, introspect as f, buildModel as g, CACHE_DIR as h, runGen as i, tableObject as k, parseSnapshot as l, pgSource as m, genCommand as n, loadSnapshot as o, managementSource as p, renderFiles as r, managementTarget as s, SOURCE_ARGS as t, serializeSnapshot as u, serializeGenerator as v, exposed as w, PERMDOCK_RULES as x, GENERATED_HEADER as y };
2241
+ export { tableObject as A, entitlementsKit as C, lineOf as D, exposedSchemas as E, policyObject as O, accessKit as S, exposed as T, restrictSchemas as _, sourceArgs as a, emitModule as b, openSource as c, snapshotFile as d, introspect as f, buildModel as g, CACHE_DIR as h, runGen as i, toCatalog as j, qualified as k, parseSnapshot as l, pgSource as m, genCommand as n, loadSnapshot as o, managementSource as p, renderFiles as r, managementTarget as s, SOURCE_ARGS as t, serializeSnapshot as u, serializeGenerator as v, catalogOf as w, PERMDOCK_RULES as x, GENERATED_HEADER as y };
@@ -488,6 +488,6 @@ export declare function emitModule(model: Model, options: EmitOptions): string;
488
488
  export declare function loadConfig(cwd: string, explicit?: string): Promise<ResolvedConfig>;
489
489
  //#endregion
490
490
  //#region src/cli/version.d.ts
491
- export declare const VERSION = "0.5.0";
491
+ export declare const VERSION = "0.5.1";
492
492
  //#endregion
493
493
  export type { Catalog, CatalogCheck, CatalogColumn, CatalogColumnGrant, CatalogEnum, CatalogForeignKey, CatalogFunction, CatalogFunctionArg, CatalogGrant, CatalogIndex, CatalogPolicy, CatalogTable, CatalogTrigger, CatalogUnique, CliArgs, CliCommandDef, CliContext, CliEnv, CliErrorCode, CliErrorOptions, CliIo, CliProblem, CliProblemExtensions, Command, CommandContext, CommandResult, ExitCode, ExtrasFunction, ExtrasHook, ExtrasHookFunction, ExtrasTable, ForeignKeyAction, GeneratorMetadata, IntrospectionSource, ManagementSourceOptions, ParsedArgs, Queryable, RunOptions, RunResult, Snapshot, SnapshotBucket, SnapshotExtras };
package/dist/cli/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { a as list, r as defineCliCommand } from "./command-CK14fP91.js";
2
2
  import { n as CliError, t as CLI_ERRORS_URL } from "./errors-4v4DTtda.js";
3
- import { i as run, n as help, r as registerCommand, t as commandNames } from "./run-BKR2W_79.js";
4
- import { t as VERSION } from "./version-BDJuO0UR.js";
3
+ import { i as run, n as help, r as registerCommand, t as commandNames } from "./run-Bo_NLT5n.js";
4
+ import { t as VERSION } from "./version-BCtlgDQx.js";
5
5
  import { a as loadConfig } from "./config-CPVpXP5I.js";
6
- import { A as toCatalog, b as emitModule, f as introspect, g as buildModel, i as runGen, l as parseSnapshot, m as pgSource, o as loadSnapshot, p as managementSource, r as renderFiles, u as serializeSnapshot, y as GENERATED_HEADER } from "./gen-C2osRcr5.js";
6
+ import { b as emitModule, f as introspect, g as buildModel, i as runGen, j as toCatalog, l as parseSnapshot, m as pgSource, o as loadSnapshot, p as managementSource, r as renderFiles, u as serializeSnapshot, y as GENERATED_HEADER } from "./gen-CjElD1Vw.js";
7
7
  //#region src/cli/introspect/from-catalog.ts
8
8
  /**
9
9
  * Builds a snapshot from catalog records: the inverse of `toCatalog`. Tests
@@ -1,6 +1,6 @@
1
1
  import { a as list, r as defineCliCommand } from "./command-CK14fP91.js";
2
2
  import { r as writeIfChanged, t as display } from "./io-k41_z-jT.js";
3
- import { t as VERSION } from "./version-BDJuO0UR.js";
3
+ import { t as VERSION } from "./version-BCtlgDQx.js";
4
4
  import { n as findConfig } from "./config-CPVpXP5I.js";
5
5
  import { n as installCommand, t as detectProject } from "./project-B_GeXyHh.js";
6
6
  import { posix, resolve } from "node:path";
@@ -2,7 +2,7 @@ import { r as defineCliCommand } from "./command-CK14fP91.js";
2
2
  import { r as writeIfChanged, t as display } from "./io-k41_z-jT.js";
3
3
  import { r as painter } from "./style-C0NFdJRL.js";
4
4
  import { t as fileDiff } from "./diff-SjkO5rNr.js";
5
- import { _ as restrictSchemas, a as sourceArgs, o as loadSnapshot, t as SOURCE_ARGS, u as serializeSnapshot, v as serializeGenerator } from "./gen-C2osRcr5.js";
5
+ import { _ as restrictSchemas, a as sourceArgs, o as loadSnapshot, t as SOURCE_ARGS, u as serializeSnapshot, v as serializeGenerator } from "./gen-CjElD1Vw.js";
6
6
  import { n as withSpinner } from "./prompts-BNcNfAve.js";
7
7
  import { resolve } from "node:path";
8
8
  import { readFile } from "node:fs/promises";