better-supabase 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +34 -31
  2. package/dist/base64-CQeB1eSf.js +42 -0
  3. package/dist/claims-CoTobNC-.js +25 -0
  4. package/dist/client/index.d.ts +1 -1
  5. package/dist/client/index.js +4 -14
  6. package/dist/{compiler-DL18ukPe.d.ts → compiler-2zqGzxVZ.d.ts} +2 -2
  7. package/dist/config/index.d.ts +4 -3
  8. package/dist/config/index.js +5 -3
  9. package/dist/config-Be4GwsqI.js +129 -0
  10. package/dist/{index-BD6wKthp.d.ts → config-C1CYcm6o.d.ts} +75 -54
  11. package/dist/{config-D-aR2ZoN.js → config-DOsGYSiT.js} +14 -136
  12. package/dist/{define-QGkq04oe.js → define-DF341GDj.js} +133 -110
  13. package/dist/{define-CTfWw-SS.d.ts → define-DUbydmJa.d.ts} +32 -14
  14. package/dist/edge/index.d.ts +4 -4
  15. package/dist/edge/index.js +4 -4
  16. package/dist/entitlements-CTEBm7IB.js +20 -0
  17. package/dist/entitlements-DvJXoI02.d.ts +42 -0
  18. package/dist/env/index.js +203 -1
  19. package/dist/events/index.d.ts +1 -1
  20. package/dist/events/index.js +139 -1
  21. package/dist/{events-2OJsVG_d.d.ts → events-Do8jXh4D.d.ts} +3 -3
  22. package/dist/{executor-Bje4suQF.d.ts → executor-B26lgbHD.d.ts} +2 -2
  23. package/dist/{executor-Ceq60hPM.js → executor-C57jx2pn.js} +7 -2
  24. package/dist/{executor-Bazhgsgq.d.ts → executor-DynmIWhq.d.ts} +2 -2
  25. package/dist/guards-BnzDvFrz.js +10 -0
  26. package/dist/hono/index.d.ts +5 -5
  27. package/dist/hono/index.js +4 -4
  28. package/dist/{index-Hf8PP9sP.d.ts → index-BNsABq7P.d.ts} +17 -13
  29. package/dist/{index-JXHBuS53.d.ts → index-CADlNHK-.d.ts} +27 -14
  30. package/dist/{index-Nxo3TJcm.d.ts → index-CTTMS6_1.d.ts} +27 -8
  31. package/dist/index-Cf-D2jk1.d.ts +50 -0
  32. package/dist/{index-Dup-nSe9.d.ts → index-CriFFJ_p.d.ts} +4 -4
  33. package/dist/{index-DQVvHzjW.d.ts → index-GV77NpRP.d.ts} +5 -4
  34. package/dist/{index-Sw43J8HG.d.ts → index-UJ-LUxtI.d.ts} +6 -6
  35. package/dist/{index-cEeweTeM.d.ts → index-kpgopLNv.d.ts} +4 -3
  36. package/dist/{index-DRid0qIv.d.ts → index-qriRM9cZ.d.ts} +7 -2
  37. package/dist/index.d.ts +13 -13
  38. package/dist/index.js +18 -8
  39. package/dist/jobs/index.d.ts +25 -24
  40. package/dist/jobs/index.js +19 -12
  41. package/dist/{json-schema-BbwO2mZg.js → json-schema--aOXAGfU.js} +9 -3
  42. package/dist/lint/index.js +1 -1
  43. package/dist/list/index.d.ts +2 -2
  44. package/dist/list/index.js +128 -93
  45. package/dist/{live-6b9i72Ux.d.ts → live-QE3UPbGm.d.ts} +27 -6
  46. package/dist/logger-glM28NVN.js +21 -0
  47. package/dist/mcp/index.d.ts +59 -15
  48. package/dist/mcp/index.js +229 -67
  49. package/dist/{mfa-CpogP66z.js → mfa-BxoR1RkN.js} +97 -22
  50. package/dist/{mfa-TVia554c.d.ts → mfa-x3V3vGh-.d.ts} +1 -1
  51. package/dist/next/index.d.ts +51 -38
  52. package/dist/next/index.js +31 -25
  53. package/dist/openapi/index.d.ts +1 -1
  54. package/dist/openapi/index.js +58 -35
  55. package/dist/orpc/index.d.ts +6 -6
  56. package/dist/orpc/index.js +3 -3
  57. package/dist/otel/index.d.ts +3 -3
  58. package/dist/otel/index.js +5 -3
  59. package/dist/{template-BeOGnynM.js → permdock-sql-Bta_G0v-.js} +32 -3
  60. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  61. package/dist/{plugin-HvGqCurB.d.ts → plugin-DVRB8qNE.d.ts} +5 -4
  62. package/dist/plugins/actor/index.d.ts +1 -1
  63. package/dist/plugins/rules/index.d.ts +4 -3
  64. package/dist/plugins/rules/index.js +8 -15
  65. package/dist/plugins/soft-delete/index.d.ts +3 -3
  66. package/dist/plugins/soft-delete/index.js +2 -2
  67. package/dist/plugins/tenant/index.d.ts +10 -4
  68. package/dist/plugins/tenant/index.js +15 -13
  69. package/dist/plugins/timestamps/index.d.ts +1 -1
  70. package/dist/plugins/timestamps/index.js +1 -1
  71. package/dist/plugins/validation/index.d.ts +2 -2
  72. package/dist/pool-B9qsLyFR.d.ts +71 -0
  73. package/dist/postgres/index.d.ts +10 -5
  74. package/dist/postgres/index.js +36 -16
  75. package/dist/{postgrest-Dm-vhY6R.d.ts → postgrest-DmBlevh-.d.ts} +1 -1
  76. package/dist/{problem-DPu6Fh-t.js → problem-CGt1ADix.js} +14 -8
  77. package/dist/{problem-DlgB0lYl.d.ts → problem-Z2RTQjZD.d.ts} +6 -4
  78. package/dist/query/index.d.ts +2 -2
  79. package/dist/query/index.js +1 -1
  80. package/dist/{query-ChvDbsDE.js → query-BdKLTPlj.js} +2 -2
  81. package/dist/react/index.d.ts +7 -7
  82. package/dist/react/index.js +3 -3
  83. package/dist/react/server.d.ts +5 -5
  84. package/dist/react/server.js +1 -1
  85. package/dist/react/session.d.ts +21 -2
  86. package/dist/react/session.js +19 -1
  87. package/dist/{react-CrOvrnIP.js → react-DNBYNazu.js} +28 -23
  88. package/dist/{read-set-DhX2d04c.js → read-set-D0qBcboM.js} +3 -3
  89. package/dist/realtime/index.d.ts +4 -2
  90. package/dist/realtime/index.js +30 -14
  91. package/dist/{resolve-DW8h9NPm.d.ts → resolve-BRciX2jK.d.ts} +25 -9
  92. package/dist/{resource-SkPTRJDD.d.ts → resource-CwVgiI5q.d.ts} +16 -7
  93. package/dist/{resource-CYiu6Ygi.js → resource-D30rp0GN.js} +38 -16
  94. package/dist/{respond-4-dhoV7x.js → respond-C44EZAJo.js} +38 -19
  95. package/dist/{respond-PoTpy5DT.d.ts → respond-DAkPIdTH.d.ts} +23 -17
  96. package/dist/{result-CTkCZ6JA.d.ts → result-c2OYgCV2.d.ts} +13 -8
  97. package/dist/server/index.d.ts +35 -10
  98. package/dist/server/index.js +38 -9
  99. package/dist/{simplify-oHiDKRVQ.js → simplify-LakqK1vH.js} +22 -28
  100. package/dist/{spec-pins-t6sPaHvl.js → spec-pins-C3oc-Tdp.js} +1 -1
  101. package/dist/{spec-pins-CJPVghHy.d.ts → spec-pins-CqorWDnO.d.ts} +1 -1
  102. package/dist/sql/index.d.ts +139 -0
  103. package/dist/sql/index.js +1983 -0
  104. package/dist/{sql-M8l3fnS0.js → sql-BIgk12ew.js} +72 -16
  105. package/dist/ssr/index.d.ts +5 -5
  106. package/dist/ssr/index.js +3 -3
  107. package/dist/{standard-CyhRzctb.d.ts → standard-eudBiRk1.d.ts} +1 -1
  108. package/dist/{stats-3iy8u3_P.js → stats-D78UQ8Yu.js} +8 -7
  109. package/dist/storage/index.d.ts +4 -2
  110. package/dist/storage/index.js +530 -1
  111. package/dist/{tables-rZxRxNPW.js → tables-Dhha3BXM.js} +5 -5
  112. package/dist/temporal-BwshQk0W.js +27 -0
  113. package/dist/temporal-required-CMxeSVPo.js +19 -0
  114. package/dist/testing/index.d.ts +49 -16
  115. package/dist/testing/index.js +233 -66
  116. package/dist/{types-C2fslP1z.d.ts → types-DONv9SHN.d.ts} +68 -12
  117. package/dist/{version-Cqq1m3UY.js → version-BfpG2gks.js} +1 -1
  118. package/dist/view-3Po_PVlr.d.ts +79 -0
  119. package/dist/{view-B6F9QbG-.js → view-wZ-9qIUz.js} +12 -2
  120. package/dist/webhooks/index.d.ts +19 -17
  121. package/dist/webhooks/index.js +1 -1
  122. package/dist/{webhooks-DbMTJJX2.js → webhooks-CAIFQSTM.js} +12 -10
  123. package/package.json +30 -30
  124. package/schemas/config-v1.json +80 -5
  125. package/schemas/doctor-report-v1.json +18 -1
  126. package/schemas/snapshot-v2.json +39 -0
  127. package/skills/better-supabase/SKILL.md +23 -11
  128. package/skills/better-supabase/references/plugins.md +28 -16
  129. package/skills/better-supabase/references/troubleshooting.md +15 -15
  130. package/skills/better-supabase-api/SKILL.md +9 -9
  131. package/skills/better-supabase-api/references/adapters.md +30 -25
  132. package/skills/better-supabase-testing/SKILL.md +15 -11
  133. package/bin/better-supabase.js +0 -3
  134. package/dist/casing-da0uRTqt.js +0 -14
  135. package/dist/cli/bin.d.ts +0 -1
  136. package/dist/cli/bin.js +0 -15
  137. package/dist/cli/index.d.ts +0 -226
  138. package/dist/cli/index.js +0 -3
  139. package/dist/cli-CoByWYja.js +0 -6472
  140. package/dist/entitlements-BBg61DQZ.d.ts +0 -20
  141. package/dist/entitlements-BC_khwRy.js +0 -17
  142. package/dist/env-BEToab0I.js +0 -204
  143. package/dist/events-BuzTgn2D.js +0 -135
  144. package/dist/pool-BFeqzwQS.d.ts +0 -37
  145. package/dist/seed-CzecODcJ.js +0 -101
  146. package/dist/session-C3osL21k.d.ts +0 -21
  147. package/dist/session-CD-3orO_.js +0 -19
  148. package/dist/storage-BWXVUXGk.js +0 -461
  149. package/dist/view-B0ltUmTi.d.ts +0 -43
