better-supabase 0.5.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 (62) hide show
  1. package/README.md +133 -16
  2. package/dist/actor-DaJjqD5R.d.ts +43 -0
  3. package/dist/cli/bin.js +1 -1
  4. package/dist/cli/{doctor-L7Z8mYTx.js → doctor-C_A-KEAz.js} +6 -4
  5. package/dist/cli/gen-CIdSQZad.js +2 -0
  6. package/dist/cli/{gen-C2osRcr5.js → gen-CjElD1Vw.js} +72 -35
  7. package/dist/cli/index.d.ts +1 -1
  8. package/dist/cli/index.js +3 -3
  9. package/dist/cli/{init-Saf5-R9T.js → init-DMa6M7hV.js} +1 -1
  10. package/dist/cli/{introspect--H7oGmcd.js → introspect-BKlaIu_M.js} +1 -1
  11. package/dist/cli/{read-sets-BfMQcKy8.js → read-sets-VQcPgxU9.js} +173 -26
  12. package/dist/cli/{run-BKR2W_79.js → run-Bo_NLT5n.js} +7 -7
  13. package/dist/cli/{sql-DzPbfngx.js → sql-CVx-1O8G.js} +30 -5
  14. package/dist/cli/{version-BDJuO0UR.js → version-BCtlgDQx.js} +1 -1
  15. package/dist/client/index.js +1 -1
  16. package/dist/config/index.d.ts +2 -2
  17. package/dist/{config-T_ejwyd3.d.ts → config-Bv7gvfzC.d.ts} +10 -1
  18. package/dist/edge/index.d.ts +2 -2
  19. package/dist/edge/index.js +2 -2
  20. package/dist/{entitlements-DUxU4oHe.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
  21. package/dist/hono/index.d.ts +3 -3
  22. package/dist/hono/index.js +2 -2
  23. package/dist/impersonation-BgSHAYLf.js +133 -0
  24. package/dist/{resolve-C15TJq77.d.ts → impersonation-BmePsuxz.d.ts} +25 -1
  25. package/dist/{index-Dxn2a5cw.d.ts → index-CWMDdEr4.d.ts} +1 -1
  26. package/dist/{index-BGdgBPdf.d.ts → index-Dp2VDwog.d.ts} +1 -1
  27. package/dist/mcp/index.d.ts +2 -2
  28. package/dist/mcp/index.js +2 -2
  29. package/dist/{mfa-CusDgPaD.d.ts → mfa-Cu96oJdw.d.ts} +1 -19
  30. package/dist/next/index.d.ts +6 -6
  31. package/dist/next/index.js +3 -3
  32. package/dist/orpc/index.d.ts +2 -2
  33. package/dist/orpc/index.js +1 -1
  34. package/dist/otel/index.js +5 -5
  35. package/dist/react/index.d.ts +5 -5
  36. package/dist/react/server.d.ts +5 -5
  37. package/dist/react/session.d.ts +2 -2
  38. package/dist/{refresh-BTrQ_ZTp.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
  39. package/dist/{resource-D8iqWMr-.d.ts → resource-DNRFP7KR.d.ts} +1 -1
  40. package/dist/{resource-CRxxwkQq.js → resource-mqfJW4YF.js} +1 -1
  41. package/dist/{respond-D2OQl62-.d.ts → respond-BR1fJWOB.d.ts} +12 -6
  42. package/dist/server/index.d.ts +9 -8
  43. package/dist/server/index.js +5 -5
  44. package/dist/{server-Csx2sRjg.js → server-B1mZ7CgQ.js} +11 -6
  45. package/dist/sql/index.d.ts +38 -3
  46. package/dist/sql/index.js +237 -118
  47. package/dist/ssr/index.d.ts +4 -4
  48. package/dist/ssr/index.js +1 -1
  49. package/dist/{support-DT-OhSxD.d.ts → support-CEPwlw2G.d.ts} +2 -1
  50. package/dist/{support-view-Da5peKFg.d.ts → support-view-B8SlToQX.d.ts} +1 -1
  51. package/dist/{support-Dk6vdmEw.js → support-wHnK6zp-.js} +4 -2
  52. package/dist/testing/index.d.ts +36 -4
  53. package/dist/testing/index.js +141 -1
  54. package/dist/{version-DM_Roffb.js → version-DOYTYwHP.js} +1 -1
  55. package/dist/{view-Bq64ki8-.js → view-Bn6HNKUe.js} +7 -65
  56. package/dist/{view-BEf0diC5.d.ts → view-CbgObAeb.d.ts} +17 -31
  57. package/package.json +13 -13
  58. package/schemas/doctor-report-v1.json +1 -0
  59. package/skills/better-supabase-auth/SKILL.md +15 -7
  60. package/skills/better-supabase-auth/references/permdock.md +22 -0
  61. package/dist/cli/gen-BJc9vdyu.js +0 -2
  62. package/dist/impersonation-VfaGc4Q_.js +0 -41
package/dist/sql/index.js CHANGED
@@ -6,7 +6,7 @@ import { n as resolveKitModule, o as resolveJsonSchema } from "../kits-CGLLrNPd.
6
6
  import { i as sqlString, r as sqlIdent } from "../template-TxrGI5jR.js";
7
7
  import { t as compileSql } from "../sql-Coh6n4cC.js";
8
8
  import { i as permdockKeys, r as permdockKeyStatus, t as PERMDOCK_SCHEMA } from "../permdock-sql-DgOfeixi.js";
9
- import { t as VERSION } from "../version-DM_Roffb.js";
9
+ import { t as VERSION } from "../version-DOYTYwHP.js";
10
10
  //#region src/sql/context.ts
11
11
  /** The scope id types kit modules render. */
12
12
  const KIT_ID_TYPES = [
@@ -249,6 +249,135 @@ function migrationOptionUses(kits = {}) {
249
249
  });
250
250
  }
251
251
  //#endregion
252
+ //#region src/sql/modules/access-model.ts
253
+ /**
254
+ * The `roles` model's defaults. Kit actions name these keys (see
255
+ * `KIT_PERMISSIONS`); `*` grants everything and `prefix.*` a prefix.
256
+ */
257
+ const DEFAULT_ROLES = {
258
+ owner: ["*"],
259
+ admin: [
260
+ "organization.read",
261
+ "organization.update",
262
+ "members.*",
263
+ "audit.read",
264
+ "webhooks.*",
265
+ "notifications.*",
266
+ "billing.*"
267
+ ],
268
+ member: [
269
+ "organization.read",
270
+ "members.read",
271
+ "notifications.read"
272
+ ],
273
+ viewer: ["organization.read"]
274
+ };
275
+ /**
276
+ * The permission key each kit action checks by default, overridable per
277
+ * module in `kits.<name>.permissions`. Keys are `<area>.<verb>`, with `read`
278
+ * for viewing; platform-wide actions use the `platform` area or a key that
279
+ * `is_platform()` checks.
280
+ */
281
+ const KIT_PERMISSIONS = {
282
+ organizations: {
283
+ update: "organization.update",
284
+ delete: "organization.delete",
285
+ removeMember: "members.remove",
286
+ updateRole: "members.update_role",
287
+ transferOwnership: "organization.transfer_ownership"
288
+ },
289
+ invitations: {
290
+ invite: "members.invite",
291
+ revoke: "members.invite",
292
+ view: "members.invite",
293
+ invitePlatform: "platform.invite"
294
+ },
295
+ audit: {
296
+ view: "audit.read",
297
+ viewAll: "audit.read"
298
+ },
299
+ "support-sessions": {
300
+ start: "support.start",
301
+ view: "support.read",
302
+ revoke: "support.revoke"
303
+ },
304
+ notifications: {
305
+ send: "notifications.send",
306
+ read: "notifications.read"
307
+ },
308
+ "webhooks-out": {
309
+ manage: "webhooks.manage",
310
+ view: "webhooks.read"
311
+ }
312
+ };
313
+ /**
314
+ * Where each kit action checks its key: `tenant` through `member_can`,
315
+ * `can` or `tenant_ids_with` (PermDock's `permitted_<scope>_ids`), and
316
+ * `platform` through `is_platform` or `platform_can` (`permdock_has`).
317
+ */
318
+ const KIT_PERMISSION_SCOPES = {
319
+ organizations: {
320
+ update: "tenant",
321
+ delete: "tenant",
322
+ removeMember: "tenant",
323
+ updateRole: "tenant",
324
+ transferOwnership: "tenant"
325
+ },
326
+ invitations: {
327
+ invite: "tenant",
328
+ revoke: "tenant",
329
+ view: "tenant",
330
+ invitePlatform: "platform"
331
+ },
332
+ audit: {
333
+ view: "tenant",
334
+ viewAll: "platform"
335
+ },
336
+ "support-sessions": {
337
+ start: "platform",
338
+ view: "platform",
339
+ revoke: "platform"
340
+ },
341
+ notifications: {
342
+ send: "tenant",
343
+ read: "tenant"
344
+ },
345
+ "webhooks-out": {
346
+ manage: "tenant",
347
+ view: "tenant"
348
+ }
349
+ };
350
+ function accessModel(ctx) {
351
+ return ctx.kits.access?.model ?? "roles";
352
+ }
353
+ function rolesOf(ctx) {
354
+ return ctx.kits.access?.roles ?? DEFAULT_ROLES;
355
+ }
356
+ const roleNames = (ctx) => Object.keys(rolesOf(ctx));
357
+ /**
358
+ * The scope name tenants use in `can(scope, id, permission)`
359
+ * (`kits.access.options.scope`, default `organization`). `tenant` is
360
+ * accepted too.
361
+ */
362
+ const tenantScope = (ctx) => ctx.of("access").text("scope", "organization");
363
+ /** Whether the catalog has platform roles, and so platform invitations. */
364
+ function hasPlatformRoles(ctx) {
365
+ return accessModel(ctx) === "catalog" && ctx.of("access").hasTable("platformAssignments");
366
+ }
367
+ /**
368
+ * Whether catalog role row `alias` is a tenant or a platform role, or
369
+ * `undefined` when roles carry no scope. Managed catalogs have a `scope`
370
+ * column; an adopted one maps `roles.scope` and names its values in
371
+ * `kits.access.options.tenantRoleScope` and `platformRoleScope`.
372
+ */
373
+ function roleScopeIs(ctx, alias, scope) {
374
+ if (accessModel(ctx) !== "catalog") return void 0;
375
+ const access = ctx.of("access");
376
+ if (!access.manages && access.config.columns["roles"]?.["scope"] == null) return;
377
+ const value = scope === "tenant" ? access.text("tenantRoleScope", "tenant") : access.text("platformRoleScope", "platform");
378
+ return `${alias}.${access.col("roles", "scope")}::text = ${sqlString(value)}`;
379
+ }
380
+ //#endregion
252
381
  //#region src/sql/shared.ts
253
382
  const SCHEMA = `create schema if not exists better_supabase;
254
383
  grant usage on schema better_supabase to anon, authenticated, service_role;`;
@@ -459,98 +588,6 @@ end;
459
588
  $$;
460
589
  revoke execute on function better_supabase.replace_equivalent_triggers(regclass, text, text, boolean) from public, anon, authenticated;`;
461
590
  //#endregion
462
- //#region src/sql/modules/access-model.ts
463
- /**
464
- * The `roles` model's defaults. Kit actions name these keys (see
465
- * `KIT_PERMISSIONS`); `*` grants everything and `prefix.*` a prefix.
466
- */
467
- const DEFAULT_ROLES = {
468
- owner: ["*"],
469
- admin: [
470
- "organization.read",
471
- "organization.update",
472
- "members.*",
473
- "audit.read",
474
- "webhooks.*",
475
- "notifications.*",
476
- "billing.*"
477
- ],
478
- member: [
479
- "organization.read",
480
- "members.read",
481
- "notifications.read"
482
- ],
483
- viewer: ["organization.read"]
484
- };
485
- /**
486
- * The permission key each kit action checks by default, overridable per
487
- * module in `kits.<name>.permissions`. Keys are `<area>.<verb>`, with `read`
488
- * for viewing; platform-wide actions use the `platform` area or a key that
489
- * `is_platform()` checks.
490
- */
491
- const KIT_PERMISSIONS = {
492
- organizations: {
493
- update: "organization.update",
494
- delete: "organization.delete",
495
- removeMember: "members.remove",
496
- updateRole: "members.update_role",
497
- transferOwnership: "organization.transfer_ownership"
498
- },
499
- invitations: {
500
- invite: "members.invite",
501
- revoke: "members.invite",
502
- view: "members.invite",
503
- invitePlatform: "platform.invite"
504
- },
505
- audit: {
506
- view: "audit.read",
507
- viewAll: "audit.read"
508
- },
509
- "support-sessions": {
510
- start: "support.start",
511
- view: "support.read",
512
- revoke: "support.revoke"
513
- },
514
- notifications: {
515
- send: "notifications.send",
516
- read: "notifications.read"
517
- },
518
- "webhooks-out": {
519
- manage: "webhooks.manage",
520
- view: "webhooks.read"
521
- }
522
- };
523
- function accessModel(ctx) {
524
- return ctx.kits.access?.model ?? "roles";
525
- }
526
- function rolesOf(ctx) {
527
- return ctx.kits.access?.roles ?? DEFAULT_ROLES;
528
- }
529
- const roleNames = (ctx) => Object.keys(rolesOf(ctx));
530
- /**
531
- * The scope name tenants use in `can(scope, id, permission)`
532
- * (`kits.access.options.scope`, default `organization`). `tenant` is
533
- * accepted too.
534
- */
535
- const tenantScope = (ctx) => ctx.of("access").text("scope", "organization");
536
- /** Whether the catalog has platform roles, and so platform invitations. */
537
- function hasPlatformRoles(ctx) {
538
- return accessModel(ctx) === "catalog" && ctx.of("access").hasTable("platformAssignments");
539
- }
540
- /**
541
- * Whether catalog role row `alias` is a tenant or a platform role, or
542
- * `undefined` when roles carry no scope. Managed catalogs have a `scope`
543
- * column; an adopted one maps `roles.scope` and names its values in
544
- * `kits.access.options.tenantRoleScope` and `platformRoleScope`.
545
- */
546
- function roleScopeIs(ctx, alias, scope) {
547
- if (accessModel(ctx) !== "catalog") return void 0;
548
- const access = ctx.of("access");
549
- if (!access.manages && access.config.columns["roles"]?.["scope"] == null) return;
550
- const value = scope === "tenant" ? access.text("tenantRoleScope", "tenant") : access.text("platformRoleScope", "platform");
551
- return `${alias}.${access.col("roles", "scope")}::text = ${sqlString(value)}`;
552
- }
553
- //#endregion
554
591
  //#region src/sql/modules/access.ts
555
592
  const SERVICE = `coalesce(auth.jwt() ->> 'role', '') = 'service_role'`;
556
593
  const NOT_ACTING = `(platform_can.member is distinct from auth.uid() or auth.jwt() -> 'act' is null)`;
@@ -888,27 +925,53 @@ function permdockCanAssign(ctx) {
888
925
  const owner = sqlString(ctx.of("organizations").text("ownerRole", "owner"));
889
926
  return `can_assign.role <> ${owner} or better_supabase.has_org_role(can_assign.tenant, array[${owner}])`;
890
927
  }
928
+ /**
929
+ * PermDock's schema and scope for the `permdock` model: from the manifest
930
+ * (`layout.accessPermdock`, which `sql add` reads), else both set in
931
+ * `kits.access.permdock`. Never a default, so a project whose helpers live
932
+ * elsewhere never gets functions that call helpers that don't exist.
933
+ */
934
+ function permdockTarget(ctx, layout) {
935
+ const configured = ctx.kits.access?.permdock;
936
+ const manifest = layout.accessPermdock;
937
+ if (manifest) {
938
+ if (manifest.idType !== ctx.idType) throw new TypeError(`kits.access: PermDock's manifest gives scope "${manifest.scope}" the type ${manifest.idType}, but the access module renders ${ctx.idType} ids. Set kits.access.idType to "${manifest.idType}".`);
939
+ return {
940
+ schema: manifest.schema,
941
+ scope: manifest.scope
942
+ };
943
+ }
944
+ if (configured?.schema === void 0 || configured.scope === void 0) throw new TypeError("kits.access.model 'permdock' needs PermDock's manifest (`permdock supabase inspect --out`), which `better-supabase sql add` reads, or both kits.access.permdock.schema and kits.access.permdock.scope.");
945
+ return {
946
+ schema: configured.schema,
947
+ scope: configured.scope
948
+ };
949
+ }
891
950
  function permdockFunctions(ctx, layout) {
892
951
  const id = ctx.idType;
893
- const permdock = ctx.kits.access?.permdock ?? {};
894
- const schema = sqlIdent(permdock.schema ?? layout.permdock?.schema ?? "permdock");
895
- const scope = permdock.scope ?? layout.permdock?.scope ?? "organization";
896
- const permitted = `${schema}.${sqlIdent(`permitted_${scope}_ids`)}`;
952
+ const target = permdockTarget(ctx, layout);
953
+ const schema = sqlIdent(target.schema);
954
+ const permitted = `${schema}.${sqlIdent(`permitted_${target.scope}_ids`)}`;
897
955
  return `
898
956
  -- The permdock model: PermDock's ${permitted}() and ${schema}.permdock_has(),
899
- -- from \`permdock rls generate\`. They answer for the caller only.
957
+ -- from \`permdock rls generate\`. They read auth.uid(), so member_can() and
958
+ -- can_user() answer for the caller only and raise 0A000 for anyone else.
900
959
  create or replace function better_supabase.member_can(member uuid, tenant ${id}, permission text)
901
960
  returns boolean
902
- language sql
961
+ language plpgsql
903
962
  stable
904
963
  security definer
905
964
  set search_path = ''
906
965
  as $$
907
- select case when member is distinct from auth.uid() then null::boolean
908
- else not better_supabase.user_disabled(member)
909
- and not better_supabase.tenant_disabled(tenant)
910
- and tenant::text in (select t.id::text from ${permitted}(permission) as t(id))
911
- end
966
+ begin
967
+ if member is distinct from auth.uid() then
968
+ raise exception 'The permdock access model answers for the caller only (kits.access.model)'
969
+ using errcode = '0A000', hint = 'ACCESS_CALLER_ONLY';
970
+ end if;
971
+ return not better_supabase.user_disabled(member)
972
+ and not better_supabase.tenant_disabled(tenant)
973
+ and tenant::text in (select t.id::text from ${permitted}(permission) as t(id));
974
+ end;
912
975
  $$;
913
976
 
914
977
  create or replace function better_supabase.member_permissions(member uuid, tenant ${id})
@@ -942,8 +1005,10 @@ as $$
942
1005
  and not better_supabase.tenant_disabled(t.id::${id})
943
1006
  $$;
944
1007
 
945
- -- kits.access.functions.canAssign decides who assigns which role. Without
946
- -- it, only owners assign the owner role, and only with the tenant module.
1008
+ -- kits.access.functions.canAssign decides who assigns which role, usually
1009
+ -- ${schema}.permdock_can_assign({role}, {tenant}::text). Without it, only the
1010
+ -- service role assigns roles, unless the tenant module is installed: then
1011
+ -- only owners assign the owner role and this check passes other roles.
947
1012
  create or replace function better_supabase.can_assign(tenant ${id}, role text)
948
1013
  returns boolean
949
1014
  language sql
@@ -1115,19 +1180,25 @@ as $$
1115
1180
  $$;
1116
1181
 
1117
1182
  -- The same question for another user, for checks such as an inviter's
1118
- -- authority at accept time. Null when the model can't answer for others.
1183
+ -- authority at accept time. ${model === "permdock" ? "The permdock model can't answer for others." : "Null when the model can't answer for others."}
1119
1184
  create or replace function better_supabase.can_user(member uuid, scope text, scope_id ${id}, permission text)
1120
1185
  returns boolean
1121
- language sql
1186
+ language ${model === "permdock" ? "plpgsql" : "sql"}
1122
1187
  stable
1123
1188
  security definer
1124
1189
  set search_path = ''
1125
- as $$
1126
- select case
1190
+ as $$${model === "permdock" ? `
1191
+ begin
1192
+ if member is distinct from auth.uid() then
1193
+ raise exception 'The permdock access model answers for the caller only (kits.access.model)'
1194
+ using errcode = '0A000', hint = 'ACCESS_CALLER_ONLY';
1195
+ end if;
1196
+ return case` : `
1197
+ select case`}
1127
1198
  when can_user.scope = 'platform' then better_supabase.platform_can(can_user.member, can_user.permission)
1128
1199
  when can_user.scope in (${scope}, 'tenant') then better_supabase.member_can(can_user.member, can_user.scope_id, can_user.permission)
1129
1200
  else false
1130
- end
1201
+ end${model === "permdock" ? ";\nend;" : ""}
1131
1202
  $$;
1132
1203
 
1133
1204
  create or replace function better_supabase.is_platform(permission text)
@@ -3582,7 +3653,9 @@ function accept(ctx) {
3582
3653
  and not better_supabase.can_assign_as(${inviter}, invite.${c("tenant")}, invite.${c("role")}::text) then
3583
3654
  ${fail("INVITATION_INVITER_REVOKED", "The person who invited you can no longer assign that role")}
3584
3655
  end if;` : "";
3585
- const recheck = inviter ? `
3656
+ const recheck = inviter && model === "permdock" ? `
3657
+ -- The permdock model answers for the caller only, so the inviter's
3658
+ -- authority was checked when they invited, not here.` : inviter ? `
3586
3659
  if ${inviter} is not null
3587
3660
  and better_supabase.can_user(${inviter}, ${sqlString(tenantScope(ctx))}, invite.${c("tenant")}, ${invitePermission}) is false then
3588
3661
  ${fail("INVITATION_INVITER_REVOKED", "The person who invited you can no longer invite members")}
@@ -4610,13 +4683,17 @@ function notify(ctx, n) {
4610
4683
  )
4611
4684
  );
