@fuzdev/fuz_app 0.114.0 → 0.116.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 (68) hide show
  1. package/dist/auth/CLAUDE.md +52 -8
  2. package/dist/auth/admin_actions.d.ts.map +1 -1
  3. package/dist/auth/admin_actions.js +4 -3
  4. package/dist/auth/audit_log_schema.d.ts +3 -1
  5. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  6. package/dist/auth/audit_log_schema.js +13 -3
  7. package/dist/auth/cell_action_specs.js +1 -1
  8. package/dist/auth/cell_actions.d.ts.map +1 -1
  9. package/dist/auth/cell_actions.js +33 -4
  10. package/dist/auth/cell_authorize.d.ts +3 -0
  11. package/dist/auth/cell_authorize.d.ts.map +1 -1
  12. package/dist/auth/cell_authorize.js +7 -4
  13. package/dist/auth/request_context.d.ts +12 -3
  14. package/dist/auth/request_context.d.ts.map +1 -1
  15. package/dist/auth/request_context.js +12 -3
  16. package/dist/auth/role_grant_offer_action_specs.d.ts +30 -5
  17. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  18. package/dist/auth/role_grant_offer_action_specs.js +53 -17
  19. package/dist/auth/role_grant_offer_actions.d.ts +23 -12
  20. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  21. package/dist/auth/role_grant_offer_actions.js +132 -38
  22. package/dist/auth/role_grant_offer_queries.d.ts +36 -0
  23. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  24. package/dist/auth/role_grant_offer_queries.js +76 -0
  25. package/dist/auth/role_schema.d.ts +12 -0
  26. package/dist/auth/role_schema.d.ts.map +1 -1
  27. package/dist/auth/role_schema.js +12 -0
  28. package/dist/auth/self_service_role_actions.d.ts +14 -0
  29. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  30. package/dist/auth/self_service_role_actions.js +25 -1
  31. package/dist/server/serve_fact_route.d.ts +3 -1
  32. package/dist/server/serve_fact_route.d.ts.map +1 -1
  33. package/dist/server/serve_fact_route.js +7 -4
  34. package/dist/testing/CLAUDE.md +12 -4
  35. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  36. package/dist/testing/cross_backend/cell_gated_create.js +23 -1
  37. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  38. package/dist/testing/cross_backend/role_grant_participation.js +34 -0
  39. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +12 -1
  40. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  41. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +13 -1
  42. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  43. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +9 -7
  44. package/dist/testing/entities.d.ts +6 -3
  45. package/dist/testing/entities.d.ts.map +1 -1
  46. package/dist/testing/entities.js +4 -1
  47. package/dist/ui/AppShell.svelte +303 -33
  48. package/dist/ui/AppShell.svelte.d.ts +80 -11
  49. package/dist/ui/AppShell.svelte.d.ts.map +1 -1
  50. package/dist/ui/BootstrapForm.svelte +14 -10
  51. package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -1
  52. package/dist/ui/CLAUDE.md +152 -12
  53. package/dist/ui/LoginForm.svelte +8 -4
  54. package/dist/ui/LoginForm.svelte.d.ts.map +1 -1
  55. package/dist/ui/RoleGrantOfferForm.svelte +29 -14
  56. package/dist/ui/RoleGrantOfferForm.svelte.d.ts +2 -3
  57. package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -1
  58. package/dist/ui/SignupForm.svelte +14 -13
  59. package/dist/ui/form_state.svelte.d.ts +15 -7
  60. package/dist/ui/form_state.svelte.d.ts.map +1 -1
  61. package/dist/ui/form_state.svelte.js +10 -2
  62. package/dist/ui/role_grant_offers_state.svelte.d.ts +25 -15
  63. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
  64. package/dist/ui/role_grant_offers_state.svelte.js +7 -1
  65. package/dist/ui/sidebar_state.svelte.d.ts +62 -2
  66. package/dist/ui/sidebar_state.svelte.d.ts.map +1 -1
  67. package/dist/ui/sidebar_state.svelte.js +78 -9
  68. package/package.json +1 -21
@@ -16,6 +16,7 @@ import { columns_sql, qualify_columns } from '../db/sql_columns.js';
16
16
  import { ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr, ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT } from './role_grant_offer_ddl.js';
17
17
  import { query_create_role_grant, query_role_grant_by_id } from './role_grant_queries.js';
18
18
  import { query_audit_log } from './audit_log_queries.js';