@@ -1,13 +1,15 @@
1
- import { d as GeneratorInput, u as Generator, v as ResolvedConfig } from "../index-BD6wKthp.js";
2
- import { E as SchemaMeta, P as Update, h as Insert, k as TableKey, n as AnyModels, t as AnyFunctions } from "../types-C2fslP1z.js";
3
- import { r as Executor } from "../executor-Bazhgsgq.js";
4
- import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-CTfWw-SS.js";
5
- import { h as RequestContext, n as AnyPlugin } from "../plugin-HvGqCurB.js";
6
- import { t as SqlClient } from "../executor-Bje4suQF.js";
7
- import { t as Postgres } from "../pool-BFeqzwQS.js";
8
- import { n as AuthResolver } from "../resolve-DW8h9NPm.js";
9
- import { r as EventSink, t as CloudEvent } from "../index-DQVvHzjW.js";
1
+ import { A as SchemaMeta, N as TableKey, R as Update, g as Insert, n as AnyModels, t as AnyFunctions } from "../types-DONv9SHN.js";
2
+ import { r as Executor } from "../executor-DynmIWhq.js";
3
+ import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-DUbydmJa.js";
4
+ import { h as RequestContext, n as AnyPlugin } from "../plugin-DVRB8qNE.js";
5
+ import { b as ResolvedConfig, d as Generator, f as GeneratorInput } from "../config-C1CYcm6o.js";
6
+ import "../index-Cf-D2jk1.js";
7
+ import { t as SqlClient } from "../executor-B26lgbHD.js";
8
+ import { r as Postgres } from "../pool-B9qsLyFR.js";
9
+ import { n as AuthResolver } from "../resolve-BRciX2jK.js";
10
+ import { r as EventSink, t as CloudEvent } from "../index-GV77NpRP.js";
10
11
  import { SupabaseClient } from "@supabase/supabase-js";
