better-supabase 0.3.0 → 0.5.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/README.md +9 -6
- package/bin/better-supabase.js +4 -0
- package/dist/{base64-CQeB1eSf.js → base64-D2gsioCY.js} +2 -2
- package/dist/channel-FQW-CnGR.d.ts +70 -0
- package/dist/claims-BAf6c7is.js +48 -0
- package/dist/cli/_format-Cax5EsM9.js +101 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +48 -0
- package/dist/cli/codemod-DqV7612m.js +388 -0
- package/dist/cli/command-CK14fP91.js +502 -0
- package/dist/cli/compare-BmIWVEui.js +9 -0
- package/dist/cli/config-CPVpXP5I.js +1965 -0
- package/dist/cli/config-dtoekM0v.js +2 -0
- package/dist/cli/db-DOBPt660.js +52 -0
- package/dist/cli/diff-SjkO5rNr.js +752 -0
- package/dist/cli/dist-DBPdlPYK.js +1112 -0
- package/dist/cli/dist-DreZ24Sz.js +129 -0
- package/dist/cli/dist-xn4wlboc.js +732 -0
- package/dist/cli/doctor-L7Z8mYTx.js +2863 -0
- package/dist/cli/env-DmKv_G4A.js +108 -0
- package/dist/cli/env-jIFUADLY.js +511 -0
- package/dist/cli/errors-4v4DTtda.js +56 -0
- package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
- package/dist/cli/gen-BJc9vdyu.js +2 -0
- package/dist/cli/gen-C2osRcr5.js +2204 -0
- package/dist/cli/index.d.ts +493 -0
- package/dist/cli/index.js +184 -0
- package/dist/cli/init-Saf5-R9T.js +550 -0
- package/dist/cli/introspect--H7oGmcd.js +81 -0
- package/dist/cli/io-k41_z-jT.js +24 -0
- package/dist/cli/json5-ChRycquj.js +787 -0
- package/dist/cli/jsonc-BUniUpOk.js +2 -0
- package/dist/cli/jsonc-D5Fu67jw.js +45 -0
- package/dist/cli/keys-ByKHnvP9.js +83 -0
- package/dist/cli/ohash-D7bG_EOb.js +277 -0
- package/dist/cli/openapi-Cp5Fyta-.js +66 -0
- package/dist/cli/project-B_GeXyHh.js +70 -0
- package/dist/cli/prompts-BNcNfAve.js +1142 -0
- package/dist/cli/prompts-BTYQGunc.js +2 -0
- package/dist/cli/read-sets-BfMQcKy8.js +289 -0
- package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
- package/dist/cli/run-BKR2W_79.js +351 -0
- package/dist/cli/seed-CUlqWh-8.js +98 -0
- package/dist/cli/skills-LJJIhLFR.js +137 -0
- package/dist/cli/sql-DzPbfngx.js +348 -0
- package/dist/cli/style-C0NFdJRL.js +29 -0
- package/dist/cli/toml-DB-MNnFs.js +424 -0
- package/dist/cli/version-BDJuO0UR.js +4 -0
- package/dist/cli/yaml-CYCbJ6et.js +2252 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +15 -22
- package/dist/clone-BfgmLU90.js +21 -0
- package/dist/{compiler-2zqGzxVZ.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
- package/dist/config/index.d.ts +4 -4
- package/dist/config/index.js +5 -5
- package/dist/{config-DOsGYSiT.js → config-DreERlKo.js} +1 -1
- package/dist/{config-C1CYcm6o.d.ts → config-T_ejwyd3.d.ts} +176 -11
- package/dist/{define-DUbydmJa.d.ts → define-Bp_BZa5h.d.ts} +37 -29
- package/dist/{define-DF341GDj.js → define-cNKzqwL_.js} +356 -89
- package/dist/edge/index.d.ts +20 -13
- package/dist/edge/index.js +55 -26
- package/dist/{entitlements-DvJXoI02.d.ts → entitlements-DUxU4oHe.d.ts} +2 -2
- package/dist/env/index.d.ts +2 -2
- package/dist/env/index.js +34 -1
- package/dist/{errors-Bl3rkOzo.js → errors-DtwXa6b8.js} +2 -1
- package/dist/events/index.d.ts +3 -2
- package/dist/events/index.js +69 -16
- package/dist/events-aIO7vsIk.d.ts +230 -0
- package/dist/executor-BunPOO-k.d.ts +402 -0
- package/dist/{executor-B26lgbHD.d.ts → executor-Cghdstwu.d.ts} +1 -2
- package/dist/{executor-C57jx2pn.js → executor-XycESQPH.js} +3 -3
- package/dist/hono/index.d.ts +27 -23
- package/dist/hono/index.js +46 -11
- package/dist/impersonation-VfaGc4Q_.js +41 -0
- package/dist/{index-BNsABq7P.d.ts → index-BGdgBPdf.d.ts} +66 -25
- package/dist/{index-qriRM9cZ.d.ts → index-C7f8cOBM.d.ts} +4 -4
- package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
- package/dist/{index-GV77NpRP.d.ts → index-DOESKG27.d.ts} +34 -9
- package/dist/{index-CriFFJ_p.d.ts → index-DgILJRXj.d.ts} +43 -17
- package/dist/{index-UJ-LUxtI.d.ts → index-DmXSxbLi.d.ts} +14 -14
- package/dist/{index-Cf-D2jk1.d.ts → index-Dxn2a5cw.d.ts} +2 -2
- package/dist/{index-CTTMS6_1.d.ts → index-SaTsEfGP.d.ts} +86 -9
- package/dist/{index-CADlNHK-.d.ts → index-fag8VJZ2.d.ts} +5 -6
- package/dist/index.d.ts +34 -17
- package/dist/index.js +15 -14
- package/dist/jobs/index.d.ts +147 -95
- package/dist/jobs/index.js +773 -250
- package/dist/{json-schema--aOXAGfU.js → json-schema-m1fiwKgN.js} +23 -2
- package/dist/kit-events-CTFDfSwb.js +60 -0
- package/dist/kit-transport-B82k1djG.d.ts +33 -0
- package/dist/kit-transport-DJlHQUBU.js +55 -0
- package/dist/{config-Be4GwsqI.js → kits-CGLLrNPd.js} +38 -10
- package/dist/list/index.d.ts +1 -1
- package/dist/list/index.js +3 -3
- package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
- package/dist/{live-QE3UPbGm.d.ts → live-D9W2M5aO.d.ts} +16 -15
- package/dist/lookup-BaP5bI27.js +18 -0
- package/dist/mcp/index.d.ts +39 -22
- package/dist/mcp/index.js +39 -25
- package/dist/{mfa-x3V3vGh-.d.ts → mfa-CusDgPaD.d.ts} +5 -1
- package/dist/next/image/index.js +6 -3
- package/dist/next/index.d.ts +60 -36
- package/dist/next/index.js +167 -50
- package/dist/notifications/index.d.ts +162 -0
- package/dist/notifications/index.js +264 -0
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +6 -6
- package/dist/orgs/index.d.ts +132 -0
- package/dist/orgs/index.js +245 -0
- package/dist/orpc/index.d.ts +37 -20
- package/dist/orpc/index.js +24 -11
- package/dist/otel/index.d.ts +24 -5
- package/dist/otel/index.js +124 -64
- package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
- package/dist/permdock-sql-DgOfeixi.js +51 -0
- package/dist/plugin-DwARMuQp.js +59 -0
- package/dist/{plugin-DVRB8qNE.d.ts → plugin-o144f322.d.ts} +17 -5
- package/dist/plugins/actor/index.d.ts +5 -2
- package/dist/plugins/actor/index.js +22 -8
- package/dist/plugins/rules/index.d.ts +2 -2
- package/dist/plugins/rules/index.js +52 -17
- package/dist/plugins/soft-delete/index.d.ts +3 -3
- package/dist/plugins/soft-delete/index.js +14 -5
- package/dist/plugins/tenant/index.d.ts +3 -3
- package/dist/plugins/tenant/index.js +45 -21
- package/dist/plugins/timestamps/index.d.ts +1 -1
- package/dist/plugins/timestamps/index.js +4 -3
- package/dist/plugins/validation/index.d.ts +2 -2
- package/dist/plugins/validation/index.js +26 -6
- package/dist/{pool-B9qsLyFR.d.ts → pool-C9voM4Kw.d.ts} +24 -6
- package/dist/postgres/index.d.ts +5 -5
- package/dist/postgres/index.js +20 -10
- package/dist/{postgrest-DmBlevh-.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
- package/dist/{problem-CGt1ADix.js → problem-BW1KhzSG.js} +2 -1
- package/dist/{problem-Z2RTQjZD.d.ts → problem-Blxe8S31.d.ts} +1 -1
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +3 -3
- package/dist/{query-BdKLTPlj.js → query-CCbo44lG.js} +18 -18
- package/dist/queue-BYUCjr59.d.ts +252 -0
- package/dist/react/index.d.ts +8 -7
- package/dist/react/index.js +4 -3
- package/dist/react/server.d.ts +16 -5
- package/dist/react/server.js +12 -4
- package/dist/react/session.d.ts +10 -4
- package/dist/react/session.js +11 -3
- package/dist/{react-DNBYNazu.js → react-C_s7kiNz.js} +138 -46
- package/dist/{read-set-D0qBcboM.js → read-set-DQFt1N_Y.js} +71 -15
- package/dist/realtime/index.d.ts +3 -3
- package/dist/realtime/index.js +14 -12
- package/dist/{index-kpgopLNv.d.ts → refresh-BTrQ_ZTp.d.ts} +6 -3
- package/dist/{resolve-BRciX2jK.d.ts → resolve-C15TJq77.d.ts} +22 -11
- package/dist/{resource-D30rp0GN.js → resource-CRxxwkQq.js} +24 -9
- package/dist/{resource-CwVgiI5q.d.ts → resource-D8iqWMr-.d.ts} +6 -6
- package/dist/respond-D2OQl62-.d.ts +371 -0
- package/dist/{result-DWXatkd6.js → result-BKiliPY7.js} +2 -2
- package/dist/server/index.d.ts +17 -15
- package/dist/server/index.js +16 -22
- package/dist/server-Csx2sRjg.js +655 -0
- package/dist/{shared-_6-uPL7E.js → shared-D1dRUrAg.js} +19 -4
- package/dist/signers-Cc6r98SW.d.ts +162 -0
- package/dist/{simplify-LakqK1vH.js → simplify-_0oUKqCd.js} +27 -95
- package/dist/sql/index.d.ts +247 -10
- package/dist/sql/index.js +8591 -780
- package/dist/{sql-BIgk12ew.js → sql-Coh6n4cC.js} +11 -3
- package/dist/ssr/index.d.ts +4 -4
- package/dist/ssr/index.js +1 -2
- package/dist/{standard-FUi3Z3-7.js → standard-D7r6dMUu.js} +6 -3
- package/dist/{standard-eudBiRk1.d.ts → standard-J4YgMOLm.d.ts} +1 -1
- package/dist/{stats-D78UQ8Yu.js → stats-Bqob3WqT.js} +34 -9
- package/dist/storage/index.d.ts +4 -4
- package/dist/storage/index.js +174 -27
- package/dist/support-DT-OhSxD.d.ts +88 -0
- package/dist/support-Dk6vdmEw.js +291 -0
- package/dist/support-view-Da5peKFg.d.ts +17 -0
- package/dist/support-view-g4YLBeqX.js +18 -0
- package/dist/{tables-Dhha3BXM.js → tables-Cb-3_VWc.js} +14 -0
- package/dist/{permdock-sql-Bta_G0v-.js → template-TxrGI5jR.js} +2 -31
- package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
- package/dist/{temporal-required-CMxeSVPo.js → temporal-required-CAt4Dc9Z.js} +2 -2
- package/dist/testing/index.d.ts +113 -24
- package/dist/testing/index.js +375 -47
- package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
- package/dist/user-change-CHFEUro1.js +49 -0
- package/dist/{webhooks-CAIFQSTM.js → verify-Ce49ETPz.js} +38 -19
- package/dist/{version-BfpG2gks.js → version-DM_Roffb.js} +1 -1
- package/dist/{view-3Po_PVlr.d.ts → view-BEf0diC5.d.ts} +10 -5
- package/dist/{mfa-BxoR1RkN.js → view-Bq64ki8-.js} +210 -64
- package/dist/webhooks/index.d.ts +136 -5
- package/dist/webhooks/index.js +588 -2
- package/dist/wire-D2dT86vi.js +35 -0
- package/package.json +50 -10
- package/schemas/config-v1.json +131 -1
- package/schemas/doctor-report-v1.json +10 -0
- package/skills/better-supabase/SKILL.md +60 -10
- package/skills/better-supabase/references/plugins.md +13 -14
- package/skills/better-supabase/references/troubleshooting.md +25 -4
- package/skills/better-supabase-api/SKILL.md +74 -19
- package/skills/better-supabase-api/references/adapters.md +48 -27
- package/skills/better-supabase-auth/SKILL.md +149 -0
- package/skills/better-supabase-auth/references/permdock.md +100 -0
- package/skills/better-supabase-testing/SKILL.md +58 -4
- package/dist/claims-CoTobNC-.js +0 -25
- package/dist/events-Do8jXh4D.d.ts +0 -84
- package/dist/executor-DynmIWhq.d.ts +0 -195
- package/dist/invalidate-DbxuL2jL.js +0 -18
- package/dist/permdock-sql-DImJULY-.d.ts +0 -17
- package/dist/plugin-BqR6wKMB.js +0 -23
- package/dist/respond-C44EZAJo.js +0 -399
- package/dist/respond-DAkPIdTH.d.ts +0 -175
- package/dist/result-c2OYgCV2.d.ts +0 -203
- package/dist/view-wZ-9qIUz.js +0 -45
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { c as mapDbError, n as dbError } from "./errors-DtwXa6b8.js";
|
|
2
|
+
import { t as temporal } from "./temporal-required-CAt4Dc9Z.js";
|
|
3
|
+
import { i as ok, n as err, o as toDbError, t as AsyncResult } from "./result-BKiliPY7.js";
|
|
4
|
+
import { t as validate } from "./standard-D7r6dMUu.js";
|
|
5
|
+
import { c as supportCookie, l as supportCookieValue, s as clearSupportCookie } from "./server-Csx2sRjg.js";
|
|
6
|
+
import { r as sqlIdent } from "./template-TxrGI5jR.js";
|
|
7
|
+
import { n as emitKitEvent } from "./kit-events-CTFDfSwb.js";
|
|
8
|
+
import { t as fromPgError } from "./executor-XycESQPH.js";
|
|
9
|
+
//#region src/auth/support.ts
|
|
10
|
+
/** A session from the jsonb the `support-sessions` functions return. */
|
|
11
|
+
function supportSessionFromRow(row) {
|
|
12
|
+
const instant = (value) => temporal().Instant.from(value);
|
|
13
|
+
return {
|
|
14
|
+
id: row.id,
|
|
15
|
+
adminId: row.admin_id,
|
|
16
|
+
targetUserId: row.target_user_id,
|
|
17
|
+
reason: row.reason,
|
|
18
|
+
readOnly: row.read_only ?? true,
|
|
19
|
+
...row.tenant === null ? {} : { tenant: String(row.tenant) },
|
|
20
|
+
startedAt: instant(row.started_at),
|
|
21
|
+
expiresAt: instant(row.expires_at),
|
|
22
|
+
...row.ended_at === null ? {} : { endedAt: instant(row.ended_at) },
|
|
23
|
+
...row.ended_by === null ? {} : { endedBy: row.ended_by },
|
|
24
|
+
metadata: row.metadata ?? {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Support sessions in the `support-sessions` SQL kit module. `start` runs
|
|
29
|
+
* as the admin, so the module's `is_platform()` gate applies; the reads run
|
|
30
|
+
* on the admin connection.
|
|
31
|
+
*/
|
|
32
|
+
function sqlSupportStore(postgres, options = {}) {
|
|
33
|
+
const fn = (name) => `${sqlIdent(options.schema ?? "better_supabase")}.${sqlIdent(name)}`;
|
|
34
|
+
const as = (claims) => claims ? postgres.asUser(claims) : postgres.admin;
|
|
35
|
+
const one = async (client, text, params) => {
|
|
36
|
+
const [row] = await client.queryRaw(text, params);
|
|
37
|
+
return row?.session ?? null;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
apiVersion: 1,
|
|
41
|
+
name: "sql",
|
|
42
|
+
async start(input) {
|
|
43
|
+
const row = await one(postgres.asUser(input.adminClaims), `select ${fn("start_support_session")}($1, $2, $3::interval, $4, $5, $6, $7) as session`, [
|
|
44
|
+
input.targetUserId,
|
|
45
|
+
input.reason,
|
|
46
|
+
`${String(input.ttlSeconds)} seconds`,
|
|
47
|
+
input.readOnly,
|
|
48
|
+
JSON.stringify(input.metadata),
|
|
49
|
+
input.tenant ?? null,
|
|
50
|
+
input.adminId
|
|
51
|
+
]);
|
|
52
|
+
if (!row) throw new Error("start_support_session returned no session");
|
|
53
|
+
return supportSessionFromRow(row);
|
|
54
|
+
},
|
|
55
|
+
async get(sessionId, adminId, claims) {
|
|
56
|
+
const row = await one(as(claims), `select ${fn("active_support_session")}($1, $2) as session`, [sessionId, adminId]);
|
|
57
|
+
return row ? supportSessionFromRow(row) : void 0;
|
|
58
|
+
},
|
|
59
|
+
async end(sessionId, endedBy, claims) {
|
|
60
|
+
const [row] = await as(claims).queryRaw(`select ${fn("end_support_session")}($1, $2) as ended`, [sessionId, endedBy]);
|
|
61
|
+
return row?.ended === true;
|
|
62
|
+
},
|
|
63
|
+
async list(filter = {}, claims) {
|
|
64
|
+
return (await as(claims).queryRaw(`select session from ${fn("list_support_sessions")}($1, $2, $3, $4) as session`, [
|
|
65
|
+
filter.adminId ?? null,
|
|
66
|
+
filter.targetUserId ?? null,
|
|
67
|
+
filter.active ?? null,
|
|
68
|
+
filter.limit ?? 50
|
|
69
|
+
])).map((row) => supportSessionFromRow(row.session));
|
|
70
|
+
},
|
|
71
|
+
async claims(targetUserId) {
|
|
72
|
+
const [row] = await postgres.admin.queryRaw(`select ${fn("support_target_claims")}($1) as claims`, [targetUserId]);
|
|
73
|
+
return row?.claims ?? {};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The claims a support session runs with: the target's, plus `act` naming
|
|
79
|
+
* the admin, the reason and the session (the audit module records all three).
|
|
80
|
+
* `role` falls back to `authenticated`; the admin's claims never leak in.
|
|
81
|
+
*/
|
|
82
|
+
function supportClaims(session, target) {
|
|
83
|
+
const role = target["role"];
|
|
84
|
+
return {
|
|
85
|
+
...target,
|
|
86
|
+
sub: session.targetUserId,
|
|
87
|
+
role: typeof role === "string" && role !== "" ? role : "authenticated",
|
|
88
|
+
act: {
|
|
89
|
+
sub: session.adminId,
|
|
90
|
+
reason: session.reason,
|
|
91
|
+
session_id: session.id,
|
|
92
|
+
read_only: session.readOnly
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/server/support.ts
|
|
98
|
+
const denied = (message, code) => dbError("forbidden", message, { code });
|
|
99
|
+
const invalid = (message, code) => dbError("invalid_request", message, { code });
|
|
100
|
+
function storeError(cause) {
|
|
101
|
+
const raw = fromPgError(cause);
|
|
102
|
+
return raw ? mapDbError(raw) : toDbError(cause);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Support sessions for `createServer(betterSupabase, { support })`. Apps that
|
|
106
|
+
* never import it don't bundle the support code.
|
|
107
|
+
*/
|
|
108
|
+
function supportSessions(options) {
|
|
109
|
+
return {
|
|
110
|
+
options,
|
|
111
|
+
create: (events, claimsSchema) => createSupport(options, events, claimsSchema)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function createSupport(options, events, claimsSchema) {
|
|
115
|
+
const policy = options.policy ?? {};
|
|
116
|
+
const ttl = policy.ttl ?? 1800;
|
|
117
|
+
const maxTtl = policy.maxTtl ?? 14400;
|
|
118
|
+
const cookieName = options.cookie?.name;
|
|
119
|
+
const emit = (type, data) => {
|
|
120
|
+
emitKitEvent(events, type, data, {
|
|
121
|
+
actorId: data.adminId,
|
|
122
|
+
...data.sessionId ? { subject: `support_sessions/${data.sessionId}` } : {}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const targetClaims = async (store, targetUserId) => {
|
|
126
|
+
if (options.claims) return options.claims(targetUserId);
|
|
127
|
+
if (store.claims) return store.claims(targetUserId);
|
|
128
|
+
return { role: "authenticated" };
|
|
129
|
+
};
|
|
130
|
+
const checkPolicy = (admin, request, reason, seconds) => {
|
|
131
|
+
if (admin.claims["act"] !== void 0) return denied("An impersonated session cannot start a support session", "SUPPORT_NESTED");
|
|
132
|
+
if (request.targetUserId === admin.user.id) return invalid("An admin cannot start a support session as themselves", "SUPPORT_SELF");
|
|
133
|
+
if ((policy.requireReason ?? true) && reason === "") return invalid("A reason is required", "SUPPORT_REASON_REQUIRED");
|
|
134
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || seconds > maxTtl) return invalid(`ttl must be between 1 and ${String(maxTtl)} seconds`, "SUPPORT_TTL");
|
|
135
|
+
};
|
|
136
|
+
const authorized = async (input) => {
|
|
137
|
+
if (!options.authorize) return true;
|
|
138
|
+
try {
|
|
139
|
+
return await options.authorize(input);
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
start(admin, request) {
|
|
146
|
+
return AsyncResult.from(async () => {
|
|
147
|
+
if (admin.kind !== "user") return err(dbError("unauthorized", "Sign in to start a support session"));
|
|
148
|
+
const reason = request.reason?.trim() ?? "";
|
|
149
|
+
const seconds = request.ttl ?? ttl;
|
|
150
|
+
const readOnly = policy.readOnly === "default" ? request.readOnly ?? true : true;
|
|
151
|
+
const deny = (error, denial) => {
|
|
152
|
+
emit("support.denied", {
|
|
153
|
+
adminId: admin.user.id,
|
|
154
|
+
targetUserId: request.targetUserId,
|
|
155
|
+
...reason ? { reason } : {},
|
|
156
|
+
denial
|
|
157
|
+
});
|
|
158
|
+
return err(error);
|
|
159
|
+
};
|
|
160
|
+
const violation = checkPolicy(admin, request, reason, seconds);
|
|
161
|
+
if (violation) return deny(violation, "policy");
|
|
162
|
+
const input = {
|
|
163
|
+
admin,
|
|
164
|
+
targetUserId: request.targetUserId,
|
|
165
|
+
reason,
|
|
166
|
+
readOnly,
|
|
167
|
+
...request.tenant === void 0 ? {} : { tenant: request.tenant },
|
|
168
|
+
metadata: request.metadata ?? {}
|
|
169
|
+
};
|
|
170
|
+
if (!await authorized(input)) return deny(denied("Not allowed to start a support session", "SUPPORT_FORBIDDEN"), "authorize");
|
|
171
|
+
let session;
|
|
172
|
+
try {
|
|
173
|
+
session = await options.store.start({
|
|
174
|
+
adminId: admin.user.id,
|
|
175
|
+
adminClaims: admin.claims,
|
|
176
|
+
targetUserId: request.targetUserId,
|
|
177
|
+
reason,
|
|
178
|
+
ttlSeconds: seconds,
|
|
179
|
+
readOnly,
|
|
180
|
+
...request.tenant === void 0 ? {} : { tenant: request.tenant },
|
|
181
|
+
metadata: request.metadata ?? {}
|
|
182
|
+
});
|
|
183
|
+
} catch (cause) {
|
|
184
|
+
const error = storeError(cause);
|
|
185
|
+
return deny(error, error.code === "42501" ? "permission" : "store");
|
|
186
|
+
}
|
|
187
|
+
emit("support.started", {
|
|
188
|
+
sessionId: session.id,
|
|
189
|
+
adminId: session.adminId,
|
|
190
|
+
targetUserId: session.targetUserId,
|
|
191
|
+
reason: session.reason,
|
|
192
|
+
readOnly: session.readOnly,
|
|
193
|
+
expiresAt: session.expiresAt
|
|
194
|
+
});
|
|
195
|
+
return ok({
|
|
196
|
+
session,
|
|
197
|
+
cookie: supportCookie(session, options.cookie)
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
stop(admin, sessionId) {
|
|
202
|
+
return AsyncResult.from(async () => {
|
|
203
|
+
const cookie = clearSupportCookie(options.cookie);
|
|
204
|
+
if (admin.kind !== "user") return err(dbError("unauthorized", "Sign in to stop a support session"));
|
|
205
|
+
try {
|
|
206
|
+
const session = await options.store.get(sessionId, admin.user.id, admin.claims);
|
|
207
|
+
if (!session) return ok({
|
|
208
|
+
ended: false,
|
|
209
|
+
cookie
|
|
210
|
+
});
|
|
211
|
+
const ended = await options.store.end(sessionId, "admin", admin.claims);
|
|
212
|
+
if (ended) emit("support.ended", {
|
|
213
|
+
sessionId,
|
|
214
|
+
adminId: session.adminId,
|
|
215
|
+
targetUserId: session.targetUserId,
|
|
216
|
+
endedBy: "admin"
|
|
217
|
+
});
|
|
218
|
+
return ok({
|
|
219
|
+
ended,
|
|
220
|
+
cookie
|
|
221
|
+
});
|
|
222
|
+
} catch (cause) {
|
|
223
|
+
return err(storeError(cause));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
revoke(caller, sessionId) {
|
|
228
|
+
return AsyncResult.from(async () => {
|
|
229
|
+
if (caller.kind !== "user") return err(dbError("unauthorized", "Sign in to revoke a support session"));
|
|
230
|
+
try {
|
|
231
|
+
return ok(await options.store.end(sessionId, "revoked", caller.claims));
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
return err(storeError(cause));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
async current(request, auth) {
|
|
238
|
+
if (auth.kind !== "user") return;
|
|
239
|
+
if (auth.claims["act"] !== void 0) return;
|
|
240
|
+
const id = supportCookieValue(request.headers.get("cookie"), cookieName);
|
|
241
|
+
if (!id) return;
|
|
242
|
+
let session;
|
|
243
|
+
let claims;
|
|
244
|
+
try {
|
|
245
|
+
session = await options.store.get(id, auth.user.id, auth.claims);
|
|
246
|
+
if (!session || session.expiresAt.epochMilliseconds <= Date.now()) return;
|
|
247
|
+
if (policy.readOnly !== "default" && !session.readOnly) session = {
|
|
248
|
+
...session,
|
|
249
|
+
readOnly: true
|
|
250
|
+
};
|
|
251
|
+
claims = supportClaims(session, await targetClaims(options.store, session.targetUserId));
|
|
252
|
+
} catch (cause) {
|
|
253
|
+
events.logger.warn("Could not load the support session", { cause });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (claimsSchema) {
|
|
257
|
+
const checked = await validate(claimsSchema, claims, "claims");
|
|
258
|
+
if (!checked.ok) {
|
|
259
|
+
events.logger.warn("The support target's claims fail the claims schema", { sessionId: session.id });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (typeof checked.data === "object" && checked.data !== null) claims = {
|
|
263
|
+
...claims,
|
|
264
|
+
...checked.data
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
session,
|
|
269
|
+
admin: {
|
|
270
|
+
id: auth.user.id,
|
|
271
|
+
...auth.user.email ? { email: auth.user.email } : {}
|
|
272
|
+
},
|
|
273
|
+
claims
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
list(caller, filter) {
|
|
277
|
+
return AsyncResult.from(async () => {
|
|
278
|
+
if (caller.kind !== "user") return err(dbError("unauthorized", "Sign in to list support sessions"));
|
|
279
|
+
try {
|
|
280
|
+
return ok(await options.store.list(filter, caller.claims));
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
return err(storeError(cause));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
sessionIdOf: (request) => supportCookieValue(request.headers.get("cookie"), cookieName),
|
|
287
|
+
clearCookie: () => clearSupportCookie(options.cookie)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
export { sqlSupportStore as n, supportClaims as r, supportSessions as t };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { t as AuthSession } from "./view-BEf0diC5.js";
|
|
2
|
+
//#region src/auth/support-view.d.ts
|
|
3
|
+
/** A running support session as the target's view shows it, for a banner. */
|
|
4
|
+
interface SupportView {
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
/** The admin viewing the app. */
|
|
7
|
+
readonly adminId: string;
|
|
8
|
+
readonly targetUserId: string;
|
|
9
|
+
readonly reason?: string;
|
|
10
|
+
readonly readOnly: boolean;
|
|
11
|
+
/** Seconds since epoch. */
|
|
12
|
+
readonly expiresAt: number | null;
|
|
13
|
+
}
|
|
14
|
+
/** The support session behind `session`, or `undefined` outside one. */
|
|
15
|
+
declare function supportOf(session: AuthSession): SupportView | undefined;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { supportOf as n, SupportView as t };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/auth/support-view.ts
|
|
2
|
+
/** The support session behind `session`, or `undefined` outside one. */
|
|
3
|
+
function supportOf(session) {
|
|
4
|
+
if (session.kind !== "user") return;
|
|
5
|
+
const { impersonator } = session;
|
|
6
|
+
const sessionId = impersonator?.sessionId;
|
|
7
|
+
if (!impersonator || !sessionId) return;
|
|
8
|
+
return {
|
|
9
|
+
sessionId,
|
|
10
|
+
adminId: impersonator.id,
|
|
11
|
+
targetUserId: session.user.id,
|
|
12
|
+
...impersonator.reason ? { reason: impersonator.reason } : {},
|
|
13
|
+
readOnly: impersonator.readOnly ?? true,
|
|
14
|
+
expiresAt: session.expiresAt
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { supportOf as t };
|
|
@@ -42,6 +42,20 @@ function visitCondition(condition, tables) {
|
|
|
42
42
|
* followed through chains of cascades.
|
|
43
43
|
*/
|
|
44
44
|
function invalidationTargets(meta, table) {
|
|
45
|
+
let byTable = targetsByMeta.get(meta);
|
|
46
|
+
if (!byTable) {
|
|
47
|
+
byTable = /* @__PURE__ */ new Map();
|
|
48
|
+
targetsByMeta.set(meta, byTable);
|
|
49
|
+
}
|
|
50
|
+
let targets = byTable.get(table);
|
|
51
|
+
if (!targets) {
|
|
52
|
+
targets = Object.freeze(computeTargets(meta, table));
|
|
53
|
+
byTable.set(table, targets);
|
|
54
|
+
}
|
|
55
|
+
return targets;
|
|
56
|
+
}
|
|
57
|
+
const targetsByMeta = /* @__PURE__ */ new WeakMap();
|
|
58
|
+
function computeTargets(meta, table) {
|
|
45
59
|
const targets = /* @__PURE__ */ new Set([table]);
|
|
46
60
|
const queue = [table];
|
|
47
61
|
for (let key = queue.shift(); key !== void 0; key = queue.shift()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as dbError, t as DbException } from "./errors-
|
|
1
|
+
import { n as dbError, t as DbException } from "./errors-DtwXa6b8.js";
|
|
2
2
|
//#region src/core/template.ts
|
|
3
3
|
const PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
4
4
|
function parseSegment(segment) {
|
|
@@ -90,33 +90,4 @@ function slug(value) {
|
|
|
90
90
|
return value.replaceAll(/[^A-Za-z0-9]+/g, "_").replaceAll(/^_|_$/g, "").toLowerCase();
|
|
91
91
|
}
|
|
92
92
|
//#endregion
|
|
93
|
-
|
|
94
|
-
const SCOPE = /^[a-z_][a-z0-9_]*$/;
|
|
95
|
-
/** PermDock splits a key with row conditions into `key#1`, `key#2`, ...: those need its own policies. */
|
|
96
|
-
const SPLIT_KEY = /#\d+$/;
|
|
97
|
-
/**
|
|
98
|
-
* The SQL condition for one PermDock permission key. `id` is the text
|
|
99
|
-
* expression holding the scope id; it is compared as text, so a malformed
|
|
100
|
-
* path never raises a cast error.
|
|
101
|
-
*
|
|
102
|
-
* The helpers check role and scope, not a permission's row conditions, so
|
|
103
|
-
* the condition is only correct for permissions whose grants have none
|
|
104
|
-
* beyond the scope. `#n` keys are refused, and so are keys the catalog marks
|
|
105
|
-
* with `rowConditions: true` when one is passed.
|
|
106
|
-
*/
|
|
107
|
-
function permdockCheck(where, target, key, id, catalog) {
|
|
108
|
-
if (key === "") throw new TypeError(`${where}: a PermDock permission key is empty`);
|
|
109
|
-
if (SPLIT_KEY.test(key)) throw new TypeError(`${where}: "${key}" is one part of a permission PermDock splits by row condition. Use PermDock's generated policies for it.`);
|
|
110
|
-
if (catalog?.permissions.some((permission) => permission.key === key && permission.rowConditions === true)) throw new TypeError(`${where}: "${key}" has row conditions in PermDock's catalog (rowConditions: true), which the SQL helpers don't check. Use PermDock's generated policies for it.`);
|
|
111
|
-
const schema = sqlIdent(target.schema ?? "public");
|
|
112
|
-
if (target.scope === "global") return `(select ${schema}.permdock_has(${sqlString(key)}))`;
|
|
113
|
-
if (!SCOPE.test(target.scope)) throw new TypeError(`${where}: invalid PermDock scope "${target.scope}"`);
|
|
114
|
-
if (id === void 0) throw new TypeError(`${where}: scope "${target.scope}" needs a segment`);
|
|
115
|
-
return `${id} in (select t.id::text from ${schema}.${sqlIdent(`permitted_${target.scope}_ids`)}(${sqlString(key)}) as t(id))`;
|
|
116
|
-
}
|
|
117
|
-
/** The keys a bucket or topic PermDock policy names. */
|
|
118
|
-
function permdockKeys(policy) {
|
|
119
|
-
return Object.values(policy.permdock).filter((key) => typeof key === "string");
|
|
120
|
-
}
|
|
121
|
-
//#endregion
|
|
122
|
-
export { sqlIdent as a, slug as i, permdockKeys as n, sqlString as o, parseTemplate as r, permdockCheck as t };
|
|
93
|
+
export { sqlString as i, slug as n, sqlIdent as r, parseTemplate as t };
|
|
@@ -12,16 +12,29 @@ function nowInstant() {
|
|
|
12
12
|
if (namespace === void 0) throw new TypeError(TEMPORAL_MISSING);
|
|
13
13
|
return namespace.Now.instant();
|
|
14
14
|
}
|
|
15
|
+
const ISO_TAGS = /* @__PURE__ */ new Set([
|
|
16
|
+
"Temporal.Instant",
|
|
17
|
+
"Temporal.PlainDateTime",
|
|
18
|
+
"Temporal.PlainDate",
|
|
19
|
+
"Temporal.PlainTime"
|
|
20
|
+
]);
|
|
15
21
|
/**
|
|
16
22
|
* The text Postgres reads back for a Temporal value: ISO 8601, with a
|
|
17
23
|
* `ZonedDateTime` sent as its instant because Postgres rejects the
|
|
18
24
|
* `[Region/City]` annotation. `undefined` for anything else.
|
|
25
|
+
*
|
|
26
|
+
* Values are recognized by `Symbol.toStringTag`, not `instanceof`, so values
|
|
27
|
+
* from another realm or a second copy of the polyfill still encode.
|
|
19
28
|
*/
|
|
20
29
|
function temporalText(value) {
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
if (
|
|
30
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
31
|
+
if (!(Symbol.toStringTag in value)) return void 0;
|
|
32
|
+
const tag = value[Symbol.toStringTag];
|
|
33
|
+
if (ISO_TAGS.has(tag)) return String(value);
|
|
34
|
+
return tag === "Temporal.ZonedDateTime" && isZoned(value) ? String(value.toInstant()) : void 0;
|
|
35
|
+
}
|
|
36
|
+
function isZoned(value) {
|
|
37
|
+
return "toInstant" in value && typeof value.toInstant === "function";
|
|
25
38
|
}
|
|
26
39
|
//#endregion
|
|
27
40
|
export { temporalText as i, nowInstant as n, optionalTemporal as r, TEMPORAL_MISSING as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as dbError, t as DbException } from "./errors-
|
|
2
|
-
import { r as optionalTemporal, t as TEMPORAL_MISSING } from "./temporal-
|
|
1
|
+
import { n as dbError, t as DbException } from "./errors-DtwXa6b8.js";
|
|
2
|
+
import { r as optionalTemporal, t as TEMPORAL_MISSING } from "./temporal-CVlIV2DZ.js";
|
|
3
3
|
//#region src/core/temporal-required.ts
|
|
4
4
|
/** The error for a runtime without Temporal, naming the polyfill to load. */
|
|
5
5
|
function temporalMissing() {
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as Executor } from "../executor-
|
|
3
|
-
import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-
|
|
4
|
-
import {
|
|
5
|
-
import { b as ResolvedConfig, d as Generator, f as GeneratorInput } from "../config-
|
|
6
|
-
import "../
|
|
7
|
-
import { t as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { r as EventSink, t as CloudEvent } from "../index-
|
|
1
|
+
import { P as TableKey, _ as Insert, j as SchemaMeta, n as AnyFunctions, r as AnyModels, z as Update } from "../types-Dnf_wRDz.js";
|
|
2
|
+
import { r as Executor } from "../executor-BunPOO-k.js";
|
|
3
|
+
import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-Bp_BZa5h.js";
|
|
4
|
+
import { g as RequestContext, n as AnyPlugin } from "../plugin-o144f322.js";
|
|
5
|
+
import { b as ResolvedConfig, d as Generator, f as GeneratorInput } from "../config-T_ejwyd3.js";
|
|
6
|
+
import { t as SqlClient } from "../executor-Cghdstwu.js";
|
|
7
|
+
import { t as BetterPostgres } from "../pool-C9voM4Kw.js";
|
|
8
|
+
import { n as AuthResolver } from "../resolve-C15TJq77.js";
|
|
9
|
+
import { o as SupportSessionStore } from "../support-DT-OhSxD.js";
|
|
10
|
+
import { r as EventSink, t as CloudEvent } from "../index-DOESKG27.js";
|
|
11
|
+
import { p as QueueBackend } from "../queue-BYUCjr59.js";
|
|
12
|
+
import { t as NotificationChannel } from "../channel-FQW-CnGR.js";
|
|
13
|
+
import { c as WebhookSecretStore, i as WebhookSigner, m as WebhookTransport } from "../signers-Cc6r98SW.js";
|
|
11
14
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
12
15
|
import { webcrypto } from "node:crypto";
|
|
13
16
|
//#region src/testing/jwt.d.ts
|
|
@@ -26,7 +29,7 @@ interface TestJwtClaims {
|
|
|
26
29
|
export declare function signTestJwt(secret: string, claims: TestJwtClaims): Promise<string>;
|
|
27
30
|
/**
|
|
28
31
|
* Accepts `signTestJwt` tokens as users, for API servers tested against the
|
|
29
|
-
* local stack: `createServer(
|
|
32
|
+
* local stack: `createServer(betterSupabase, { auth: { resolvers: [localAuth(secret)] } })`.
|
|
30
33
|
* Tokens that are not HS256-signed with `secret` fall through. Test-only.
|
|
31
34
|
*/
|
|
32
35
|
export declare function localAuth(secret: string): AuthResolver;
|
|
@@ -82,7 +85,7 @@ interface LocalStack {
|
|
|
82
85
|
/** Service-role key for `expectTenantIsolation`; defaults to `$SUPABASE_SECRET_KEY`. */
|
|
83
86
|
readonly secretKey?: string;
|
|
84
87
|
/** Enables `sql`: the same user over direct Postgres. */
|
|
85
|
-
readonly postgres?:
|
|
88
|
+
readonly postgres?: BetterPostgres;
|
|
86
89
|
}
|
|
87
90
|
interface TestUser<M extends AnyModels, F extends AnyFunctions, E> {
|
|
88
91
|
readonly token: string;
|
|
@@ -101,11 +104,11 @@ interface TestUser<M extends AnyModels, F extends AnyFunctions, E> {
|
|
|
101
104
|
* still uses the shared JWT secret.
|
|
102
105
|
*
|
|
103
106
|
* ```ts
|
|
104
|
-
* const alice = await asUser(
|
|
107
|
+
* const alice = await asUser(betterSupabase, { sub: aliceId, tenant_id: acme });
|
|
105
108
|
* expect(await alice.db.customers.count().orThrow()).toBe(3);
|
|
106
109
|
* ```
|
|
107
110
|
*/
|
|
108
|
-
export declare function asUser<M extends AnyModels, D, F extends AnyFunctions, E>(
|
|
111
|
+
export declare function asUser<M extends AnyModels, D, F extends AnyFunctions, E>(betterSupabase: BetterSupabase<M, D, F, E>, claims: TestJwtClaims, stack?: LocalStack): Promise<TestUser<M, F, E>>;
|
|
109
112
|
//#endregion
|
|
110
113
|
//#region src/testing/budget.d.ts
|
|
111
114
|
/** The part of a Playwright `Response` the budget check reads. */
|
|
@@ -148,7 +151,7 @@ interface MeasuredRender {
|
|
|
148
151
|
/**
|
|
149
152
|
* Measures every document and RSC navigation response during `during` and
|
|
150
153
|
* fails when one render makes more database calls or waves than allowed.
|
|
151
|
-
* Needs `createNext(
|
|
154
|
+
* Needs `createNext(betterSupabase, { debug })` and `bs.debugRoute()` in the app.
|
|
152
155
|
*/
|
|
153
156
|
export declare function expectDbBudget(page: BudgetPage, expectation: DbBudgetExpectation): Promise<readonly MeasuredRender[]>;
|
|
154
157
|
//#endregion
|
|
@@ -171,7 +174,7 @@ export declare class ConformanceError extends Error {
|
|
|
171
174
|
type AnySupabase = BetterSupabase<any, any, any, any>;
|
|
172
175
|
interface TestExecutorOptions {
|
|
173
176
|
/** A definition whose schema matches the executor's database. Install no plugins. */
|
|
174
|
-
readonly
|
|
177
|
+
readonly betterSupabase: AnySupabase;
|
|
175
178
|
/** App key of a table with rows the executor can read. Defaults to the first table. */
|
|
176
179
|
readonly table?: string;
|
|
177
180
|
/** A row to insert and delete again, to check writes. Skips write checks when omitted. */
|
|
@@ -183,7 +186,7 @@ interface TestExecutorOptions {
|
|
|
183
186
|
* than thrown, and (with `create`) a write round trip.
|
|
184
187
|
*
|
|
185
188
|
* ```ts
|
|
186
|
-
* it('conforms', () => testExecutor(myExecutor, {
|
|
189
|
+
* it('conforms', () => testExecutor(myExecutor, { betterSupabase, table: 'tags', create: { name: 'x' } }));
|
|
187
190
|
* ```
|
|
188
191
|
*/
|
|
189
192
|
export declare function testExecutor(executor: Executor, options: TestExecutorOptions): Promise<ConformanceReport>;
|
|
@@ -200,6 +203,35 @@ interface TestEventSinkOptions {
|
|
|
200
203
|
}
|
|
201
204
|
/** Proves an `EventSink` accepts empty and full batches without mutating them, and delivers them when readable. */
|
|
202
205
|
export declare function testEventSink(sink: EventSink, options?: TestEventSinkOptions): Promise<ConformanceReport>;
|
|
206
|
+
interface TestQueueBackendOptions {
|
|
207
|
+
/** A queue the kit may fill and drain; use a fresh name per run. */
|
|
208
|
+
readonly queue: string;
|
|
209
|
+
/** Check that a repeated dedupe key returns the first id. Defaults to `backend.leases`. */
|
|
210
|
+
readonly dedupe?: boolean;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Proves a `QueueBackend` round-trips payloads, hides claimed messages for
|
|
214
|
+
* their lease, counts attempts, retries and dead-letters on `fail`, and
|
|
215
|
+
* (when it has `leases`) rejects a stale attempt and extends a lease. It
|
|
216
|
+
* writes to `options.queue` on the backend's real store.
|
|
217
|
+
*/
|
|
218
|
+
export declare function testQueueBackend(backend: QueueBackend, options: TestQueueBackendOptions): Promise<ConformanceReport>;
|
|
219
|
+
interface TestSupportSessionStoreOptions {
|
|
220
|
+
/** A user who may start support sessions in the store. */
|
|
221
|
+
readonly admin: {
|
|
222
|
+
readonly id: string;
|
|
223
|
+
readonly claims: Readonly<Record<string, unknown>>;
|
|
224
|
+
};
|
|
225
|
+
/** Two other users the admin may view the app as. */
|
|
226
|
+
readonly targets: readonly [string, string];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Proves a `SupportSessionStore` starts sessions with the input it was
|
|
230
|
+
* given, shows a running session only to its admin, ends the admin's
|
|
231
|
+
* previous session on start, ends a session once, and lists sessions.
|
|
232
|
+
* It writes sessions to the store; they are all ended when it returns.
|
|
233
|
+
*/
|
|
234
|
+
export declare function testSupportSessionStore(store: SupportSessionStore, options: TestSupportSessionStoreOptions): Promise<ConformanceReport>;
|
|
203
235
|
interface TestAuthResolverOptions {
|
|
204
236
|
/** Requests with credentials this resolver owns but that must not verify. */
|
|
205
237
|
readonly invalid: readonly Request[];
|
|
@@ -228,7 +260,7 @@ interface TestGeneratorOptions {
|
|
|
228
260
|
export declare function testGenerator(generator: Generator, options: TestGeneratorOptions): Promise<ConformanceReport>;
|
|
229
261
|
interface TestPluginOptions {
|
|
230
262
|
/** The definition without this plugin. */
|
|
231
|
-
readonly
|
|
263
|
+
readonly betterSupabase: AnySupabase;
|
|
232
264
|
/** Table to exercise. Defaults to the first table. */
|
|
233
265
|
readonly table?: string;
|
|
234
266
|
/** Context the plugin needs, such as `{ tenant }` for `tenant()`. */
|
|
@@ -273,12 +305,12 @@ interface TenantIsolationOptions<M extends AnyModels> {
|
|
|
273
305
|
/**
|
|
274
306
|
* Checks row-level security between two tenants against a running stack: for
|
|
275
307
|
* each table, a user of one tenant must not select, insert, update or delete
|
|
276
|
-
* the other tenant's rows. Runs without `
|
|
308
|
+
* the other tenant's rows. Runs without `betterSupabase`'s plugins, so the `tenant()`
|
|
277
309
|
* plugin can't hide a missing policy. Throws a `ConformanceError` naming each
|
|
278
310
|
* table and command that leaks; removes every row it created.
|
|
279
311
|
*
|
|
280
312
|
* ```ts
|
|
281
|
-
* await expectTenantIsolation(
|
|
313
|
+
* await expectTenantIsolation(betterSupabase, {
|
|
282
314
|
* tenants: [
|
|
283
315
|
* { id: ACME, claims: { sub: alice, tenant_id: ACME } },
|
|
284
316
|
* { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
|
|
@@ -292,7 +324,7 @@ interface TenantIsolationOptions<M extends AnyModels> {
|
|
|
292
324
|
* });
|
|
293
325
|
* ```
|
|
294
326
|
*/
|
|
295
|
-
export declare function expectTenantIsolation<M extends AnyModels, D, F extends AnyFunctions, E>(
|
|
327
|
+
export declare function expectTenantIsolation<M extends AnyModels, D, F extends AnyFunctions, E>(betterSupabase: BetterSupabase<M, D, F, E>, options: TenantIsolationOptions<M>): Promise<ConformanceReport>;
|
|
296
328
|
//#endregion
|
|
297
329
|
//#region src/testing/seed.d.ts
|
|
298
330
|
/** Named rows per table, in app casing: `{ customers: { acme: { ... } } }`. */
|
|
@@ -316,12 +348,69 @@ export declare function isSeed(value: unknown): value is Seed<unknown>;
|
|
|
316
348
|
* casing and Insert types; `better-supabase seed` renders them to SQL.
|
|
317
349
|
*
|
|
318
350
|
* ```ts
|
|
319
|
-
* export const seed = defineSeed(
|
|
351
|
+
* export const seed = defineSeed(betterSupabase, {
|
|
320
352
|
* organizations: { acme: { id: ACME, name: 'Acme' } },
|
|
321
353
|
* customers: { first: { organizationId: ACME, name: 'First customer' } },
|
|
322
354
|
* });
|
|
323
355
|
* ```
|
|
324
356
|
*/
|
|
325
|
-
export declare function defineSeed<M extends AnyModels, D, F extends AnyFunctions, E, const S extends SeedFixtures<M>>(
|
|
357
|
+
export declare function defineSeed<M extends AnyModels, D, F extends AnyFunctions, E, const S extends SeedFixtures<M>>(betterSupabase: BetterSupabase<M, D, F, E>, fixtures: S & ExactSeed<M, S>): Seed<S>;
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/testing/notification-channel.d.ts
|
|
360
|
+
interface TestNotificationChannelOptions {
|
|
361
|
+
/** The recipient's email in the sample message. Defaults to `null`. */
|
|
362
|
+
readonly email?: string | null;
|
|
363
|
+
/**
|
|
364
|
+
* The delivery ids the provider received, read after the kit sends. When
|
|
365
|
+
* given, the kit checks that the sample message arrived.
|
|
366
|
+
*/
|
|
367
|
+
readonly received?: () => Promise<readonly string[]>;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Runs the `NotificationChannel` contract against `channel`: the API version,
|
|
371
|
+
* a name, and one frozen sample message it must send without mutating.
|
|
372
|
+
* Throws a `ConformanceError` listing every failed check.
|
|
373
|
+
*/
|
|
374
|
+
export declare function testNotificationChannel(channel: NotificationChannel, options?: TestNotificationChannelOptions): Promise<ConformanceReport>;
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/testing/webhooks.d.ts
|
|
377
|
+
interface TestWebhookSignerOptions {
|
|
378
|
+
/**
|
|
379
|
+
* The receiver's check, e.g. `verifyWebhook`. When given, the kit checks
|
|
380
|
+
* that the signed sample verifies with its secret.
|
|
381
|
+
*/
|
|
382
|
+
readonly verify?: (request: {
|
|
383
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
384
|
+
readonly body: string;
|
|
385
|
+
}, secret: string) => Promise<boolean>;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Runs the `WebhookSigner` contract against `signer`: the API version, a
|
|
389
|
+
* name, string headers that depend on the body and are stable for the same
|
|
390
|
+
* input, and, with `verify`, a signature the receiver accepts.
|
|
391
|
+
*/
|
|
392
|
+
export declare function testWebhookSigner(signer: WebhookSigner, options?: TestWebhookSignerOptions): Promise<ConformanceReport>;
|
|
393
|
+
interface TestWebhookTransportOptions {
|
|
394
|
+
/** A receiver the transport may POST the sample to, e.g. a local server. */
|
|
395
|
+
readonly url: string;
|
|
396
|
+
/** The bodies the receiver got, read after the kit sends. */
|
|
397
|
+
readonly received?: () => Promise<readonly string[]>;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Runs the `WebhookTransport` contract against `transport`: the API version,
|
|
401
|
+
* a name, and one POST to `options.url` that returns an HTTP status and a
|
|
402
|
+
* string body.
|
|
403
|
+
*/
|
|
404
|
+
export declare function testWebhookTransport(transport: WebhookTransport, options: TestWebhookTransportOptions): Promise<ConformanceReport>;
|
|
405
|
+
interface TestWebhookSecretStoreOptions {
|
|
406
|
+
/** An endpoint the store may read and, when it can rotate, rotate. */
|
|
407
|
+
readonly endpointId: string;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Runs the `WebhookSecretStore` contract against `store`: the API version, a
|
|
411
|
+
* list of strings, and, when it can rotate, a new secret listed first while
|
|
412
|
+
* the previous one keeps signing.
|
|
413
|
+
*/
|
|
414
|
+
export declare function testWebhookSecretStore(store: WebhookSecretStore, options: TestWebhookSecretStoreOptions): Promise<ConformanceReport>;
|
|
326
415
|
//#endregion
|
|
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 };
|
|
416
|
+
export type { BudgetPage, BudgetResponse, ConformanceCheck, ConformanceReport, DbBudgetExpectation, ExactSeed, IsolationTable, IsolationTables, IsolationTenant, LocalStack, MeasuredRender, Seed, SeedFixtures, SigningJwk, TenantIsolationOptions, TestAuthResolverOptions, TestEventSinkOptions, TestExecutorOptions, TestGeneratorOptions, TestJwtClaims, TestNotificationChannelOptions, TestPluginOptions, TestQueueBackendOptions, TestSigner, TestSupportSessionStoreOptions, TestUser, TestWebhookSecretStoreOptions, TestWebhookSignerOptions, TestWebhookTransportOptions };
|