19
+ import { is_builtin_role, ROLE_ADMIN, ROLE_KEEPER } from './role_schema.js';
19
20
  /**
20
21
  * Error thrown by offer-lifecycle queries when the offer is in a non-pending
21
22
  * state (accepted / declined / retracted / superseded) and therefore not
@@ -81,6 +82,22 @@ export class RoleGrantOfferActorMismatchError extends Error {
81
82
  this.name = 'RoleGrantOfferActorMismatchError';
82
83
  }
83
84
  }
85
+ /**
86
+ * Error thrown by `query_accept_offer` when the offer confers a builtin role
87
+ * (`admin` / `keeper`) that this accept must not mint — `refusal` names the
88
+ * rule. Carries the locked (still pending, untouched) offer so the caller can
89
+ * audit the refusal with the offer's role and scope.
90
+ */
91
+ export class RoleGrantOfferBuiltinRefusedError extends Error {
92
+ offer;
93
+ refusal;
94
+ constructor(offer, refusal) {
95
+ super(`Offer ${offer.id} is refused: ${refusal}`);
96
+ this.name = 'RoleGrantOfferBuiltinRefusedError';
97
+ this.offer = offer;
98
+ this.refusal = refusal;
99
+ }
100
+ }
84
101
  /**
85
102
  * Error thrown when `query_role_grant_offer_create` is called with a
86
103
  * `to_actor_id` that does not exist or does not belong to `to_account_id`.
@@ -307,6 +324,35 @@ export const query_role_grant_offer_sweep_expired = async (deps) => {
307
324
  AND expires_at <= NOW()
308
325
  ORDER BY role_grant_offer.expires_at ASC`);
309
326
  };
327
+ /**
328
+ * Whether `grantor_actor_id` holds an active **global** `admin` on an active
329
+ * actor of an active account — the authority a builtin-role offer needs at the
330
+ * moment its grant is minted.
331
+ *
332
+ * A locking read: `FOR SHARE OF rg` takes a share lock on the grant row that
333
+ * admits, so a concurrent revoke (an `UPDATE` of that row) is strictly ordered
334
+ * against this accept — it either commits first, and the recheck of the locked
335
+ * row sees `revoked_at` set and excludes it, or it waits for this transaction
336
+ * to commit the grant it authorized. A plain `EXISTS` could read the grant as
337
+ * active while a revoke commits in between. The actor and account tombstones
338
+ * are read but not locked — ordering a delete racing an accept isn't worth
339
+ * locking two more tables. Twin of the Rust `grantor_holds_global_admin`.
340
+ */
341
+ const query_grantor_holds_global_admin = async (deps, grantor_actor_id) => {
342
+ const row = await deps.db.query_one(`SELECT 1 AS held FROM role_grant rg
343
+ JOIN actor act ON act.id = rg.actor_id
344
+ JOIN account a ON a.id = act.account_id
345
+ WHERE rg.actor_id = $1
346
+ AND rg.role = $2
347
+ AND rg.scope_id IS NULL
348
+ AND rg.revoked_at IS NULL
349
+ AND (rg.expires_at IS NULL OR rg.expires_at > NOW())
350
+ AND act.deleted_at IS NULL
351
+ AND a.deleted_at IS NULL
352
+ LIMIT 1
353
+ FOR SHARE OF rg`, [grantor_actor_id, ROLE_ADMIN]);
354
+ return row != null;
355
+ };
310
356
  /**
311
357
  * Accept an offer atomically: mark accepted, insert the role_grant, stamp
312
358
  * `resulting_role_grant_id`, supersede sibling pending offers for the same
@@ -325,6 +371,11 @@ export const query_role_grant_offer_sweep_expired = async (deps) => {
325
371
  * - `RoleGrantOfferAlreadyTerminalError` — offer is declined, retracted, or
326
372
  * superseded.
327
373
  * - `RoleGrantOfferExpiredError` — offer is pending but past `expires_at`.
374
+ * - `RoleGrantOfferActorMismatchError` — actor-targeted offer, different actor.
375
+ * - `RoleGrantOfferBuiltinRefusedError` — a builtin-role offer that must not
376
+ * mint: `keeper` at all, a builtin bound to a scope, or one whose grantor no
377
+ * longer holds an active global `admin` (read under `FOR SHARE`). The offer
378
+ * stays pending.
328
379
  *
329
380
  * Sibling supersede is what closes the "accept a pre-revoke sibling offer
330
381
  * to bypass a revoke" path: once A is accepted, B/C/... can no longer be
@@ -337,6 +388,8 @@ export const query_role_grant_offer_sweep_expired = async (deps) => {
337
388
  * @throws RoleGrantOfferNotFoundError if the offer is missing or belongs to another recipient
338
389
  * @throws RoleGrantOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
339
390
  * @throws RoleGrantOfferExpiredError if the offer is pending but past `expires_at`
391
+ * @throws RoleGrantOfferActorMismatchError if the offer is actor-targeted to a different actor
392
+ * @throws RoleGrantOfferBuiltinRefusedError if a builtin-role offer must not mint (see `BuiltinOfferRefusal`)
340
393
  * @throws Error if the accepting `actor_id` does not belong to `to_account_id`, or invariant assertions fail
341
394
  */