12
+ import { webcrypto } from "node:crypto";
11
13
  //#region src/testing/jwt.d.ts
12
14
  interface TestJwtClaims {
13
15
  readonly sub: string;
@@ -31,13 +33,32 @@ export declare function localAuth(secret: string): AuthResolver;
31
33
  interface TestSigner {
32
34
  /** Public JWKS; pass as `jwks` to `resolveAuth` / `createServer({ auth: { jwks } })`. */
33
35
  readonly jwks: {
34
- readonly keys: readonly JsonWebKey[];
36
+ readonly keys: readonly webcrypto.JsonWebKey[];
35
37
  };
36
38
  sign(claims: TestJwtClaims): Promise<string>;
37
39
  }
40
+ /** A private ES256 JWK, as `better-supabase keys` writes to `supabase/signing_keys.json`. */
41
+ interface SigningJwk extends webcrypto.JsonWebKey {
42
+ readonly kid: string;
43
+ }
44
+ /**
45
+ * Signs an ES256 access token with a private JWK, the way the local stack
46
+ * signs them once `signing_keys_path` is set. Test-only.
47
+ */
48
+ export declare function signTestJwtWithKey(key: SigningJwk, claims: TestJwtClaims): Promise<string>;
38
49
  /** An in-memory ES256 key pair that signs tokens like Supabase Auth's asymmetric keys. */
39
50
  export declare function createTestSigner(): Promise<TestSigner>;
40
51
  //#endregion
52
+ //#region src/testing/local-key.d.ts
53
+ /**
54
+ * The key the local stack signs with: the first entry of the file
55
+ * `better-supabase keys` wrote. `path` defaults to `$SUPABASE_SIGNING_KEYS_PATH`,
56
+ * then `signing_keys_path` from the nearest `supabase/config.toml`.
57
+ */
58
+ export declare function localSigningKey(path?: string): Promise<SigningJwk>;
59
+ /** Signs a token with the local stack's ES256 key (see `localSigningKey`). Test-only. */
60
+ export declare function signLocalJwt(claims: TestJwtClaims, path?: string): Promise<string>;
61
+ //#endregion
41
62
  //#region src/testing/as-user.d.ts
42
63
  /** The local CLI stack's default JWT secret. */
43
64
  export declare const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long";
@@ -46,7 +67,17 @@ interface LocalStack {
46
67
  readonly url?: string;
47
68
  /** Defaults to `$SUPABASE_PUBLISHABLE_KEY`, then `$SUPABASE_ANON_KEY`. */
48
69
  readonly publishableKey?: string;
49
- /** Defaults to `$SUPABASE_JWT_SECRET`, then the CLI default. */
70
+ /**
71
+ * `ES256` (default) signs with the key `better-supabase keys` wrote, as
72
+ * the hosted project's asymmetric keys do. `HS256` signs with the shared
73
+ * JWT secret and only works against a local stack.
74
+ */
75
+ readonly alg?: "ES256" | "HS256";
76
+ /** The private ES256 key. Defaults to the first key in `signingKeysPath`. */
77
+ readonly signingKey?: SigningJwk;
78
+ /** Defaults to `$SUPABASE_SIGNING_KEYS_PATH`, then `signing_keys_path` in `supabase/config.toml`. */
79
+ readonly signingKeysPath?: string;
80
+ /** For `alg: 'HS256'`. Defaults to `$SUPABASE_JWT_SECRET`, then the CLI default. */
50
81
  readonly jwtSecret?: string;
51
82
  /** Service-role key for `expectTenantIsolation`; defaults to `$SUPABASE_SECRET_KEY`. */
52
83
  readonly secretKey?: string;
@@ -65,10 +96,12 @@ interface TestUser<M extends AnyModels, F extends AnyFunctions, E> {
65
96
  }
66
97
  /**
67
98
  * A test user against the local stack: a signed token and repositories that
68
- * run as that user, for live RLS tests.
99
+ * run as that user, for live RLS tests. The token is ES256-signed with the
100
+ * key from `better-supabase keys`; pass `{ alg: 'HS256' }` for a stack that
101
+ * still uses the shared JWT secret.
69
102
  *
70
103
  * ```ts
71
- * const alice = await asUser(sb, { sub: aliceId, org_id: acme });
104
+ * const alice = await asUser(sb, { sub: aliceId, tenant_id: acme });
72
105
  * expect(await alice.db.customers.count().orThrow()).toBe(3);
73
106
  * ```
74
107
  */
@@ -247,8 +280,8 @@ interface TenantIsolationOptions<M extends AnyModels> {
247
280
  * ```ts
248
281
  * await expectTenantIsolation(sb, {
249
282
  * tenants: [
250
- * { id: ACME, claims: { sub: alice, org_id: ACME } },
251
- * { id: GLOBEX, claims: { sub: bob, org_id: GLOBEX } },
283
+ * { id: ACME, claims: { sub: alice, tenant_id: ACME } },
284
+ * { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
252
285
  * ],
253
286
  * tables: {
254
287
  * tags: {
@@ -291,4 +324,4 @@ export declare function isSeed(value: unknown): value is Seed<unknown>;
291
324
  */
292
325
  export declare function defineSeed<M extends AnyModels, D, F extends AnyFunctions, E, const S extends SeedFixtures<M>>(sb: BetterSupabase<M, D, F, E>, fixtures: S & ExactSeed<M, S>): Seed<S>;
293
326
  //#endregion
294
- export type { BudgetPage, BudgetResponse, ConformanceCheck, ConformanceReport, DbBudgetExpectation, ExactSeed, IsolationTable, IsolationTables, IsolationTenant, LocalStack, MeasuredRender, Seed, SeedFixtures, TenantIsolationOptions, TestAuthResolverOptions, TestEventSinkOptions, TestExecutorOptions, TestGeneratorOptions, TestJwtClaims, TestPluginOptions, TestSigner, TestUser };
327
+ export type { BudgetPage, BudgetResponse, ConformanceCheck, ConformanceReport, DbBudgetExpectation, ExactSeed, IsolationTable, IsolationTables, IsolationTenant, LocalStack, MeasuredRender, Seed, SeedFixtures, SigningJwk, TenantIsolationOptions, TestAuthResolverOptions, TestEventSinkOptions, TestExecutorOptions, TestGeneratorOptions, TestJwtClaims, TestPluginOptions, TestSigner, TestUser };
@@ -1,20 +1,20 @@
1
1
  import { n as dbError } from "../errors-Bl3rkOzo.js";
2
+ import { i as temporalText } from "../temporal-BwshQk0W.js";
3
+ import { t as temporal } from "../temporal-required-CMxeSVPo.js";
2
4
  import { i as ok } from "../result-DWXatkd6.js";
3
- import { t as BetterSupabase } from "../define-QGkq04oe.js";
4
- import { r as resolveConfig } from "../config-D-aR2ZoN.js";
5
- import { t as ENV_VARIABLES } from "../env-BEToab0I.js";
6
- import { n as postgresExecutor } from "../executor-Ceq60hPM.js";
7
- import { o as toCloudEvents } from "../events-BuzTgn2D.js";
8
- import { n as isSeed, t as defineSeed } from "../seed-CzecODcJ.js";
5
+ import { t as BetterSupabase } from "../define-DF341GDj.js";
6
+ import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-CQeB1eSf.js";
7
+ import { r as resolveConfig } from "../config-Be4GwsqI.js";
8
+ import { t as isList } from "../guards-BnzDvFrz.js";
9
+ import { ENV_VARIABLES } from "../env/index.js";
10
+ import { n as postgresExecutor } from "../executor-C57jx2pn.js";
11
+ import { toCloudEvents } from "../events/index.js";
9
12
  import { createClient } from "@supabase/supabase-js";
13
+ import { existsSync } from "node:fs";
14
+ import { readFile } from "node:fs/promises";
15
+ import { dirname, join, resolve } from "node:path";
10
16
  //#region src/testing/jwt.ts
11
17
  const encoder = new TextEncoder();
12
- function base64url(input) {
13
- const bytes = typeof input === "string" ? encoder.encode(input) : input;
14
- let binary = "";
15
- for (const byte of bytes) binary += String.fromCharCode(byte);
16
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
17
- }
18
18
  /**
19
19
  * Signs an HS256 access token the local Supabase stack accepts. Test-only:
20
20
  * production tokens come from Supabase Auth.
@@ -30,21 +30,17 @@ async function signTestJwt(secret, claims) {
30
30
  exp: now + expiresIn,
31
31
  ...rest
32
32
  };
33
- const header = base64url(JSON.stringify({
33
+ const header = toBase64Url(JSON.stringify({
34
34
  alg: "HS256",
35
35
  typ: "JWT"
36
36
  }));
37
- const body = base64url(JSON.stringify(payload));
37
+ const body = toBase64Url(JSON.stringify(payload));
38
38
  const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
39
39
  name: "HMAC",
40
40
  hash: "SHA-256"
41
41
  }, false, ["sign"]);
42
42
  const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${header}.${body}`));
43
- return `${header}.${body}.${base64url(new Uint8Array(signature))}`;
44
- }
45
- function base64urlDecode(input) {
46
- const binary = atob(input.replaceAll("-", "+").replaceAll("_", "/"));
47
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
43
+ return `${header}.${body}.${toBase64Url(new Uint8Array(signature))}`;
48
44
  }
