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.
- package/README.md +133 -16
- package/dist/actor-DaJjqD5R.d.ts +43 -0
- package/dist/cli/bin.js +1 -1
- package/dist/cli/{doctor-L7Z8mYTx.js → doctor-C_A-KEAz.js} +6 -4
- package/dist/cli/gen-CIdSQZad.js +2 -0
- package/dist/cli/{gen-C2osRcr5.js → gen-CjElD1Vw.js} +72 -35
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/{init-Saf5-R9T.js → init-DMa6M7hV.js} +1 -1
- package/dist/cli/{introspect--H7oGmcd.js → introspect-BKlaIu_M.js} +1 -1
- package/dist/cli/{read-sets-BfMQcKy8.js → read-sets-VQcPgxU9.js} +173 -26
- package/dist/cli/{run-BKR2W_79.js → run-Bo_NLT5n.js} +7 -7
- package/dist/cli/{sql-DzPbfngx.js → sql-CVx-1O8G.js} +30 -5
- package/dist/cli/{version-BDJuO0UR.js → version-BCtlgDQx.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/{config-T_ejwyd3.d.ts → config-Bv7gvfzC.d.ts} +10 -1
- package/dist/edge/index.d.ts +2 -2
- package/dist/edge/index.js +2 -2
- package/dist/{entitlements-DUxU4oHe.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
- package/dist/hono/index.d.ts +3 -3
- package/dist/hono/index.js +2 -2
- package/dist/impersonation-BgSHAYLf.js +133 -0
- package/dist/{resolve-C15TJq77.d.ts → impersonation-BmePsuxz.d.ts} +25 -1
- package/dist/{index-Dxn2a5cw.d.ts → index-CWMDdEr4.d.ts} +1 -1
- package/dist/{index-BGdgBPdf.d.ts → index-Dp2VDwog.d.ts} +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.js +2 -2
- package/dist/{mfa-CusDgPaD.d.ts → mfa-Cu96oJdw.d.ts} +1 -19
- package/dist/next/index.d.ts +6 -6
- package/dist/next/index.js +3 -3
- package/dist/orpc/index.d.ts +2 -2
- package/dist/orpc/index.js +1 -1
- package/dist/otel/index.js +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/server.d.ts +5 -5
- package/dist/react/session.d.ts +2 -2
- package/dist/{refresh-BTrQ_ZTp.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
- package/dist/{resource-D8iqWMr-.d.ts → resource-DNRFP7KR.d.ts} +1 -1
- package/dist/{resource-CRxxwkQq.js → resource-mqfJW4YF.js} +1 -1
- package/dist/{respond-D2OQl62-.d.ts → respond-BR1fJWOB.d.ts} +12 -6
- package/dist/server/index.d.ts +9 -8
- package/dist/server/index.js +5 -5
- package/dist/{server-Csx2sRjg.js → server-B1mZ7CgQ.js} +11 -6
- package/dist/sql/index.d.ts +38 -3
- package/dist/sql/index.js +237 -118
- package/dist/ssr/index.d.ts +4 -4
- package/dist/ssr/index.js +1 -1
- package/dist/{support-DT-OhSxD.d.ts → support-CEPwlw2G.d.ts} +2 -1
- package/dist/{support-view-Da5peKFg.d.ts → support-view-B8SlToQX.d.ts} +1 -1
- package/dist/{support-Dk6vdmEw.js → support-wHnK6zp-.js} +4 -2
- package/dist/testing/index.d.ts +36 -4
- package/dist/testing/index.js +141 -1
- package/dist/{version-DM_Roffb.js → version-DOYTYwHP.js} +1 -1
- package/dist/{view-Bq64ki8-.js → view-Bn6HNKUe.js} +7 -65
- package/dist/{view-BEf0diC5.d.ts → view-CbgObAeb.d.ts} +17 -31
- package/package.json +13 -13
- package/schemas/doctor-report-v1.json +1 -0
- package/skills/better-supabase-auth/SKILL.md +15 -7
- package/skills/better-supabase-auth/references/permdock.md +22 -0
- package/dist/cli/gen-BJc9vdyu.js +0 -2
- 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-
|
|
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
|
|
894
|
-
const schema = sqlIdent(
|
|
895
|
-
const
|
|
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
|
|
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
|
|
961
|
+
language plpgsql
|
|
903
962
|
stable
|
|
904
963
|
security definer
|
|
905
964
|
set search_path = ''
|
|
906
965
|
as $$
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
|
946
|
-
--
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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 };
|
package/dist/ssr/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as toSession, t as AuthSession } from "../view-
|
|
3
|
-
import { n as MembershipClaim, r as hasEntitlement, t as EntitlementKey } from "../entitlements-
|
|
4
|
-
import { n as RefreshOutcome, r as refreshSession, t as RefreshOptions } from "../refresh-
|
|
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-
|
|
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>;
|
|
@@ -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-
|
|
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,
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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-
|
|
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 {
|
|
9
|
-
import { o as SupportSessionStore } from "../support-
|
|
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 };
|