4612
4685
  end if;` : "";
4613
- const members = access ? `
4686
+ const readFilter = accessModel(ctx) === "permdock" ? `
4687
+ -- The permdock model answers for the caller only, so recipients are not
4688
+ -- filtered by their read permission: the sender and notification_audience
4689
+ -- decide who gets it.` : `
4614
4690
  if v_tenant is not null then
4615
4691
  v_recipients := array(
4616
4692
  select x from unnest(v_recipients) x
4617
4693
  where coalesce(better_supabase.member_can(x, v_tenant, ${n.readPermission}), false)
4618
4694
  );
4619
- end if;` : "";
4695
+ end if;`;
4696
+ const members = access ? readFilter : "";
4620
4697
  const authorize = access ? `
4621
4698
  if not (${SERVICE_CALLER}) then
4622
4699
  if v_tenant is null or not coalesce(better_supabase.member_can(auth.uid(), v_tenant, ${n.sendPermission}), false) then
@@ -9452,9 +9529,14 @@ function kitContext(name, layout = {}, installed) {
9452
9529
  ...layout.kits ? { kits: layout.kits } : {},
9453
9530
  ...layout.claims ? { claims: layout.claims } : {},
9454
9531
  ...installed ? { installed } : {},
9455
- ...layout.permdock ? { permdockIdType: layout.permdock.idType } : {}
9532
+ ...permdockIdType(layout)
9456
9533
  });
