better-supabase 0.4.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.
Files changed (179) hide show
  1. package/README.md +2 -0
  2. package/dist/channel-FQW-CnGR.d.ts +70 -0
  3. package/dist/cli/bin.js +1 -1
  4. package/dist/cli/codemod-DqV7612m.js +388 -0
  5. package/dist/cli/{config-D9R0o1LH.js → config-CPVpXP5I.js} +78 -8
  6. package/dist/cli/{config-SSKSTUl2.js → config-dtoekM0v.js} +1 -1
  7. package/dist/cli/{read-sets-CMa29t9a.js → diff-SjkO5rNr.js} +1 -220
  8. package/dist/cli/{dist-CFgGknmZ.js → dist-DBPdlPYK.js} +4 -4
  9. package/dist/cli/{dist-B1MvLv-M.js → dist-DreZ24Sz.js} +1 -1
  10. package/dist/cli/{dist-Du7VnEUG.js → dist-xn4wlboc.js} +112 -1
  11. package/dist/cli/{doctor-BOym3mbL.js → doctor-L7Z8mYTx.js} +531 -50
  12. package/dist/cli/{env-BZ0uy8XK.js → env-DmKv_G4A.js} +1 -1
  13. package/dist/cli/gen-BJc9vdyu.js +2 -0
  14. package/dist/cli/{gen-Ccn_p90L.js → gen-C2osRcr5.js} +283 -87
  15. package/dist/cli/index.d.ts +1 -1
  16. package/dist/cli/index.js +4 -4
  17. package/dist/cli/{init-ClnxRYBF.js → init-Saf5-R9T.js} +2 -2
  18. package/dist/cli/{introspect-DxqfkVIv.js → introspect--H7oGmcd.js} +2 -2
  19. package/dist/cli/{openapi-8WHjxiTJ.js → openapi-Cp5Fyta-.js} +1 -1
  20. package/dist/cli/read-sets-BfMQcKy8.js +289 -0
  21. package/dist/cli/{run-CP20LKk_.js → run-BKR2W_79.js} +16 -12
  22. package/dist/cli/{seed-5J6eu_Xg.js → seed-CUlqWh-8.js} +1 -1
  23. package/dist/cli/sql-DzPbfngx.js +348 -0
  24. package/dist/cli/{version-5mW_8sY1.js → version-BDJuO0UR.js} +1 -1
  25. package/dist/client/index.d.ts +1 -1
  26. package/dist/client/index.js +3 -10
  27. package/dist/clone-BfgmLU90.js +21 -0
  28. package/dist/{compiler-DB3aSFOA.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  29. package/dist/config/index.d.ts +4 -4
  30. package/dist/config/index.js +4 -4
  31. package/dist/{config-CbvFmGD8.js → config-DreERlKo.js} +1 -1
  32. package/dist/{config-C1CYcm6o.d.ts → config-T_ejwyd3.d.ts} +176 -11
  33. package/dist/{define-CRS2MRV3.d.ts → define-Bp_BZa5h.d.ts} +11 -7
  34. package/dist/{define-Bh0AqTik.js → define-cNKzqwL_.js} +202 -16
  35. package/dist/edge/index.d.ts +16 -5
  36. package/dist/edge/index.js +10 -7
  37. package/dist/{entitlements-Dm-FHpNH.d.ts → entitlements-DUxU4oHe.d.ts} +1 -1
  38. package/dist/env/index.d.ts +2 -2
  39. package/dist/env/index.js +34 -1
  40. package/dist/{errors-L0xK41y_.js → errors-DtwXa6b8.js} +1 -0
  41. package/dist/events/index.d.ts +3 -2
  42. package/dist/events/index.js +65 -12
  43. package/dist/events-aIO7vsIk.d.ts +230 -0
  44. package/dist/{result-CqEsbgYs.d.ts → executor-BunPOO-k.d.ts} +200 -1
  45. package/dist/{executor-CZ7ImRiD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  46. package/dist/{executor-CVt6dD0B.js → executor-XycESQPH.js} +3 -3
  47. package/dist/hono/index.d.ts +17 -6
  48. package/dist/hono/index.js +40 -5
  49. package/dist/impersonation-VfaGc4Q_.js +41 -0
  50. package/dist/{index-BBdvo-5d.d.ts → index-BGdgBPdf.d.ts} +49 -6
  51. package/dist/{index-BhHEC04r.d.ts → index-C7f8cOBM.d.ts} +2 -2
  52. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  53. package/dist/{index-D5IfUIDh.d.ts → index-DOESKG27.d.ts} +33 -8
  54. package/dist/{index-DFmOpBGC.d.ts → index-DgILJRXj.d.ts} +35 -9
  55. package/dist/{index-RlLXd8dL.d.ts → index-DmXSxbLi.d.ts} +4 -4
  56. package/dist/{index-Cf-D2jk1.d.ts → index-Dxn2a5cw.d.ts} +2 -2
  57. package/dist/{index-528Ugmyp.d.ts → index-SaTsEfGP.d.ts} +76 -8
  58. package/dist/{index-BAy4tsAP.d.ts → index-fag8VJZ2.d.ts} +3 -4
  59. package/dist/index.d.ts +30 -13
  60. package/dist/index.js +10 -9
  61. package/dist/jobs/index.d.ts +147 -101
  62. package/dist/jobs/index.js +744 -237
  63. package/dist/{json-schema-DP6V7cbr.js → json-schema-m1fiwKgN.js} +22 -1
  64. package/dist/kit-events-CTFDfSwb.js +60 -0
  65. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  66. package/dist/kit-transport-DJlHQUBU.js +55 -0
  67. package/dist/{config-BpxDfnbg.js → kits-CGLLrNPd.js} +37 -9
  68. package/dist/list/index.d.ts +1 -1
  69. package/dist/{live-BHakSWWo.d.ts → live-D9W2M5aO.d.ts} +6 -5
  70. package/dist/mcp/index.d.ts +25 -8
  71. package/dist/mcp/index.js +23 -14
  72. package/dist/{mfa-C7MNQ1Ob.d.ts → mfa-CusDgPaD.d.ts} +5 -1
  73. package/dist/next/index.d.ts +31 -10
  74. package/dist/next/index.js +136 -27
  75. package/dist/notifications/index.d.ts +162 -0
  76. package/dist/notifications/index.js +264 -0
  77. package/dist/openapi/index.d.ts +1 -1
  78. package/dist/openapi/index.js +2 -2
  79. package/dist/orgs/index.d.ts +132 -0
  80. package/dist/orgs/index.js +245 -0
  81. package/dist/orpc/index.d.ts +28 -8
  82. package/dist/orpc/index.js +16 -3
  83. package/dist/otel/index.d.ts +21 -2
  84. package/dist/otel/index.js +50 -9
  85. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  86. package/dist/permdock-sql-DgOfeixi.js +51 -0
  87. package/dist/plugin-DwARMuQp.js +59 -0
  88. package/dist/{plugin-bNhvJwRh.d.ts → plugin-o144f322.d.ts} +17 -5
  89. package/dist/plugins/actor/index.d.ts +5 -2
  90. package/dist/plugins/actor/index.js +22 -8
  91. package/dist/plugins/rules/index.d.ts +2 -2
  92. package/dist/plugins/rules/index.js +15 -10
  93. package/dist/plugins/soft-delete/index.d.ts +3 -3
  94. package/dist/plugins/soft-delete/index.js +14 -5
  95. package/dist/plugins/tenant/index.d.ts +2 -2
  96. package/dist/plugins/tenant/index.js +38 -20
  97. package/dist/plugins/timestamps/index.d.ts +1 -1
  98. package/dist/plugins/timestamps/index.js +4 -3
  99. package/dist/plugins/validation/index.d.ts +2 -2
  100. package/dist/plugins/validation/index.js +26 -6
  101. package/dist/{pool-DFwEQwmN.d.ts → pool-C9voM4Kw.d.ts} +17 -6
  102. package/dist/postgres/index.d.ts +5 -5
  103. package/dist/postgres/index.js +20 -11
  104. package/dist/{postgrest-Bc-0Xw62.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  105. package/dist/{problem-BihjPR0t.js → problem-BW1KhzSG.js} +2 -1
  106. package/dist/{problem-U8UEKccv.d.ts → problem-Blxe8S31.d.ts} +1 -1
  107. package/dist/query/index.d.ts +2 -2
  108. package/dist/query/index.js +3 -3
  109. package/dist/{query-BMMSWbI7.js → query-CCbo44lG.js} +2 -2
  110. package/dist/queue-BYUCjr59.d.ts +252 -0
  111. package/dist/react/index.d.ts +8 -7
  112. package/dist/react/index.js +4 -3
  113. package/dist/react/server.d.ts +14 -3
  114. package/dist/react/server.js +10 -2
  115. package/dist/react/session.d.ts +7 -1
  116. package/dist/react/session.js +9 -1
  117. package/dist/{react-CQHMaESR.js → react-C_s7kiNz.js} +103 -11
  118. package/dist/{read-set-FWK8qQ58.js → read-set-DQFt1N_Y.js} +22 -1
  119. package/dist/realtime/index.d.ts +3 -3
  120. package/dist/realtime/index.js +5 -4
  121. package/dist/{refresh-BwYkyNOQ.d.ts → refresh-BTrQ_ZTp.d.ts} +1 -1
  122. package/dist/{resolve-BAqQcwMW.d.ts → resolve-C15TJq77.d.ts} +11 -5
  123. package/dist/{resource-My_y-Syh.js → resource-CRxxwkQq.js} +22 -7
  124. package/dist/{resource-BJcaIfxi.d.ts → resource-D8iqWMr-.d.ts} +5 -5
  125. package/dist/respond-D2OQl62-.d.ts +371 -0
  126. package/dist/{result-Bj6yt33D.js → result-BKiliPY7.js} +1 -1
  127. package/dist/server/index.d.ts +14 -12
  128. package/dist/server/index.js +11 -17
  129. package/dist/{server-BA75B0mT.js → server-Csx2sRjg.js} +251 -34
  130. package/dist/{shared-DnoCdMDr.js → shared-D1dRUrAg.js} +16 -2
  131. package/dist/signers-Cc6r98SW.d.ts +162 -0
  132. package/dist/{simplify-Cqvfr9dj.js → simplify-_0oUKqCd.js} +4 -115
  133. package/dist/sql/index.d.ts +247 -10
  134. package/dist/sql/index.js +8590 -779
  135. package/dist/{sql-B34HU2DI.js → sql-Coh6n4cC.js} +9 -2
  136. package/dist/ssr/index.d.ts +4 -4
  137. package/dist/ssr/index.js +1 -2
  138. package/dist/{standard-BRyBGTtp.js → standard-D7r6dMUu.js} +6 -3
  139. package/dist/{standard-BsaCYQJc.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  140. package/dist/{stats-C_YxA2r1.js → stats-Bqob3WqT.js} +4 -4
  141. package/dist/storage/index.d.ts +4 -4
  142. package/dist/storage/index.js +155 -26
  143. package/dist/support-DT-OhSxD.d.ts +88 -0
  144. package/dist/support-Dk6vdmEw.js +291 -0
  145. package/dist/support-view-Da5peKFg.d.ts +17 -0
  146. package/dist/support-view-g4YLBeqX.js +18 -0
  147. package/dist/{permdock-sql-C4KFzLc9.js → template-TxrGI5jR.js} +2 -31
  148. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  149. package/dist/{temporal-required-CBNHT6ni.js → temporal-required-CAt4Dc9Z.js} +2 -2
  150. package/dist/testing/index.d.ts +100 -10
  151. package/dist/testing/index.js +336 -8
  152. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  153. package/dist/user-change-CHFEUro1.js +49 -0
  154. package/dist/{webhooks-Bh_Jf_-l.js → verify-Ce49ETPz.js} +5 -5
  155. package/dist/{version-Bmf7_FzM.js → version-DM_Roffb.js} +1 -1
  156. package/dist/{view-B64o81wp.d.ts → view-BEf0diC5.d.ts} +8 -3
  157. package/dist/{mfa-byfjruR9.js → view-Bq64ki8-.js} +95 -38
  158. package/dist/webhooks/index.d.ts +135 -4
  159. package/dist/webhooks/index.js +588 -2
  160. package/dist/wire-D2dT86vi.js +35 -0
  161. package/package.json +16 -8
  162. package/schemas/config-v1.json +131 -1
  163. package/schemas/doctor-report-v1.json +10 -0
  164. package/skills/better-supabase/SKILL.md +9 -6
  165. package/skills/better-supabase/references/plugins.md +6 -3
  166. package/skills/better-supabase/references/troubleshooting.md +1 -0
  167. package/skills/better-supabase-api/SKILL.md +9 -2
  168. package/skills/better-supabase-api/references/adapters.md +11 -8
  169. package/skills/better-supabase-auth/SKILL.md +3 -0
  170. package/skills/better-supabase-auth/references/permdock.md +56 -11
  171. package/dist/cli/gen-D5cLCULx.js +0 -2
  172. package/dist/cli/sql-D4VbqmXi.js +0 -199
  173. package/dist/events-B21UOhvP.d.ts +0 -84
  174. package/dist/executor-B5IoUxLE.d.ts +0 -195
  175. package/dist/invalidate-CgxmRamI.js +0 -24
  176. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  177. package/dist/plugin-BqR6wKMB.js +0 -23
  178. package/dist/respond-DgIbdrt8.d.ts +0 -194
  179. package/dist/view-BrJI2LKp.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 };
@@ -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,16 @@
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-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";
10
14
  import { SupabaseClient } from "@supabase/supabase-js";
11
15
  import { webcrypto } from "node:crypto";
12
16
  //#region src/testing/jwt.d.ts
@@ -199,6 +203,35 @@ interface TestEventSinkOptions {
199
203
  }
200
204
  /** Proves an `EventSink` accepts empty and full batches without mutating them, and delivers them when readable. */
201
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>;
202
235
  interface TestAuthResolverOptions {
203
236
  /** Requests with credentials this resolver owns but that must not verify. */
204
237
  readonly invalid: readonly Request[];
@@ -323,4 +356,61 @@ export declare function isSeed(value: unknown): value is Seed<unknown>;
323
356
  */
324
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>;
325
358
  //#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 };
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>;
415
+ //#endregion
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 };