better-supabase 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +135 -16
  2. package/dist/actor-DaJjqD5R.d.ts +43 -0
  3. package/dist/channel-FQW-CnGR.d.ts +70 -0
  4. package/dist/cli/bin.js +1 -1
  5. package/dist/cli/codemod-DqV7612m.js +388 -0
  6. package/dist/cli/{config-D9R0o1LH.js → config-CPVpXP5I.js} +78 -8
  7. package/dist/cli/{config-SSKSTUl2.js → config-dtoekM0v.js} +1 -1
  8. package/dist/cli/{read-sets-CMa29t9a.js → diff-SjkO5rNr.js} +1 -220
  9. package/dist/cli/{dist-CFgGknmZ.js → dist-DBPdlPYK.js} +4 -4
  10. package/dist/cli/{dist-B1MvLv-M.js → dist-DreZ24Sz.js} +1 -1
  11. package/dist/cli/{dist-Du7VnEUG.js → dist-xn4wlboc.js} +112 -1
  12. package/dist/cli/{doctor-BOym3mbL.js → doctor-C_A-KEAz.js} +533 -50
  13. package/dist/cli/{env-BZ0uy8XK.js → env-DmKv_G4A.js} +1 -1
  14. package/dist/cli/gen-CIdSQZad.js +2 -0
  15. package/dist/cli/{gen-Ccn_p90L.js → gen-CjElD1Vw.js} +321 -88
  16. package/dist/cli/index.d.ts +1 -1
  17. package/dist/cli/index.js +4 -4
  18. package/dist/cli/{init-ClnxRYBF.js → init-DMa6M7hV.js} +2 -2
  19. package/dist/cli/{introspect-DxqfkVIv.js → introspect-BKlaIu_M.js} +2 -2
  20. package/dist/cli/{openapi-8WHjxiTJ.js → openapi-Cp5Fyta-.js} +1 -1
  21. package/dist/cli/read-sets-VQcPgxU9.js +436 -0
  22. package/dist/cli/{run-CP20LKk_.js → run-Bo_NLT5n.js} +16 -12
  23. package/dist/cli/{seed-5J6eu_Xg.js → seed-CUlqWh-8.js} +1 -1
  24. package/dist/cli/sql-CVx-1O8G.js +373 -0
  25. package/dist/cli/{version-5mW_8sY1.js → version-BCtlgDQx.js} +1 -1
  26. package/dist/client/index.d.ts +1 -1
  27. package/dist/client/index.js +3 -10
  28. package/dist/clone-BfgmLU90.js +21 -0
  29. package/dist/{compiler-DB3aSFOA.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  30. package/dist/config/index.d.ts +4 -4
  31. package/dist/config/index.js +4 -4
  32. package/dist/{config-C1CYcm6o.d.ts → config-Bv7gvfzC.d.ts} +185 -11
  33. package/dist/{config-CbvFmGD8.js → config-DreERlKo.js} +1 -1
  34. package/dist/{define-CRS2MRV3.d.ts → define-Bp_BZa5h.d.ts} +11 -7
  35. package/dist/{define-Bh0AqTik.js → define-cNKzqwL_.js} +202 -16
  36. package/dist/edge/index.d.ts +16 -5
  37. package/dist/edge/index.js +10 -7
  38. package/dist/{entitlements-Dm-FHpNH.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
  39. package/dist/env/index.d.ts +2 -2
  40. package/dist/env/index.js +34 -1
  41. package/dist/{errors-L0xK41y_.js → errors-DtwXa6b8.js} +1 -0
  42. package/dist/events/index.d.ts +3 -2
  43. package/dist/events/index.js +65 -12
  44. package/dist/events-aIO7vsIk.d.ts +230 -0
  45. package/dist/{result-CqEsbgYs.d.ts → executor-BunPOO-k.d.ts} +200 -1
  46. package/dist/{executor-CZ7ImRiD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  47. package/dist/{executor-CVt6dD0B.js → executor-XycESQPH.js} +3 -3
  48. package/dist/hono/index.d.ts +17 -6
  49. package/dist/hono/index.js +40 -5
  50. package/dist/impersonation-BgSHAYLf.js +133 -0
  51. package/dist/{resolve-BAqQcwMW.d.ts → impersonation-BmePsuxz.d.ts} +36 -6
  52. package/dist/{index-BhHEC04r.d.ts → index-C7f8cOBM.d.ts} +2 -2
  53. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  54. package/dist/{index-Cf-D2jk1.d.ts → index-CWMDdEr4.d.ts} +2 -2
  55. package/dist/{index-D5IfUIDh.d.ts → index-DOESKG27.d.ts} +33 -8
  56. package/dist/{index-DFmOpBGC.d.ts → index-DgILJRXj.d.ts} +35 -9
  57. package/dist/{index-RlLXd8dL.d.ts → index-DmXSxbLi.d.ts} +4 -4
  58. package/dist/{index-BBdvo-5d.d.ts → index-Dp2VDwog.d.ts} +49 -6
  59. package/dist/{index-528Ugmyp.d.ts → index-SaTsEfGP.d.ts} +76 -8
  60. package/dist/{index-BAy4tsAP.d.ts → index-fag8VJZ2.d.ts} +3 -4
  61. package/dist/index.d.ts +30 -13
  62. package/dist/index.js +10 -9
  63. package/dist/jobs/index.d.ts +147 -101
  64. package/dist/jobs/index.js +744 -237
  65. package/dist/{json-schema-DP6V7cbr.js → json-schema-m1fiwKgN.js} +22 -1
  66. package/dist/kit-events-CTFDfSwb.js +60 -0
  67. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  68. package/dist/kit-transport-DJlHQUBU.js +55 -0
  69. package/dist/{config-BpxDfnbg.js → kits-CGLLrNPd.js} +37 -9
  70. package/dist/list/index.d.ts +1 -1
  71. package/dist/{live-BHakSWWo.d.ts → live-D9W2M5aO.d.ts} +6 -5
  72. package/dist/mcp/index.d.ts +25 -8
  73. package/dist/mcp/index.js +23 -14
  74. package/dist/{mfa-C7MNQ1Ob.d.ts → mfa-Cu96oJdw.d.ts} +2 -16
  75. package/dist/next/index.d.ts +31 -10
  76. package/dist/next/index.js +136 -27
  77. package/dist/notifications/index.d.ts +162 -0
  78. package/dist/notifications/index.js +264 -0
  79. package/dist/openapi/index.d.ts +1 -1
  80. package/dist/openapi/index.js +2 -2
  81. package/dist/orgs/index.d.ts +132 -0
  82. package/dist/orgs/index.js +245 -0
  83. package/dist/orpc/index.d.ts +28 -8
  84. package/dist/orpc/index.js +16 -3
  85. package/dist/otel/index.d.ts +21 -2
  86. package/dist/otel/index.js +50 -9
  87. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  88. package/dist/permdock-sql-DgOfeixi.js +51 -0
  89. package/dist/plugin-DwARMuQp.js +59 -0
  90. package/dist/{plugin-bNhvJwRh.d.ts → plugin-o144f322.d.ts} +17 -5
  91. package/dist/plugins/actor/index.d.ts +5 -2
  92. package/dist/plugins/actor/index.js +22 -8
  93. package/dist/plugins/rules/index.d.ts +2 -2
  94. package/dist/plugins/rules/index.js +15 -10
  95. package/dist/plugins/soft-delete/index.d.ts +3 -3
  96. package/dist/plugins/soft-delete/index.js +14 -5
  97. package/dist/plugins/tenant/index.d.ts +2 -2
  98. package/dist/plugins/tenant/index.js +38 -20
  99. package/dist/plugins/timestamps/index.d.ts +1 -1
  100. package/dist/plugins/timestamps/index.js +4 -3
  101. package/dist/plugins/validation/index.d.ts +2 -2
  102. package/dist/plugins/validation/index.js +26 -6
  103. package/dist/{pool-DFwEQwmN.d.ts → pool-C9voM4Kw.d.ts} +17 -6
  104. package/dist/postgres/index.d.ts +5 -5
  105. package/dist/postgres/index.js +20 -11
  106. package/dist/{postgrest-Bc-0Xw62.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  107. package/dist/{problem-BihjPR0t.js → problem-BW1KhzSG.js} +2 -1
  108. package/dist/{problem-U8UEKccv.d.ts → problem-Blxe8S31.d.ts} +1 -1
  109. package/dist/query/index.d.ts +2 -2
  110. package/dist/query/index.js +3 -3
  111. package/dist/{query-BMMSWbI7.js → query-CCbo44lG.js} +2 -2
  112. package/dist/queue-BYUCjr59.d.ts +252 -0
  113. package/dist/react/index.d.ts +8 -7
  114. package/dist/react/index.js +4 -3
  115. package/dist/react/server.d.ts +14 -3
  116. package/dist/react/server.js +10 -2
  117. package/dist/react/session.d.ts +7 -1
  118. package/dist/react/session.js +9 -1
  119. package/dist/{react-CQHMaESR.js → react-C_s7kiNz.js} +103 -11
  120. package/dist/{read-set-FWK8qQ58.js → read-set-DQFt1N_Y.js} +22 -1
  121. package/dist/realtime/index.d.ts +3 -3
  122. package/dist/realtime/index.js +5 -4
  123. package/dist/{refresh-BwYkyNOQ.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
  124. package/dist/{resource-BJcaIfxi.d.ts → resource-DNRFP7KR.d.ts} +5 -5
  125. package/dist/{resource-My_y-Syh.js → resource-mqfJW4YF.js} +22 -7
  126. package/dist/respond-BR1fJWOB.d.ts +377 -0
  127. package/dist/{result-Bj6yt33D.js → result-BKiliPY7.js} +1 -1
  128. package/dist/server/index.d.ts +15 -12
  129. package/dist/server/index.js +11 -17
  130. package/dist/{server-BA75B0mT.js → server-B1mZ7CgQ.js} +259 -37
  131. package/dist/{shared-DnoCdMDr.js → shared-D1dRUrAg.js} +16 -2
  132. package/dist/signers-Cc6r98SW.d.ts +162 -0
  133. package/dist/{simplify-Cqvfr9dj.js → simplify-_0oUKqCd.js} +4 -115
  134. package/dist/sql/index.d.ts +283 -11
  135. package/dist/sql/index.js +8705 -775
  136. package/dist/{sql-B34HU2DI.js → sql-Coh6n4cC.js} +9 -2
  137. package/dist/ssr/index.d.ts +4 -4
  138. package/dist/ssr/index.js +1 -2
  139. package/dist/{standard-BRyBGTtp.js → standard-D7r6dMUu.js} +6 -3
  140. package/dist/{standard-BsaCYQJc.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  141. package/dist/{stats-C_YxA2r1.js → stats-Bqob3WqT.js} +4 -4
  142. package/dist/storage/index.d.ts +4 -4
  143. package/dist/storage/index.js +155 -26
  144. package/dist/support-CEPwlw2G.d.ts +89 -0
  145. package/dist/support-view-B8SlToQX.d.ts +17 -0
  146. package/dist/support-view-g4YLBeqX.js +18 -0
  147. package/dist/support-wHnK6zp-.js +293 -0
  148. package/dist/{permdock-sql-C4KFzLc9.js → template-TxrGI5jR.js} +2 -31
  149. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  150. package/dist/{temporal-required-CBNHT6ni.js → temporal-required-CAt4Dc9Z.js} +2 -2
  151. package/dist/testing/index.d.ts +132 -10
  152. package/dist/testing/index.js +476 -8
  153. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  154. package/dist/user-change-CHFEUro1.js +49 -0
  155. package/dist/{webhooks-Bh_Jf_-l.js → verify-Ce49ETPz.js} +5 -5
  156. package/dist/{version-Bmf7_FzM.js → version-DOYTYwHP.js} +1 -1
  157. package/dist/{mfa-byfjruR9.js → view-Bn6HNKUe.js} +97 -98
  158. package/dist/{view-B64o81wp.d.ts → view-CbgObAeb.d.ts} +22 -31
  159. package/dist/webhooks/index.d.ts +135 -4
  160. package/dist/webhooks/index.js +588 -2
  161. package/dist/wire-D2dT86vi.js +35 -0
  162. package/package.json +27 -19
  163. package/schemas/config-v1.json +131 -1
  164. package/schemas/doctor-report-v1.json +11 -0
  165. package/skills/better-supabase/SKILL.md +9 -6
  166. package/skills/better-supabase/references/plugins.md +6 -3
  167. package/skills/better-supabase/references/troubleshooting.md +1 -0
  168. package/skills/better-supabase-api/SKILL.md +9 -2
  169. package/skills/better-supabase-api/references/adapters.md +11 -8
  170. package/skills/better-supabase-auth/SKILL.md +18 -7
  171. package/skills/better-supabase-auth/references/permdock.md +78 -11
  172. package/dist/cli/gen-D5cLCULx.js +0 -2
  173. package/dist/cli/sql-D4VbqmXi.js +0 -199
  174. package/dist/events-B21UOhvP.d.ts +0 -84
  175. package/dist/executor-B5IoUxLE.d.ts +0 -195
  176. package/dist/invalidate-CgxmRamI.js +0 -24
  177. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  178. package/dist/plugin-BqR6wKMB.js +0 -23
  179. package/dist/respond-DgIbdrt8.d.ts +0 -194
  180. package/dist/view-BrJI2LKp.js +0 -45
@@ -0,0 +1,293 @@
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-B1mZ7CgQ.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
+ * marked `kind: "support"` so `actorOf` never reads it as an OAuth client.
81
+ * `role` falls back to `authenticated`; the admin's claims never leak in.
82
+ */
83
+ function supportClaims(session, target) {
84
+ const role = target["role"];
85
+ return {
86
+ ...target,
87
+ sub: session.targetUserId,
88
+ role: typeof role === "string" && role !== "" ? role : "authenticated",
89
+ act: {
90
+ kind: "support",
91
+ sub: session.adminId,
92
+ reason: session.reason,
93
+ session_id: session.id,
94
+ read_only: session.readOnly
95
+ }
96
+ };
97
+ }
98
+ //#endregion
99
+ //#region src/server/support.ts
100
+ const denied = (message, code) => dbError("forbidden", message, { code });
101
+ const invalid = (message, code) => dbError("invalid_request", message, { code });
102
+ function storeError(cause) {
103
+ const raw = fromPgError(cause);
104
+ return raw ? mapDbError(raw) : toDbError(cause);
105
+ }
106
+ /**
107
+ * Support sessions for `createServer(betterSupabase, { support })`. Apps that
108
+ * never import it don't bundle the support code.
109
+ */
110
+ function supportSessions(options) {
111
+ return {
112
+ options,
113
+ create: (events, claimsSchema) => createSupport(options, events, claimsSchema)
114
+ };
115
+ }
116
+ function createSupport(options, events, claimsSchema) {
117
+ const policy = options.policy ?? {};
118
+ const ttl = policy.ttl ?? 1800;
119
+ const maxTtl = policy.maxTtl ?? 14400;
120
+ const cookieName = options.cookie?.name;
121
+ const emit = (type, data) => {
122
+ emitKitEvent(events, type, data, {
123
+ actorId: data.adminId,
124
+ ...data.sessionId ? { subject: `support_sessions/${data.sessionId}` } : {}
125
+ });
126
+ };
127
+ const targetClaims = async (store, targetUserId) => {
128
+ if (options.claims) return options.claims(targetUserId);
129
+ if (store.claims) return store.claims(targetUserId);
130
+ return { role: "authenticated" };
131
+ };
132
+ const checkPolicy = (admin, request, reason, seconds) => {
133
+ if (admin.claims["act"] !== void 0) return denied("An impersonated session cannot start a support session", "SUPPORT_NESTED");
134
+ if (request.targetUserId === admin.user.id) return invalid("An admin cannot start a support session as themselves", "SUPPORT_SELF");
135
+ if ((policy.requireReason ?? true) && reason === "") return invalid("A reason is required", "SUPPORT_REASON_REQUIRED");
136
+ if (!Number.isFinite(seconds) || seconds <= 0 || seconds > maxTtl) return invalid(`ttl must be between 1 and ${String(maxTtl)} seconds`, "SUPPORT_TTL");
137
+ };
138
+ const authorized = async (input) => {
139
+ if (!options.authorize) return true;
140
+ try {
141
+ return await options.authorize(input);
142
+ } catch {
143
+ return false;
144
+ }
145
+ };
146
+ return {
147
+ start(admin, request) {
148
+ return AsyncResult.from(async () => {
149
+ if (admin.kind !== "user") return err(dbError("unauthorized", "Sign in to start a support session"));
150
+ const reason = request.reason?.trim() ?? "";
151
+ const seconds = request.ttl ?? ttl;
152
+ const readOnly = policy.readOnly === "default" ? request.readOnly ?? true : true;
153
+ const deny = (error, denial) => {
154
+ emit("support.denied", {
155
+ adminId: admin.user.id,
156
+ targetUserId: request.targetUserId,
157
+ ...reason ? { reason } : {},
158
+ denial
159
+ });
160
+ return err(error);
161
+ };
162
+ const violation = checkPolicy(admin, request, reason, seconds);
163
+ if (violation) return deny(violation, "policy");
164
+ const input = {
165
+ admin,
166
+ targetUserId: request.targetUserId,
167
+ reason,
168
+ readOnly,
169
+ ...request.tenant === void 0 ? {} : { tenant: request.tenant },
170
+ metadata: request.metadata ?? {}
171
+ };
172
+ if (!await authorized(input)) return deny(denied("Not allowed to start a support session", "SUPPORT_FORBIDDEN"), "authorize");
173
+ let session;
174
+ try {
175
+ session = await options.store.start({
176
+ adminId: admin.user.id,
177
+ adminClaims: admin.claims,
178
+ targetUserId: request.targetUserId,
179
+ reason,
180
+ ttlSeconds: seconds,
181
+ readOnly,
182
+ ...request.tenant === void 0 ? {} : { tenant: request.tenant },
183
+ metadata: request.metadata ?? {}
184
+ });
185
+ } catch (cause) {
186
+ const error = storeError(cause);
187
+ return deny(error, error.code === "42501" ? "permission" : "store");
188
+ }
189
+ emit("support.started", {
190
+ sessionId: session.id,
191
+ adminId: session.adminId,
192
+ targetUserId: session.targetUserId,
193
+ reason: session.reason,
194
+ readOnly: session.readOnly,
195
+ expiresAt: session.expiresAt
196
+ });
197
+ return ok({
198
+ session,
199
+ cookie: supportCookie(session, options.cookie)
200
+ });
201
+ });
202
+ },
203
+ stop(admin, sessionId) {
204
+ return AsyncResult.from(async () => {
205
+ const cookie = clearSupportCookie(options.cookie);
206
+ if (admin.kind !== "user") return err(dbError("unauthorized", "Sign in to stop a support session"));
207
+ try {
208
+ const session = await options.store.get(sessionId, admin.user.id, admin.claims);
209
+ if (!session) return ok({
210
+ ended: false,
211
+ cookie
212
+ });
213
+ const ended = await options.store.end(sessionId, "admin", admin.claims);
214
+ if (ended) emit("support.ended", {
215
+ sessionId,
216
+ adminId: session.adminId,
217
+ targetUserId: session.targetUserId,
218
+ endedBy: "admin"
219
+ });
220
+ return ok({
221
+ ended,
222
+ cookie
223
+ });
224
+ } catch (cause) {
225
+ return err(storeError(cause));
226
+ }
227
+ });
228
+ },
229
+ revoke(caller, sessionId) {
230
+ return AsyncResult.from(async () => {
231
+ if (caller.kind !== "user") return err(dbError("unauthorized", "Sign in to revoke a support session"));
232
+ try {
233
+ return ok(await options.store.end(sessionId, "revoked", caller.claims));
234
+ } catch (cause) {
235
+ return err(storeError(cause));
236
+ }
237
+ });
238
+ },
239
+ async current(request, auth) {
240
+ if (auth.kind !== "user") return;
241
+ if (auth.claims["act"] !== void 0) return;
242
+ const id = supportCookieValue(request.headers.get("cookie"), cookieName);
243
+ if (!id) return;
244
+ let session;
245
+ let claims;
246
+ try {
247
+ session = await options.store.get(id, auth.user.id, auth.claims);
248
+ if (!session || session.expiresAt.epochMilliseconds <= Date.now()) return;
249
+ if (policy.readOnly !== "default" && !session.readOnly) session = {
250
+ ...session,
251
+ readOnly: true
252
+ };
253
+ claims = supportClaims(session, await targetClaims(options.store, session.targetUserId));
254
+ } catch (cause) {
255
+ events.logger.warn("Could not load the support session", { cause });
256
+ return;
257
+ }
258
+ if (claimsSchema) {
259
+ const checked = await validate(claimsSchema, claims, "claims");
260
+ if (!checked.ok) {
261
+ events.logger.warn("The support target's claims fail the claims schema", { sessionId: session.id });
262
+ return;
263
+ }
264
+ if (typeof checked.data === "object" && checked.data !== null) claims = {
265
+ ...claims,
266
+ ...checked.data
267
+ };
268
+ }
269
+ return {
270
+ session,
271
+ admin: {
272
+ id: auth.user.id,
273
+ ...auth.user.email ? { email: auth.user.email } : {}
274
+ },
275
+ claims
276
+ };
277
+ },
278
+ list(caller, filter) {
279
+ return AsyncResult.from(async () => {
280
+ if (caller.kind !== "user") return err(dbError("unauthorized", "Sign in to list support sessions"));
281
+ try {
282
+ return ok(await options.store.list(filter, caller.claims));
283
+ } catch (cause) {
284
+ return err(storeError(cause));
285
+ }
286
+ });
287
+ },
288
+ sessionIdOf: (request) => supportCookieValue(request.headers.get("cookie"), cookieName),
289
+ clearCookie: () => clearSupportCookie(options.cookie)
290
+ };
291
+ }
292
+ //#endregion
293
+ export { sqlSupportStore as n, supportClaims as r, supportSessions as t };
@@ -1,4 +1,4 @@
1
- import { n as dbError, t as DbException } from "./errors-L0xK41y_.js";
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
- //#region src/core/permdock-sql.ts
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
- const namespace = optionalTemporal();
22
- if (namespace === void 0) return void 0;
23
- if (value instanceof namespace.ZonedDateTime) return value.toInstant().toString();
24
- if (value instanceof namespace.Instant || value instanceof namespace.PlainDateTime || value instanceof namespace.PlainDate || value instanceof namespace.PlainTime) return value.toString();
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-L0xK41y_.js";
2
- import { r as optionalTemporal, t as TEMPORAL_MISSING } from "./temporal-BwshQk0W.js";
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() {
@@ -1,12 +1,17 @@
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-B5IoUxLE.js";
3
- import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-CRS2MRV3.js";
4
- import { h as RequestContext, n as AnyPlugin } from "../plugin-bNhvJwRh.js";
5
- import { b as ResolvedConfig, d as Generator, f as GeneratorInput } from "../config-C1CYcm6o.js";
6
- import { t as SqlClient } from "../executor-CZ7ImRiD.js";
7
- import { t as BetterPostgres } from "../pool-DFwEQwmN.js";
8
- import { n as AuthResolver } from "../resolve-BAqQcwMW.js";
9
- import { r as EventSink, t as CloudEvent } from "../index-D5IfUIDh.js";
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-Bv7gvfzC.js";
6
+ import { t as SqlClient } from "../executor-Cghdstwu.js";
7
+ import { t as BetterPostgres } from "../pool-C9voM4Kw.js";
8
+ import { a as AuthResolver, n as Impersonator } from "../impersonation-BmePsuxz.js";
9
+ import { o as SupportSessionStore } from "../support-CEPwlw2G.js";
10
+ import { r as EventSink, t as CloudEvent } from "../index-DOESKG27.js";
11
+ import { p as QueueBackend } from "../queue-BYUCjr59.js";
12
+ import { n as SessionActor, r as SessionDelegation } from "../actor-DaJjqD5R.js";
13
+ import { t as NotificationChannel } from "../channel-FQW-CnGR.js";
14
+ import { c as WebhookSecretStore, i as WebhookSigner, m as WebhookTransport } from "../signers-Cc6r98SW.js";
10
15
  import { SupabaseClient } from "@supabase/supabase-js";
11
16
  import { webcrypto } from "node:crypto";
12
17
  //#region src/testing/jwt.d.ts
@@ -199,6 +204,35 @@ interface TestEventSinkOptions {
199
204
  }
200
205
  /** Proves an `EventSink` accepts empty and full batches without mutating them, and delivers them when readable. */
201
206
  export declare function testEventSink(sink: EventSink, options?: TestEventSinkOptions): Promise<ConformanceReport>;
207
+ interface TestQueueBackendOptions {
208
+ /** A queue the kit may fill and drain; use a fresh name per run. */
209
+ readonly queue: string;
210
+ /** Check that a repeated dedupe key returns the first id. Defaults to `backend.leases`. */
211
+ readonly dedupe?: boolean;
212
+ }
213
+ /**
214
+ * Proves a `QueueBackend` round-trips payloads, hides claimed messages for
215
+ * their lease, counts attempts, retries and dead-letters on `fail`, and
216
+ * (when it has `leases`) rejects a stale attempt and extends a lease. It
217
+ * writes to `options.queue` on the backend's real store.
218
+ */
219
+ export declare function testQueueBackend(backend: QueueBackend, options: TestQueueBackendOptions): Promise<ConformanceReport>;
220
+ interface TestSupportSessionStoreOptions {
221
+ /** A user who may start support sessions in the store. */
222
+ readonly admin: {
223
+ readonly id: string;
224
+ readonly claims: Readonly<Record<string, unknown>>;
225
+ };
226
+ /** Two other users the admin may view the app as. */
227
+ readonly targets: readonly [string, string];
228
+ }
229
+ /**
230
+ * Proves a `SupportSessionStore` starts sessions with the input it was
231
+ * given, shows a running session only to its admin, ends the admin's
232
+ * previous session on start, ends a session once, and lists sessions.
233
+ * It writes sessions to the store; they are all ended when it returns.
234
+ */
235
+ export declare function testSupportSessionStore(store: SupportSessionStore, options: TestSupportSessionStoreOptions): Promise<ConformanceReport>;
202
236
  interface TestAuthResolverOptions {
203
237
  /** Requests with credentials this resolver owns but that must not verify. */
204
238
  readonly invalid: readonly Request[];
@@ -293,6 +327,37 @@ interface TenantIsolationOptions<M extends AnyModels> {
293
327
  */
294
328
  export declare function expectTenantIsolation<M extends AnyModels, D, F extends AnyFunctions, E>(betterSupabase: BetterSupabase<M, D, F, E>, options: TenantIsolationOptions<M>): Promise<ConformanceReport>;
295
329
  //#endregion
330
+ //#region src/testing/claims-fixtures.d.ts
331
+ /**
332
+ * Claims of a Supabase access token where someone acts for the user, with
333
+ * what `toSession` reads from them: `actor`, `impersonator` and
334
+ * `delegation`. Plain data in the shape of PermDock's `supabaseClaimFixtures`
335
+ * (`permdock/testing`), so both packages test the same `act` contract: every
336
+ * fixture passes PermDock's `schemas/supabase-claims-v1.json`.
337
+ */
338
+ interface SupabaseClaimFixture {
339
+ /** Without `iat` and `exp`, so a test signer sets them. */
340
+ readonly claims: {
341
+ readonly sub: string;
342
+ readonly [claim: string]: unknown;
343
+ };
344
+ readonly expect: {
345
+ readonly actor: SessionActor;
346
+ /** Set for support and impersonated sessions only. */
347
+ readonly impersonator?: Impersonator;
348
+ /** Set for an `oauth-client` actor only. */
349
+ readonly delegation?: SessionDelegation;
350
+ };
351
+ }
352
+ type SupabaseClaimFixtureName = "supportSession" | "supportSessionReadOnly" | "impersonation" | "oauthClient" | "agentChain";
353
+ /**
354
+ * `supportSession` and `supportSessionReadOnly` are what `supportClaims`
355
+ * mints (`act.kind: "support"`), `impersonation` what `actingAs` mints
356
+ * (`act.kind: "impersonation"`), `oauthClient` a Supabase OAuth server token
357
+ * and `agentChain` an RFC 8693 chain without `kind`.
358
+ */
359
+ export declare const supabaseClaimFixtures: Readonly<Record<SupabaseClaimFixtureName, SupabaseClaimFixture>>;
360
+ //#endregion
296
361
  //#region src/testing/seed.d.ts
297
362
  /** Named rows per table, in app casing: `{ customers: { acme: { ... } } }`. */
298
363
  type SeedFixtures<M extends AnyModels> = { readonly [K in keyof M]?: Readonly<Record<string, M[K]["Insert"]>>; };
@@ -323,4 +388,61 @@ export declare function isSeed(value: unknown): value is Seed<unknown>;
323
388
  */
324
389
  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>;
325
390
  //#endregion
326
- 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 };
391
+ //#region src/testing/notification-channel.d.ts
392
+ interface TestNotificationChannelOptions {
393
+ /** The recipient's email in the sample message. Defaults to `null`. */
394
+ readonly email?: string | null;
395
+ /**
396
+ * The delivery ids the provider received, read after the kit sends. When
397
+ * given, the kit checks that the sample message arrived.
398
+ */
399
+ readonly received?: () => Promise<readonly string[]>;
400
+ }
401
+ /**
402
+ * Runs the `NotificationChannel` contract against `channel`: the API version,
403
+ * a name, and one frozen sample message it must send without mutating.
404
+ * Throws a `ConformanceError` listing every failed check.
405
+ */
406
+ export declare function testNotificationChannel(channel: NotificationChannel, options?: TestNotificationChannelOptions): Promise<ConformanceReport>;
407
+ //#endregion
408
+ //#region src/testing/webhooks.d.ts
409
+ interface TestWebhookSignerOptions {
410
+ /**
411
+ * The receiver's check, e.g. `verifyWebhook`. When given, the kit checks
412
+ * that the signed sample verifies with its secret.
413
+ */
414
+ readonly verify?: (request: {
415
+ readonly headers: Readonly<Record<string, string>>;
416
+ readonly body: string;
417
+ }, secret: string) => Promise<boolean>;
418
+ }
419
+ /**
420
+ * Runs the `WebhookSigner` contract against `signer`: the API version, a
421
+ * name, string headers that depend on the body and are stable for the same
422
+ * input, and, with `verify`, a signature the receiver accepts.
423
+ */
424
+ export declare function testWebhookSigner(signer: WebhookSigner, options?: TestWebhookSignerOptions): Promise<ConformanceReport>;
425
+ interface TestWebhookTransportOptions {
426
+ /** A receiver the transport may POST the sample to, e.g. a local server. */
427
+ readonly url: string;
428
+ /** The bodies the receiver got, read after the kit sends. */
429
+ readonly received?: () => Promise<readonly string[]>;
430
+ }
431
+ /**
432
+ * Runs the `WebhookTransport` contract against `transport`: the API version,
433
+ * a name, and one POST to `options.url` that returns an HTTP status and a
434
+ * string body.
435
+ */
436
+ export declare function testWebhookTransport(transport: WebhookTransport, options: TestWebhookTransportOptions): Promise<ConformanceReport>;
437
+ interface TestWebhookSecretStoreOptions {
438
+ /** An endpoint the store may read and, when it can rotate, rotate. */
439
+ readonly endpointId: string;
440
+ }
441
+ /**
442
+ * Runs the `WebhookSecretStore` contract against `store`: the API version, a
443
+ * list of strings, and, when it can rotate, a new secret listed first while
444
+ * the previous one keeps signing.
445
+ */
446
+ export declare function testWebhookSecretStore(store: WebhookSecretStore, options: TestWebhookSecretStoreOptions): Promise<ConformanceReport>;
447
+ //#endregion
448
+ export type { BudgetPage, BudgetResponse, ConformanceCheck, ConformanceReport, DbBudgetExpectation, ExactSeed, IsolationTable, IsolationTables, IsolationTenant, LocalStack, MeasuredRender, Seed, SeedFixtures, SigningJwk, SupabaseClaimFixture, SupabaseClaimFixtureName, TenantIsolationOptions, TestAuthResolverOptions, TestEventSinkOptions, TestExecutorOptions, TestGeneratorOptions, TestJwtClaims, TestNotificationChannelOptions, TestPluginOptions, TestQueueBackendOptions, TestSigner, TestSupportSessionStoreOptions, TestUser, TestWebhookSecretStoreOptions, TestWebhookSignerOptions, TestWebhookTransportOptions };