@fuzdev/fuz_app 0.101.0 → 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.
- package/dist/auth/CLAUDE.md +14 -11
- package/dist/auth/role_grant_offer_action_specs.d.ts +56 -3
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +48 -3
- package/dist/auth/role_grant_offer_actions.d.ts +9 -19
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +91 -36
- package/dist/db/status.d.ts +42 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +48 -5
- package/dist/testing/CLAUDE.md +38 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +9 -5
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +12 -6
- package/dist/testing/cross_backend/role_grant_participation.d.ts +11 -0
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_participation.js +73 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +12 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +12 -0
- package/package.json +1 -1
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
|
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-
|
|
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
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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 —
|
|
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 (
|
|
629
|
-
|
|
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":"AAAA
|
|
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,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
|
-
*
|
|
5
|
-
* retract / list / history) plus `role_grant_revoke`
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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
|
|
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
|
-
*
|
|
5
|
-
* retract / list / history) plus `role_grant_revoke`
|
|
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,
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
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
|
|
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
|
};
|
package/dist/db/status.d.ts
CHANGED
|
@@ -4,10 +4,41 @@
|
|
|
4
4
|
* Queries migration state and table info without a running server.
|
|
5
5
|
* Returns structured data that consumer scripts can print however they like.
|
|
6
6
|
*
|
|
7
|
+
* The migration check is **name-divergence aware**: it name-prefix-verifies the
|
|
8
|
+
* applied migrations against the code's list (mirroring `run_migrations`), so a
|
|
9
|
+
* divergent history (same count, different names) reports a `divergence` and
|
|
10
|
+
* renders `DIVERGED` rather than a false `up_to_date`.
|
|
11
|
+
*
|
|
7
12
|
* @module
|
|
8
13
|
*/
|
|
9
14
|
import type { Db } from './db.js';
|
|
10
15
|
import type { MigrationNamespace } from './migrate.js';
|
|
16
|
+
/**
|
|
17
|
+
* A divergence between the recorded migration tracker and the code's list.
|
|
18
|
+
*
|
|
19
|
+
* Either variant is a state the migration runner refuses to boot against — a
|
|
20
|
+
* re-bootstrap (drop + migrate) is needed. Structured (not a pre-formatted
|
|
21
|
+
* string) so programmatic consumers can branch on `kind`; `format_db_status`
|
|
22
|
+
* renders the operator-facing line. The discriminated-union twin of the Rust
|
|
23
|
+
* `fuz_db` `Divergence` enum.
|
|
24
|
+
*/
|
|
25
|
+
export type Divergence = {
|
|
26
|
+
/** An applied name doesn't match the code's name at `position`. */
|
|
27
|
+
kind: 'name_mismatch';
|
|
28
|
+
/** Sequence position of the first mismatch. */
|
|
29
|
+
position: number;
|
|
30
|
+
/** The name recorded in the tracker at `position`. */
|
|
31
|
+
applied: string;
|
|
32
|
+
/** The name the code declares at `position`. */
|
|
33
|
+
expected: string;
|
|
34
|
+
} | {
|
|
35
|
+
/** The tracker records more migrations than the code declares. */
|
|
36
|
+
kind: 'binary_older';
|
|
37
|
+
/** Count recorded in the tracker. */
|
|
38
|
+
applied: number;
|
|
39
|
+
/** Count the code declares. */
|
|
40
|
+
declared: number;
|
|
41
|
+
};
|
|
11
42
|
/**
|
|
12
43
|
* Migration status for a single namespace.
|
|
13
44
|
*/
|
|
@@ -17,8 +48,18 @@ export interface MigrationStatus {
|
|
|
17
48
|
applied_names: Array<string>;
|
|
18
49
|
/** Names of code migrations not yet applied (suffix of the code array). */
|
|
19
50
|
pending_names: Array<string>;
|
|
20
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Whether `applied_names` is the full code array with no name divergence
|
|
53
|
+
* (no pending work, no diverged history).
|
|
54
|
+
*/
|
|
21
55
|
up_to_date: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The first applied/code divergence, if any. Absent when the applied names
|
|
58
|
+
* are a clean prefix of the code's list (the only state the runner boots
|
|
59
|
+
* against). Present means a divergent bootstrap history — a re-bootstrap
|
|
60
|
+
* (drop + migrate) is needed.
|
|
61
|
+
*/
|
|
62
|
+
divergence?: Divergence;
|
|
22
63
|
}
|
|
23
64
|
/**
|
|
24
65
|
* Table info with row count.
|
package/dist/db/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/status.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAErD;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GACnB;IACA,mEAAmE;IACnE,IAAI,EAAE,eAAe,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;CAChB,GACD;IACA,kEAAkE;IAClE,IAAI,EAAE,cAAc,CAAC;IACrB,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAChB,CAAC;AAEL;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,2EAA2E;IAC3E,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,yCAAyC;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAC3B,sCAAsC;IACtC,UAAU,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IACnC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC5B;AA8BD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC3B,IAAI,EAAE,EACN,aAAa,KAAK,CAAC,kBAAkB,CAAC,KACpC,OAAO,CAAC,QAAQ,CA2GlB,CAAC;AAeF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,QAAQ,KAAG,MAoDnD,CAAC"}
|
package/dist/db/status.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Queries migration state and table info without a running server.
|
|
5
5
|
* Returns structured data that consumer scripts can print however they like.
|
|
6
6
|
*
|
|
7
|
+
* The migration check is **name-divergence aware**: it name-prefix-verifies the
|
|
8
|
+
* applied migrations against the code's list (mirroring `run_migrations`), so a
|
|
9
|
+
* divergent history (same count, different names) reports a `divergence` and
|
|
10
|
+
* renders `DIVERGED` rather than a false `up_to_date`.
|
|
11
|
+
*
|
|
7
12
|
* @module
|
|
8
13
|
*/
|
|
9
14
|
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
@@ -78,14 +83,37 @@ export const query_db_status = async (db, namespaces) => {
|
|
|
78
83
|
ORDER BY sequence ASC`, [namespace]);
|
|
79
84
|
const applied_names = rows.map((r) => r.name);
|
|
80
85
|
const code_names = ns_migrations.map((m) => m.name);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
const total = code_names.length;
|
|
87
|
+
const applied = applied_names.length;
|
|
88
|
+
// Name-prefix verify, mirroring the migration runner: the applied
|
|
89
|
+
// names must equal the first `applied` code names by position. A
|
|
90
|
+
// divergent history (a renamed/reordered migration, same count or not)
|
|
91
|
+
// is the exact state `run_migrations` refuses to boot against, so a
|
|
92
|
+
// count-only check would report a DB the runner rejects as up-to-date.
|
|
93
|
+
let divergence;
|
|
94
|
+
if (applied > total) {
|
|
95
|
+
divergence = { kind: 'binary_older', applied, declared: total };
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const position = applied_names.findIndex((name, i) => name !== code_names[i]);
|
|
99
|
+
if (position !== -1) {
|
|
100
|
+
divergence = {
|
|
101
|
+
kind: 'name_mismatch',
|
|
102
|
+
position,
|
|
103
|
+
applied: applied_names[position],
|
|
104
|
+
expected: code_names[position],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// pending = the suffix of code names past the applied count (clamped:
|
|
109
|
+
// a binary-older history has no pending tail)
|
|
110
|
+
const pending_names = code_names.slice(Math.min(applied, total));
|
|
84
111
|
migrations.push({
|
|
85
112
|
namespace,
|
|
86
113
|
applied_names,
|
|
87
114
|
pending_names,
|
|
88
|
-
up_to_date:
|
|
115
|
+
up_to_date: applied === total && pending_names.length === 0 && divergence === undefined,
|
|
116
|
+
...(divergence ? { divergence } : {}),
|
|
89
117
|
});
|
|
90
118
|
}
|
|
91
119
|
}
|
|
@@ -110,6 +138,18 @@ export const query_db_status = async (db, namespaces) => {
|
|
|
110
138
|
...(old_tracker_shape ? { old_tracker_shape: true } : {}),
|
|
111
139
|
};
|
|
112
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* Render a `Divergence` as the operator-facing detail line. Twin of the Rust
|
|
143
|
+
* `Divergence` `Display`.
|
|
144
|
+
*/
|
|
145
|
+
const format_divergence = (divergence) => {
|
|
146
|
+
switch (divergence.kind) {
|
|
147
|
+
case 'name_mismatch':
|
|
148
|
+
return `position ${divergence.position}: database has '${divergence.applied}', code has '${divergence.expected}'`;
|
|
149
|
+
case 'binary_older':
|
|
150
|
+
return `database has ${divergence.applied} applied but code declares ${divergence.declared} (binary older than database)`;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
113
153
|
/**
|
|
114
154
|
* Format a `DbStatus` as a human-readable string for CLI output.
|
|
115
155
|
*
|
|
@@ -141,7 +181,10 @@ export const format_db_status = (status) => {
|
|
|
141
181
|
lines.push(' Migrations:');
|
|
142
182
|
for (const m of status.migrations) {
|
|
143
183
|
const total = m.applied_names.length + m.pending_names.length;
|
|
144
|
-
if (m.
|
|
184
|
+
if (m.divergence) {
|
|
185
|
+
lines.push(` ${m.namespace}: DIVERGED (${m.applied_names.length}/${total}) — ${format_divergence(m.divergence)}`);
|
|
186
|
+
}
|
|
187
|
+
else if (m.up_to_date) {
|
|
145
188
|
lines.push(` ${m.namespace}: up to date (${m.applied_names.length}/${total})`);
|
|
146
189
|
}
|
|
147
190
|
else {
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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;
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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)
|