49
45
  /**
50
46
  * Accepts `signTestJwt` tokens as users, for API servers tested against the
@@ -61,9 +57,9 @@ function localAuth(secret) {
61
57
  async resolve(request) {
62
58
  const token = /^Bearer (.+)$/i.exec(request.headers.get("authorization") ?? "")?.[1];
63
59
  const [header, body, signature] = token?.split(".") ?? [];
64
- if (!token || !header || !body || !signature) return void 0;
65
- if (!await crypto.subtle.verify("HMAC", await key, base64urlDecode(signature), encoder.encode(`${header}.${body}`)).catch(() => false)) return void 0;
66
- const claims = JSON.parse(new TextDecoder().decode(base64urlDecode(body)));
60
+ if (!token || !header || !body || !signature) return;
61
+ if (!await crypto.subtle.verify("HMAC", await key, fromBase64(signature), encoder.encode(`${header}.${body}`)).catch(() => false)) return;
62
+ const claims = JSON.parse(base64ToText(body));
67
63
  if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) return {
68
64
  kind: "invalid",
69
65
  error: dbError("unauthorized", "The token has expired")
@@ -83,6 +79,40 @@ function localAuth(secret) {
83
79
  }
84
80
  };
85
81
  }
82
+ async function signEs256(key, kid, claims) {
83
+ const { expiresIn = 3600, ...rest } = claims;
84
+ const now = Math.floor(Date.now() / 1e3);
85
+ const payload = {
86
+ aud: "authenticated",
87
+ role: "authenticated",
88
+ aal: "aal1",
89
+ iat: now,
90
+ exp: now + expiresIn,
91
+ ...rest
92
+ };
93
+ const header = toBase64Url(JSON.stringify({
94
+ alg: "ES256",
95
+ typ: "JWT",
96
+ kid
97
+ }));
98
+ const body = toBase64Url(JSON.stringify(payload));
99
+ const signature = await crypto.subtle.sign({
100
+ name: "ECDSA",
101
+ hash: "SHA-256"
102
+ }, key, encoder.encode(`${header}.${body}`));
103
+ return `${header}.${body}.${toBase64Url(new Uint8Array(signature))}`;
104
+ }
105
+ /**
106
+ * Signs an ES256 access token with a private JWK, the way the local stack
107
+ * signs them once `signing_keys_path` is set. Test-only.
108
+ */
109
+ async function signTestJwtWithKey(key, claims) {
110
+ const { kid, ...jwk } = key;
111
+ return signEs256(await crypto.subtle.importKey("jwk", jwk, {
112
+ name: "ECDSA",
113
+ namedCurve: "P-256"
114
+ }, false, ["sign"]), kid, claims);
115
+ }
86
116
  /** An in-memory ES256 key pair that signs tokens like Supabase Auth's asymmetric keys. */
