@fuzdev/fuz_app 0.101.1 → 0.102.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.
@@ -436,7 +436,7 @@ are excluded.
436
436
  records, regardless of outcome. Default
437
437
  `default_action_account_rate_limit` is 1200/15min per actor.
438
438
 
439
- ### Admin actions — fourteen specs
439
+ ### Admin actions
440
440
 
441
441
  `create_admin_actions(deps, options?)` in `auth/admin_actions.ts`.
442
442
 
@@ -501,7 +501,7 @@ visibility — `target_account_id` is null (FK rejects missing ids), and the
501
501
  probed id is preserved under `metadata.attempted_account_id`. Every gated
502
502
  event additionally records `credential_type` in metadata (defense in depth).
503
503
 
504
- ### Role-grant-offer actions — seven specs
504
+ ### Role-grant-offer actions
505
505
 
506
506
  `create_role_grant_offer_actions(deps, options?)` in
507
507
  `auth/role_grant_offer_actions.ts`.
@@ -525,19 +525,22 @@ event additionally records `credential_type` in metadata (defense in depth).
525
525
  - `role_grant_offer_list_action_spec` — input `{account_id?}`; output `{offers}`.
526
526
  - `role_grant_offer_history_action_spec` — input `{account_id?, limit?, offset?}`; output `{offers}`.
527
527
  - `role_grant_revoke_action_spec` — input `{actor_id, role_grant_id, reason?}`; output `{ok, revoked}`.
