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.
- package/README.md +133 -16
- package/dist/actor-DaJjqD5R.d.ts +43 -0
- package/dist/cli/bin.js +1 -1
- package/dist/cli/{doctor-L7Z8mYTx.js → doctor-C_A-KEAz.js} +6 -4
- package/dist/cli/gen-CIdSQZad.js +2 -0
- package/dist/cli/{gen-C2osRcr5.js → gen-CjElD1Vw.js} +72 -35
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/{init-Saf5-R9T.js → init-DMa6M7hV.js} +1 -1
- package/dist/cli/{introspect--H7oGmcd.js → introspect-BKlaIu_M.js} +1 -1
- package/dist/cli/{read-sets-BfMQcKy8.js → read-sets-VQcPgxU9.js} +173 -26
- package/dist/cli/{run-BKR2W_79.js → run-Bo_NLT5n.js} +7 -7
- package/dist/cli/{sql-DzPbfngx.js → sql-CVx-1O8G.js} +30 -5
- package/dist/cli/{version-BDJuO0UR.js → version-BCtlgDQx.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/{config-T_ejwyd3.d.ts → config-Bv7gvfzC.d.ts} +10 -1
- package/dist/edge/index.d.ts +2 -2
- package/dist/edge/index.js +2 -2
- package/dist/{entitlements-DUxU4oHe.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
- package/dist/hono/index.d.ts +3 -3
- package/dist/hono/index.js +2 -2
- package/dist/impersonation-BgSHAYLf.js +133 -0
- package/dist/{resolve-C15TJq77.d.ts → impersonation-BmePsuxz.d.ts} +25 -1
- package/dist/{index-Dxn2a5cw.d.ts → index-CWMDdEr4.d.ts} +1 -1
- package/dist/{index-BGdgBPdf.d.ts → index-Dp2VDwog.d.ts} +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.js +2 -2
- package/dist/{mfa-CusDgPaD.d.ts → mfa-Cu96oJdw.d.ts} +1 -19
- package/dist/next/index.d.ts +6 -6
- package/dist/next/index.js +3 -3
- package/dist/orpc/index.d.ts +2 -2
- package/dist/orpc/index.js +1 -1
- package/dist/otel/index.js +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/server.d.ts +5 -5
- package/dist/react/session.d.ts +2 -2
- package/dist/{refresh-BTrQ_ZTp.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
- package/dist/{resource-D8iqWMr-.d.ts → resource-DNRFP7KR.d.ts} +1 -1
- package/dist/{resource-CRxxwkQq.js → resource-mqfJW4YF.js} +1 -1
- package/dist/{respond-D2OQl62-.d.ts → respond-BR1fJWOB.d.ts} +12 -6
- package/dist/server/index.d.ts +9 -8
- package/dist/server/index.js +5 -5
- package/dist/{server-Csx2sRjg.js → server-B1mZ7CgQ.js} +11 -6
- package/dist/sql/index.d.ts +38 -3
- package/dist/sql/index.js +237 -118
- package/dist/ssr/index.d.ts +4 -4
- package/dist/ssr/index.js +1 -1
- package/dist/{support-DT-OhSxD.d.ts → support-CEPwlw2G.d.ts} +2 -1
- package/dist/{support-view-Da5peKFg.d.ts → support-view-B8SlToQX.d.ts} +1 -1
- package/dist/{support-Dk6vdmEw.js → support-wHnK6zp-.js} +4 -2
- package/dist/testing/index.d.ts +36 -4
- package/dist/testing/index.js +141 -1
- package/dist/{version-DM_Roffb.js → version-DOYTYwHP.js} +1 -1
- package/dist/{view-Bq64ki8-.js → view-Bn6HNKUe.js} +7 -65
- package/dist/{view-BEf0diC5.d.ts → view-CbgObAeb.d.ts} +17 -31
- package/package.json +13 -13
- package/schemas/doctor-report-v1.json +1 -0
- package/skills/better-supabase-auth/SKILL.md +15 -7
- package/skills/better-supabase-auth/references/permdock.md +22 -0
- package/dist/cli/gen-BJc9vdyu.js +0 -2
- package/dist/impersonation-VfaGc4Q_.js +0 -41
package/README.md
CHANGED
|
@@ -1,12 +1,64 @@
|
|
|
1
1
|
# better-supabase
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Typed repositories, auth glue and a CLI for Supabase apps, APIs, MCP servers and jobs.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/better-supabase)
|
|
6
6
|
[](https://github.com/ScaleDockHQ/better-supabase/blob/main/LICENSE)
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
select: ["id", "name"],
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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)
|
|
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
|
@@ -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-
|
|
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 {
|
|
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
|
|
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;
|
|
@@ -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-
|
|
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
|
|
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,
|
|
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
|
-
|
|
455
|
-
|
|
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
|
|
912
|
-
*
|
|
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
|
|
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 {
|
|
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 };
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -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.
|
|
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-
|
|
4
|
-
import { t as VERSION } from "./version-
|
|
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 {
|
|
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-
|
|
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-
|
|
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";
|