87
117
  async function createTestSigner() {
88
118
  const pair = await crypto.subtle.generateKey({
@@ -97,32 +127,44 @@ async function createTestSigner() {
97
127
  alg: "ES256",
98
128
  use: "sig"
99
129
  }] },
100
- async sign(claims) {
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
- }
130
+ sign: (claims) => signEs256(pair.privateKey, kid, claims)
123
131
  };
124
132
  }
125
133
  //#endregion
134
+ //#region src/testing/local-key.ts
135
+ const SIGNING_KEYS_PATH = /^\s*signing_keys_path\s*=\s*"([^"]+)"/m;
136
+ /** `supabase/config.toml`'s `signing_keys_path`, else `supabase/signing_keys.json`, in `from` or a parent. */
137
+ async function findSigningKeys(from) {
138
+ for (let dir = resolve(from);; dir = dirname(dir)) {
139
+ const supabase = join(dir, "supabase");
140
+ const toml = join(supabase, "config.toml");
141
+ if (existsSync(toml)) {
142
+ const configured = SIGNING_KEYS_PATH.exec(await readFile(toml, "utf8"))?.[1];
143
+ const path = resolve(supabase, configured ?? "signing_keys.json");
144
+ if (existsSync(path)) return path;
145
+ }
146
+ if (dirname(dir) === dir) return void 0;
147
+ }
148
+ }
149
+ const isSigningKey = (value) => typeof value === "object" && value !== null && "kid" in value && typeof value.kid === "string" && "d" in value && typeof value.d === "string";
150
+ /**
151
+ * The key the local stack signs with: the first entry of the file
152
+ * `better-supabase keys` wrote. `path` defaults to `$SUPABASE_SIGNING_KEYS_PATH`,
153
+ * then `signing_keys_path` from the nearest `supabase/config.toml`.
154
+ */
155
+ async function localSigningKey(path) {
156
+ const file = path ?? process.env["SUPABASE_SIGNING_KEYS_PATH"] ?? await findSigningKeys(process.cwd());
157
+ 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.");
158
+ const keys = JSON.parse(await readFile(file, "utf8"));
159
+ const first = Array.isArray(keys) ? keys[0] : void 0;
160
+ if (!isSigningKey(first)) throw new TypeError(`${file} has no private ES256 key first.`);
161
+ return first;
162
+ }
163
+ /** Signs a token with the local stack's ES256 key (see `localSigningKey`). Test-only. */
164
+ async function signLocalJwt(claims, path) {
165
+ return signTestJwtWithKey(await localSigningKey(path), claims);
166
+ }
167
+ //#endregion
126
168
  //#region src/testing/as-user.ts