528
+ - `role_grant_assign_action_spec` — admin-only immediate conferral (no consent step); input `{to_account_id, to_actor_id?, role, scope_id?}`; output `{ok, role_grant_id}`. Runs the same admin-grant-path gate as offer-create, resolves the target actor (named `to_actor_id` ∈ the account's active actors, else its sole active actor — multi-actor → `invalid_params`, actorless → 404), writes via the idempotent `query_create_role_grant` (`scope_kind` stays null), and emits a `role_grant_create` audit row. No WS notification in v1. Reasons: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH`.
528
529
 
529
530
  Every input carries `acting?: ActingActor` (registry-time invariant 2).
530
531
  `role_grant_revoke` keys on **`actor_id`**, not `account_id` — role_grants
531
532
  are actor-scoped and deriving actor from account collapses under multi-actor
532
533
  accounts.
533
534
 
534
- `role_grant_offer_create` runs the **admin-grant-path gate first** (offered
535
+ `role_grant_offer_create` runs the **admin-grantability gate first** (offered
535
536
  role's `RoleSpec.grant_paths` must include `'admin'`), then the
536
- `RoleGrantOfferCreateAuthorize` callback. Default: caller holds the offered
537
- role globally. Pre-built `authorize_admin_or_holder` admits any admin and
538
- otherwise falls back to the default — drop into `create_role_grant_offer_actions({authorize: authorize_admin_or_holder})`
539
- or `create_standard_rpc_actions` for "admins offer anything; users offer
540
- what they hold."
537
+ `RoleGrantOfferCreateAuthorize` callback. **Default `default_authorize`: the
538
+ caller must hold a _global_ `admin` grant** — conferral is admin-only, with no
539
+ holder-propagation (holding the offered role confers no power to offer it). The
540
+ global check (`has_scoped_role(_, ROLE_ADMIN, null)`) stops a scoped `admin`
541
+ escalating into global authority over the offer surface. A consumer needing
542
+ scope-aware delegation (e.g. a classroom teacher offering within their own
543
+ scope) supplies a custom `authorize` callback.
541
544
 
542
545
  Error reasons (`as const` literals):
543
546
 
@@ -589,7 +592,7 @@ Options: `roles?: RoleSchemaResult` (drives admin-grant-path lookup),
589
592
  `default_ttl_ms?` (defaults to `ROLE_GRANT_OFFER_DEFAULT_TTL_MS` = 30 days),
590
593
  `authorize?: RoleGrantOfferCreateAuthorize`.
591
594
 
592
- ### Account actions — seven self-service specs
595
+ ### Account actions — self-service
593
596
 
594
597
  `create_account_actions(deps, options?)` in `auth/account_actions.ts`.
595
598
 
@@ -625,8 +628,8 @@ arbitrary sockets.
625
628
  `create_standard_rpc_actions(deps, options)` in `auth/standard_rpc_actions.ts`
626
629
  spreads `create_admin_actions`, `create_role_grant_offer_actions`, and
627
630
  `create_account_actions` into a single `Array<RpcAction>` — the canonical
628
- fuz_app "standard" surface (28 actions: 14 admin + 7 role-grant-offer + 7
629
- account; the two app-settings methods are always wired). Frontend mirror is
631
+ fuz_app "standard" surface (admin + role-grant-offer + account; the two
632
+ app-settings methods are always wired). Frontend mirror is
630
633
  `all_standard_action_specs` in `auth/standard_action_specs.ts`.
631
634
 
632
635
  Option routing — `roles` is shared between admin + role-grant-offer;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Role grant offer RPC action specs — declarative contract for the
3
- * consentful-role-grants surface (offer lifecycle + admin revoke).
3
+ * consentful-role-grants surface (offer lifecycle + admin revoke + the
4
+ * immediate admin-only `role_grant_assign`).
4
5
  *
5
6
  * Import this module for the specs, Input/Output schemas, `ERROR_ROLE_GRANT_OFFER_*`
6
7
  * reason constants, and the `all_role_grant_offer_action_specs` registry.
@@ -209,6 +210,27 @@ export declare const RoleGrantRevokeOutput: z.ZodObject<{
209
210
  revoked: z.ZodLiteral<true>;
210
211
  }, z.core.$strict>;
211
212
  export type RoleGrantRevokeOutput = z.infer<typeof RoleGrantRevokeOutput>;
213
+ /**
214
+ * Input for `role_grant_assign` — the immediate admin-only conferral path, the
215
+ * consent-free sibling of `role_grant_offer_create`. An admin assigns a
216
+ * role_grant straight onto the target actor; there is no offer for a grantee to
217
+ * accept. No `message` (no offer to carry it on) and no `scope_kind` (`scope_id`
218
+ * alone is the v1 scope discriminator — the grant row's `scope_kind` stays null).
219
+ */
220
+ export declare const RoleGrantAssignInput: z.ZodObject<{
221
+ to_account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
222
+ to_actor_id: z.ZodOptional<z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>>;
223
+ role: z.ZodString;
224
+ scope_id: z.ZodOptional<z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>>;
225
+ acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
226
+ }, z.core.$strict>;
227
+ export type RoleGrantAssignInput = z.infer<typeof RoleGrantAssignInput>;
228
+ /** Output for `role_grant_assign`. */
229
+ export declare const RoleGrantAssignOutput: z.ZodObject<{
230
+ ok: z.ZodLiteral<true>;
231
+ role_grant_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
232
+ }, z.core.$strict>;
233
+ export type RoleGrantAssignOutput = z.infer<typeof RoleGrantAssignOutput>;
212
234
  /**
213
235
  * `rate_limit: 'account'` throttles offer-spam at the authenticated
214
236
  * grantor and bounds the account-existence oracle on `to_account_id` —
@@ -447,10 +469,41 @@ export declare const role_grant_revoke_action_spec: {
447
469
  error_reasons: ("role_not_web_grantable" | "role_grant_not_found")[];
448
470
  rate_limit: "account";
449
471
  };
472
+ /**
473
+ * `rate_limit: 'account'` bounds admin-side burn of `role_grant_assign` — the
474
+ * action is admin-gated and audit-trailed, but the per-account cap matches
475
+ * `role_grant_revoke` so a single admin script can't churn grants in a loop.
476
+ */
477
+ export declare const role_grant_assign_action_spec: {
478
+ method: string;
479
+ kind: "request_response";
480
+ initiator: "frontend";
481
+ auth: {
482
+ account: "required";
483
+ actor: "required";
484
+ roles: string[];
485
+ };
486
+ side_effects: true;
487
+ input: z.ZodObject<{
488
+ to_account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
489
+ to_actor_id: z.ZodOptional<z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>>;
490
+ role: z.ZodString;
491
+ scope_id: z.ZodOptional<z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>>;
492
+ acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
493
+ }, z.core.$strict>;
494
+ output: z.ZodObject<{
495
+ ok: z.ZodLiteral<true>;
496
+ role_grant_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
497
+ }, z.core.$strict>;
498
+ async: true;
499
+ description: string;
500
+ error_reasons: ("role_not_web_grantable" | "role_grant_offer_actor_account_mismatch")[];
501
+ rate_limit: "account";
502
+ };
450
503
  /**
451
504
  * All role-grant-offer action specs — a codegen-ready registry. Consumers spread
452
- * this into their own action-spec array to include offer lifecycle + revoke
453
- * methods in a typed client surface.
505
+ * this into their own action-spec array to include offer lifecycle + revoke +
506
+ * assign methods in a typed client surface.
454
507
  */
455
508
  export declare const all_role_grant_offer_action_specs: Array<RequestResponseActionSpec>;
456
509
  //# sourceMappingURL=role_grant_offer_action_specs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_action_specs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"role_grant_offer_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,2BAA2B,CAAC;AAUzE,oEAAoE;AACpE,eAAO,MAAM,kCAAkC,EAAG,8BAAuC,CAAC;AAC1F,kEAAkE;AAClE,eAAO,MAAM,+BAA+B,EAAG,2BAAoC,CAAC;AACpF,sDAAsD;AACtD,eAAO,MAAM,8BAA8B,EAAG,0BAAmC,CAAC;AAClF,wGAAwG;AACxG,eAAO,MAAM,gCAAgC,EAAG,4BAAqC,CAAC;AACtF,uIAAuI;AACvI,eAAO,MAAM,yCAAyC,EACrD,qCAA8C,CAAC;AAChD,gKAAgK;AAChK,eAAO,MAAM,qCAAqC,EAAG,iCAA0C,CAAC;AAChG,6FAA6F;AAC7F,eAAO,MAAM,qCAAqC,EAAG,iCAA0C,CAAC;AAChG,wHAAwH;AACxH,eAAO,MAAM,6CAA6C,EACzD,yCAAkD,CAAC;AAIpD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;kBAoBpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,2CAA2C;AAC3C,eAAO,MAAM,yBAAyB;;;kBAGpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,4CAA4C;AAC5C,eAAO,MAAM,0BAA0B;;;;kBAQrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF,4CAA4C;AAC5C,eAAO,MAAM,0BAA0B;;;kBAGrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF,uGAAuG;AACvG,eAAO,MAAM,uBAAuB;;;mBAOvB,CAAC;AACd,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE9E;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;;;;;kBAQ/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;;mBAa1B,CAAC;AACd,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF,4CAA4C;AAC5C,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;kBAErC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF,4CAA4C;AAC5C,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;kBAIrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB;;kBAAwC,CAAC;AAC5E,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,0CAA0C;AAC1C,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;kBAAwD,CAAC;AAC9F,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF,6CAA6C;AAC7C,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;kBAAwD,CAAC;AACjG,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEtF,sCAAsC;AACtC,eAAO,MAAM,qBAAqB;;;kBAGhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;;;;;;kBAa/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,sCAAsC;AACtC,eAAO,MAAM,qBAAqB;;;kBAGhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAI1E;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkBX,CAAC;AAEtC,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiBX,CAAC;AAEtC,eAAO,MAAM,oCAAoC;;;;;;;;;;;;;;;;;;;;CAWZ,CAAC;AAEtC,eAAO,MAAM,oCAAoC;;;;;;;;;;;;;;;;;;;CAWZ,CAAC;AAEtC,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWT,CAAC;AAEtC,eAAO,MAAM,oCAAoC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWZ,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;CAaL,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;CAaL,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,EAAE,KAAK,CAAC,yBAAyB,CAS9E,CAAC"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Role grant offer RPC action specs — declarative contract for the
3
- * consentful-role-grants surface (offer lifecycle + admin revoke).
3
+ * consentful-role-grants surface (offer lifecycle + admin revoke + the
4
+ * immediate admin-only `role_grant_assign`).
4
5
  *
5
6
  * Import this module for the specs, Input/Output schemas, `ERROR_ROLE_GRANT_OFFER_*`
6
7
  * reason constants, and the `all_role_grant_offer_action_specs` registry.
@@ -156,6 +157,31 @@ export const RoleGrantRevokeOutput = z.strictObject({
156
157
  ok: z.literal(true),
157
158
  revoked: z.literal(true),
158
159
  });
160
+ /**
161
+ * Input for `role_grant_assign` — the immediate admin-only conferral path, the
162
+ * consent-free sibling of `role_grant_offer_create`. An admin assigns a
163
+ * role_grant straight onto the target actor; there is no offer for a grantee to
164
+ * accept. No `message` (no offer to carry it on) and no `scope_kind` (`scope_id`
165
+ * alone is the v1 scope discriminator — the grant row's `scope_kind` stays null).
166
+ */
167
+ export const RoleGrantAssignInput = z.strictObject({
168
+ to_account_id: Uuid.meta({ description: 'Account id of the grantee.' }),
169
+ to_actor_id: Uuid.nullish().meta({
170
+ description: "Optional actor-grain target on the grantee account. When set, must belong to `to_account_id`. Omit to resolve the account's sole active actor — a multi-actor account must name one.",
171
+ }),
172
+ role: RoleName.meta({
173
+ description: "Role to assign. Must be admin-grantable (its `grant_paths` includes `'admin'`).",
174
+ }),
175
+ scope_id: Uuid.nullish().meta({
176
+ description: 'Scope id for resource-scoped grants. `null` for a global role_grant.',
177
+ }),
178
+ acting: ActingActor,
179
+ });
180
+ /** Output for `role_grant_assign`. */
181
+ export const RoleGrantAssignOutput = z.strictObject({
182
+ ok: z.literal(true),
183
+ role_grant_id: Uuid,
184
+ });
159
185
  // -- Action specs -----------------------------------------------------------
160
186
  /**
161
187
  * `rate_limit: 'account'` throttles offer-spam at the authenticated
@@ -267,10 +293,28 @@ export const role_grant_revoke_action_spec = {
267
293
  error_reasons: [ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE],
268
294
  rate_limit: 'account',
269
295
  };
296
+ /**
297
+ * `rate_limit: 'account'` bounds admin-side burn of `role_grant_assign` — the
298
+ * action is admin-gated and audit-trailed, but the per-account cap matches
299
+ * `role_grant_revoke` so a single admin script can't churn grants in a loop.
300
+ */
301
+ export const role_grant_assign_action_spec = {
302
+ method: 'role_grant_assign',
303
+ kind: 'request_response',
304
+ initiator: 'frontend',
305
+ auth: { account: 'required', actor: 'required', roles: ['admin'] },
306
+ side_effects: true,
307
+ input: RoleGrantAssignInput,
308
+ output: RoleGrantAssignOutput,
309
+ async: true,
310
+ description: "Immediately assign a role_grant to a target account — admin-only, no consent step (the capability-unlock UX). The role's `grant_paths` must include `'admin'`. Idempotent: re-assigning an active grant returns it. Emits a role_grant_create audit event.",
311
+ error_reasons: [ERROR_ROLE_NOT_WEB_GRANTABLE, ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH],
312
+ rate_limit: 'account',
313
+ };
270
314
  /**
271
315
  * All role-grant-offer action specs — a codegen-ready registry. Consumers spread
272
- * this into their own action-spec array to include offer lifecycle + revoke
273
- * methods in a typed client surface.
316
+ * this into their own action-spec array to include offer lifecycle + revoke +
317
+ * assign methods in a typed client surface.
274
318
  */
275
319
  export const all_role_grant_offer_action_specs = [
276
320
  role_grant_offer_create_action_spec,
@@ -280,4 +324,5 @@ export const all_role_grant_offer_action_specs = [
280
324
  role_grant_offer_list_action_spec,
281
325
  role_grant_offer_history_action_spec,
282
326
  role_grant_revoke_action_spec,
327
+ role_grant_assign_action_spec,
283
328
  ];
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Role grant offer RPC action handlers — the consentful-role-grants action surface.
3
3
  *
4
- * Seven actions: six offer-lifecycle methods (create / accept / decline /
5
- * retract / list / history) plus `role_grant_revoke` (admin-only). All mount
4
+ * Eight actions: six offer-lifecycle methods (create / accept / decline /
5
+ * retract / list / history) plus `role_grant_revoke` and `role_grant_assign`
6
+ * (both admin-only). All mount
6
7
  * on a consumer's JSON-RPC endpoint via `create_rpc_endpoint`. The action
7
8
  * specs themselves live in `auth/role_grant_offer_action_specs.ts`. Mutations
8
9
  * declare `side_effects: true` so the RPC dispatcher wraps the handler in
@@ -23,6 +24,10 @@
23
24
  * dispatcher rejects non-admin callers before the handler runs.
24
25
  * The admin-grant-path gate prevents revoking keeper / daemon-scoped
25
26
  * roles via this surface. Keys on `actor_id` to survive multi-actor accounts.
27
+ * - `role_grant_assign` — spec-level `auth: {role: 'admin'}`; the immediate
28
+ * consent-free conferral path. The admin-grant-path gate prevents assigning
29
+ * keeper / daemon-scoped roles via this surface. No holder-propagation — only
30
+ * admins confer (see participation-gates.md Decision 7).
26
31
  *
27
32
  * Audit events are emitted in-transaction by the query layer (atomic with
28
33
  * the role_grant write on accept/revoke) or by the handler via the bound
@@ -76,23 +81,8 @@ export interface RoleGrantOfferActionOptions {
76
81
  authorize?: RoleGrantOfferCreateAuthorize;
77
82
  }
78
83
  /**
79
- * Authorization callback that admits any admin and otherwise falls back to
80
- * the symmetric default (caller must hold the offered role globally).
81
- *
82
- * The admin-grant-path filter in `create_handler` runs **before** the
83
- * `authorize` callback, so this never sees roles whose `grant_paths`
84
- * omits `'admin'`. Drop into
85
- * `create_role_grant_offer_actions({authorize: authorize_admin_or_holder})`
86
- * (or any factory that forwards `authorize`, e.g. `create_standard_rpc_actions`)
87
- * for the common "admins offer anything; users offer what they hold"
88
- * pattern. Scope-aware policies (e.g. classroom_teacher offering
89
- * classroom_student in their own scope) wrap this and short-circuit `true`
90
- * before delegating.
91
- */
92
- export declare const authorize_admin_or_holder: RoleGrantOfferCreateAuthorize;
93
- /**
94
- * Create the seven role-grant-offer RPC actions (six offer-lifecycle methods
95
- * plus `role_grant_revoke`).
84
+ * Create the eight role-grant-offer RPC actions (six offer-lifecycle methods
85
+ * plus `role_grant_revoke` and the immediate `role_grant_assign`).
96
86
  *
97
87
  * @param deps - `RouteFactoryDeps` (`log`, `audit`, …) plus optional `notification_sender` for WS fan-out — when absent, WS fan-out is silently skipped (DB-only side effects still happen). Consumers wiring `BackendWebsocketTransport` assign its instance directly (the transport's `send_to_account` signature accepts the broader `JsonrpcMessageFromServerToClient`, which is contravariantly compatible)
98
88
  * @param options - role schema, default TTL, authorization override
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA0B1B,OAAO,EAA4C,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACpG,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAiC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AA6BD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,EAAE,6BAavC,CAAC;AAIF;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG;IAC/C,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,CAodjB,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAIH,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,EAA4C,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACpG,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAoC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AAgCD;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG;IAC/C,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,CAkiBjB,CAAC"}
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Role grant offer RPC action handlers — the consentful-role-grants action surface.
3
3
  *
4
- * Seven actions: six offer-lifecycle methods (create / accept / decline /
5
- * retract / list / history) plus `role_grant_revoke` (admin-only). All mount
4
+ * Eight actions: six offer-lifecycle methods (create / accept / decline /
5
+ * retract / list / history) plus `role_grant_revoke` and `role_grant_assign`
6
+ * (both admin-only). All mount
6
7
  * on a consumer's JSON-RPC endpoint via `create_rpc_endpoint`. The action
7
8
  * specs themselves live in `auth/role_grant_offer_action_specs.ts`. Mutations
8
9
  * declare `side_effects: true` so the RPC dispatcher wraps the handler in
@@ -23,6 +24,10 @@
23
24
  * dispatcher rejects non-admin callers before the handler runs.
24
25
  * The admin-grant-path gate prevents revoking keeper / daemon-scoped
25
26
  * roles via this surface. Keys on `actor_id` to survive multi-actor accounts.
27
+ * - `role_grant_assign` — spec-level `auth: {role: 'admin'}`; the immediate
28
+ * consent-free conferral path. The admin-grant-path gate prevents assigning
29
+ * keeper / daemon-scoped roles via this surface. No holder-propagation — only
30
+ * admins confer (see participation-gates.md Decision 7).
26
31
  *
27
32
  * Audit events are emitted in-transaction by the query layer (atomic with
28
33
  * the role_grant write on accept/revoke) or by the handler via the bound
@@ -44,12 +49,12 @@ import { builtin_role_specs_by_name, ROLE_ADMIN, role_has_grant_path, } from './
44
49
  import { GRANT_PATH_ADMIN } from './grant_path_schema.js';
45
50
  import { ROLE_GRANT_OFFER_DEFAULT_TTL_MS, to_role_grant_offer_json, } from './role_grant_offer_schema.js';
46
51
  import { query_role_grant_offer_create, query_role_grant_offer_decline, query_role_grant_offer_retract, query_role_grant_offer_list, query_role_grant_offer_history_for_account, query_accept_offer, RoleGrantOfferActorAccountMismatchError, RoleGrantOfferActorMismatchError, RoleGrantOfferAlreadyTerminalError, RoleGrantOfferExpiredError, RoleGrantOfferNotFoundError, RoleGrantOfferSelfTargetError, } from './role_grant_offer_queries.js';
47
- import { query_role_grant_find_active_role_for_actor, query_revoke_role_grant, } from './role_grant_queries.js';
48
- import { query_actor_by_id } from './account_queries.js';
52
+ import { query_create_role_grant, query_role_grant_find_active_role_for_actor, query_revoke_role_grant, } from './role_grant_queries.js';
53
+ import { query_actor_by_id, query_active_actors_by_account } from './account_queries.js';
49
54
  import { has_scoped_role } from './request_context.js';
50
55
  import { build_role_grant_offer_accepted_notification, build_role_grant_offer_declined_notification, build_role_grant_offer_received_notification, build_role_grant_offer_retracted_notification, build_role_grant_offer_supersede_notification, build_role_grant_revoke_notification, } from './role_grant_offer_notifications.js';
51
56
  import { ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE } from '../http/error_schemas.js';
52
- import { ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_EXPIRED, ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE, ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL, role_grant_offer_create_action_spec, role_grant_offer_accept_action_spec, role_grant_offer_decline_action_spec, role_grant_offer_retract_action_spec, role_grant_offer_list_action_spec, role_grant_offer_history_action_spec, role_grant_revoke_action_spec, } from './role_grant_offer_action_specs.js';
57
+ import { ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_EXPIRED, ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE, ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL, role_grant_offer_create_action_spec, role_grant_offer_accept_action_spec, role_grant_offer_decline_action_spec, role_grant_offer_retract_action_spec, role_grant_offer_list_action_spec, role_grant_offer_history_action_spec, role_grant_revoke_action_spec, role_grant_assign_action_spec, } from './role_grant_offer_action_specs.js';
53
58
  // -- Helpers ----------------------------------------------------------------
54
59
  /**
55
60
  * Fan out a batch of pre-written audit rows to the bound emitter's
@@ -67,40 +72,18 @@ const fan_out_audit_events = (events, audit) => {
67
72
  }
68
73
  };
69
74
  // eslint-disable-next-line @typescript-eslint/require-await
70
- const default_authorize = async (auth, input, _deps, _ctx) => {
71
- // Caller must hold an active role_grant for the offered role. Global (no scope)
72
- // check — the scope-aware "only in this classroom" policy is consumer-level.
73
- // Reads from the in-memory `auth.role_grants` snapshot loaded once per request
74
- // by `create_request_context_middleware`; no DB roundtrip needed.
75
- return has_scoped_role(auth, input.role, null);
76
- };
77
- /**
78
- * Authorization callback that admits any admin and otherwise falls back to
79
- * the symmetric default (caller must hold the offered role globally).
80
- *
81
- * The admin-grant-path filter in `create_handler` runs **before** the
82
- * `authorize` callback, so this never sees roles whose `grant_paths`
83
- * omits `'admin'`. Drop into
84
- * `create_role_grant_offer_actions({authorize: authorize_admin_or_holder})`
85
- * (or any factory that forwards `authorize`, e.g. `create_standard_rpc_actions`)
86
- * for the common "admins offer anything; users offer what they hold"
87
- * pattern. Scope-aware policies (e.g. classroom_teacher offering
88
- * classroom_student in their own scope) wrap this and short-circuit `true`
89
- * before delegating.
90
- */
91
- export const authorize_admin_or_holder = async (auth, input, _deps, _ctx) => {
92
- // Admin bypass keys on **global** admin role_grants only — `has_scoped_role(_, _, null)`
93
- // rejects scoped admin role_grants. Without this, a `{role: 'admin', scope_id: scope_X}`
94
- // role_grant would let the holder offer any admin-grant-path role without holding it
95
- // themselves, escalating scoped admin to global authority over the offer surface.
96
- if (has_scoped_role(auth, ROLE_ADMIN, null))
97
- return true;
98
- return has_scoped_role(auth, input.role, null);
75
+ const default_authorize = async (auth, _input, _deps, _ctx) => {
76
+ // Admin-only conferral: the caller must hold a *global* `admin` grant. Holding
77
+ // the offered role itself confers no power to offer it (no holder-propagation —
78
+ // see participation-gates.md Decision 6). The global check (scope_id null) stops
79
+ // a scoped `admin` escalating into global authority over the offer surface.
80
+ // Reads the in-memory `auth.role_grants` snapshot; no DB roundtrip.
81
+ return has_scoped_role(auth, ROLE_ADMIN, null);
99
82
  };
100
83
  // -- Action factory ---------------------------------------------------------
101
84
  /**
102
- * Create the seven role-grant-offer RPC actions (six offer-lifecycle methods
103
- * plus `role_grant_revoke`).
85
+ * Create the eight role-grant-offer RPC actions (six offer-lifecycle methods
86
+ * plus `role_grant_revoke` and the immediate `role_grant_assign`).
104
87
  *
105
88
  * @param deps - `RouteFactoryDeps` (`log`, `audit`, …) plus optional `notification_sender` for WS fan-out — when absent, WS fan-out is silently skipped (DB-only side effects still happen). Consumers wiring `BackendWebsocketTransport` assign its instance directly (the transport's `send_to_account` signature accepts the broader `JsonrpcMessageFromServerToClient`, which is contravariantly compatible)
106
89
  * @param options - role schema, default TTL, authorization override
@@ -464,6 +447,77 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
464
447
  }
465
448
  return { ok: true, revoked: true };
466
449
  };
450
+ // The immediate admin-only conferral path — the consent-free sibling of
451
+ // `role_grant_offer_create`. Admin-gated at the dispatcher (`roles:
452
+ // ['admin']`), it re-checks admin-grantability (so keeper / CLI-only roles
453
+ // can't be web-assigned), resolves the target actor, writes the idempotent
454
+ // grant, and emits a `role_grant_create` audit row — the same event the
455
+ // offer-accept and self-service paths emit. No WS notification in v1 (the
456
+ // grantee picks the capability up on its next authenticated request).
457
+ const assign_handler = async (input, ctx) => {
458
+ const auth = ctx.auth;
459
+ // Admin-grant-path gate — the same registry check the offer/revoke gates
460
+ // use; keeper / daemon-scoped roles stay CLI-only. Denial is
461
+ // forensic-audited (`role_grant_id` omitted — no grant was created).
462
+ if (!role_has_grant_path(role_specs, input.role, GRANT_PATH_ADMIN)) {
463
+ audit.emit_role_grant_target(ctx, auth, {
464
+ event_type: 'role_grant_create',
465
+ outcome: 'failure',
466
+ target_account_id: input.to_account_id,
467
+ target_actor_id: input.to_actor_id ?? null,
468
+ metadata: { role: input.role, scope_id: input.scope_id ?? null },
469
+ });
470
+ throw jsonrpc_errors.forbidden('role not web-grantable', {
471
+ reason: ERROR_ROLE_NOT_WEB_GRANTABLE,
472
+ });
473
+ }
474
+ // Resolve the grant's target actor. A supplied `to_actor_id` must be an
475
+ // active actor of `to_account_id`; otherwise the account's sole active
476
+ // actor is used (a multi-actor account must name one). A missing /
477
+ // actorless account 404-masks. Inlined (not a helper) so the
478
+ // actor-account-mismatch throw stays in the handler body the
479
+ // error_reasons drift scan reads.
480
+ const actors = await query_active_actors_by_account(ctx, input.to_account_id);
481
+ let target_actor_id;
482
+ if (input.to_actor_id != null) {
483
+ if (!actors.some((a) => a.id === input.to_actor_id)) {
484
+ throw jsonrpc_errors.invalid_params('to_actor_id does not belong to to_account_id', {
485
+ reason: ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH,
486
+ });
487
+ }
488
+ target_actor_id = input.to_actor_id;
489
+ }
490
+ else if (actors.length === 1) {
491
+ target_actor_id = actors[0].id;
492
+ }
493
+ else if (actors.length === 0) {
494
+ throw jsonrpc_errors.not_found('account');
495
+ }
496
+ else {
497
+ throw jsonrpc_errors.invalid_params('to_actor_id is required for a multi-actor account');
498
+ }
499
+ // Idempotent write — re-assigning an already-held grant returns the
500
+ // existing row. `scope_kind` stays null: `scope_id` alone is the v1
501
+ // discriminator. `granted_by` is the assigning admin.
502
+ const grant = await query_create_role_grant(ctx, {
503
+ actor_id: target_actor_id,
504
+ role: input.role,
505
+ scope_kind: null,
506
+ scope_id: input.scope_id ?? null,
507
+ granted_by: auth.actor.id,
508
+ });
509
+ audit.emit_role_grant_target(ctx, auth, {
510
+ event_type: 'role_grant_create',
511
+ target_account_id: input.to_account_id,
512
+ target_actor_id,
513
+ metadata: {
514
+ role: grant.role,
515
+ role_grant_id: grant.id,
516
+ scope_id: grant.scope_id,
517
+ },
518
+ });
519
+ return { ok: true, role_grant_id: grant.id };
520
+ };
467
521
  return [
468
522
  rpc_action(role_grant_offer_create_action_spec, create_handler),
469
523
  rpc_action(role_grant_offer_accept_action_spec, accept_handler),
@@ -472,5 +526,6 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
472
526
  rpc_action(role_grant_offer_list_action_spec, list_handler),
473
527
  rpc_action(role_grant_offer_history_action_spec, history_handler),
474
528
  rpc_action(role_grant_revoke_action_spec, revoke_handler),
529
+ rpc_action(role_grant_assign_action_spec, assign_handler),
475
530
  ];
476
531
  };
@@ -1452,6 +1452,44 @@ The in-process counterparts already exist (`describe_rate_limiting_tests` plus t
1452
1452
  proxy middleware tests), so there's no in-process leg. `npm run test:cross:security`.
1453
1453
  Cited property: `docs/security.md` §"Rate Limiting" + §"Trusted Proxy / Client IP".
1454
1454
 
1455
+ ### Role-gated participation parity — `cross_backend/role_grant_participation.ts`
1456
+
1457
+ The role-gated-participation capstone — TS↔Rust agreement that an **app-defined
1458
+ role is conferred identically** on both spines. A `participant`
1459
+ (`grant_paths: ['admin']`) app-role is registered on both spines (`spine_roles`
1460
+ in `default_spine_surface.ts` + the Rust `testing_spine_stub`'s `RoleRegistry`
1461
+ and `known_roles`); the cross legs prove the three conferral properties agree.
1462
+
1463
+ **Declarative-max, imperative-as-escape-hatch.** The single-request matrix is
1464
+ declarative (`conformance_participation_cases.ts`, run via
1465
+ `describe_conformance_table_tests`): **(a) grantability** —
1466
+ `role_grant_assign` of `participant` passes the admin-grant-path gate and dies
1467
+ _past_ it (404, nonexistent account) while a non-grantable role (`keeper`,
1468
+ unregistered) is refused _at_ it (403 `role_not_web_grantable`), and the
1469
+ `role_grant_offer_create` grant-path gate matches (403
1470
+ `role_grant_offer_role_not_grantable`); **(b) admin-only conferral** — a
1471
+ non-admin _holder_ of `participant` (and a fresh non-admin) can't offer it (403
1472
+ `role_grant_offer_not_authorized` — no holder-propagation); **(c)
1473
+ `role_grant_assign` is admin-only** — non-admin holder / fresh → 403
1474
+ `insufficient_permissions` (dispatcher gate), anon → 401; plus the
1475
+ `to_actor_id`-must-belong-to-`to_account_id` resolution arm (400
1476
+ `role_grant_offer_actor_account_mismatch`).
1477
+
1478
+ `describe_role_grant_participation_cross_tests` (this module) is the **imperative
1479
+ escape hatch** for the multi-step success paths a static row can't express (they
1480
+ need a real recipient): admin **assigns** `participant` to a fresh account
1481
+ (`{ok, role_grant_id}` + idempotent re-assign), and admin **offers**
1482
+ `participant` → recipient **accepts** → a role_grant lands. The `role_holder`
1483
+ principal is seeded via `extra_accounts: [{username, roles: ['participant']}]`
1484
+ (bootstrap-cradle direct grant) and named through `principals.role_holder`.
1485
+
1486
+ Both legs run in-process (`conformance_participation.db.test.ts`, plain
1487
+ `gro test`) and cross-process (`conformance_participation.cross.test.ts`, TS
1488
+ node/deno/bun + Rust stub). Landing the TS `role_grant_assign` action also turned
1489
+ the `cross_backend_parity` action-manifest gate green (it was Rust-only before).
1490
+ Cited property: `docs/identity.md` + the role-gated-participation design
1491
+ (admin-only conferral, immediate assign).
1492
+
1455
1493
  ### Building a TS test-server binary — `testing_server_core.ts` + adapters
1456
1494
 
1457
1495
  The reusable shape for standing up a **spawnable TS** cross-process test
@@ -36,11 +36,15 @@ import type { AppServerContext } from '../../server/app_server_context.js';
36
36
  */
37
37
  export declare const spine_session_options: SessionOptions<string>;
38
38
  /**
39
- * The spine's closed role registry: built-ins plus `SPINE_CELL_EDITOR_ROLE`.
40
- * Threaded into the cell spec set's role-validity gate; the Rust stub mirrors
41
- * the same membership. When the spine grows additional grantable roles,
42
- * thread their registry through `create_role_schema` here so the admin suite
43
- * picks up grant-path coverage.
39
+ * The spine's closed role registry: built-ins plus two app roles —
40
+ * `SPINE_CELL_EDITOR_ROLE` (no grant path; the role-shaped-`cell_grant`
41
+ * suite's bootstrap-seeded role) and `SPINE_PARTICIPANT_ROLE`
42
+ * (`grant_paths: ['admin']`; the role-gated-participation suite's
43
+ * admin-grantable role). Threaded into the cell spec set's role-validity gate
44
+ * **and** the auth grantability gates; the Rust stub mirrors the same
45
+ * membership in both its `RoleRegistry` and `known_roles`. The `participant`
46
+ * entry also gives the admin suite real app-role grant-path coverage
47
+ * (`admin_account_list.grantable_roles` carries it on both spines).
44
48
  */
45
49
  export declare const spine_roles: RoleSchemaResult;
46
50
  /** Options for {@link spine_rpc_endpoints}. */
@@ -1 +1 @@
1
- {"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAczE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAElG;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,gBAExB,CAAC;AAEH,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,gBAAgB,EACrB,UAAU,wBAAwB,KAChC,KAAK,CAAC,eAAe,CAQvB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,gBAAgB,KAAG,KAAK,CAAC,SAAS,CA+B/E,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,SAC6B,CAAC;AAE3F;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QAAO,cAM1C,CAAC"}
1
+ {"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAEpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAezE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAElG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,EAAE,gBAGxB,CAAC;AAEH,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,gBAAgB,EACrB,UAAU,wBAAwB,KAChC,KAAK,CAAC,eAAe,CAQvB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,gBAAgB,KAAG,KAAK,CAAC,SAAS,CA+B/E,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,SAC6B,CAAC;AAE3F;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QAAO,cAM1C,CAAC"}
@@ -2,6 +2,7 @@ import '../assert_dev_env.js';
2
2
  import { create_account_route_specs } from '../../auth/account_routes.js';
3
3
  import { create_audit_log_route_specs } from '../../auth/audit_log_routes.js';
4
4
  import { create_role_schema } from '../../auth/role_schema.js';
5
+ import { GRANT_PATH_ADMIN } from '../../auth/grant_path_schema.js';
5
6
  import { create_session_config } from '../../auth/session_cookie.js';
6
7
  import { create_signup_route_specs } from '../../auth/signup_routes.js';
7
8
  import { create_standard_rpc_actions } from '../../auth/standard_rpc_actions.js';
@@ -13,21 +14,26 @@ import { create_test_app_surface_spec } from '../stubs.js';
13
14
  // without dragging this module's eager in-process route handlers
14
15
  // (→ `session_middleware` → `hono/cookie`). Import only the constants this
15
16
  // module uses internally; callers that need a bare constant reach for the leaf.
16
- import { SPINE_CELL_EDITOR_ROLE, SPINE_EXPECTED_SCHEMA_URL, SPINE_RPC_PATH, } from './spine_surface_constants.js';
17
+ import { SPINE_CELL_EDITOR_ROLE, SPINE_EXPECTED_SCHEMA_URL, SPINE_PARTICIPANT_ROLE, SPINE_RPC_PATH, } from './spine_surface_constants.js';
17
18
  /**
18
19
  * Session config — cookie name matches the binary's issued session cookie
19
20
  * (`fuz_session`) so cookie-attribute assertions + jar extraction line up.
20
21
  */
21
22
  export const spine_session_options = create_session_config('fuz_session');
22
23
  /**
23
- * The spine's closed role registry: built-ins plus `SPINE_CELL_EDITOR_ROLE`.
24
- * Threaded into the cell spec set's role-validity gate; the Rust stub mirrors
25
- * the same membership. When the spine grows additional grantable roles,
26
- * thread their registry through `create_role_schema` here so the admin suite
27
- * picks up grant-path coverage.
24
+ * The spine's closed role registry: built-ins plus two app roles —
25
+ * `SPINE_CELL_EDITOR_ROLE` (no grant path; the role-shaped-`cell_grant`
26
+ * suite's bootstrap-seeded role) and `SPINE_PARTICIPANT_ROLE`
27
+ * (`grant_paths: ['admin']`; the role-gated-participation suite's
28
+ * admin-grantable role). Threaded into the cell spec set's role-validity gate
29
+ * **and** the auth grantability gates; the Rust stub mirrors the same
30
+ * membership in both its `RoleRegistry` and `known_roles`. The `participant`
31
+ * entry also gives the admin suite real app-role grant-path coverage
32
+ * (`admin_account_list.grantable_roles` carries it on both spines).
28
33
  */
29
34
  export const spine_roles = create_role_schema([
30
35
  { name: SPINE_CELL_EDITOR_ROLE, grant_paths: [] },
36
+ { name: SPINE_PARTICIPANT_ROLE, grant_paths: [GRANT_PATH_ADMIN] },
31
37
  ]);
32
38
  /**
33
39
  * Factory-form RPC endpoints over the per-test `ctx.deps`. `create_app_server`
@@ -0,0 +1,11 @@
1
+ import '../assert_dev_env.js';
2
+ import type { SetupTest } from './setup.js';
3
+ /** Options for the role-gated-participation success-path parity suite. */
4
+ export interface RoleGrantParticipationCrossTestOptions {
5
+ /** Per-test fixture producer (in-process or cross-process). */
6
+ readonly setup_test: SetupTest;
7
+ /** RPC endpoint path. Default `/api/rpc`. */
8
+ readonly rpc_path?: string;
9
+ }
10
+ export declare const describe_role_grant_participation_cross_tests: (options: RoleGrantParticipationCrossTestOptions) => void;
11
+ //# sourceMappingURL=role_grant_participation.d.ts.map
@@ -0,0 +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,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAE1C,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,IA+EF,CAAC"}
@@ -0,0 +1,73 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend parity suite for **role-gated participation conferral — the
4
+ * success paths**. The imperative escape hatch beneath the declarative
5
+ * `conformance_participation_cases.ts`: the multi-step flows that need a real
6
+ * recipient account (so a static conformance row can't express them) — an
7
+ * admin assigns the `participant` app-role, and an admin offers it through the
8
+ * consent flow and the recipient accepts.
9
+ *
10
+ * Proves the two conferral *write* paths agree on both spines (TS spine binary
11
+ * + Rust `testing_spine_stub`):
12
+ *
13
+ * - **immediate assign** — `role_grant_assign` of the admin-grantable
14
+ * `participant` role lands a grant and returns `{ok, role_grant_id}`;
15
+ * re-assigning the active grant is idempotent (same id).
16
+ * - **consent flow** — `role_grant_offer_create` of `participant` (admin-only)
17
+ * → the recipient `role_grant_offer_accept`s → a role_grant lands.
18
+ *
19
+ * The single-request gate/denial matrix (grantability refusal, admin-only
20
+ * conferral, dispatcher admin gate, auth) lives in the declarative table
21
+ * (`conformance_participation_cases.ts`) — keep new single-request assertions
22
+ * there; this suite is only for flows the table cannot carry.
23
+ *
24
+ * The `participant` role is registered admin-grantable on both spines
25
+ * (`spine_roles` / the Rust stub's `RoleRegistry`), so the suite is ungated;
26
+ * every spine mounts the standard RPC surface it drives.
27
+ *
28
+ * `$lib`-free by contract (relative specifiers only), like the sibling
29
+ * cross-backend suites.
30
+ *
31
+ * @module
32
+ */
33
+ import { describe, test, assert } from 'vitest';
34
+ import { rpc_call } from '../rpc_helpers.js';
35
+ import { SPINE_PARTICIPANT_ROLE, SPINE_RPC_PATH } from './spine_surface_constants.js';
36
+ export const describe_role_grant_participation_cross_tests = (options) => {
37
+ const { setup_test } = options;
38
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
39
+ const rpc = (transport, method, params, headers) => rpc_call({ app: transport, path: rpc_path, method, params, headers });
40
+ describe('role-gated participation conferral (success paths)', () => {
41
+ test('admin assigns the participant app-role to a fresh account (idempotent)', async () => {
42
+ const fixture = await setup_test();
43
+ const recipient = await fixture.create_account({ username: 'participation_assignee' });
44
+ // The keeper holds ROLE_ADMIN (seeded via extra_keeper_roles), so it
45
+ // clears the dispatcher admin gate; `participant` is admin-grantable, so
46
+ // it clears the grant-path gate; the recipient is single-actor, so the
47
+ // target actor resolves without `to_actor_id`.
48
+ const first = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role: SPINE_PARTICIPANT_ROLE }, fixture.create_session_headers());
49
+ assert.ok(first.ok, `assign must succeed: ${JSON.stringify(first)}`);
50
+ const assigned = first.result;
51
+ assert.strictEqual(assigned.ok, true);
52
+ assert.ok(typeof assigned.role_grant_id === 'string' && assigned.role_grant_id.length > 0, 'assign returns the new role_grant id');
53
+ // Idempotent — re-assigning the active grant returns the existing id.
54
+ const second = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role: SPINE_PARTICIPANT_ROLE }, fixture.create_session_headers());
55
+ assert.ok(second.ok, `re-assign must succeed: ${JSON.stringify(second)}`);
56
+ assert.strictEqual(second.result.role_grant_id, assigned.role_grant_id, 're-assigning an active grant returns the existing role_grant id');
57
+ });
58
+ test('admin offers the participant app-role and the recipient accepts', async () => {
59
+ const fixture = await setup_test();
60
+ const recipient = await fixture.create_account({ username: 'participation_offeree' });
61
+ // Admin-only conferral via the consent flow: the keeper (admin) offers
62
+ // the app-role, proving the widened grant-path gate admits it through
63
+ // the offer path too.
64
+ const created = await rpc(fixture.transport, 'role_grant_offer_create', { to_account_id: recipient.account.id, role: SPINE_PARTICIPANT_ROLE }, fixture.create_session_headers());
65
+ assert.ok(created.ok, `offer create must succeed: ${JSON.stringify(created)}`);
66
+ const offer_id = created.result.offer.id;
67
+ // The recipient accepts → a role_grant lands.
68
+ const accepted = await rpc(fixture.fresh_transport(), 'role_grant_offer_accept', { offer_id }, recipient.create_session_headers());
69
+ assert.ok(accepted.ok, `accept must succeed: ${JSON.stringify(accepted)}`);
70
+ assert.ok(typeof accepted.result.role_grant_id === 'string', 'accept lands a role_grant for the app-role');
71
+ });
72
+ });
73
+ };
@@ -32,6 +32,18 @@ export declare const SPINE_SSE_PATH = "/api/admin/audit/stream";
32
32
  * Rust `testing_spine_stub`'s `known_roles` (cross-language test contract).
33
33
  */
34
34
  export declare const SPINE_CELL_EDITOR_ROLE = "cell_editor";
35
+ /**
36
+ * Admin-grantable app role the role-gated-participation cross suite exercises.
37
+ * Registered with `grant_paths: ['admin']` so it enters the admin grant flow —
38
+ * the cross-backend proof that an app-defined role is conferrable (offer /
39
+ * `role_grant_assign`) admin-only on **both** spines. Must match the
40
+ * `participant` entry in the Rust `testing_spine_stub`'s `RoleRegistry`
41
+ * **and** its `known_roles` (the registry feeds the cell vocabulary too) — a
42
+ * cross-language test contract. Distinct from `SPINE_CELL_EDITOR_ROLE`
43
+ * (no grant path): this one is the *grantable* role, that one is the
44
+ * bootstrap-seed-only cell role.
45
+ */
46
+ export declare const SPINE_PARTICIPANT_ROLE = "participant";
35
47
  /**
36
48
  * Committed expected-schema fixture for the spine `/ready` deploy gate — the
37
49
  * column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
@@ -1 +1 @@
1
- {"version":3,"file":"spine_surface_constants.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spine_surface_constants.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;GAcG;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC"}
1
+ {"version":3,"file":"spine_surface_constants.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spine_surface_constants.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;GAcG;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC"}
@@ -32,6 +32,18 @@ export const SPINE_SSE_PATH = '/api/admin/audit/stream';
32
32
  * Rust `testing_spine_stub`'s `known_roles` (cross-language test contract).
33
33
  */
34
34
  export const SPINE_CELL_EDITOR_ROLE = 'cell_editor';
35
+ /**
36
+ * Admin-grantable app role the role-gated-participation cross suite exercises.
37
+ * Registered with `grant_paths: ['admin']` so it enters the admin grant flow —
38
+ * the cross-backend proof that an app-defined role is conferrable (offer /
39
+ * `role_grant_assign`) admin-only on **both** spines. Must match the
40
+ * `participant` entry in the Rust `testing_spine_stub`'s `RoleRegistry`
41
+ * **and** its `known_roles` (the registry feeds the cell vocabulary too) — a
42
+ * cross-language test contract. Distinct from `SPINE_CELL_EDITOR_ROLE`
43
+ * (no grant path): this one is the *grantable* role, that one is the
44
+ * bootstrap-seed-only cell role.
45
+ */
46
+ export const SPINE_PARTICIPANT_ROLE = 'participant';
35
47
  /**
36
48
  * Committed expected-schema fixture for the spine `/ready` deploy gate — the
37
49
  * column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.101.1",
3
+ "version": "0.102.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",