better-supabase 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/better-supabase.js +2 -1
- package/dist/claims-CoTobNC-.js +25 -0
- package/dist/cli/bin.js +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +1 -1
- package/dist/{cli-CoByWYja.js → cli-DrSgDOQC.js} +333 -159
- package/dist/client/index.d.ts +1 -1
- package/dist/{compiler-DL18ukPe.d.ts → compiler-CZ9nIuqy.d.ts} +2 -2
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +2 -2
- package/dist/{config-D-aR2ZoN.js → config-ClDXmVtY.js} +11 -10
- package/dist/{define-QGkq04oe.js → define-BXMYD9AY.js} +65 -62
- package/dist/{define-CTfWw-SS.d.ts → define-e3zI4_4g.d.ts} +27 -10
- package/dist/edge/index.d.ts +4 -4
- package/dist/edge/index.js +3 -3
- package/dist/entitlements-CTEBm7IB.js +20 -0
- package/dist/entitlements-Dng-ctuT.d.ts +42 -0
- package/dist/events/index.d.ts +1 -1
- package/dist/{events-2OJsVG_d.d.ts → events-DOt3nSO-.d.ts} +2 -2
- package/dist/{executor-Bje4suQF.d.ts → executor-BYXQ9AGn.d.ts} +1 -1
- package/dist/{executor-Bazhgsgq.d.ts → executor-CGdNQ27g.d.ts} +1 -1
- package/dist/{executor-Ceq60hPM.js → executor-DeppvirK.js} +1 -1
- package/dist/guards-BnzDvFrz.js +10 -0
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +3 -3
- package/dist/{index-JXHBuS53.d.ts → index-Bw1bwtC0.d.ts} +3 -3
- package/dist/{index-Hf8PP9sP.d.ts → index-CBDufH1M.d.ts} +15 -11
- package/dist/{index-cEeweTeM.d.ts → index-CG69s15v.d.ts} +3 -3
- package/dist/{index-DQVvHzjW.d.ts → index-Ciu5Xlml.d.ts} +3 -3
- package/dist/{index-Sw43J8HG.d.ts → index-D4v7sncu.d.ts} +6 -6
- package/dist/{index-BD6wKthp.d.ts → index-DOsjFvMf.d.ts} +28 -7
- package/dist/{index-DRid0qIv.d.ts → index-DbxFr7XG.d.ts} +2 -2
- package/dist/{index-Nxo3TJcm.d.ts → index-DuP14VHo.d.ts} +11 -3
- package/dist/{index-Dup-nSe9.d.ts → index-K-3WM2Hp.d.ts} +2 -2
- package/dist/index.d.ts +9 -9
- package/dist/index.js +7 -6
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/index.js +6 -5
- package/dist/{json-schema-BbwO2mZg.js → json-schema-BpYqkCKf.js} +2 -1
- package/dist/list/index.d.ts +1 -1
- package/dist/{live-6b9i72Ux.d.ts → live-DJyZMD6w.d.ts} +14 -3
- package/dist/logger-glM28NVN.js +21 -0
- package/dist/mcp/index.d.ts +41 -4
- package/dist/mcp/index.js +194 -61
- package/dist/{mfa-CpogP66z.js → mfa-CpLfJSde.js} +23 -3
- package/dist/next/index.d.ts +23 -23
- package/dist/next/index.js +9 -8
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +3 -3
- package/dist/orpc/index.d.ts +4 -4
- package/dist/orpc/index.js +2 -2
- package/dist/otel/index.d.ts +3 -3
- package/dist/otel/index.js +1 -1
- package/dist/{template-BeOGnynM.js → permdock-sql-CghlHPuH.js} +25 -1
- package/dist/{plugin-HvGqCurB.d.ts → plugin-C4TR9J0Z.d.ts} +2 -2
- package/dist/plugins/actor/index.d.ts +1 -1
- package/dist/plugins/rules/index.d.ts +4 -3
- package/dist/plugins/rules/index.js +4 -11
- package/dist/plugins/soft-delete/index.d.ts +2 -2
- package/dist/plugins/tenant/index.d.ts +10 -4
- package/dist/plugins/tenant/index.js +15 -13
- package/dist/plugins/timestamps/index.d.ts +1 -1
- package/dist/plugins/validation/index.d.ts +1 -1
- package/dist/{pool-BFeqzwQS.d.ts → pool-R2m_DkXi.d.ts} +1 -1
- package/dist/postgres/index.d.ts +4 -4
- package/dist/postgres/index.js +2 -2
- package/dist/{postgrest-Dm-vhY6R.d.ts → postgrest-cAEQ_IQN.d.ts} +1 -1
- package/dist/{problem-DPu6Fh-t.js → problem-BTkTtRV9.js} +4 -4
- package/dist/query/index.d.ts +1 -1
- package/dist/react/index.d.ts +6 -6
- package/dist/react/index.js +3 -3
- package/dist/react/server.d.ts +5 -5
- package/dist/react/server.js +1 -1
- package/dist/react/session.d.ts +1 -1
- package/dist/react/session.js +1 -1
- package/dist/{react-CrOvrnIP.js → react-C83BdBGt.js} +9 -8
- package/dist/{read-set-DhX2d04c.js → read-set-4lI3W_gZ.js} +1 -1
- package/dist/realtime/index.d.ts +3 -2
- package/dist/realtime/index.js +26 -11
- package/dist/{resolve-DW8h9NPm.d.ts → resolve-CjHILk2w.d.ts} +21 -6
- package/dist/{resource-CYiu6Ygi.js → resource-BHEWmQJK.js} +1 -1
- package/dist/{resource-SkPTRJDD.d.ts → resource-CgZa3YG_.d.ts} +4 -4
- package/dist/{respond-4-dhoV7x.js → respond-DMcbQwfN.js} +7 -4
- package/dist/{respond-PoTpy5DT.d.ts → respond-Dne1ARSa.d.ts} +14 -14
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +5 -5
- package/dist/{session-C3osL21k.d.ts → session-C3LxnQ0e.d.ts} +4 -3
- package/dist/{session-CD-3orO_.js → session-CqEwzVup.js} +2 -1
- package/dist/{simplify-oHiDKRVQ.js → simplify-bwJpLnaS.js} +12 -12
- package/dist/{spec-pins-t6sPaHvl.js → spec-pins-C3oc-Tdp.js} +1 -1
- package/dist/{spec-pins-CJPVghHy.d.ts → spec-pins-CqorWDnO.d.ts} +1 -1
- package/dist/{sql-M8l3fnS0.js → sql-DTjA1LPj.js} +1 -1
- package/dist/ssr/index.d.ts +5 -5
- package/dist/ssr/index.js +3 -3
- package/dist/{stats-3iy8u3_P.js → stats-C5W-rBfR.js} +1 -1
- package/dist/storage/index.d.ts +3 -2
- package/dist/storage/index.js +1 -1
- package/dist/{storage-BWXVUXGk.js → storage-DuGn38fr.js} +72 -6
- package/dist/testing/index.d.ts +46 -15
- package/dist/testing/index.js +111 -43
- package/dist/{types-C2fslP1z.d.ts → types-C83v5SQz.d.ts} +55 -2
- package/dist/{view-B0ltUmTi.d.ts → view-D1s0wa0T.d.ts} +9 -3
- package/dist/{view-B6F9QbG-.js → view-aOYizTeE.js} +2 -1
- package/dist/webhooks/index.d.ts +3 -3
- package/dist/webhooks/index.js +1 -1
- package/dist/{webhooks-DbMTJJX2.js → webhooks-DMipjhSP.js} +1 -1
- package/package.json +8 -4
- package/schemas/config-v1.json +49 -4
- package/schemas/doctor-report-v1.json +1 -0
- package/schemas/snapshot-v2.json +4 -0
- package/skills/better-supabase/references/plugins.md +9 -0
- package/skills/better-supabase-api/SKILL.md +2 -2
- package/skills/better-supabase-testing/SKILL.md +4 -3
- package/dist/entitlements-BBg61DQZ.d.ts +0 -20
- package/dist/entitlements-BC_khwRy.js +0 -17
package/dist/testing/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { n as dbError } from "../errors-Bl3rkOzo.js";
|
|
2
2
|
import { i as ok } from "../result-DWXatkd6.js";
|
|
3
|
-
import { t as BetterSupabase } from "../define-
|
|
4
|
-
import {
|
|
3
|
+
import { t as BetterSupabase } from "../define-BXMYD9AY.js";
|
|
4
|
+
import { t as isList } from "../guards-BnzDvFrz.js";
|
|
5
|
+
import { r as resolveConfig } from "../config-ClDXmVtY.js";
|
|
5
6
|
import { t as ENV_VARIABLES } from "../env-BEToab0I.js";
|
|
6
|
-
import { n as postgresExecutor } from "../executor-
|
|
7
|
+
import { n as postgresExecutor } from "../executor-DeppvirK.js";
|
|
7
8
|
import { o as toCloudEvents } from "../events-BuzTgn2D.js";
|
|
8
9
|
import { n as isSeed, t as defineSeed } from "../seed-CzecODcJ.js";
|
|
9
10
|
import { createClient } from "@supabase/supabase-js";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
14
|
//#region src/testing/jwt.ts
|
|
11
15
|
const encoder = new TextEncoder();
|
|
12
16
|
function base64url(input) {
|
|
@@ -83,6 +87,40 @@ function localAuth(secret) {
|
|
|
83
87
|
}
|
|
84
88
|
};
|
|
85
89
|
}
|
|
90
|
+
async function signEs256(key, kid, claims) {
|
|
91
|
+
const { expiresIn = 3600, ...rest } = claims;
|
|
92
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
93
|
+
const payload = {
|
|
94
|
+
aud: "authenticated",
|
|
95
|
+
role: "authenticated",
|
|
96
|
+
aal: "aal1",
|
|
97
|
+
iat: now,
|
|
98
|
+
exp: now + expiresIn,
|
|
99
|
+
...rest
|
|
100
|
+
};
|
|
101
|
+
const header = base64url(JSON.stringify({
|
|
102
|
+
alg: "ES256",
|
|
103
|
+
typ: "JWT",
|
|
104
|
+
kid
|
|
105
|
+
}));
|
|
106
|
+
const body = base64url(JSON.stringify(payload));
|
|
107
|
+
const signature = await crypto.subtle.sign({
|
|
108
|
+
name: "ECDSA",
|
|
109
|
+
hash: "SHA-256"
|
|
110
|
+
}, key, encoder.encode(`${header}.${body}`));
|
|
111
|
+
return `${header}.${body}.${base64url(new Uint8Array(signature))}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Signs an ES256 access token with a private JWK, the way the local stack
|
|
115
|
+
* signs them once `signing_keys_path` is set. Test-only.
|
|
116
|
+
*/
|
|
117
|
+
async function signTestJwtWithKey(key, claims) {
|
|
118
|
+
const { kid, ...jwk } = key;
|
|
119
|
+
return signEs256(await crypto.subtle.importKey("jwk", jwk, {
|
|
120
|
+
name: "ECDSA",
|
|
121
|
+
namedCurve: "P-256"
|
|
122
|
+
}, false, ["sign"]), kid, claims);
|
|
123
|
+
}
|
|
86
124
|
/** An in-memory ES256 key pair that signs tokens like Supabase Auth's asymmetric keys. */
|
|
87
125
|
async function createTestSigner() {
|
|
88
126
|
const pair = await crypto.subtle.generateKey({
|
|
@@ -97,32 +135,44 @@ async function createTestSigner() {
|
|
|
97
135
|
alg: "ES256",
|
|
98
136
|
use: "sig"
|
|
99
137
|
}] },
|
|
100
|
-
|
|
101
|
-
const { expiresIn = 3600, ...rest } = claims;
|
|
102
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
103
|
-
const payload = {
|
|
104
|
-
aud: "authenticated",
|
|
105
|
-
role: "authenticated",
|
|
106
|
-
aal: "aal1",
|
|
107
|
-
iat: now,
|
|
108
|
-
exp: now + expiresIn,
|
|
109
|
-
...rest
|
|
110
|
-
};
|
|
111
|
-
const header = base64url(JSON.stringify({
|
|
112
|
-
alg: "ES256",
|
|
113
|
-
typ: "JWT",
|
|
114
|
-
kid
|
|
115
|
-
}));
|
|
116
|
-
const body = base64url(JSON.stringify(payload));
|
|
117
|
-
const signature = await crypto.subtle.sign({
|
|
118
|
-
name: "ECDSA",
|
|
119
|
-
hash: "SHA-256"
|
|
120
|
-
}, pair.privateKey, encoder.encode(`${header}.${body}`));
|
|
121
|
-
return `${header}.${body}.${base64url(new Uint8Array(signature))}`;
|
|
122
|
-
}
|
|
138
|
+
sign: (claims) => signEs256(pair.privateKey, kid, claims)
|
|
123
139
|
};
|
|
124
140
|
}
|
|
125
141
|
//#endregion
|
|
142
|
+
//#region src/testing/local-key.ts
|
|
143
|
+
const SIGNING_KEYS_PATH = /^\s*signing_keys_path\s*=\s*"([^"]+)"/m;
|
|
144
|
+
/** `supabase/config.toml`'s `signing_keys_path`, else `supabase/signing_keys.json`, in `from` or a parent. */
|
|
145
|
+
async function findSigningKeys(from) {
|
|
146
|
+
for (let dir = resolve(from);; dir = dirname(dir)) {
|
|
147
|
+
const supabase = join(dir, "supabase");
|
|
148
|
+
const toml = join(supabase, "config.toml");
|
|
149
|
+
if (existsSync(toml)) {
|
|
150
|
+
const configured = SIGNING_KEYS_PATH.exec(await readFile(toml, "utf8"))?.[1];
|
|
151
|
+
const path = resolve(supabase, configured ?? "signing_keys.json");
|
|
152
|
+
if (existsSync(path)) return path;
|
|
153
|
+
}
|
|
154
|
+
if (dirname(dir) === dir) return void 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const isSigningKey = (value) => typeof value === "object" && value !== null && "kid" in value && typeof value.kid === "string" && "d" in value && typeof value.d === "string";
|
|
158
|
+
/**
|
|
159
|
+
* The key the local stack signs with: the first entry of the file
|
|
160
|
+
* `better-supabase keys` wrote. `path` defaults to `$SUPABASE_SIGNING_KEYS_PATH`,
|
|
161
|
+
* then `signing_keys_path` from the nearest `supabase/config.toml`.
|
|
162
|
+
*/
|
|
163
|
+
async function localSigningKey(path) {
|
|
164
|
+
const file = path ?? process.env["SUPABASE_SIGNING_KEYS_PATH"] ?? await findSigningKeys(process.cwd());
|
|
165
|
+
if (!file || !existsSync(file)) throw new TypeError("No local signing key. Run `better-supabase keys`, set `signing_keys_path` in supabase/config.toml and restart the stack, or pass { alg: \"HS256\" } to sign with the local JWT secret.");
|
|
166
|
+
const keys = JSON.parse(await readFile(file, "utf8"));
|
|
167
|
+
const first = Array.isArray(keys) ? keys[0] : void 0;
|
|
168
|
+
if (!isSigningKey(first)) throw new TypeError(`${file} has no private ES256 key first.`);
|
|
169
|
+
return first;
|
|
170
|
+
}
|
|
171
|
+
/** Signs a token with the local stack's ES256 key (see `localSigningKey`). Test-only. */
|
|
172
|
+
async function signLocalJwt(claims, path) {
|
|
173
|
+
return signTestJwtWithKey(await localSigningKey(path), claims);
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
126
176
|
//#region src/testing/as-user.ts
|
|
127
177
|
/** The local CLI stack's default JWT secret. */
|
|
128
178
|
const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long";
|
|
@@ -133,12 +183,30 @@ function env$1(...names) {
|
|
|
133
183
|
if (value) return value;
|
|
134
184
|
}
|
|
135
185
|
}
|
|
186
|
+
const LOCAL_HOSTS = /* @__PURE__ */ new Set([
|
|
187
|
+
"localhost",
|
|
188
|
+
"127.0.0.1",
|
|
189
|
+
"[::1]",
|
|
190
|
+
"host.docker.internal"
|
|
191
|
+
]);
|
|
192
|
+
async function signFor(stack, url, claims) {
|
|
193
|
+
const alg = stack.alg ?? "ES256";
|
|
194
|
+
switch (alg) {
|
|
195
|
+
case "ES256": return signTestJwtWithKey(stack.signingKey ?? await localSigningKey(stack.signingKeysPath), claims);
|
|
196
|
+
case "HS256":
|
|
197
|
+
if (!LOCAL_HOSTS.has(new URL(url).hostname)) throw new TypeError(`asUser signs HS256 tokens only for a local stack, not ${url}. Use the ES256 key from \`better-supabase keys\`.`);
|
|
198
|
+
return signTestJwt(stack.jwtSecret ?? env$1("SUPABASE_JWT_SECRET") ?? "super-secret-jwt-token-with-at-least-32-characters-long", claims);
|
|
199
|
+
default: throw new TypeError(`asUser: unknown alg "${String(alg)}"`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
136
202
|
/**
|
|
137
203
|
* A test user against the local stack: a signed token and repositories that
|
|
138
|
-
* run as that user, for live RLS tests.
|
|
204
|
+
* run as that user, for live RLS tests. The token is ES256-signed with the
|
|
205
|
+
* key from `better-supabase keys`; pass `{ alg: 'HS256' }` for a stack that
|
|
206
|
+
* still uses the shared JWT secret.
|
|
139
207
|
*
|
|
140
208
|
* ```ts
|
|
141
|
-
* const alice = await asUser(sb, { sub: aliceId,
|
|
209
|
+
* const alice = await asUser(sb, { sub: aliceId, tenant_id: acme });
|
|
142
210
|
* expect(await alice.db.customers.count().orThrow()).toBe(3);
|
|
143
211
|
* ```
|
|
144
212
|
*/
|
|
@@ -146,7 +214,7 @@ async function asUser(sb, claims, stack = {}) {
|
|
|
146
214
|
const url = stack.url ?? env$1(...ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
|
|
147
215
|
const publishableKey = stack.publishableKey ?? env$1(...ENV_VARIABLES.publishableKey, "SUPABASE_ANON_KEY");
|
|
148
216
|
if (!publishableKey) throw new TypeError("asUser needs publishableKey or $SUPABASE_PUBLISHABLE_KEY. Run `better-supabase env` to write it to .env.local.");
|
|
149
|
-
const token = await
|
|
217
|
+
const token = await signFor(stack, url, claims);
|
|
150
218
|
const full = {
|
|
151
219
|
role: "authenticated",
|
|
152
220
|
...claims
|
|
@@ -313,7 +381,7 @@ function testExecutor(executor, options) {
|
|
|
313
381
|
const { sb } = options;
|
|
314
382
|
const table = tableKey(sb, options.table);
|
|
315
383
|
const context = { errorMappers: [] };
|
|
316
|
-
const read = () => captureOp(sb, (db) => db[table]
|
|
384
|
+
const read = () => captureOp(sb, (db) => db[table]["findMany"]({ limit: 2 }));
|
|
317
385
|
const create = options.create;
|
|
318
386
|
return conform(`Executor "${executor.name}"`, [
|
|
319
387
|
hasName(executor),
|
|
@@ -321,7 +389,7 @@ function testExecutor(executor, options) {
|
|
|
321
389
|
const op = await read();
|
|
322
390
|
const result = await executor.execute(op, context);
|
|
323
391
|
expect(result.ok, `select failed: ${result.ok ? "" : result.error.message}`);
|
|
324
|
-
expect(
|
|
392
|
+
expect(isList(result.data.rows), "rows must be an array");
|
|
325
393
|
expect(result.data.rows.length <= 2, `limit 2 returned ${result.data.rows.length} rows`);
|
|
326
394
|
expect(result.data.count === null || typeof result.data.count === "number", "count must be a number or null");
|
|
327
395
|
if (op.kind !== "select") return;
|
|
@@ -332,7 +400,7 @@ function testExecutor(executor, options) {
|
|
|
332
400
|
}
|
|
333
401
|
}],
|
|
334
402
|
["counts rows", async () => {
|
|
335
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
403
|
+
const op = await captureOp(sb, (db) => db[table]["count"]());
|
|
336
404
|
const result = await executor.execute(op, context);
|
|
337
405
|
expect(result.ok, `count failed: ${result.ok ? "" : result.error.message}`);
|
|
338
406
|
expect(typeof result.data.count === "number" && result.data.count >= 0, "count must be a number");
|
|
@@ -380,7 +448,7 @@ function testExecutor(executor, options) {
|
|
|
380
448
|
}],
|
|
381
449
|
executor.batch && ["batch returns one result per operation, in order", async () => {
|
|
382
450
|
const rows = await read();
|
|
383
|
-
const count = await captureOp(sb, (db) => db[table]
|
|
451
|
+
const count = await captureOp(sb, (db) => db[table]["count"]());
|
|
384
452
|
const missing = {
|
|
385
453
|
...rows,
|
|
386
454
|
table: {
|
|
@@ -401,15 +469,15 @@ function testExecutor(executor, options) {
|
|
|
401
469
|
}],
|
|
402
470
|
create && ["round-trips a write", async () => {
|
|
403
471
|
const repository = sb.connect(executor)[table];
|
|
404
|
-
const created = await repository
|
|
472
|
+
const created = await repository["create"](create);
|
|
405
473
|
expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
|
|
406
474
|
const row = created.data;
|
|
407
475
|
for (const [key, value] of Object.entries(create)) expect(same(row[key], value), `created row has ${key} = ${JSON.stringify(row[key])}`);
|
|
408
476
|
const [primary] = sb.meta.tables[table].primaryKey;
|
|
409
477
|
expect(primary, "write checks need a table with a primary key");
|
|
410
|
-
const removed = await repository
|
|
478
|
+
const removed = await repository["delete"](row[primary]);
|
|
411
479
|
expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
|
|
412
|
-
const again = await repository
|
|
480
|
+
const again = await repository["findFirst"]({ where: { [primary]: row[primary] } });
|
|
413
481
|
expect(again.ok && again.data === null, "the deleted row is still readable");
|
|
414
482
|
}]
|
|
415
483
|
]);
|
|
@@ -557,7 +625,7 @@ function testGenerator(generator, options) {
|
|
|
557
625
|
hasName(generator),
|
|
558
626
|
["writes files inside the project", async () => {
|
|
559
627
|
const files = await generator.generate(input());
|
|
560
|
-
expect(
|
|
628
|
+
expect(isList(files), "generate() must return an array");
|
|
561
629
|
for (const file of files) {
|
|
562
630
|
expect(typeof file.path === "string" && typeof file.contents === "string", "files need a path and contents");
|
|
563
631
|
expect(!file.path.startsWith("/") && !/^[A-Za-z]:/.test(file.path), `${file.path} must be relative`);
|
|
@@ -598,14 +666,14 @@ function testPlugin(plugin, options) {
|
|
|
598
666
|
for (const key of Object.keys(sb.meta.tables)) expect(db[key], `db.${key} is missing`);
|
|
599
667
|
}],
|
|
600
668
|
plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
|
|
601
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
669
|
+
const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
|
|
602
670
|
const first = plugin.transformQuery(frozenCopy(op), hook(op));
|
|
603
671
|
const second = plugin.transformQuery(frozenCopy(op), hook(op));
|
|
604
672
|
expect(typeof first === "object" && first !== null && first.kind === op.kind, "must return an operation of the same kind");
|
|
605
673
|
expect(same(first, second), "two calls with the same input differ");
|
|
606
674
|
}],
|
|
607
675
|
plugin.beforeMutation && create && ["beforeMutation is pure and deterministic", async () => {
|
|
608
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
676
|
+
const op = await captureOp(sb, (db) => db[table]["create"](create));
|
|
609
677
|
if (op.kind === "select") throw new Violation("create did not produce a mutation");
|
|
610
678
|
const first = await plugin.beforeMutation(frozenCopy(op), hook(op));
|
|
611
679
|
const second = await plugin.beforeMutation(frozenCopy(op), hook(op));
|
|
@@ -613,7 +681,7 @@ function testPlugin(plugin, options) {
|
|
|
613
681
|
expect(same(first, second), "two calls with the same input differ");
|
|
614
682
|
}],
|
|
615
683
|
plugin.wrapExecutor && ["wrapExecutor keeps results intact", async () => {
|
|
616
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
684
|
+
const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
|
|
617
685
|
const data = {
|
|
618
686
|
rows: [{ marker: "conformance" }],
|
|
619
687
|
count: 1
|
|
@@ -664,8 +732,8 @@ function env(names) {
|
|
|
664
732
|
* ```ts
|
|
665
733
|
* await expectTenantIsolation(sb, {
|
|
666
734
|
* tenants: [
|
|
667
|
-
* { id: ACME, claims: { sub: alice,
|
|
668
|
-
* { id: GLOBEX, claims: { sub: bob,
|
|
735
|
+
* { id: ACME, claims: { sub: alice, tenant_id: ACME } },
|
|
736
|
+
* { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
|
|
669
737
|
* ],
|
|
670
738
|
* tables: {
|
|
671
739
|
* tags: {
|
|
@@ -748,4 +816,4 @@ async function expectTenantIsolation(sb, options) {
|
|
|
748
816
|
}
|
|
749
817
|
}
|
|
750
818
|
//#endregion
|
|
751
|
-
export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, signTestJwt, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testPlugin };
|
|
819
|
+
export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testPlugin };
|
|
@@ -99,16 +99,69 @@ interface SchemaMeta {
|
|
|
99
99
|
readonly topics?: Readonly<Record<string, string>>;
|
|
100
100
|
/** Tables that broadcast change signals (`config.realtime.tables`), keyed by app key. */
|
|
101
101
|
readonly realtime?: Readonly<Record<string, RealtimeTableMeta>>;
|
|
102
|
+
/** Claim names from `config.claims` that differ from the defaults. */
|
|
103
|
+
readonly claims?: Partial<ClaimsMeta>;
|
|
104
|
+
}
|
|
105
|
+
/** The claim names the SQL kit, codegen and runtime defaults agree on. */
|
|
106
|
+
interface ClaimsMeta {
|
|
107
|
+
/** Top-level claim holding the active tenant id. Defaults to `tenant_id`. */
|
|
108
|
+
readonly tenant: string;
|
|
109
|
+
/** `scope` of the tenant entries in the `memberships` claim. Defaults to `tenant`. */
|
|
110
|
+
readonly scope: string;
|
|
111
|
+
/** Claim holding plan features per tenant (`{ [tenantId]: string[] }`). Defaults to `features`. */
|
|
112
|
+
readonly features: string;
|
|
102
113
|
}
|
|
103
114
|
interface RealtimeTableMeta {
|
|
104
115
|
/** App column whose value scopes the broadcast topic. */
|
|
105
116
|
readonly tenant?: string;
|
|
106
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Storage policies that call PermDock's generated SQL helpers:
|
|
120
|
+
* `<schema>.permitted_<scope>_ids(key)` for a scope keyed by a path segment,
|
|
121
|
+
* `<schema>.permdock_has(key)` for `scope: 'global'`. Only for permissions
|
|
122
|
+
* whose grants have no row conditions beyond the scope.
|
|
123
|
+
*/
|
|
124
|
+
interface PermdockBucketPolicy {
|
|
125
|
+
readonly permdock: {
|
|
126
|
+
/** Downloads, signed URLs, renders and metadata reads. */
|
|
127
|
+
readonly read: string;
|
|
128
|
+
/** Listing; without it `read` covers listing too. */
|
|
129
|
+
readonly list?: string;
|
|
130
|
+
/** Uploads, updates and moves; also deletes unless `delete` is set. */
|
|
131
|
+
readonly write: string;
|
|
132
|
+
readonly delete?: string;
|
|
133
|
+
};
|
|
134
|
+
/** A PermDock scope such as `organization`, or `global`. */
|
|
135
|
+
readonly scope: string;
|
|
136
|
+
/** 1-based path segment holding the scope id. Defaults to the `{orgId}` segment. */
|
|
137
|
+
readonly segment?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Schema of the helpers `permdock rls generate` writes: PermDock's
|
|
140
|
+
* `rls.schema`. Defaults to `public`.
|
|
141
|
+
*/
|
|
142
|
+
readonly schema?: string;
|
|
143
|
+
}
|
|
144
|
+
/** A PermDock topic policy: `select` (receive) and `insert` (send) on `realtime.messages`. */
|
|
145
|
+
interface PermdockTopicPolicy {
|
|
146
|
+
readonly receive: string;
|
|
147
|
+
/** Lets clients send on the topic with this permission. */
|
|
148
|
+
readonly send?: string;
|
|
149
|
+
/** A PermDock scope such as `organization`, or `global`. */
|
|
150
|
+
readonly scope: string;
|
|
151
|
+
/** 1-based `:`-separated topic segment holding the scope id. Defaults to the `{orgId}` segment. */
|
|
152
|
+
readonly segment?: number;
|
|
153
|
+
readonly schema?: string;
|
|
154
|
+
}
|
|
155
|
+
type BucketPolicyName = "tenant" | "owner" | "public" | "none";
|
|
107
156
|
interface BucketMeta {
|
|
108
157
|
readonly id: string;
|
|
109
158
|
readonly public: boolean;
|
|
110
159
|
readonly path: string;
|
|
111
|
-
readonly policy?:
|
|
160
|
+
readonly policy?: BucketPolicyName | PermdockBucketPolicy;
|
|
161
|
+
/** Claim paths for the tenant policy, when `config.claims.tenant` is not the default. */
|
|
162
|
+
readonly tenant?: {
|
|
163
|
+
readonly claim: readonly string[];
|
|
164
|
+
};
|
|
112
165
|
readonly fileSizeLimit?: string;
|
|
113
166
|
readonly allowedMimeTypes?: readonly string[];
|
|
114
167
|
}
|
|
@@ -192,4 +245,4 @@ type IsUnion<T, U = T> = T extends unknown ? [U] extends [T] ? false : true : ne
|
|
|
192
245
|
type PrimaryKeyValue<M extends AnyModels, T extends keyof M> = true extends IsUnion<PrimaryKeyColumn<M, T>> ? { readonly [K in PrimaryKeyColumn<M, T>]: Row<M, T>[K]; } : Row<M, T>[PrimaryKeyColumn<M, T>];
|
|
193
246
|
type Simplify<T> = { [K in keyof T]: T[K]; } & {};
|
|
194
247
|
//#endregion
|
|
195
|
-
export {
|
|
248
|
+
export { SchemaMeta as A, PrimaryKeyValue as C, Relations as D, RelationShape as E, UniqueConstraint as F, UniqueKeyName as I, UniqueWhere as L, TableFlags as M, TableKey as N, Row as O, TableMeta as P, Update as R, PrimaryKeyColumn as S, RelationMeta as T, Insert as _, Casing as a, PermdockBucketPolicy as b, Codec as c, Flags as d, FlagsShape as f, FunctionsOf as g, FunctionShape as h, BucketPolicyName as i, Simplify as j, Schema as k, ColumnMeta as l, FunctionMeta as m, AnyModels as n, CheckConstraint as o, ForeignKeyConstraint as p, BucketMeta as r, ClaimsMeta as s, AnyFunctions as t, DatabaseOf as u, ModelShape as v, RealtimeTableMeta as w, PermdockTopicPolicy as x, ModelsOf as y };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { p as DbError } from "./result-CTkCZ6JA.js";
|
|
2
2
|
import { n as AmrEntry, s as Impersonator, t as Aal } from "./mfa-TVia554c.js";
|
|
3
|
-
import { i as InvalidReason, r as AuthState } from "./resolve-
|
|
3
|
+
import { i as InvalidReason, r as AuthState } from "./resolve-CjHILk2w.js";
|
|
4
4
|
import { JWTClaims, UserClaims } from "@supabase/server";
|
|
5
5
|
//#region src/auth/view.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* The verified caller as plain data: no token, no clients. Safe to return
|
|
8
8
|
* from a `'use cache: private'` function and to pass to Client Components.
|
|
9
9
|
*/
|
|
10
|
-
type AuthSession<C = unknown> = {
|
|
10
|
+
type AuthSession<C = unknown, P = unknown> = {
|
|
11
11
|
readonly kind: "user";
|
|
12
12
|
readonly user: UserClaims;
|
|
13
13
|
/**
|
|
@@ -17,6 +17,12 @@ type AuthSession<C = unknown> = {
|
|
|
17
17
|
readonly claims: JWTClaims & C;
|
|
18
18
|
/** Seconds since epoch, from the token's `exp`. */
|
|
19
19
|
readonly expiresAt: number | null;
|
|
20
|
+
/**
|
|
21
|
+
* `user_metadata` parsed by `sb.userMetadata(schema)`, for display.
|
|
22
|
+
* Absent without a schema or when the metadata fails it. Users
|
|
23
|
+
* write this data with `auth.updateUser()`: never base access on it.
|
|
24
|
+
*/
|
|
25
|
+
readonly profile?: P;
|
|
20
26
|
/** `aal2` once the user verified a second factor in this session. */
|
|
21
27
|
readonly aal: Aal;
|
|
22
28
|
/** How the user signed in (`password`, `totp`, `sso/saml`, ...). */
|
|
@@ -38,6 +44,6 @@ type AnonReason = Extract<AuthState, {
|
|
|
38
44
|
kind: "anon";
|
|
39
45
|
}>["reason"];
|
|
40
46
|
/** Drops the token from an `AuthState`, leaving only serializable fields. */
|
|
41
|
-
declare function toSession<C>(auth: AuthState<C>): AuthSession<C>;
|
|
47
|
+
declare function toSession<C, P>(auth: AuthState<C, P>): AuthSession<C, P>;
|
|
42
48
|
//#endregion
|
|
43
49
|
export { toSession as n, AuthSession as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as impersonatorOf, n as amrOf, t as aalOf } from "./mfa-
|
|
1
|
+
import { g as impersonatorOf, n as amrOf, t as aalOf } from "./mfa-CpLfJSde.js";
|
|
2
2
|
//#region src/auth/view.ts
|
|
3
3
|
/** Drops the token from an `AuthState`, leaving only serializable fields. */
|
|
4
4
|
function toSession(auth) {
|
|
@@ -10,6 +10,7 @@ function toSession(auth) {
|
|
|
10
10
|
user: auth.user,
|
|
11
11
|
claims: auth.claims,
|
|
12
12
|
expiresAt: auth.expiresAt,
|
|
13
|
+
...auth.profile === void 0 ? {} : { profile: auth.profile },
|
|
13
14
|
aal: aalOf(auth.claims),
|
|
14
15
|
amr: amrOf(auth.claims),
|
|
15
16
|
...impersonator ? { impersonator } : {}
|
package/dist/webhooks/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { N as TableKey, O as Row, n as AnyModels, t as AnyFunctions } from "../types-C83v5SQz.js";
|
|
2
2
|
import { o as Result } from "../result-CTkCZ6JA.js";
|
|
3
|
-
import { t as BetterSupabase } from "../define-
|
|
3
|
+
import { t as BetterSupabase } from "../define-e3zI4_4g.js";
|
|
4
4
|
//#region src/webhooks/index.d.ts
|
|
5
5
|
export interface VerifiedWebhook<T = unknown> {
|
|
6
6
|
readonly id: string;
|
|
@@ -125,7 +125,7 @@ export declare function hookError(status: number, message: string): AuthHookErro
|
|
|
125
125
|
*
|
|
126
126
|
* ```ts
|
|
127
127
|
* export const POST = authHook('custom_access_token', env.AUTH_HOOK_SECRET, async ({ claims, user_id }) => ({
|
|
128
|
-
* claims: { ...claims,
|
|
128
|
+
* claims: { ...claims, tenant_id: await orgFor(user_id) },
|
|
129
129
|
* }));
|
|
130
130
|
* ```
|
|
131
131
|
*/
|
package/dist/webhooks/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as signWebhook, c as verifyWebhook, i as isDatabaseWebhook, n as databaseChange, o as timingSafeEqual, r as hookError, s as verifySharedSecret, t as authHook } from "../webhooks-
|
|
1
|
+
import { a as signWebhook, c as verifyWebhook, i as isDatabaseWebhook, n as databaseChange, o as timingSafeEqual, r as hookError, s as verifySharedSecret, t as authHook } from "../webhooks-DMipjhSP.js";
|
|
2
2
|
export { authHook, databaseChange, hookError, isDatabaseWebhook, signWebhook, timingSafeEqual, verifySharedSecret, verifyWebhook };
|
|
@@ -98,7 +98,7 @@ function hookError(status, message) {
|
|
|
98
98
|
*
|
|
99
99
|
* ```ts
|
|
100
100
|
* export const POST = authHook('custom_access_token', env.AUTH_HOOK_SECRET, async ({ claims, user_id }) => ({
|
|
101
|
-
* claims: { ...claims,
|
|
101
|
+
* claims: { ...claims, tenant_id: await orgFor(user_id) },
|
|
102
102
|
* }));
|
|
103
103
|
* ```
|
|
104
104
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-supabase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Strongly typed Supabase: generated schema, typed repositories, plugins, auth glue and framework integrations, with a CLI from day one.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hono",
|
|
@@ -184,6 +184,7 @@
|
|
|
184
184
|
},
|
|
185
185
|
"devDependencies": {
|
|
186
186
|
"@arethetypeswrong/cli": "0.18.5",
|
|
187
|
+
"@better-supabase/ox-config": "0.0.0",
|
|
187
188
|
"@better-supabase/typescript-config": "0.0.0",
|
|
188
189
|
"@effect/platform-node": "4.0.0-rc.117",
|
|
189
190
|
"@opentelemetry/api": "1.9.1",
|
|
@@ -192,15 +193,17 @@
|
|
|
192
193
|
"@supabase/supabase-js": "2.117.2",
|
|
193
194
|
"@tanstack/query-core": "5.104.0",
|
|
194
195
|
"@tanstack/react-query": "5.104.0",
|
|
195
|
-
"@types/node": "
|
|
196
|
+
"@types/node": "24.19.0",
|
|
196
197
|
"@types/pg": "8.23.1",
|
|
197
198
|
"@types/react": "19.3.0",
|
|
198
199
|
"@types/react-dom": "19.3.0",
|
|
200
|
+
"@valibot/to-json-schema": "1.8.0",
|
|
199
201
|
"@vitest/coverage-v8": "5.0.2",
|
|
200
202
|
"effect": "4.0.0-rc.117",
|
|
201
203
|
"hono": "4.13.10",
|
|
202
|
-
"next": "16.3.
|
|
204
|
+
"next": "16.3.7",
|
|
203
205
|
"oxfmt": "0.71.0",
|
|
206
|
+
"oxlint": "1.86.0",
|
|
204
207
|
"pg": "8.23.0",
|
|
205
208
|
"publint": "0.3.24",
|
|
206
209
|
"react": "19.3.0",
|
|
@@ -252,9 +255,10 @@
|
|
|
252
255
|
"node": ">=24"
|
|
253
256
|
},
|
|
254
257
|
"scripts": {
|
|
255
|
-
"bench": "node
|
|
258
|
+
"bench": "node tests/next/next.bench.ts",
|
|
256
259
|
"build": "tsdown",
|
|
257
260
|
"check:publish": "publint && pnpm pack --out pack.tgz && attw --profile esm-only pack.tgz && rm -f pack.tgz",
|
|
261
|
+
"lint": "oxlint",
|
|
258
262
|
"test": "vitest run --coverage",
|
|
259
263
|
"typecheck": "tsc --noEmit --checkers 4"
|
|
260
264
|
}
|
package/schemas/config-v1.json
CHANGED
|
@@ -148,8 +148,7 @@
|
|
|
148
148
|
"type": "object",
|
|
149
149
|
"additionalProperties": false,
|
|
150
150
|
"properties": {
|
|
151
|
-
"column": { "type": "string" }
|
|
152
|
-
"claim": { "type": "string" }
|
|
151
|
+
"column": { "type": "string" }
|
|
153
152
|
}
|
|
154
153
|
}
|
|
155
154
|
]
|
|
@@ -169,6 +168,25 @@
|
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
},
|
|
171
|
+
"claims": {
|
|
172
|
+
"description": "Claim names shared by the SQL kit, codegen and runtime defaults (PermDock's claim contract).",
|
|
173
|
+
"type": "object",
|
|
174
|
+
"additionalProperties": false,
|
|
175
|
+
"properties": {
|
|
176
|
+
"tenant": {
|
|
177
|
+
"type": "string",
|
|
178
|
+
"description": "Top-level claim holding the active tenant id. Defaults to `tenant_id`."
|
|
179
|
+
},
|
|
180
|
+
"scope": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"description": "`scope` of the tenant entries in the `memberships` claim. Defaults to `tenant`."
|
|
183
|
+
},
|
|
184
|
+
"features": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "Claim holding plan features per tenant. Defaults to `features`."
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
172
190
|
"buckets": {
|
|
173
191
|
"type": "object",
|
|
174
192
|
"additionalProperties": {
|
|
@@ -182,7 +200,33 @@
|
|
|
182
200
|
"type": "string",
|
|
183
201
|
"description": "Path template, e.g. `{orgId}/{customerId}/logo.webp`."
|
|
184
202
|
},
|
|
185
|
-
"policy": {
|
|
203
|
+
"policy": {
|
|
204
|
+
"oneOf": [
|
|
205
|
+
{ "enum": ["tenant", "owner", "public", "none"] },
|
|
206
|
+
{
|
|
207
|
+
"type": "object",
|
|
208
|
+
"description": "Policies that call PermDock's permitted_<scope>_ids or permdock_has helpers.",
|
|
209
|
+
"required": ["permdock", "scope"],
|
|
210
|
+
"additionalProperties": false,
|
|
211
|
+
"properties": {
|
|
212
|
+
"permdock": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"required": ["read", "write"],
|
|
215
|
+
"additionalProperties": false,
|
|
216
|
+
"properties": {
|
|
217
|
+
"read": { "type": "string" },
|
|
218
|
+
"list": { "type": "string" },
|
|
219
|
+
"write": { "type": "string" },
|
|
220
|
+
"delete": { "type": "string" }
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"scope": { "type": "string" },
|
|
224
|
+
"segment": { "type": "integer", "minimum": 1 },
|
|
225
|
+
"schema": { "type": "string" }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
186
230
|
"fileSizeLimit": { "type": "string" },
|
|
187
231
|
"allowedMimeTypes": { "type": "array", "items": { "type": "string" } }
|
|
188
232
|
}
|
|
@@ -314,7 +358,8 @@
|
|
|
314
358
|
},
|
|
315
359
|
"strict": { "type": "boolean" },
|
|
316
360
|
"sources": { "type": "array", "items": { "type": "string" } },
|
|
317
|
-
"policyHelperLimit": { "type": "integer", "minimum": 1 }
|
|
361
|
+
"policyHelperLimit": { "type": "integer", "minimum": 1 },
|
|
362
|
+
"claimsLimit": { "type": "integer", "minimum": 1 }
|
|
318
363
|
}
|
|
319
364
|
}
|
|
320
365
|
},
|
package/schemas/snapshot-v2.json
CHANGED
|
@@ -53,4 +53,13 @@ filled it.
|
|
|
53
53
|
RLS still decides what a user can read and write. `tenant()` makes queries
|
|
54
54
|
explicit and fills the column; it doesn't replace the policy.
|
|
55
55
|
|
|
56
|
+
The tenant comes from the verified `tenant_id` claim (renamed with
|
|
57
|
+
`claims.tenant` in `better-supabase.config.ts`), then `app_metadata.tenant_id`.
|
|
58
|
+
Never read it from `user_metadata`, a URL or a request body. When the project
|
|
59
|
+
has a `permdock.config.ts`, PermDock's hook writes that claim and the
|
|
60
|
+
memberships; don't run `sql add tenant` there or call `membership_claims()`
|
|
61
|
+
from a hook. `sql add entitlements` is fine: `features` is not a PermDock
|
|
62
|
+
claim, but PermDock's hook doesn't write it yet, so read entitlements from the
|
|
63
|
+
database on the server instead of `hasEntitlement(session, ...)`.
|
|
64
|
+
|
|
56
65
|
Docs: https://bettersupabase.com/docs/plugins.md
|
|
@@ -60,8 +60,8 @@ and a replayed webhook or POST doesn't run twice.
|
|
|
60
60
|
|
|
61
61
|
## Testing
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
Sign tokens with `signLocalJwt` or `asUser` from `better-supabase/testing`;
|
|
64
|
+
the adapter verifies them against the local JWKS. See the `better-supabase-testing` skill.
|
|
65
65
|
|
|
66
66
|
Docs: https://bettersupabase.com/docs/frameworks/hono.md (and `next`,
|
|
67
67
|
`orpc`, `edge`, `mcp` under `/docs/frameworks/`).
|
|
@@ -25,6 +25,7 @@ again once you restore it.
|
|
|
25
25
|
## Setup
|
|
26
26
|
|
|
27
27
|
- `better-supabase env` writes the URL and keys to `.env.local`; load it in the test setup.
|
|
28
|
+
- `better-supabase keys` creates `supabase/signing_keys.json`. Set `signing_keys_path = "./signing_keys.json"` under `[auth]` in `supabase/config.toml`, gitignore the file and restart the stack. `asUser` signs ES256 tokens with it.
|
|
28
29
|
- Typed fixtures live in `supabase/seed.ts`:
|
|
29
30
|
|
|
30
31
|
```ts
|
|
@@ -43,7 +44,7 @@ export const seed = defineSeed(sb, {
|
|
|
43
44
|
## RLS tests
|
|
44
45
|
|
|
45
46
|
```ts
|
|
46
|
-
const alice = await asUser(sb, { sub: aliceId,
|
|
47
|
+
const alice = await asUser(sb, { sub: aliceId, tenant_id: ACME }, { postgres });
|
|
47
48
|
expect(await alice.db.customers.count().orThrow()).toBe(1);
|
|
48
49
|
expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok: false });
|
|
49
50
|
```
|
|
@@ -54,8 +55,8 @@ expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok
|
|
|
54
55
|
|
|
55
56
|
## API tests
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
Send `authorization: Bearer ${alice.token}` (or a token from `signLocalJwt`).
|
|
59
|
+
The adapter verifies it against the local JWKS; no test-only resolver is needed.
|
|
59
60
|
|
|
60
61
|
## pgTAP
|
|
61
62
|
|