127
169
  /** The local CLI stack's default JWT secret. */
128
170
  const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long";
@@ -133,12 +175,30 @@ function env$1(...names) {
133
175
  if (value) return value;
134
176
  }
135
177
  }
178
+ const LOCAL_HOSTS = /* @__PURE__ */ new Set([
179
+ "localhost",
180
+ "127.0.0.1",
181
+ "[::1]",
182
+ "host.docker.internal"
183
+ ]);
184
+ async function signFor(stack, url, claims) {
185
+ const alg = stack.alg ?? "ES256";
186
+ switch (alg) {
187
+ case "ES256": return signTestJwtWithKey(stack.signingKey ?? await localSigningKey(stack.signingKeysPath), claims);
188
+ case "HS256":
189
+ 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\`.`);
190
+ return signTestJwt(stack.jwtSecret ?? env$1("SUPABASE_JWT_SECRET") ?? "super-secret-jwt-token-with-at-least-32-characters-long", claims);
191
+ default: throw new TypeError(`asUser: unknown alg "${String(alg)}"`);
192
+ }
193
+ }
136
194
  /**
137
195
  * A test user against the local stack: a signed token and repositories that
138
- * run as that user, for live RLS tests.
196
+ * run as that user, for live RLS tests. The token is ES256-signed with the
197
+ * key from `better-supabase keys`; pass `{ alg: 'HS256' }` for a stack that
198
+ * still uses the shared JWT secret.
139
199
  *
140
200
  * ```ts
141
- * const alice = await asUser(sb, { sub: aliceId, org_id: acme });
201
+ * const alice = await asUser(sb, { sub: aliceId, tenant_id: acme });
142
202
  * expect(await alice.db.customers.count().orThrow()).toBe(3);
143
203
  * ```
144
204
  */
@@ -146,7 +206,7 @@ async function asUser(sb, claims, stack = {}) {
146
206
  const url = stack.url ?? env$1(...ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
147
207
  const publishableKey = stack.publishableKey ?? env$1(...ENV_VARIABLES.publishableKey, "SUPABASE_ANON_KEY");
148
208
  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 signTestJwt(stack.jwtSecret ?? env$1("SUPABASE_JWT_SECRET") ?? "super-secret-jwt-token-with-at-least-32-characters-long", claims);
209
+ const token = await signFor(stack, url, claims);
150
210
  const full = {
151
211
  role: "authenticated",
152
212
  ...claims
@@ -179,7 +239,9 @@ function settle(promise, ms) {
179
239
  let timer;
180
240
  return Promise.race([promise, new Promise((done) => {
181
241
  timer = setTimeout(done, ms);
182
- })]).finally(() => clearTimeout(timer));
242
+ })]).finally(() => {
243
+ clearTimeout(timer);
244
+ });
183
245
  }