9457
9534
  }
9535
+ /** The tenant id type PermDock's manifest gives, preferring the access model's scope. */
9536
+ function permdockIdType(layout) {
9537
+ const idType = layout.accessPermdock?.idType ?? layout.permdock?.idType;
9538
+ return idType ? { permdockIdType: idType } : {};
9539
+ }
9458
9540
  /** Throws on a `kits` key that names no module, or a mode a module doesn't support. */
9459
9541
  function checkKits(kits = {}, modules = SQL_MODULES) {
9460
9542
  for (const [name, entry] of Object.entries(kits)) {
@@ -9496,6 +9578,42 @@ function kitPath(module, layout) {
9496
9578
  const slug = module.name.replaceAll("-", "_");
9497
9579
  return module.target === "test" ? `${testsDir}/000_better_supabase_${slug}.test.sql` : `${dir}/${prefix}_${String(ORDER.indexOf(module.name) + 1).padStart(2, "0")}_${slug}.sql`;
9498
9580
  }
9581
+ /**
9582
+ * The schema or test file `renderKit` writes for each of `names` and the
9583
+ * modules they pull in, without rendering: modules in custom mode have none.
9584
+ */
9585
+ function kitFilePaths(names, layout = {}) {
9586
+ checkKits(layout.kits);
9587
+ const modules = resolveModules(names, layout);
9588
+ const installed = modules.map((module) => module.name);
9589
+ return new Map(modules.filter((module) => kitContext(module.name, layout, installed).mode !== "custom").map((module) => [module.name, kitPath(module, layout)]));
9590
+ }
9591
+ const isPermissionModule = (name) => Object.hasOwn(KIT_PERMISSIONS, name);
9592
+ /**
9593
+ * Every permission key the modules `names` install (with what they pull
9594
+ * in) check, after `kits.<module>.permissions` overrides. Modules in custom
9595
+ * mode are skipped, and so is `invitations.invitePlatform` without platform
9596
+ * roles. PermDock's doctor runs the same catalog check from its side.
9597
+ */
9598
+ function kitPermissionKeys(kits, names) {
9599
+ const layout = { kits };
9600
+ checkKits(kits);
9601
+ const modules = resolveModules(names, layout);
9602
+ const installed = modules.map((module) => module.name);
9603
+ return modules.flatMap((module) => {
9604
+ const name = module.name;
9605
+ if (!isPermissionModule(name)) return [];
9606
+ const ctx = kitContext(name, layout, installed);
9607
+ if (ctx.mode === "custom") return [];
9608
+ const scopes = KIT_PERMISSION_SCOPES[name];
9609
+ return Object.entries(KIT_PERMISSIONS[name]).flatMap(([action, fallback]) => action === "invitePlatform" && !hasPlatformRoles(ctx) ? [] : [{
9610
+ module: name,
9611
+ action,
9612
+ key: ctx.permissionKey(action, fallback),
9613
+ scope: scopes[action]
9614
+ }]);
9615
+ });
9616
+ }
9499
9617
  /** The table `kitModuleRow` writes to, created by every schema module's file. */
9500
9618
  const KIT_MODULES_TABLE = `
9501
9619
  create schema if not exists better_supabase;
@@ -9600,7 +9718,7 @@ function upgradePlan(installed, layout = {}, modules = SQL_MODULES) {
9600
9718
  ...layout.kits ? { kits: layout.kits } : {},
9601
9719
  ...layout.claims ? { claims: layout.claims } : {},
9602
9720
  installed: names,
9603
- ...layout.permdock ? { permdockIdType: layout.permdock.idType } : {}
9721
+ ...permdockIdType(layout)
9604
9722
  });
9605
9723
  if (ctx.mode === "custom") return [];
9606
9724
  const steps = (module.upgrades ?? []).filter((step) => step.from >= entry.version && step.from < to).toSorted((a, b) => a.from - b.from).map((step) => ({
@@ -9651,9 +9769,10 @@ function realtimeRegistrations(tables, global, tenantColumn) {
9651
9769
  //#endregion
9652
9770
  //#region src/sql/layout.ts
9653
9771
  /** Where and how `sql add` writes kit files for this config. */
9654
- function kitLayout(config, testsDir = config.sql.testsDir, readSets = [], permdock) {
9772
+ function kitLayout(config, testsDir = config.sql.testsDir, readSets = [], permdock, accessPermdock) {
9655
9773
  return {
9656
9774
  ...permdock ? { permdock } : {},
9775
+ ...accessPermdock ? { accessPermdock } : {},
9657
9776
  dir: config.sql.dir,
9658
9777
  prefix: config.sql.prefix,
9659
9778
  testsDir,
@@ -9791,4 +9910,4 @@ async function compileReadSets(sets) {
9791
9910
  return Promise.all(names.map((name) => compileReadSet(byName.get(name))));
9792
9911
  }
9793
9912
  //#endregion
9794
- export { KIT_ID_TYPES, PERMDOCK_SCHEMA, SQL_MODULES, compileReadSet, compileReadSets, contractSignature, customContracts, isKitIdType, kitDeprecations, kitFileVersion, kitIdType, kitLayout, migrationOptionUses, moduleBody, moduleVersion, permdockKeyStatus, permdockKeys, renderKit, resolveModules, sameKitFile, upgradePlan };
9913
+ export { KIT_ID_TYPES, PERMDOCK_SCHEMA, SQL_MODULES, compileReadSet, compileReadSets, contractSignature, customContracts, isKitIdType, kitDeprecations, kitFilePaths, kitFileVersion, kitIdType, kitLayout, kitPermissionKeys, migrationOptionUses, moduleBody, moduleVersion, permdockKeyStatus, permdockKeys, renderKit, resolveModules, sameKitFile, upgradePlan };
@@ -1,5 +1,5 @@
1
- import { _ as readSession, a as ResolveAuthOptions, b as writeSession, d as CookieOptions, f as CookieRecord, g as parseCookies, h as applyCookieWrites, l as resolveAuth, m as StoredSession, n as AuthResolver, p as CookieWrite, r as AuthState, s as authContext, t as AuthResolution, u as AUTH_CACHE_HEADERS, v as serializeCookie, y as sessionCookieName } from "../resolve-C15TJq77.js";
2
- import { n as toSession, t as AuthSession } from "../view-BEf0diC5.js";
3
- import { n as MembershipClaim, r as hasEntitlement, t as EntitlementKey } from "../entitlements-DUxU4oHe.js";
4
- import { n as RefreshOutcome, r as refreshSession, t as RefreshOptions } from "../refresh-BTrQ_ZTp.js";
1
+ import { C as writeSession, S as sessionCookieName, _ as StoredSession, a as AuthResolver, b as readSession, c as ResolveAuthOptions, f as resolveAuth, g as CookieWrite, h as CookieRecord, i as AuthResolution, m as CookieOptions, o as AuthState, p as AUTH_CACHE_HEADERS, u as authContext, v as applyCookieWrites, x as serializeCookie, y as parseCookies } from "../impersonation-BmePsuxz.js";
2
+ import { n as toSession, t as AuthSession } from "../view-CbgObAeb.js";
3
+ import { n as MembershipClaim, r as hasEntitlement, t as EntitlementKey } from "../entitlements-BiJZ4t49.js";
4
+ import { n as RefreshOutcome, r as refreshSession, t as RefreshOptions } from "../refresh-CFPX0Qft.js";
5
5
  export { AUTH_CACHE_HEADERS, type AuthResolution, type AuthResolver, type AuthSession, type AuthState, type CookieOptions, type CookieRecord, type CookieWrite, type EntitlementKey, type MembershipClaim, type RefreshOptions, type RefreshOutcome, type ResolveAuthOptions, type StoredSession, applyCookieWrites, authContext, hasEntitlement, parseCookies, readSession, refreshSession, resolveAuth, serializeCookie, sessionCookieName, toSession, writeSession };
package/dist/ssr/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { t as hasEntitlement } from "../entitlements-CTEBm7IB.js";
2
- import { _ as writeSession, d as AUTH_CACHE_HEADERS, f as applyCookieWrites, g as sessionCookieName, h as serializeCookie, m as readSession, n as toSession, o as authContext, p as parseCookies, u as resolveAuth, v as refreshSession } from "../view-Bq64ki8-.js";
2
+ import { _ as writeSession, d as AUTH_CACHE_HEADERS, f as applyCookieWrites, g as sessionCookieName, h as serializeCookie, m as readSession, n as toSession, o as authContext, p as parseCookies, u as resolveAuth, v as refreshSession } from "../view-Bn6HNKUe.js";
3
3
  export { AUTH_CACHE_HEADERS, applyCookieWrites, authContext, hasEntitlement, parseCookies, readSession, refreshSession, resolveAuth, serializeCookie, sessionCookieName, toSession, writeSession };
@@ -80,7 +80,8 @@ interface SqlSupportStoreOptions {
80
80
  declare function sqlSupportStore(postgres: Pick<BetterPostgres, "admin" | "asUser">, options?: SqlSupportStoreOptions): SupportSessionStore;
81
81
  /**
82
82
  * The claims a support session runs with: the target's, plus `act` naming
83
- * the admin, the reason and the session (the audit module records all three).
83
+ * the admin, the reason and the session (the audit module records all three),
84
+ * marked `kind: "support"` so `actorOf` never reads it as an OAuth client.
84
85
  * `role` falls back to `authenticated`; the admin's claims never leak in.
85
86
  */
86
87
  declare function supportClaims(session: Pick<SupportSession, "id" | "adminId" | "targetUserId" | "reason" | "readOnly">, target: Readonly<Record<string, unknown>>): Record<string, unknown>;
@@ -1,4 +1,4 @@
1
- import { t as AuthSession } from "./view-BEf0diC5.js";
1
+ import { t as AuthSession } from "./view-CbgObAeb.js";
2
2
  //#region src/auth/support-view.d.ts
3
3
  /** A running support session as the target's view shows it, for a banner. */
4
4
  interface SupportView {
@@ -2,7 +2,7 @@ import { c as mapDbError, n as dbError } from "./errors-DtwXa6b8.js";
2
2
  import { t as temporal } from "./temporal-required-CAt4Dc9Z.js";
3
3
  import { i as ok, n as err, o as toDbError, t as AsyncResult } from "./result-BKiliPY7.js";
4
4
  import { t as validate } from "./standard-D7r6dMUu.js";
5
- import { c as supportCookie, l as supportCookieValue, s as clearSupportCookie } from "./server-Csx2sRjg.js";
5
+ import { c as supportCookie, l as supportCookieValue, s as clearSupportCookie } from "./server-B1mZ7CgQ.js";
6
6
  import { r as sqlIdent } from "./template-TxrGI5jR.js";
7
7
  import { n as emitKitEvent } from "./kit-events-CTFDfSwb.js";
8
8
  import { t as fromPgError } from "./executor-XycESQPH.js";
@@ -76,7 +76,8 @@ function sqlSupportStore(postgres, options = {}) {
76
76
  }
77
77
  /**
78
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).
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.
80
81
  * `role` falls back to `authenticated`; the admin's claims never leak in.
81
82
  */
82
83
  function supportClaims(session, target) {
@@ -86,6 +87,7 @@ function supportClaims(session, target) {
86
87
  sub: session.targetUserId,
87
88
  role: typeof role === "string" && role !== "" ? role : "authenticated",
88
89
  act: {
90
+ kind: "support",
89
91
  sub: session.adminId,
90
92
  reason: session.reason,
91
93
  session_id: session.id,
@@ -2,13 +2,14 @@ import { P as TableKey, _ as Insert, j as SchemaMeta, n as AnyFunctions, r as An
2
2
  import { r as Executor } from "../executor-BunPOO-k.js";
3
3
  import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-Bp_BZa5h.js";
4
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";
5
+ import { b as ResolvedConfig, d as Generator, f as GeneratorInput } from "../config-Bv7gvfzC.js";
6
6
  import { t as SqlClient } from "../executor-Cghdstwu.js";
7
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";
8
+ import { a as AuthResolver, n as Impersonator } from "../impersonation-BmePsuxz.js";
9
+ import { o as SupportSessionStore } from "../support-CEPwlw2G.js";
10
10
  import { r as EventSink, t as CloudEvent } from "../index-DOESKG27.js";
11
11
  import { p as QueueBackend } from "../queue-BYUCjr59.js";
12
+ import { n as SessionActor, r as SessionDelegation } from "../actor-DaJjqD5R.js";
12
13
  import { t as NotificationChannel } from "../channel-FQW-CnGR.js";
13
14
  import { c as WebhookSecretStore, i as WebhookSigner, m as WebhookTransport } from "../signers-Cc6r98SW.js";
14
15
  import { SupabaseClient } from "@supabase/supabase-js";
@@ -326,6 +327,37 @@ interface TenantIsolationOptions<M extends AnyModels> {
326
327
  */
327
328
  export declare function expectTenantIsolation<M extends AnyModels, D, F extends AnyFunctions, E>(betterSupabase: BetterSupabase<M, D, F, E>, options: TenantIsolationOptions<M>): Promise<ConformanceReport>;
328
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
329
361
  //#region src/testing/seed.d.ts
330
362
  /** Named rows per table, in app casing: `{ customers: { acme: { ... } } }`. */
331
363
  type SeedFixtures<M extends AnyModels> = { readonly [K in keyof M]?: Readonly<Record<string, M[K]["Insert"]>>; };
@@ -413,4 +445,4 @@ interface TestWebhookSecretStoreOptions {
413
445
  */
414
446
  export declare function testWebhookSecretStore(store: WebhookSecretStore, options: TestWebhookSecretStoreOptions): Promise<ConformanceReport>;
415
447
  //#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 };
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 };