@fuzdev/fuz_app 0.101.1 → 0.103.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 +29 -17
- package/dist/auth/cell_action_specs.d.ts +35 -34
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +23 -8
- package/dist/auth/cell_actions.d.ts +28 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +52 -20
- package/dist/auth/cell_data_schema.d.ts +14 -16
- package/dist/auth/cell_data_schema.d.ts.map +1 -1
- package/dist/auth/cell_data_schema.js +14 -16
- 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/CLAUDE.md +8 -5
- package/dist/db/cell_ddl.d.ts +11 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +13 -1
- package/dist/db/cell_queries.d.ts +14 -8
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +12 -11
- package/dist/testing/CLAUDE.md +66 -7
- package/dist/testing/cross_backend/capabilities.d.ts +14 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +3 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +55 -13
- package/dist/testing/cross_backend/cell_gated_create.d.ts +4 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_gated_create.js +71 -0
- package/dist/testing/cross_backend/cell_grant_role.js +4 -4
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +30 -24
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +7 -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/expected_schema.json +1 -0
- package/dist/testing/cross_backend/fact_serving.js +2 -2
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +6 -1
- 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/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
- 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/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +17 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +36 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
- package/package.json +1 -1
|
@@ -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/CLAUDE.md
CHANGED
|
@@ -51,16 +51,19 @@ The wire schemas + RPC handlers + authz predicates for this layer live in
|
|
|
51
51
|
tables; single-migration `CELL_MIGRATION_NS` (namespace `fuz_cell`).
|
|
52
52
|
`cell.visibility cell_visibility NOT NULL DEFAULT 'private'` (PG ENUM
|
|
53
53
|
`('private', 'public')`) — a top-level access-control column, peer to
|
|
54
|
-
`cell_grant`, not a `data` field.
|
|
55
|
-
|
|
56
|
-
partial-
|
|
54
|
+
`cell_grant`, not a `data` field. `cell.kind text` (nullable) is the
|
|
55
|
+
write-once capability/identity axis — likewise a top-level column, not a
|
|
56
|
+
`data` field (`idx_cell_kind` partial-active). Nullable `created_by` /
|
|
57
|
+
`updated_by` FKs to `actor` (NULL = system origin). GIN on `data` / `refs`;
|
|
58
|
+
global partial-unique on `path`.
|
|
57
59
|
- **`cell_history_ddl.ts`** — dormant `cell_history` table
|
|
58
60
|
(`CELL_HISTORY_MIGRATION_NS`, namespace `fuz_cell_history`), FK → `cell.id`.
|
|
59
61
|
Ships present-but-unwritten; no snapshot lifecycle yet.
|
|
60
62
|
- **`cell_queries.ts`** — `query_cell_create / get / get_by_path / update /
|
|
61
|
-
delete`, `
|
|
63
|
+
delete`, `_list_by_kind / _list_by_creator`, the
|
|
62
64
|
generic `query_cell_list` (filter + SQL-side visibility predicate mirroring
|
|
63
|
-
`can_view_cell`; the `
|
|
65
|
+
`can_view_cell`; the `kind` filter narrows by the `cell.kind` column, the
|
|
66
|
+
`ref` filter by `cell.refs`), and
|
|
64
67
|
`query_cell_load_many` (bulk id load, no visibility filter — feeds the
|
|
65
68
|
strict relation-read filter). `cell.refs` derived from
|
|
66
69
|
`data` via `extract_refs` on create/update. `CellRow.grant_count` is a
|
package/dist/db/cell_ddl.d.ts
CHANGED
|
@@ -62,8 +62,15 @@ export declare const CELL_VISIBILITY_TYPE = "\nDO $$ BEGIN\n\tCREATE TYPE cell_v
|
|
|
62
62
|
*
|
|
63
63
|
* `path` is the global namespace axis (no tenant/hub scoping) — globally
|
|
64
64
|
* unique on active rows via `idx_cell_path_unique`.
|
|
65
|
+
*
|
|
66
|
+
* `kind` is the capability / identity axis — a nullable top-level column
|
|
67
|
+
* (peer to `visibility` / `path`), **not** a field inside `data`. It is the
|
|
68
|
+
* discriminator a creation authorizer gates on (see `auth/cell_actions.ts`
|
|
69
|
+
* `CellCreateAuthorize`) and is **write-once**: set at INSERT and carried on
|
|
70
|
+
* no update path, so a cell's kind is fixed at birth. Content stays
|
|
71
|
+
* duck-typed in `data`; `kind` is a capability tag, not a content-type.
|
|
65
72
|
*/
|
|
66
|
-
export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n\tdata JSONB NOT NULL,\n\tvisibility cell_visibility NOT NULL DEFAULT 'private',\n\tpath TEXT,\n\trefs TEXT[],\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tupdated_at TIMESTAMPTZ,\n\tdeleted_at TIMESTAMPTZ,\n\tcreated_by UUID REFERENCES actor(id) ON DELETE SET NULL,\n\tupdated_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
|
|
73
|
+
export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n\tdata JSONB NOT NULL,\n\tkind TEXT,\n\tvisibility cell_visibility NOT NULL DEFAULT 'private',\n\tpath TEXT,\n\trefs TEXT[],\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tupdated_at TIMESTAMPTZ,\n\tdeleted_at TIMESTAMPTZ,\n\tcreated_by UUID REFERENCES actor(id) ON DELETE SET NULL,\n\tupdated_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
|
|
67
74
|
/**
|
|
68
75
|
* Cell indexes — all active-only, partial on `deleted_at IS NULL`.
|
|
69
76
|
*
|
|
@@ -71,7 +78,9 @@ export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UU
|
|
|
71
78
|
* - `idx_cell_path_unique`: global `path` uniqueness + read-side path
|
|
72
79
|
* lookup. Partial on path + active so reused paths after soft delete are
|
|
73
80
|
* allowed.
|
|
74
|
-
* - `
|
|
81
|
+
* - `idx_cell_kind`: the `cell_list` kind filter (`cell.kind = ?`) and
|
|
82
|
+
* kind-scoped scans. Active-only.
|
|
83
|
+
* - `idx_cell_data`: shape-driven queries (`data @> ...`).
|
|
75
84
|
* - `idx_cell_refs`: cells-by-fact discovery (cross-cell reference graph).
|
|
76
85
|
* - `idx_cell_created_by`: "cells this actor created" queries.
|
|
77
86
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER
|
|
1
|
+
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,0bAatB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CActC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,0iBAc5B,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAW5C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,+RAO5B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAE5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,qSAO3B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAG3C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,4DAA6D,CAAC;AAE3F,uBAAuB;AACvB,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuB5C,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/db/cell_ddl.js
CHANGED
|
@@ -65,11 +65,19 @@ END $$`;
|
|
|
65
65
|
*
|
|
66
66
|
* `path` is the global namespace axis (no tenant/hub scoping) — globally
|
|
67
67
|
* unique on active rows via `idx_cell_path_unique`.
|
|
68
|
+
*
|
|
69
|
+
* `kind` is the capability / identity axis — a nullable top-level column
|
|
70
|
+
* (peer to `visibility` / `path`), **not** a field inside `data`. It is the
|
|
71
|
+
* discriminator a creation authorizer gates on (see `auth/cell_actions.ts`
|
|
72
|
+
* `CellCreateAuthorize`) and is **write-once**: set at INSERT and carried on
|
|
73
|
+
* no update path, so a cell's kind is fixed at birth. Content stays
|
|
74
|
+
* duck-typed in `data`; `kind` is a capability tag, not a content-type.
|
|
68
75
|
*/
|
|
69
76
|
export const CELL_SCHEMA = `
|
|
70
77
|
CREATE TABLE IF NOT EXISTS cell (
|
|
71
78
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
72
79
|
data JSONB NOT NULL,
|
|
80
|
+
kind TEXT,
|
|
73
81
|
visibility cell_visibility NOT NULL DEFAULT 'private',
|
|
74
82
|
path TEXT,
|
|
75
83
|
refs TEXT[],
|
|
@@ -86,7 +94,9 @@ CREATE TABLE IF NOT EXISTS cell (
|
|
|
86
94
|
* - `idx_cell_path_unique`: global `path` uniqueness + read-side path
|
|
87
95
|
* lookup. Partial on path + active so reused paths after soft delete are
|
|
88
96
|
* allowed.
|
|
89
|
-
* - `
|
|
97
|
+
* - `idx_cell_kind`: the `cell_list` kind filter (`cell.kind = ?`) and
|
|
98
|
+
* kind-scoped scans. Active-only.
|
|
99
|
+
* - `idx_cell_data`: shape-driven queries (`data @> ...`).
|
|
90
100
|
* - `idx_cell_refs`: cells-by-fact discovery (cross-cell reference graph).
|
|
91
101
|
* - `idx_cell_created_by`: "cells this actor created" queries.
|
|
92
102
|
*
|
|
@@ -99,6 +109,8 @@ export const CELL_INDEXES = [
|
|
|
99
109
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_cell_path_unique
|
|
100
110
|
ON cell(path)
|
|
101
111
|
WHERE path IS NOT NULL AND deleted_at IS NULL`,
|
|
112
|
+
`CREATE INDEX IF NOT EXISTS idx_cell_kind ON cell(kind)
|
|
113
|
+
WHERE deleted_at IS NULL`,
|
|
102
114
|
`CREATE INDEX IF NOT EXISTS idx_cell_data ON cell USING gin(data)
|
|
103
115
|
WHERE deleted_at IS NULL`,
|
|
104
116
|
`CREATE INDEX IF NOT EXISTS idx_cell_refs ON cell USING gin(refs)
|
|
@@ -42,6 +42,7 @@ import type { CellVisibility } from '../auth/cell_action_specs.js';
|
|
|
42
42
|
export interface CellRow {
|
|
43
43
|
id: Uuid;
|
|
44
44
|
data: CellData;
|
|
45
|
+
kind: string | null;
|
|
45
46
|
visibility: CellVisibility;
|
|
46
47
|
path: string | null;
|
|
47
48
|
refs: Array<FactHash> | null;
|
|
@@ -52,9 +53,14 @@ export interface CellRow {
|
|
|
52
53
|
updated_by: Uuid | null;
|
|
53
54
|
grant_count: number;
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Input for `query_cell_create`. `refs` is derived from `data`. `kind` is
|
|
58
|
+
* the write-once capability axis (`cell.kind` column) — set here at INSERT
|
|
59
|
+
* and absent from `CellUpdatePatch`, so it can never change post-create.
|
|
60
|
+
*/
|
|
56
61
|
export interface CellCreateQueryInput {
|
|
57
62
|
data: Json;
|
|
63
|
+
kind?: string | null;
|
|
58
64
|
visibility?: CellVisibility;
|
|
59
65
|
path?: string | null;
|
|
60
66
|
created_by?: Uuid | null;
|
|
@@ -153,15 +159,15 @@ export declare const query_cell_delete: (deps: QueryDeps, id: Uuid, options?: {
|
|
|
153
159
|
deleted_by?: Uuid | null;
|
|
154
160
|
}) => Promise<boolean>;
|
|
155
161
|
/**
|
|
156
|
-
* List cells
|
|
157
|
-
*
|
|
162
|
+
* List active cells with the given `kind`, newest first. Uses the
|
|
163
|
+
* `idx_cell_kind` index (`cell.kind = ?`).
|
|
158
164
|
*
|
|
159
165
|
* @param deps - query deps
|
|
160
|
-
* @param kind - `
|
|
166
|
+
* @param kind - `cell.kind` value to match (e.g. `'collection'`, `'entry'`)
|
|
161
167
|
* @param options - pagination
|
|
162
168
|
* @returns matching active rows
|
|
163
169
|
*/
|
|
164
|
-
export declare const
|
|
170
|
+
export declare const query_cell_list_by_kind: (deps: QueryDeps, kind: string, options?: Pick<CellListOptions, "limit" | "offset">) => Promise<Array<CellRow>>;
|
|
165
171
|
/**
|
|
166
172
|
* List active cells created by an actor, newest first. Backed by the
|
|
167
173
|
* `idx_cell_created_by` partial index.
|
|
@@ -221,8 +227,8 @@ export declare const query_cell_list_by_creator: (deps: QueryDeps, actor_id: Uui
|
|
|
221
227
|
export declare const query_cell_list: (deps: QueryDeps, params: CellListParams) => Promise<Array<CellRow>>;
|
|
222
228
|
/** Parameters for `query_cell_list`. All filter dimensions are optional. */
|
|
223
229
|
export interface CellListParams {
|
|
224
|
-
/** Match `
|
|
225
|
-
|
|
230
|
+
/** Match `cell.kind = ?` (uses `idx_cell_kind`). */
|
|
231
|
+
kind?: string;
|
|
226
232
|
/**
|
|
227
233
|
* Match `cell.visibility = ?` directly on the top-level column.
|
|
228
234
|
* Additional narrowing on top of the SQL-side auth visibility
|
|
@@ -286,7 +292,7 @@ export interface CellListParams {
|
|
|
286
292
|
/**
|
|
287
293
|
* When `true`, narrow to cells admitting the caller via a
|
|
288
294
|
* `cell_grant` row AND that the caller does not own. Authenticated
|
|
289
|
-
* only (`viewer_actor_id` must be set). Combine with `
|
|
295
|
+
* only (`viewer_actor_id` must be set). Combine with `kind` /
|
|
290
296
|
* `path_prefix` etc. to scope further.
|
|
291
297
|
*/
|
|
292
298
|
shared_with_caller_only?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,0BAA0B,CAAC;AACnD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAyB,KAAK,QAAQ,EAAC,MAAM,+BAA+B,CAAC;AAGpF,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,6BAA6B,CAAC;AAC1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAEjE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,cAAc,CAAC;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,IAAI,CAAC;IACjB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACpB;AAgBD
|
|
1
|
+
{"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
|
package/dist/db/cell_queries.js
CHANGED
|
@@ -51,10 +51,11 @@ const grant_count_projection = (cell_alias) => `(SELECT COUNT(*)::int FROM cell_
|
|
|
51
51
|
export const query_cell_create = async (deps, input) => {
|
|
52
52
|
const refs = derive_refs(input.data);
|
|
53
53
|
const row = await deps.db.query_one(`INSERT INTO cell
|
|
54
|
-
(data, visibility, path, refs, created_by)
|
|
55
|
-
VALUES ($1::jsonb, COALESCE($
|
|
54
|
+
(data, kind, visibility, path, refs, created_by)
|
|
55
|
+
VALUES ($1::jsonb, $2, COALESCE($3::cell_visibility, 'private'::cell_visibility), $4, $5::text[], $6)
|
|
56
56
|
RETURNING *, ${grant_count_projection('cell')}`, [
|
|
57
57
|
JSON.stringify(input.data),
|
|
58
|
+
input.kind ?? null,
|
|
58
59
|
input.visibility ?? null,
|
|
59
60
|
input.path ?? null,
|
|
60
61
|
refs,
|
|
@@ -171,20 +172,20 @@ export const query_cell_delete = async (deps, id, options) => {
|
|
|
171
172
|
return row !== undefined;
|
|
172
173
|
};
|
|
173
174
|
/**
|
|
174
|
-
* List cells
|
|
175
|
-
*
|
|
175
|
+
* List active cells with the given `kind`, newest first. Uses the
|
|
176
|
+
* `idx_cell_kind` index (`cell.kind = ?`).
|
|
176
177
|
*
|
|
177
178
|
* @param deps - query deps
|
|
178
|
-
* @param kind - `
|
|
179
|
+
* @param kind - `cell.kind` value to match (e.g. `'collection'`, `'entry'`)
|
|
179
180
|
* @param options - pagination
|
|
180
181
|
* @returns matching active rows
|
|
181
182
|
*/
|
|
182
|
-
export const
|
|
183
|
+
export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
|
|
183
184
|
FROM cell
|
|
184
|
-
WHERE
|
|
185
|
+
WHERE kind = $1
|
|
185
186
|
AND deleted_at IS NULL
|
|
186
187
|
ORDER BY created_at DESC
|
|
187
|
-
LIMIT $2 OFFSET $3`, [
|
|
188
|
+
LIMIT $2 OFFSET $3`, [kind, options?.limit ?? null, options?.offset ?? 0]);
|
|
188
189
|
/**
|
|
189
190
|
* List active cells created by an actor, newest first. Backed by the
|
|
190
191
|
* `idx_cell_created_by` partial index.
|
|
@@ -284,7 +285,7 @@ export const query_cell_list = async (deps, params) => {
|
|
|
284
285
|
: build_general_sql(order_column, order_direction);
|
|
285
286
|
return deps.db.query(sql, [
|
|
286
287
|
params.include_deleted === true,
|
|
287
|
-
params.
|
|
288
|
+
params.kind ?? null,
|
|
288
289
|
params.ref ?? null,
|
|
289
290
|
params.created_by ?? null,
|
|
290
291
|
params.path_prefix ?? null,
|
|
@@ -333,7 +334,7 @@ const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
|
|
|
333
334
|
const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
334
335
|
SELECT c.*, ${grant_count_projection('c')} FROM cell c
|
|
335
336
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
336
|
-
AND ($2::text IS NULL OR c.
|
|
337
|
+
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
337
338
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
338
339
|
AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
|
|
339
340
|
AND ($4::uuid IS NULL OR c.created_by = $4)
|
|
@@ -369,7 +370,7 @@ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRAN
|
|
|
369
370
|
const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
370
371
|
SELECT c.*, ${grant_count_projection('c')} FROM cell c
|
|
371
372
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
372
|
-
AND ($2::text IS NULL OR c.
|
|
373
|
+
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
373
374
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
374
375
|
AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
|
|
375
376
|
AND ($4::uuid IS NULL OR c.created_by = $4)
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -897,8 +897,9 @@ source of truth for wire-shape conformance.
|
|
|
897
897
|
|
|
898
898
|
- `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
|
|
899
899
|
(the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
|
|
900
|
-
`
|
|
901
|
-
`oversized_reject_closes_connection` / `peer_request` —
|
|
900
|
+
`cell_gated_create` / `account_lifecycle` / `fact_serving` / `ready` /
|
|
901
|
+
`account_status` / `oversized_reject_closes_connection` / `peer_request` —
|
|
902
|
+
each has a `test_if`
|
|
902
903
|
reader; `peer_request` (server-initiated requests — the ActionPeer
|
|
903
904
|
`peer/ping` round-trip) is `true` for both the Rust spine and the TS spine
|
|
904
905
|
(`BackendWebsocketTransport.request_connection`); `ts_default_capabilities`
|
|
@@ -914,6 +915,13 @@ source of truth for wire-shape conformance.
|
|
|
914
915
|
parity suite — both `true` on every backend that live-mounts the full cell
|
|
915
916
|
surface (TS spine binary, in-process app, Rust stub). A backend mounting only
|
|
916
917
|
plain CRUD would declare `cell_crud: true, cell_relations: false`.
|
|
918
|
+
`cell_gated_create` gates `describe_cell_gated_create_cross_tests` (the
|
|
919
|
+
cell-creation-authorizer parity proof); unlike `cell_crud`/`cell_relations`
|
|
920
|
+
it is **cross-only** (`false` in-process) — `true` only on the reference
|
|
921
|
+
spine binaries that live-mount the `test_cell_gated_create_authorize` policy
|
|
922
|
+
(the TS spine `full_spine_mount` + the Rust `testing_spine_stub`), since the
|
|
923
|
+
authorizer adds no method/column/wire shape and so the schema + manifest
|
|
924
|
+
parity gates are blind to an authorizer divergence.
|
|
917
925
|
`account_lifecycle` gates `describe_account_lifecycle_cross_tests` (the
|
|
918
926
|
`account_delete` / `account_undelete` / `account_purge` parity suite); unlike
|
|
919
927
|
cells those verbs **are** on the declared surface (in `create_admin_actions`,
|
|
@@ -1154,7 +1162,7 @@ the stream \_can't* open on a spine without SSE — a self-cleaning tripwire for
|
|
|
1154
1162
|
the spine that should grow it, distinct from the consumer-legit capability
|
|
1155
1163
|
skip the shared suite emits.
|
|
1156
1164
|
|
|
1157
|
-
### `cross_backend/cell_crud.ts` + `cell_relations.ts` — cell parity suites
|
|
1165
|
+
### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
|
|
1158
1166
|
|
|
1159
1167
|
The cell-layer parity coverage is split across two sibling suites. Cells
|
|
1160
1168
|
can't ride the generic `describe_rpc_round_trip_tests` (stateful verbs need a
|
|
@@ -1189,10 +1197,23 @@ backdoor, cell).
|
|
|
1189
1197
|
holder who can `cell_get` still gets the IDOR 404). Only **actor-shaped**
|
|
1190
1198
|
grants are exercised — role-shaped principals need a closed role registry the
|
|
1191
1199
|
Rust spine deliberately lacks.
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1200
|
+
- **`describe_cell_gated_create_cross_tests`** (gates on
|
|
1201
|
+
`capabilities.cell_gated_create`, **cross-only**) — the cell-creation
|
|
1202
|
+
authorizer (`CellCreateAuthorize`) parity proof. Both spines mount the same
|
|
1203
|
+
`test_cell_gated_create_authorize` policy (gate `kind: 'gated'` behind the
|
|
1204
|
+
`participant` role or admin): a non-participant is denied with the
|
|
1205
|
+
`cell_not_found` 404 IDOR mask, an ungated kind is open, a `participant` is
|
|
1206
|
+
admitted, and admin bypasses. The authorizer adds no method/column/wire
|
|
1207
|
+
shape, so schema + manifest parity are **blind** to a divergence — this is the
|
|
1208
|
+
only gate that catches one. In-process hook coverage is the standalone
|
|
1209
|
+
`auth/cell_create_authorize.db.test.ts` (this cross suite skips in-process,
|
|
1210
|
+
where the default app mounts no authorizer).
|
|
1211
|
+
|
|
1212
|
+
`cell_crud` / `cell_relations` gate `true` on TS + Rust (cells run on both, no
|
|
1213
|
+
`.skip`); `cell_gated_create` is cross-only (the policy is a spine-binary
|
|
1214
|
+
fixture). Cross-process wiring is `src/test/cross_backend/cell.cross.test.ts`
|
|
1215
|
+
(all three suites); the in-process legs (plain `gro test`) for crud/relations
|
|
1216
|
+
are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
1196
1217
|
|
|
1197
1218
|
- `cell_relations_parity.db.test.ts`, sharing the full-surface
|
|
1198
1219
|
`create_cell_parity_setup` (`cell_parity_helpers.ts`) which mounts every cell
|
|
@@ -1452,6 +1473,44 @@ The in-process counterparts already exist (`describe_rate_limiting_tests` plus t
|
|
|
1452
1473
|
proxy middleware tests), so there's no in-process leg. `npm run test:cross:security`.
|
|
1453
1474
|
Cited property: `docs/security.md` §"Rate Limiting" + §"Trusted Proxy / Client IP".
|
|
1454
1475
|
|
|
1476
|
+
### Role-gated participation parity — `cross_backend/role_grant_participation.ts`
|
|
1477
|
+
|
|
1478
|
+
The role-gated-participation capstone — TS↔Rust agreement that an **app-defined
|
|
1479
|
+
role is conferred identically** on both spines. A `participant`
|
|
1480
|
+
(`grant_paths: ['admin']`) app-role is registered on both spines (`spine_roles`
|
|
1481
|
+
in `default_spine_surface.ts` + the Rust `testing_spine_stub`'s `RoleRegistry`
|
|
1482
|
+
and `known_roles`); the cross legs prove the three conferral properties agree.
|
|
1483
|
+
|
|
1484
|
+
**Declarative-max, imperative-as-escape-hatch.** The single-request matrix is
|
|
1485
|
+
declarative (`conformance_participation_cases.ts`, run via
|
|
1486
|
+
`describe_conformance_table_tests`): **(a) grantability** —
|
|
1487
|
+
`role_grant_assign` of `participant` passes the admin-grant-path gate and dies
|
|
1488
|
+
_past_ it (404, nonexistent account) while a non-grantable role (`keeper`,
|
|
1489
|
+
unregistered) is refused _at_ it (403 `role_not_web_grantable`), and the
|
|
1490
|
+
`role_grant_offer_create` grant-path gate matches (403
|
|
1491
|
+
`role_grant_offer_role_not_grantable`); **(b) admin-only conferral** — a
|
|
1492
|
+
non-admin _holder_ of `participant` (and a fresh non-admin) can't offer it (403
|
|
1493
|
+
`role_grant_offer_not_authorized` — no holder-propagation); **(c)
|
|
1494
|
+
`role_grant_assign` is admin-only** — non-admin holder / fresh → 403
|
|
1495
|
+
`insufficient_permissions` (dispatcher gate), anon → 401; plus the
|
|
1496
|
+
`to_actor_id`-must-belong-to-`to_account_id` resolution arm (400
|
|
1497
|
+
`role_grant_offer_actor_account_mismatch`).
|
|
1498
|
+
|
|
1499
|
+
`describe_role_grant_participation_cross_tests` (this module) is the **imperative
|
|
1500
|
+
escape hatch** for the multi-step success paths a static row can't express (they
|
|
1501
|
+
need a real recipient): admin **assigns** `participant` to a fresh account
|
|
1502
|
+
(`{ok, role_grant_id}` + idempotent re-assign), and admin **offers**
|
|
1503
|
+
`participant` → recipient **accepts** → a role_grant lands. The `role_holder`
|
|
1504
|
+
principal is seeded via `extra_accounts: [{username, roles: ['participant']}]`
|
|
1505
|
+
(bootstrap-cradle direct grant) and named through `principals.role_holder`.
|
|
1506
|
+
|
|
1507
|
+
Both legs run in-process (`conformance_participation.db.test.ts`, plain
|
|
1508
|
+
`gro test`) and cross-process (`conformance_participation.cross.test.ts`, TS
|
|
1509
|
+
node/deno/bun + Rust stub). Landing the TS `role_grant_assign` action also turned
|
|
1510
|
+
the `cross_backend_parity` action-manifest gate green (it was Rust-only before).
|
|
1511
|
+
Cited property: `docs/identity.md` + the role-gated-participation design
|
|
1512
|
+
(admin-only conferral, immediate assign).
|
|
1513
|
+
|
|
1455
1514
|
### Building a TS test-server binary — `testing_server_core.ts` + adapters
|
|
1456
1515
|
|
|
1457
1516
|
The reusable shape for standing up a **spawnable TS** cross-process test
|
|
@@ -159,6 +159,20 @@ export interface BackendCapabilities {
|
|
|
159
159
|
* coverage.
|
|
160
160
|
*/
|
|
161
161
|
readonly peer_request: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* A test `CellCreateAuthorize` policy is live-mounted on the backend's cell
|
|
164
|
+
* layer — creating a `kind: 'gated'` cell requires the `participant` role
|
|
165
|
+
* or admin; every other kind (and a typeless cell) is open. Gates
|
|
166
|
+
* `describe_cell_gated_create_cross_tests`, the TS↔Rust cell-creation-
|
|
167
|
+
* authorizer parity proof. The authorizer adds no method / column / wire
|
|
168
|
+
* shape, so schema-snapshot + action-manifest parity are **blind** to an
|
|
169
|
+
* authorizer divergence — this behavioral cross case is the only gate that
|
|
170
|
+
* catches one. `true` only on the reference spine binaries that mount the
|
|
171
|
+
* test policy (the TS spine `full_spine_mount` + the Rust
|
|
172
|
+
* `testing_spine_stub`); consumers and the in-process default app don't
|
|
173
|
+
* mount it.
|
|
174
|
+
*/
|
|
175
|
+
readonly cell_gated_create: boolean;
|
|
162
176
|
}
|
|
163
177
|
/**
|
|
164
178
|
* Capability declarations for the in-process Hono transport. Nearly every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwC9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwC9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAcpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
|
|
@@ -56,6 +56,9 @@ export const in_process_capabilities = Object.freeze({
|
|
|
56
56
|
account_status: true,
|
|
57
57
|
oversized_reject_closes_connection: true,
|
|
58
58
|
peer_request: false,
|
|
59
|
+
// Cross-process-only: the test `CellCreateAuthorize` policy is mounted on the
|
|
60
|
+
// spine binaries' full mount + the Rust stub, not the in-process default app.
|
|
61
|
+
cell_gated_create: false,
|
|
59
62
|
});
|
|
60
63
|
/**
|
|
61
64
|
* Shape notes for the in-process Hono transport — every wiring fact present
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2D9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAGzD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IAublF,CAAC"}
|