184
246
  /**
185
247
  * Measures every document and RSC navigation response during `during` and
@@ -299,7 +361,9 @@ function tableKey(sb, table) {
299
361
  if (!key || !(key in sb.meta.tables)) throw new TypeError(`better-supabase: unknown table "${String(key)}"`);
300
362
  return key;
301
363
  }
302
- const hasName = (subject) => ["has a name", () => expect(typeof subject.name === "string" && subject.name.length > 0, "name must be a non-empty string")];
364
+ const hasName = (subject) => ["has a name", () => {
365
+ expect(typeof subject.name === "string" && subject.name.length > 0, "name must be a non-empty string");
366
+ }];
303
367
  /**
304
368
  * Proves an `Executor` meets the contract: rows keyed by the selection's
305
369
  * aliases, counts, `aborted` errors, failures returned as `Result`s rather
@@ -313,7 +377,7 @@ function testExecutor(executor, options) {
313
377
  const { sb } = options;
314
378
  const table = tableKey(sb, options.table);
315
379
  const context = { errorMappers: [] };
316
- const read = () => captureOp(sb, (db) => db[table].findMany({ limit: 2 }));
380
+ const read = () => captureOp(sb, (db) => db[table]["findMany"]({ limit: 2 }));
317
381
  const create = options.create;
318
382
  return conform(`Executor "${executor.name}"`, [
319
383
  hasName(executor),
@@ -321,7 +385,7 @@ function testExecutor(executor, options) {
321
385
  const op = await read();
322
386
  const result = await executor.execute(op, context);
323
387
  expect(result.ok, `select failed: ${result.ok ? "" : result.error.message}`);
324
- expect(Array.isArray(result.data.rows), "rows must be an array");
388
+ expect(isList(result.data.rows), "rows must be an array");
325
389
  expect(result.data.rows.length <= 2, `limit 2 returned ${result.data.rows.length} rows`);
326
390
  expect(result.data.count === null || typeof result.data.count === "number", "count must be a number or null");
327
391
  if (op.kind !== "select") return;
@@ -332,7 +396,7 @@ function testExecutor(executor, options) {
332
396
  }
333
397
  }],
334
398
  ["counts rows", async () => {
335
- const op = await captureOp(sb, (db) => db[table].count());
399
+ const op = await captureOp(sb, (db) => db[table]["count"]());
336
400
  const result = await executor.execute(op, context);
337
401
  expect(result.ok, `count failed: ${result.ok ? "" : result.error.message}`);
338
402
  expect(typeof result.data.count === "number" && result.data.count >= 0, "count must be a number");
@@ -380,7 +444,7 @@ function testExecutor(executor, options) {
380
444
  }],
381
445
  executor.batch && ["batch returns one result per operation, in order", async () => {
382
446
  const rows = await read();
383
- const count = await captureOp(sb, (db) => db[table].count());
447
+ const count = await captureOp(sb, (db) => db[table]["count"]());
384
448
  const missing = {
385
449
  ...rows,
386
450
  table: {
@@ -401,15 +465,15 @@ function testExecutor(executor, options) {
401
465
  }],
402
466
  create && ["round-trips a write", async () => {
403
467
  const repository = sb.connect(executor)[table];
404
- const created = await repository.create(create);
468
+ const created = await repository["create"](create);
405
469
  expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
406
470
  const row = created.data;
407
471
  for (const [key, value] of Object.entries(create)) expect(same(row[key], value), `created row has ${key} = ${JSON.stringify(row[key])}`);
408
472
  const [primary] = sb.meta.tables[table].primaryKey;
409
473
  expect(primary, "write checks need a table with a primary key");
410
- const removed = await repository.delete(row[primary]);
474
+ const removed = await repository["delete"](row[primary]);
411
475
  expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
412
- const again = await repository.findFirst({ where: { [primary]: row[primary] } });
476
+ const again = await repository["findFirst"]({ where: { [primary]: row[primary] } });
413
477
  expect(again.ok && again.data === null, "the deleted row is still readable");
414
478
  }]
415
479
  ]);
@@ -557,7 +621,7 @@ function testGenerator(generator, options) {
557
621
  hasName(generator),
558
622
  ["writes files inside the project", async () => {
559
623
  const files = await generator.generate(input());
560
- expect(Array.isArray(files), "generate() must return an array");
624
+ expect(isList(files), "generate() must return an array");
561
625
  for (const file of files) {
562
626
  expect(typeof file.path === "string" && typeof file.contents === "string", "files need a path and contents");
563
627
  expect(!file.path.startsWith("/") && !/^[A-Za-z]:/.test(file.path), `${file.path} must be relative`);
@@ -587,25 +651,27 @@ function testPlugin(plugin, options) {
587
651
  schema: sb.meta,
588
652
  context,
589
653
  options: {},
590
- now: () => /* @__PURE__ */ new Date(0)
654
+ now: () => temporal().Instant.fromEpochMilliseconds(0)
591
655
  });
592
656
  const create = options.create;
593
657
  return conform(`Plugin "${plugin.name}"`, [
594
658
  hasName(plugin),
595
- ["targets plugin API v1", () => expect(plugin.apiVersion === 1, `apiVersion is ${String(plugin.apiVersion)}`)],
659
+ ["targets plugin API v1", () => {
660
+ expect(plugin.apiVersion === 1, `apiVersion is ${String(plugin.apiVersion)}`);
661
+ }],
596
662
  ["installs and builds repositories", () => {
597
663
  const db = sb.use(plugin).connect(recorder([]), context);
598
664
  for (const key of Object.keys(sb.meta.tables)) expect(db[key], `db.${key} is missing`);
599
665
  }],
600
666
  plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
601
- const op = await captureOp(sb, (db) => db[table].findMany({ limit: 1 }));
667
+ const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
602
668
  const first = plugin.transformQuery(frozenCopy(op), hook(op));
603
669
  const second = plugin.transformQuery(frozenCopy(op), hook(op));
604
670
  expect(typeof first === "object" && first !== null && first.kind === op.kind, "must return an operation of the same kind");
605
671
  expect(same(first, second), "two calls with the same input differ");
606
672
  }],