342
395
  export const query_accept_offer = async (deps, input) => {
@@ -413,6 +466,29 @@ export const query_accept_offer = async (deps, input) => {
413
466
  if (locked.to_actor_id != null && locked.to_actor_id !== actor_id) {
414
467
  throw new RoleGrantOfferActorMismatchError(offer_id);
415
468
  }
469
+ // Builtin-role offers — checked here, the chokepoint where an offer turns
470
+ // into a `role_grant`, so a direct caller of this query can't skip them. No
471
+ // current grant path creates a refused shape, but a pending row can still
472
+ // carry one (a direct `query_role_grant_offer_create`, or an offer made under
473
+ // a since-closed path).
474
+ if (is_builtin_role(locked.role)) {
475
+ let refusal = null;
476
+ if (locked.role === ROLE_KEEPER) {
477
+ // `keeper` is bootstrap-only — never conferred through an offer, even
478
+ // from a global admin.
479
+ refusal = 'bootstrap_only';
480
+ }
481
+ else if (locked.scope_id !== null) {
482
+ // Builtin roles are global-only.
483
+ refusal = 'scoped';
484
+ }
485
+ else if (!(await query_grantor_holds_global_admin(deps, locked.from_actor_id))) {
486
+ refusal = 'grantor_not_authorized';
487
+ }
488
+ if (refusal !== null) {
489
+ throw new RoleGrantOfferBuiltinRefusedError(locked, refusal);
490
+ }
491
+ }
416
492
  // Verify the accepting actor belongs to the recipient account.
417
493
  // Defense-in-depth: the action handler passes `auth.actor.id` which is
418
494
  // already session-bound, but enforcing the invariant here protects
@@ -39,6 +39,18 @@ export declare const BuiltinRole: z.ZodEnum<{
39
39
  keeper: "keeper";
40
40
  }>;
41
41
  export type BuiltinRole = z.infer<typeof BuiltinRole>;
42
+ /**
43
+ * Whether `name` is a builtin role (`keeper` / `admin`).
44
+ *
45
+ * Builtin roles are **global-only**: no grant path binds one to a scope
46
+ * (`role_grant_assign`, `role_grant_offer_create`, and
47
+ * `role_grant_offer_accept` refuse it), and every gate that reads one checks
48
+ * the global grant, so a scoped builtin row confers nothing.
49
+ *
50
+ * @param name - the role name to check
51
+ * @returns `true` for `keeper` or `admin`
52
+ */
53
+ export declare const is_builtin_role: (name: string) => name is BuiltinRole;
42
54
  /**
43
55
  * Configuration for a role.
44
56
  *
@@ -1 +1 @@
1
- {"version":3,"file":"role_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAGN,KAAK,qBAAqB,EAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAEpE,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,aAKnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIhD,sFAAsF;AACtF,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,iEAAiE;AACjE,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,+CAA+C;AAC/C,eAAO,MAAM,aAAa,8BAAqC,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,WAAW;;;EAAwB,CAAC;AACjD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,QAAQ;IACxB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAuBnE,CAAC;AAEH,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACvC,uGAAuG;IACvG,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IAC9C,+FAA+F;IAC/F,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,oFAAoF;IACpF,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAChC,0GAA0G;IAC1G,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,yEAAyE;IACzE,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC1C;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,eAAO,MAAM,kBAAkB,GAC9B,gBAAgB,aAAa,CAAC,QAAQ,CAAC,EACvC,UAAS,uBAA4B,KACnC,gBA2CF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC/B,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,MAAM,MAAM,EACZ,YAAY,MAAM,KAChB,OAGF,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,0BAA0B,GACtC,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,YAAY,MAAM,KAChB,KAAK,CAAC,MAAM,CAMd,CAAC"}
1
+ {"version":3,"file":"role_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAGN,KAAK,qBAAqB,EAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAEpE,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,aAKnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIhD,sFAAsF;AACtF,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,iEAAiE;AACjE,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,+CAA+C;AAC/C,eAAO,MAAM,aAAa,8BAAqC,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,WAAW;;;EAAwB,CAAC;AACjD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,IAAI,IAAI,WACX,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,QAAQ;IACxB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAuBnE,CAAC;AAEH,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACvC,uGAAuG;IACvG,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IAC9C,+FAA+F;IAC/F,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,oFAAoF;IACpF,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAChC,0GAA0G;IAC1G,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,yEAAyE;IACzE,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC1C;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,eAAO,MAAM,kBAAkB,GAC9B,gBAAgB,aAAa,CAAC,QAAQ,CAAC,EACvC,UAAS,uBAA4B,KACnC,gBA2CF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC/B,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,MAAM,MAAM,EACZ,YAAY,MAAM,KAChB,OAGF,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,0BAA0B,GACtC,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,YAAY,MAAM,KAChB,KAAK,CAAC,MAAM,CAMd,CAAC"}
@@ -36,6 +36,18 @@ export const ROLE_ADMIN = 'admin';
36
36
  export const BUILTIN_ROLES = [ROLE_KEEPER, ROLE_ADMIN];
37
37
  /** Zod schema for builtin roles only. */
38
38
  export const BuiltinRole = z.enum(BUILTIN_ROLES);
39
+ /**
40
+ * Whether `name` is a builtin role (`keeper` / `admin`).
41
+ *
42
+ * Builtin roles are **global-only**: no grant path binds one to a scope
43
+ * (`role_grant_assign`, `role_grant_offer_create`, and
44
+ * `role_grant_offer_accept` refuse it), and every gate that reads one checks
45
+ * the global grant, so a scoped builtin row confers nothing.
46
+ *
47
+ * @param name - the role name to check
48
+ * @returns `true` for `keeper` or `admin`
49
+ */
50
+ export const is_builtin_role = (name) => name === ROLE_KEEPER || name === ROLE_ADMIN;
39
51
  /**
40
52
  * Builtin role specs, keyed by role name. Not overridable by consumers
41
53
  * — read once at startup by `create_role_schema` and the action
@@ -39,6 +39,19 @@
39
39
  import { type RpcAction } from '../actions/action_rpc.js';
40
40
  import { type RoleSchemaResult } from './role_schema.js';
41
41
  import type { ActionFactoryDeps } from './deps.js';
42
+ /**
43
+ * Validate a deployment's self-service eligible-role set at construction.
44
+ *
45
+ * Refuses a builtin role (`admin` / `keeper`) — self-toggling one would hand
46
+ * any account instance-wide authority, so no eligible set may name it — and a
47
+ * name outside the `RoleName` grammar. The same fail-loud-at-boot shape as
48
+ * `create_role_schema`, which refuses a builtin name where only app roles
49
+ * belong. Twin of the Rust `validate_self_service_eligible_roles`.
50
+ *
51
+ * @param roles - the eligible role names
52
+ * @throws Error on a builtin role or a name outside the role grammar
53
+ */
54
+ export declare const validate_self_service_eligible_roles: (roles: Iterable<string>) => void;
42
55
  /** Options for `create_self_service_role_actions`. */
43
56
  export interface SelfServiceRoleActionsOptions {
44
57
  /**
@@ -70,6 +83,7 @@ export interface SelfServiceRoleActionsOptions {
70
83
  * @param options - optional eligible-role override plus optional role schema for default-eligibility derivation
71
84
  * @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
72
85
  * @throws Error at factory time if any `eligible_roles` entry is missing from `options.roles.role_specs`
86
+ * @throws Error at factory time if the eligible set names a builtin role or an invalid role name
73
87
  */
74
88
  export declare const create_self_service_role_actions: (deps: ActionFactoryDeps, options?: SelfServiceRoleActionsOptions) => Array<RpcAction>;
75
89
  //# sourceMappingURL=self_service_role_actions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"self_service_role_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE/F,OAAO,EAGN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAWnD,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC7C;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;OAIG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,iBAAiB,EACvB,UAAS,6BAAkC,KACzC,KAAK,CAAC,SAAS,CA2HjB,CAAC"}
1
+ {"version":3,"file":"self_service_role_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE/F,OAAO,EAKN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAWnD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oCAAoC,GAAI,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAG,IAW9E,CAAC;AAEF,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC7C;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;OAIG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,iBAAiB,EACvB,UAAS,6BAAkC,KACzC,KAAK,CAAC,SAAS,CA6HjB,CAAC"}
@@ -38,12 +38,34 @@
38
38
  */
39
39
  import { rpc_action } from '../actions/action_rpc.js';
40
40
  import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
41
- import { builtin_role_specs_by_name, list_roles_with_grant_path } from './role_schema.js';
41
+ import { builtin_role_specs_by_name, is_builtin_role, list_roles_with_grant_path, RoleName } from './role_schema.js';
42
42
  import { GRANT_PATH_SELF_SERVICE } from './grant_path_schema.js';
43
43
  import { query_create_role_grant, query_revoke_role_grant } from './role_grant_queries.js';
44
44
  import { is_role_grant_active } from './account_schema.js';
45
45
  import { has_scoped_role } from './request_context.js';
46
46
  import { ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE, self_service_role_set_action_spec } from './self_service_role_action_specs.js';
47
+ /**
48
+ * Validate a deployment's self-service eligible-role set at construction.
49
+ *
50
+ * Refuses a builtin role (`admin` / `keeper`) — self-toggling one would hand
51
+ * any account instance-wide authority, so no eligible set may name it — and a
52
+ * name outside the `RoleName` grammar. The same fail-loud-at-boot shape as
53
+ * `create_role_schema`, which refuses a builtin name where only app roles
54
+ * belong. Twin of the Rust `validate_self_service_eligible_roles`.
55
+ *
56
+ * @param roles - the eligible role names
57
+ * @throws Error on a builtin role or a name outside the role grammar
58
+ */
59
+ export const validate_self_service_eligible_roles = (roles) => {
60
+ for (const role of roles) {
61
+ if (is_builtin_role(role)) {
62
+ throw new Error(`self-service eligible role "${role}" is a builtin role — builtin roles are never self-service`);
63
+ }
64
+ if (!RoleName.safeParse(role).success) {
65
+ throw new Error(`self-service eligible role "${role}" is not a valid role name`);
66
+ }
67
+ }
68
+ };
47
69
  /**
48
70
  * Build the unified self-service role toggle RPC action.
49
71
  *
@@ -53,12 +75,14 @@ import { ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE, self_service_role_set_action_spec
53
75
  * @param options - optional eligible-role override plus optional role schema for default-eligibility derivation
54
76
  * @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
55
77
  * @throws Error at factory time if any `eligible_roles` entry is missing from `options.roles.role_specs`
78
+ * @throws Error at factory time if the eligible set names a builtin role or an invalid role name
56
79
  */
57
80
  export const create_self_service_role_actions = (deps, options = {}) => {
58
81
  const role_specs = options.roles?.role_specs ?? builtin_role_specs_by_name;
59
82
  const eligible = options.eligible_roles
60
83
  ? new Set(options.eligible_roles)
61
84
  : new Set(list_roles_with_grant_path(role_specs, GRANT_PATH_SELF_SERVICE));
85
+ validate_self_service_eligible_roles(eligible);
62
86
  if (options.eligible_roles && options.roles) {
63
87
  for (const r of eligible) {
64
88
  if (!role_specs.has(r)) {
@@ -138,7 +138,9 @@ export declare const create_serve_cell_fact_route_spec: (options: CreateServeFac
138
138
  * Auth is `{account: 'required', actor: 'required', roles: ['admin']}` —
139
139
  * the dispatcher's authorization phase resolves the acting actor and the
140
140
  * post-authorization guard enforces the admin role before the handler runs.
141
- * The handler re-checks `has_role(_, admin)` as defense-in-depth so a future
141
+ * The handler re-checks the **global** admin grant
142
+ * (`has_scoped_role(_, admin, null)`, the auth phase's own reading) as
143
+ * defense-in-depth so a future
142
144
  * mounting/auth-shape regression fails closed rather than serving by bare
143
145
  * hash to a non-admin.
144
146
  */
@@ -1 +1 @@
1
- {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAetD,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAM/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkEjD,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAgHD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SAgDF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SA0CF,CAAC"}
1
+ {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAetD,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAM/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkEjD,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAgHD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SAgDF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SA2CF,CAAC"}
@@ -93,7 +93,7 @@ import { join } from 'node:path';
93
93
  import { FactHashSchema } from '@fuzdev/fuz_util/hash_schemas.ts';
94
94
  import { Uuid } from '@fuzdev/fuz_util/id.ts';
95
95
  import { z } from 'zod';
96
- import { build_request_context, get_request_context, has_role } from '../auth/request_context.js';
96
+ import { build_request_context, get_request_context, has_scoped_role } from '../auth/request_context.js';
97
97
  import { ROLE_ADMIN } from '../auth/role_schema.js';
98
98
  import { ActingActor } from '../http/auth_shape.js';
99
99
  import { ACCOUNT_ID_KEY } from '../hono_context.js';
@@ -315,7 +315,9 @@ export const create_serve_cell_fact_route_spec = (options) => {
315
315
  * Auth is `{account: 'required', actor: 'required', roles: ['admin']}` —
316
316
  * the dispatcher's authorization phase resolves the acting actor and the
317
317
  * post-authorization guard enforces the admin role before the handler runs.
318
- * The handler re-checks `has_role(_, admin)` as defense-in-depth so a future
318
+ * The handler re-checks the **global** admin grant
319
+ * (`has_scoped_role(_, admin, null)`, the auth phase's own reading) as
320
+ * defense-in-depth so a future
319
321
  * mounting/auth-shape regression fails closed rather than serving by bare
320
322
  * hash to a non-admin.
321
323
  */
@@ -351,8 +353,9 @@ export const create_serve_fact_route_spec = (options) => {
351
353
  const { hash } = get_route_params(c);
352
354
  // Defense-in-depth: the auth phase already gated this on the admin
353
355
  // role, but re-check the resolved context so a mounting/auth-shape
354
- // regression fails closed instead of serving by bare hash.
355
- if (!has_role(get_request_context(c), ROLE_ADMIN)) {
356
+ // regression fails closed instead of serving by bare hash. Global
357
+ // grant only — a scoped `admin` row must not read every fact.
358
+ if (!has_scoped_role(get_request_context(c), ROLE_ADMIN, null)) {
356
359
  return c.body(null, 404);
357
360
  }
358
361
  return serve_fact_bytes(c, route, hash, config);
@@ -1237,8 +1237,11 @@ the option type states whether the bundle is read.
1237
1237
  (gate on `capabilities.cell_gated_create`, **cross-only**) — the parent-aware
1238
1238
  cell-creation authorizer (`CellCreateAuthorize`, **directory model**) +
1239
1239
  the `cell_moderate` verb parity proofs. Both spines mount the same
1240
- `test_cell_gated_create_authorize` policy: a `kind: 'space'` **root** is
1241
- admin-only (a non-admin → **403** `cell_create_forbidden`); a **contribution**
1240
+ `test_cell_gated_create_authorize` policy (its admin bypass and `min_role`
1241
+ read the **global** grant): a `kind: 'space'` **root** is
1242
+ admin-only (a non-admin → **403** `cell_create_forbidden`, and the same for a
1243
+ non-admin `cell_clone` of a viewable space — clone runs the authorizer for
1244
+ every cell it writes); a **contribution**
1242
1245
  under a space is gated by the root's `data.policy[kind] = {min_role,
1243
1246
  moderation_required}`, with `moderation` set per the verdict (`pending`+private
1244
1247
  vs `approved`+author-visibility); a **hidden** parent **404**-masks vs a
@@ -1619,12 +1622,17 @@ non-admin _holder_ of `participant` (and a fresh non-admin) can't offer it (403
1619
1622
  `role_grant_assign` is admin-only** — non-admin holder / fresh → 403
1620
1623
  `insufficient_permissions` (dispatcher gate), anon → 401; plus the
1621
1624
  `to_actor_id`-must-belong-to-`to_account_id` resolution arm (400
1622
- `role_grant_offer_actor_account_mismatch`).
1625
+ `role_grant_offer_actor_account_mismatch`); **(d) builtin roles are
1626
+ global-only** — an unpaired `scope_kind` / `scope_id` → 400 on assign + offer,
1627
+ a scoped `admin` → 400 `role_grant_builtin_scoped` on assign + offer, a builtin
1628
+ offer from a non-global-admin → 403 `role_grant_offer_not_authorized`.
1623
1629
 
1624
1630
  `describe_role_grant_participation_cross_tests` (this module) is the **imperative
1625
1631
  escape hatch** for the multi-step success paths a static row can't express (they
1626
1632
  need a real recipient): admin **assigns** `participant` to a fresh account
1627
- (`{ok, role_grant_id}` + idempotent re-assign), and admin **offers**
1633
+ (`{ok, role_grant_id}` + idempotent re-assign), admin **assigns** a scoped
1634
+ `participant` whose `scope_kind` + `scope_id` round-trip through
1635
+ `admin_account_list` (distinct from the global grant), and admin **offers**
1628
1636
  `participant` → recipient **accepts** → a role_grant lands. The `role_holder`
1629
1637
  principal is seeded via `extra_accounts: [{username, roles: ['participant']}]`
1630
1638
  (bootstrap-cradle direct grant) and named through `principals.role_holder`.
@@ -1 +1 @@
1
- {"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiD9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAkB1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAwPF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,IAwHF,CAAC"}
1
+ {"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyD9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAkB1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAwPF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,IAyKF,CAAC"}
@@ -30,6 +30,9 @@ import '../assert_dev_env.js';
30
30
  * **private** space the caller can't view → **404** `cell_not_found` (the
31
31
  * parent is masked); under a **public** space → **403** (you see it, you
32
32
  * can't contribute).
33
+ * - **clone is no side door** — `cell_clone` runs the authorizer for every cell
34
+ * it writes as a parentless create, so a non-admin clone of a viewable
35
+ * `space` root → **403** `cell_create_forbidden`; admin → succeeds.
33
36
  *
34
37
  * Gated on `capabilities.cell_gated_create` — `true` only on the reference
35
38
  * spine binaries that mount the policy, so it skips for generic consumers and
@@ -41,7 +44,8 @@ import '../assert_dev_env.js';
41
44
  * @module
42
45
  */
43
46
  import { describe, assert } from 'vitest';
44
- import { CellCreateOutput, CellModerateOutput } from '../../auth/cell_action_specs.js';
47
+ import { CellCloneOutput, CellCreateOutput, CellModerateOutput } from '../../auth/cell_action_specs.js';
48
+ import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
45
49
  import { test_if } from './capabilities.js';
46
50
  import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
47
51
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
@@ -211,4 +215,22 @@ export const describe_cell_moderate_cross_tests = (options) => {
211
215
  assert.strictEqual(moderated.cell.visibility, 'private', 'rejection leaves the post private');
212
216
  });
213
217
  });
218
+ describe('cell_clone runs the creation authorizer (parity)', () => {
219
+ test_if(capabilities.cell_gated_create, 'a non-admin clone of a viewable `space` root → 403, admin clone succeeds', async () => {
220
+ const fixture = await setup_test();
221
+ const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
222
+ // Every cell a clone writes is a parentless create of the source's
223
+ // kind, gated like `cell_create` — so a caller denied a `space` root
224
+ // can't clone one they can see into one they own.
225
+ const stranger = await fixture.create_account({ username: 'clone_stranger' });
226
+ const denied = await cross_rpc_call(fixture.transport, rpc_path, 'cell_clone', { source_id: space.cell.id }, stranger.create_session_headers());
227
+ assert.ok(!denied.ok, 'a non-admin must not clone a space root');
228
+ assert.strictEqual(denied.error?.code, JSONRPC_ERROR_CODES.forbidden, 'a 403');
229
+ assert.strictEqual(error_reason(denied), 'cell_create_forbidden');
230
+ const cloned = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_clone', { source_id: space.cell.id }, fixture.create_session_headers()), CellCloneOutput);
231
+ assert.strictEqual(cloned.cell.kind, SPACE_CELL_KIND, 'the admin clones the space');
232
+ assert.notStrictEqual(cloned.cell.id, space.cell.id);
233
+ assert.strictEqual(cloned.cell.moderation, null, 'a clone stamps no moderation');
234
+ });
235
+ });
214
236
  };
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_participation.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_participation.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,0EAA0E;AAC1E,MAAM,WAAW,sCAAsC;IACtD,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,6CAA6C,GACzD,SAAS,sCAAsC,KAC7C,IAgFF,CAAC"}
1
+ {"version":3,"file":"role_grant_participation.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_participation.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,0EAA0E;AAC1E,MAAM,WAAW,sCAAsC;IACtD,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,6CAA6C,GACzD,SAAS,sCAAsC,KAC7C,IA8IF,CAAC"}
@@ -15,6 +15,9 @@ import '../assert_dev_env.js';
15
15
  * re-assigning the active grant is idempotent (same id).
16
16
  * - **consent flow** — `role_grant_offer_create` of `participant` (admin-only)
17
17
  * → the recipient `role_grant_offer_accept`s → a role_grant lands.
18
+ * - **scoped assign** — `role_grant_assign` of `participant` with a paired
19
+ * `scope_kind` + `scope_id` lands a scoped grant carrying both, read back
20
+ * through `admin_account_list`, distinct from the global grant.
18
21
  *
19
22
  * The single-request gate/denial matrix (grantability refusal, admin-only
20
23
  * conferral, dispatcher admin gate, auth) lives in the declarative table
@@ -55,6 +58,37 @@ export const describe_role_grant_participation_cross_tests = (options) => {
55
58
  assert.ok(second.ok, `re-assign must succeed: ${JSON.stringify(second)}`);
56
59
  assert.strictEqual(second.result.role_grant_id, assigned.role_grant_id, 're-assigning an active grant returns the existing role_grant id');
57
60
  });
61
+ test('admin assigns a scoped participant grant that round-trips scope_kind', async () => {
62
+ const fixture = await setup_test();
63
+ const recipient = await fixture.create_account({ username: 'participation_scoped' });
64
+ const scope_id = '33333333-3333-4333-8333-333333333333';
65
+ const scoped = await rpc(fixture.transport, 'role_grant_assign', {
66
+ to_account_id: recipient.account.id,
67
+ role: SPINE_PARTICIPANT_ROLE,
68
+ scope_kind: 'space',
69
+ scope_id
70
+ }, fixture.create_session_headers());
71
+ assert.ok(scoped.ok, `scoped assign must succeed: ${JSON.stringify(scoped)}`);
72
+ const scoped_id = scoped.result.role_grant_id;
73
+ const global = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role: SPINE_PARTICIPANT_ROLE }, fixture.create_session_headers());
74
+ assert.ok(global.ok, `global assign must succeed: ${JSON.stringify(global)}`);
75
+ const global_id = global.result.role_grant_id;
76
+ assert.notStrictEqual(global_id, scoped_id, 'the global grant is a distinct row');
77
+ const listed = await rpc(fixture.transport, 'admin_account_list', {}, fixture.create_session_headers());
78
+ assert.ok(listed.ok, `admin_account_list must succeed: ${JSON.stringify(listed)}`);
79
+ const entry = listed.result.accounts.find((a) => a.account.id === recipient.account.id);
80
+ assert.ok(entry, 'the recipient is listed');
81
+ const by_id = new Map(entry.role_grants.map((g) => [g.id, g]));
82
+ const scoped_row = by_id.get(scoped_id);
83
+ assert.ok(scoped_row, 'the scoped grant is listed');
84
+ assert.strictEqual(scoped_row.role, SPINE_PARTICIPANT_ROLE);
85
+ assert.strictEqual(scoped_row.scope_kind, 'space', 'scope_kind round-trips');
86
+ assert.strictEqual(scoped_row.scope_id, scope_id, 'scope_id round-trips');
87
+ const global_row = by_id.get(global_id);
88
+ assert.ok(global_row, 'the global grant is listed');
89
+ assert.strictEqual(global_row.scope_kind, null);
90
+ assert.strictEqual(global_row.scope_id, null);
91
+ });
58
92
  test('admin offers the participant app-role and the recipient accepts', async () => {
59
93
  const fixture = await setup_test();
60
94
  const recipient = await fixture.create_account({ username: 'participation_offeree' });
@@ -2,6 +2,14 @@ import '../assert_dev_env.js';
2
2
  import type { BackendConfig } from './backend_config.js';
3
3
  /** Env var naming the prebuilt `testing_spine_stub` binary. Required when `binary_path` is omitted. */
4
4
  export declare const RUST_SPINE_STUB_BIN_ENV = "FUZ_TESTING_RUST_SPINE_STUB_BIN";
5
+ /**
6
+ * Env var overriding the stub's Postgres URL when `database_url` is omitted —
7
+ * e.g. a scratch cluster on a spare port, keeping the run off the machine's
8
+ * default server. Pair it with the libpq `PGHOST` / `PGPORT` the runner's
9
+ * idempotent `createdb` reads, so the database it ensures is the one the URL
10
+ * names.
11
+ */
12
+ export declare const RUST_SPINE_STUB_DATABASE_URL_ENV = "FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL";
5
13
  /**
6
14
  * Env var the stub reads for the absolute path of the committed
7
15
  * `expected_schema.json` its `/ready` gate introspects against. Pointed at the
@@ -16,7 +24,10 @@ export declare const RUST_SPINE_STUB_DEFAULT_DATABASE_URL = "postgres://localhos
16
24
  export interface SpineStubBackendConfigOptions {
17
25
  /** Listening port. Default `RUST_SPINE_STUB_DEFAULT_PORT`. */
18
26
  readonly port?: number;
19
- /** Postgres connection URL. Default `RUST_SPINE_STUB_DEFAULT_DATABASE_URL`. */
27
+ /**
28
+ * Postgres connection URL. Default `$FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL`,
29
+ * then `RUST_SPINE_STUB_DEFAULT_DATABASE_URL`.
30
+ */
20
31
  readonly database_url?: string;
21
32
  /**
22
33
  * Prebuilt binary path. Overrides the `FUZ_TESTING_RUST_SPINE_STUB_BIN` env
@@ -1 +1 @@
1
- {"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAkBnG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAiDF,CAAC"}
1
+ {"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAkBnG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAkDF,CAAC"}
@@ -25,6 +25,9 @@ import '../assert_dev_env.js';
25
25
  * createdb fuz_app_test_rust_spine_stub 2>/dev/null || true
26
26
  * ```
27
27
  *
28
+ * `FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL` points the stub at another
29
+ * server (e.g. a scratch cluster on a spare port).
30
+ *
28
31
  * The binary self-wipes the auth-namespace schema on every boot
29
32
  * (`FUZ_TESTING_RESET_DB_ON_STARTUP=true`, set by the Rust-family builder),
30
33
  * so no manual `DROP TABLE` between sessions is needed; per-test reset is
@@ -39,6 +42,14 @@ import { SPINE_EXPECTED_SCHEMA_URL } from './spine_surface_constants.js';
39
42
  import { LOGIN_RATE_LIMIT_ENABLED_ENV, make_default_rust_backend_config, rust_default_capabilities } from './default_backend_configs.js';
40
43
  /** Env var naming the prebuilt `testing_spine_stub` binary. Required when `binary_path` is omitted. */
41
44
  export const RUST_SPINE_STUB_BIN_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_BIN';
45
+ /**
46
+ * Env var overriding the stub's Postgres URL when `database_url` is omitted —
47
+ * e.g. a scratch cluster on a spare port, keeping the run off the machine's
48
+ * default server. Pair it with the libpq `PGHOST` / `PGPORT` the runner's
49
+ * idempotent `createdb` reads, so the database it ensures is the one the URL
50
+ * names.
51
+ */
52
+ export const RUST_SPINE_STUB_DATABASE_URL_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL';
42
53
  /**
43
54
  * Env var the stub reads for the absolute path of the committed
44
55
  * `expected_schema.json` its `/ready` gate introspects against. Pointed at the
@@ -77,7 +88,8 @@ export const RUST_SPINE_STUB_DEFAULT_DATABASE_URL = 'postgres://localhost/fuz_ap
77
88
  * @throws Error when no binary path is available.
78
89
  */
79
90
  export const rust_spine_stub_backend_config = (options = {}) => {
80
- const { port = RUST_SPINE_STUB_DEFAULT_PORT, database_url = RUST_SPINE_STUB_DEFAULT_DATABASE_URL, binary_path = process.env[RUST_SPINE_STUB_BIN_ENV], enable_login_rate_limit, trusted_proxies } = options;
91
+ const { port = RUST_SPINE_STUB_DEFAULT_PORT, database_url = process.env[RUST_SPINE_STUB_DATABASE_URL_ENV] ||
92
+ RUST_SPINE_STUB_DEFAULT_DATABASE_URL, binary_path = process.env[RUST_SPINE_STUB_BIN_ENV], enable_login_rate_limit, trusted_proxies } = options;
81
93
  if (!binary_path) {
82
94
  throw new Error(`rust_spine_stub_backend_config: no binary path — set ${RUST_SPINE_STUB_BIN_ENV} to a prebuilt ` +
83
95
  '`testing_spine_stub` binary (build it with `cargo build -p testing_spine_stub --release`) ' +
@@ -1 +1 @@
1
- {"version":3,"file":"test_cell_gated_create_authorize.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/test_cell_gated_create_authorize.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+B9B,OAAO,KAAK,EACX,mBAAmB,EAGnB,MAAM,4BAA4B,CAAC;AAGpC,8EAA8E;AAC9E,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAAyB,CAAC;AAEzD,yFAAyF;AACzF,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAYD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,EAAE,mBAoB9C,CAAC"}
1
+ {"version":3,"file":"test_cell_gated_create_authorize.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/test_cell_gated_create_authorize.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+B9B,OAAO,KAAK,EACX,mBAAmB,EAGnB,MAAM,4BAA4B,CAAC;AAGpC,8EAA8E;AAC9E,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAAyB,CAAC;AAEzD,yFAAyF;AACzF,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAYD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,EAAE,mBAsB9C,CAAC"}
@@ -6,15 +6,15 @@ import '../assert_dev_env.js';
6
6
  * the parent-aware cell-creation authorizer agrees TS↔Rust. The twin of the
7
7
  * Rust `TestCellGatedCreateAuthorize`, **directory model**.
8
8
  *
9
- * Admin bypasses everything (`{allow: true, moderation_required: false}`). For
10
- * a non-admin:
9
+ * A **global** admin bypasses everything (`{allow: true, moderation_required:
10
+ * false}`) — a scoped `admin` row is no admin here. For anyone else:
11
11
  *
12
12
  * - **Root creation** (`root_id` null): `kind: 'space'` is admin-only (denied);
13
13
  * every other parentless kind stays open, so the plain-create `cell_crud` /
14
14
  * `cell_relations` suites are unaffected.
15
15
  * - **Contribution** (`root_id` set): the governing root's
16
16
  * `data.policy[kind] = {min_role?, moderation_required?}` decides — a missing
17
- * entry denies, a present `min_role` the actor lacks denies, and otherwise it
17
+ * entry denies, a present `min_role` the actor lacks globally denies, and otherwise it
18
18
  * admits with the entry's `moderation_required` folded into the verdict. The
19
19
  * root's `data` arrives in `input.root_data` (the handler read it in-tx), so
20
20
  * the predicate is **pure** — no DB read of its own (which also dodges the
@@ -25,7 +25,7 @@ import '../assert_dev_env.js';
25
25
  *
26
26
  * @module
27
27
  */
28
- import { has_role } from '../../auth/request_context.js';
28
+ import { has_scoped_role } from '../../auth/request_context.js';
29
29
  import { ROLE_ADMIN } from '../../auth/role_schema.js';
30
30
  import { SPINE_PARTICIPANT_ROLE } from './spine_surface_constants.js';
31
31
  /** The admin-only root kind the test policy gates (talk's `space` analog). */
@@ -54,8 +54,9 @@ const get_contribution_rule = (data, kind) => {
54
54
  * handler). Byte-equivalent with the Rust `TestCellGatedCreateAuthorize`.
55
55
  */
56
56
  export const test_cell_gated_create_authorize = (auth, input) => {
57
- // Admin bypass — admins create roots + any kind, live immediately.
58
- if (has_role(auth, ROLE_ADMIN))
57
+ // Admin bypass — admins create roots + any kind, live immediately. The
58
+ // global grant only: a scoped `admin` row bypasses nothing.
59
+ if (has_scoped_role(auth, ROLE_ADMIN, null))
59
60
  return { allow: true, moderation_required: false };
60
61
  // Root creation (no governing root): `space` is admin-only; other parentless
61
62
  // kinds stay open (the plain-create suites).
@@ -68,7 +69,8 @@ export const test_cell_gated_create_authorize = (auth, input) => {
68
69
  const rule = get_contribution_rule(input.root_data, input.kind ?? '');
69
70
  if (!rule)
70
71
  return { allow: false };
71
- const admitted = rule.min_role === undefined || has_role(auth, rule.min_role);
72
+ // `min_role` names no scope, so only the global grant satisfies it.
73
+ const admitted = rule.min_role === undefined || has_scoped_role(auth, rule.min_role, null);
72
74
  return admitted
73
75
  ? { allow: true, moderation_required: rule.moderation_required ?? false }
74
76
  : { allow: false };
@@ -1,7 +1,7 @@
1
1
  import './assert_dev_env.js';
2
2
  import type { Account, Actor, RoleGrant } from '../auth/account_schema.js';
3
3
  import type { AuditLogEvent } from '../auth/audit_log_schema.js';
4
- import type { RequestContext } from '../auth/request_context.js';
4
+ import type { RequestActorContext } from '../auth/request_context.js';
5
5
  /** Override type for `create_test_account` — id-like fields accept plain `string`. */
6
6
  export type TestAccountOverrides = Partial<Omit<Account, 'id' | 'created_by' | 'updated_by'>> & {
7
7
  id?: string;
@@ -30,8 +30,11 @@ export type TestRoleGrantOverrides = Partial<Omit<RoleGrant, 'id' | 'actor_id' |
30
30
  };
31
31
  /** Create a test `RoleGrant` with sensible defaults. */
32
32
  export declare const create_test_role_grant: (overrides?: TestRoleGrantOverrides) => RoleGrant;
33
- /** Create a test `RequestContext` with role_grants from partial overrides. */
34
- export declare const create_test_context: (role_grants?: Array<TestRoleGrantOverrides>) => RequestContext;
33
+ /**
34
+ * Create a test `RequestActorContext` (a `RequestContext` with a resolved
35
+ * actor) with role_grants from partial overrides.
36
+ */
37
+ export declare const create_test_context: (role_grants?: Array<TestRoleGrantOverrides>) => RequestActorContext;
35
38
  /** Override type for `create_test_audit_event` — id-like fields accept plain `string`. */
36
39
  export type TestAuditEventOverrides = Partial<Omit<AuditLogEvent, 'id' | 'actor_id' | 'account_id' | 'target_account_id' | 'target_actor_id'>> & {
37
40
  id?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"entities.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/entities.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,sFAAsF;AACtF,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,GAAG;IAC/F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,GAAI,YAAY,oBAAoB,KAAG,OAarE,CAAC;AAEH,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,GAAG;IAC3F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,GAAI,YAAY,kBAAkB,KAAG,KAUjE,CAAC;AAEH,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAC3C,IAAI,CACH,SAAS,EACT,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,GAAG,YAAY,GAAG,iBAAiB,CAC/F,CACD,GAAG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,GAAI,YAAY,sBAAsB,KAAG,SAgB3E,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,GAC/B,cAAa,KAAK,CAAC,sBAAsB,CAAQ,KAC/C,cAID,CAAC;AAEH,0FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAC5C,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,mBAAmB,GAAG,iBAAiB,CAAC,CAC/F,GAAG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,uBAAuB,GAAI,YAAY,uBAAuB,KAAG,aAa5E,CAAC"}
1
+ {"version":3,"file":"entities.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/entities.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,sFAAsF;AACtF,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,GAAG;IAC/F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,GAAI,YAAY,oBAAoB,KAAG,OAarE,CAAC;AAEH,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,GAAG;IAC3F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,GAAI,YAAY,kBAAkB,KAAG,KAUjE,CAAC;AAEH,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAC3C,IAAI,CACH,SAAS,EACT,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,GAAG,YAAY,GAAG,iBAAiB,CAC/F,CACD,GAAG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,GAAI,YAAY,sBAAsB,KAAG,SAgB3E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAa,KAAK,CAAC,sBAAsB,CAAQ,KAC/C,mBAID,CAAC;AAEH,0FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAC5C,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,mBAAmB,GAAG,iBAAiB,CAAC,CAC/F,GAAG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,uBAAuB,GAAI,YAAY,uBAAuB,KAAG,aAa5E,CAAC"}
@@ -44,7 +44,10 @@ export const create_test_role_grant = (overrides) => {
44
44
  };
45
45
  return overrides ? { ...base, ...overrides } : base;
46
46
  };
47
- /** Create a test `RequestContext` with role_grants from partial overrides. */
47
+ /**
48
+ * Create a test `RequestActorContext` (a `RequestContext` with a resolved
49
+ * actor) with role_grants from partial overrides.
50
+ */
48
51
  export const create_test_context = (role_grants = [{}]) => ({
49
52
  account: create_test_account(),
50
53
  actor: create_test_actor(),