607
673
  plugin.beforeMutation && create && ["beforeMutation is pure and deterministic", async () => {
608
- const op = await captureOp(sb, (db) => db[table].create(create));
674
+ const op = await captureOp(sb, (db) => db[table]["create"](create));
609
675
  if (op.kind === "select") throw new Violation("create did not produce a mutation");
610
676
  const first = await plugin.beforeMutation(frozenCopy(op), hook(op));
611
677
  const second = await plugin.beforeMutation(frozenCopy(op), hook(op));
@@ -613,7 +679,7 @@ function testPlugin(plugin, options) {
613
679
  expect(same(first, second), "two calls with the same input differ");
614
680
  }],
615
681
  plugin.wrapExecutor && ["wrapExecutor keeps results intact", async () => {
616
- const op = await captureOp(sb, (db) => db[table].findMany({ limit: 1 }));
682
+ const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
617
683
  const data = {
618
684
  rows: [{ marker: "conformance" }],
619
685
  count: 1
@@ -664,8 +730,8 @@ function env(names) {
664
730
  * ```ts
665
731
  * await expectTenantIsolation(sb, {
666
732
  * tenants: [
667
- * { id: ACME, claims: { sub: alice, org_id: ACME } },
668
- * { id: GLOBEX, claims: { sub: bob, org_id: GLOBEX } },
733
+ * { id: ACME, claims: { sub: alice, tenant_id: ACME } },
734
+ * { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
669
735
  * ],
670
736
  * tables: {
671
737
  * tags: {
@@ -748,4 +814,105 @@ async function expectTenantIsolation(sb, options) {
748
814
  }
749
815
  }
750
816
  //#endregion
751
- export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, signTestJwt, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testPlugin };
817
+ //#region src/testing/seed.ts
818
+ const SEED = Symbol.for("better-supabase.seed");
819
+ function isSeed(value) {
820
+ return typeof value === "object" && value !== null && SEED in value;
821
+ }
822
+ const quote = (text) => `'${text.replaceAll("'", "''")}'`;
823
+ function arrayLiteral(values) {
824
+ return `{${values.map((value) => {
825
+ if (value === null) return "NULL";
826
+ if (Array.isArray(value)) return arrayLiteral(value);
827
+ return `"${(temporalText(value) ?? String(value)).replaceAll(/[\\"]/g, "\\$&")}"`;
828
+ }).join(",")}}`;
829
+ }
830
+ function literal(value, column, where) {
831
+ if (value === null) return "null";
832
+ if (column.json) return quote(JSON.stringify(value));
833
+ if (Array.isArray(value)) return quote(arrayLiteral(value));
834
+ const temporalValue = temporalText(value);
835
+ if (temporalValue !== void 0) return quote(temporalValue);
836
+ const kind = typeof value;
837
+ switch (kind) {
838
+ case "string": return quote(value);
839
+ case "boolean":
840
+ case "bigint": return String(value);
841
+ case "number":
842
+ if (!Number.isFinite(value)) throw new TypeError(`${where}: ${String(value)} is not a finite number`);
843
+ return String(value);
844
+ case "object":
845
+ case "function":
846
+ case "symbol":
847
+ case "undefined": throw new TypeError(`${where}: unsupported value of type ${kind}`);
848
+ default: throw new TypeError(`${where}: unsupported value of type ${String(kind)}`);
849
+ }
850
+ }
851
+ const ident = (name) => `"${name.replaceAll("\"", "\"\"")}"`;
852
+ /** Parents first (by forward relations), otherwise in the order given. */
853
+ function order(meta, keys) {
854
+ const sorted = [];
855
+ const visiting = /* @__PURE__ */ new Set();
856
+ const visit = (key) => {
857
+ if (sorted.includes(key) || visiting.has(key)) return;
858
+ visiting.add(key);
859
+ for (const relation of Object.values(meta.tables[key]?.relations ?? {})) if (relation.direction === "forward" && relation.table !== key && keys.includes(relation.table)) visit(relation.table);
860
+ visiting.delete(key);
861
+ sorted.push(key);
862
+ };
863
+ for (const key of keys) visit(key);
864
+ return sorted;
865
+ }
866
+ function statement(table, rows) {
867
+ const entries = Object.entries(rows);
868
+ if (entries.length === 0) return void 0;
869
+ const columns = [];
870
+ for (const [, row] of entries) for (const [key, value] of Object.entries(row)) {
871
+ if (value === void 0 || columns.includes(key)) continue;
872
+ const column = table.columns[key];
873
+ if (!column) throw new TypeError(`seed ${table.key}: unknown column "${key}"`);
874
+ if (column.generated) throw new TypeError(`seed ${table.key}: "${key}" is a generated column`);
875
+ columns.push(key);
876
+ }
877
+ const values = entries.map(([name, row]) => {
878
+ return ` (${columns.map((key) => row[key] === void 0 ? "default" : literal(row[key], table.columns[key], `seed ${table.key}.${name}.${key}`)).join(", ")})`;
879
+ });
880
+ return `insert into ${`${ident(table.schema)}.${ident(table.name)}`} (${columns.map((key) => ident(table.columns[key].db)).join(", ")}) values\n${values.join(",\n")}\non conflict do nothing;`;
881
+ }
882
+ /**
883
+ * Typed fixtures shared by `supabase/seeds` and your tests. Rows use app
884
+ * casing and Insert types; `better-supabase seed` renders them to SQL.
885
+ *
886
+ * ```ts
887
+ * export const seed = defineSeed(sb, {
888
+ * organizations: { acme: { id: ACME, name: 'Acme' } },
889
+ * customers: { first: { organizationId: ACME, name: 'First customer' } },
890
+ * });
891
+ * ```
892
+ */
893
+ function defineSeed(sb, fixtures) {
894
+ const meta = sb.meta;
895
+ const statements = () => {
896
+ const keys = Object.keys(fixtures).filter((key) => fixtures[key] !== void 0);
897
+ for (const key of keys) {
898
+ const table = meta.tables[key];
899
+ if (!table) throw new TypeError(`seed: unknown table "${key}"`);
900
+ if (table.kind === "view") throw new TypeError(`seed: "${key}" is a view`);
901
+ }
902
+ return order(meta, keys).flatMap((key) => {
903
+ const sql = statement(meta.tables[key], fixtures[key]);
904
+ return sql ? [sql] : [];
905
+ });
906
+ };
907
+ return {
908
+ [SEED]: true,
909
+ rows: fixtures,
910
+ statements,
911
+ sql: () => statements().join("\n\n"),
912
+ async insert(sql) {
913
+ for (const text of statements()) await sql.queryRaw(text);
914
+ }
915
+ };
916
+ }
917
+ //#endregion
918
+ export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testPlugin };