@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.
Files changed (58) hide show
  1. package/dist/auth/CLAUDE.md +29 -17
  2. package/dist/auth/cell_action_specs.d.ts +35 -34
  3. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  4. package/dist/auth/cell_action_specs.js +23 -8
  5. package/dist/auth/cell_actions.d.ts +28 -0
  6. package/dist/auth/cell_actions.d.ts.map +1 -1
  7. package/dist/auth/cell_actions.js +52 -20
  8. package/dist/auth/cell_data_schema.d.ts +14 -16
  9. package/dist/auth/cell_data_schema.d.ts.map +1 -1
  10. package/dist/auth/cell_data_schema.js +14 -16
  11. package/dist/auth/role_grant_offer_action_specs.d.ts +56 -3
  12. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  13. package/dist/auth/role_grant_offer_action_specs.js +48 -3
  14. package/dist/auth/role_grant_offer_actions.d.ts +9 -19
  15. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  16. package/dist/auth/role_grant_offer_actions.js +91 -36
  17. package/dist/db/CLAUDE.md +8 -5
  18. package/dist/db/cell_ddl.d.ts +11 -2
  19. package/dist/db/cell_ddl.d.ts.map +1 -1
  20. package/dist/db/cell_ddl.js +13 -1
  21. package/dist/db/cell_queries.d.ts +14 -8
  22. package/dist/db/cell_queries.d.ts.map +1 -1
  23. package/dist/db/cell_queries.js +12 -11
  24. package/dist/testing/CLAUDE.md +66 -7
  25. package/dist/testing/cross_backend/capabilities.d.ts +14 -0
  26. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  27. package/dist/testing/cross_backend/capabilities.js +3 -0
  28. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  29. package/dist/testing/cross_backend/cell_crud.js +55 -13
  30. package/dist/testing/cross_backend/cell_gated_create.d.ts +4 -0
  31. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -0
  32. package/dist/testing/cross_backend/cell_gated_create.js +71 -0
  33. package/dist/testing/cross_backend/cell_grant_role.js +4 -4
  34. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  35. package/dist/testing/cross_backend/cell_relations.js +30 -24
  36. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  37. package/dist/testing/cross_backend/default_backend_configs.js +7 -0
  38. package/dist/testing/cross_backend/default_spine_surface.d.ts +9 -5
  39. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  40. package/dist/testing/cross_backend/default_spine_surface.js +12 -6
  41. package/dist/testing/cross_backend/expected_schema.json +1 -0
  42. package/dist/testing/cross_backend/fact_serving.js +2 -2
  43. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  44. package/dist/testing/cross_backend/full_spine_mount.js +6 -1
  45. package/dist/testing/cross_backend/role_grant_participation.d.ts +11 -0
  46. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -0
  47. package/dist/testing/cross_backend/role_grant_participation.js +73 -0
  48. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  49. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
  50. package/dist/testing/cross_backend/spine_surface_constants.d.ts +12 -0
  51. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  52. package/dist/testing/cross_backend/spine_surface_constants.js +12 -0
  53. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +17 -0
  54. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -0
  55. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +36 -0
  56. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
  58. 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
- * Seven actions: six offer-lifecycle methods (create / accept / decline /
5
- * retract / list / history) plus `role_grant_revoke` (admin-only). All mount
4
+ * Eight actions: six offer-lifecycle methods (create / accept / decline /
5
+ * retract / list / history) plus `role_grant_revoke` and `role_grant_assign`
6
+ * (both admin-only). All mount
6
7
  * on a consumer's JSON-RPC endpoint via `create_rpc_endpoint`. The action
7
8
  * specs themselves live in `auth/role_grant_offer_action_specs.ts`. Mutations
8
9
  * declare `side_effects: true` so the RPC dispatcher wraps the handler in
@@ -23,6 +24,10 @@
23
24
  * dispatcher rejects non-admin callers before the handler runs.
24
25
  * The admin-grant-path gate prevents revoking keeper / daemon-scoped
25
26
  * roles via this surface. Keys on `actor_id` to survive multi-actor accounts.
27
+ * - `role_grant_assign` — spec-level `auth: {role: 'admin'}`; the immediate
28
+ * consent-free conferral path. The admin-grant-path gate prevents assigning
29
+ * keeper / daemon-scoped roles via this surface. No holder-propagation — only
30
+ * admins confer (see participation-gates.md Decision 7).
26
31
  *
27
32
  * Audit events are emitted in-transaction by the query layer (atomic with
28
33
  * the role_grant write on accept/revoke) or by the handler via the bound
@@ -44,12 +49,12 @@ import { builtin_role_specs_by_name, ROLE_ADMIN, role_has_grant_path, } from './
44
49
  import { GRANT_PATH_ADMIN } from './grant_path_schema.js';
45
50
  import { ROLE_GRANT_OFFER_DEFAULT_TTL_MS, to_role_grant_offer_json, } from './role_grant_offer_schema.js';
46
51
  import { query_role_grant_offer_create, query_role_grant_offer_decline, query_role_grant_offer_retract, query_role_grant_offer_list, query_role_grant_offer_history_for_account, query_accept_offer, RoleGrantOfferActorAccountMismatchError, RoleGrantOfferActorMismatchError, RoleGrantOfferAlreadyTerminalError, RoleGrantOfferExpiredError, RoleGrantOfferNotFoundError, RoleGrantOfferSelfTargetError, } from './role_grant_offer_queries.js';
47
- import { query_role_grant_find_active_role_for_actor, query_revoke_role_grant, } from './role_grant_queries.js';
48
- import { query_actor_by_id } from './account_queries.js';
52
+ import { query_create_role_grant, query_role_grant_find_active_role_for_actor, query_revoke_role_grant, } from './role_grant_queries.js';
53
+ import { query_actor_by_id, query_active_actors_by_account } from './account_queries.js';
49
54
  import { has_scoped_role } from './request_context.js';
50
55
  import { build_role_grant_offer_accepted_notification, build_role_grant_offer_declined_notification, build_role_grant_offer_received_notification, build_role_grant_offer_retracted_notification, build_role_grant_offer_supersede_notification, build_role_grant_revoke_notification, } from './role_grant_offer_notifications.js';
51
56
  import { ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE } from '../http/error_schemas.js';
52
- import { ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_EXPIRED, ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE, ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL, role_grant_offer_create_action_spec, role_grant_offer_accept_action_spec, role_grant_offer_decline_action_spec, role_grant_offer_retract_action_spec, role_grant_offer_list_action_spec, role_grant_offer_history_action_spec, role_grant_revoke_action_spec, } from './role_grant_offer_action_specs.js';
57
+ import { ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_EXPIRED, ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE, ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL, role_grant_offer_create_action_spec, role_grant_offer_accept_action_spec, role_grant_offer_decline_action_spec, role_grant_offer_retract_action_spec, role_grant_offer_list_action_spec, role_grant_offer_history_action_spec, role_grant_revoke_action_spec, role_grant_assign_action_spec, } from './role_grant_offer_action_specs.js';
53
58
  // -- Helpers ----------------------------------------------------------------
54
59
  /**
55
60
  * Fan out a batch of pre-written audit rows to the bound emitter's
@@ -67,40 +72,18 @@ const fan_out_audit_events = (events, audit) => {
67
72
  }
68
73
  };
69
74
  // eslint-disable-next-line @typescript-eslint/require-await
70
- const default_authorize = async (auth, input, _deps, _ctx) => {
71
- // Caller must hold an active role_grant for the offered role. Global (no scope)
72
- // check — the scope-aware "only in this classroom" policy is consumer-level.
73
- // Reads from the in-memory `auth.role_grants` snapshot loaded once per request
74
- // by `create_request_context_middleware`; no DB roundtrip needed.
75
- return has_scoped_role(auth, input.role, null);
76
- };
77
- /**
78
- * Authorization callback that admits any admin and otherwise falls back to
79
- * the symmetric default (caller must hold the offered role globally).
80
- *
81
- * The admin-grant-path filter in `create_handler` runs **before** the
82
- * `authorize` callback, so this never sees roles whose `grant_paths`
83
- * omits `'admin'`. Drop into
84
- * `create_role_grant_offer_actions({authorize: authorize_admin_or_holder})`
85
- * (or any factory that forwards `authorize`, e.g. `create_standard_rpc_actions`)
86
- * for the common "admins offer anything; users offer what they hold"
87
- * pattern. Scope-aware policies (e.g. classroom_teacher offering
88
- * classroom_student in their own scope) wrap this and short-circuit `true`
89
- * before delegating.
90
- */
91
- export const authorize_admin_or_holder = async (auth, input, _deps, _ctx) => {
92
- // Admin bypass keys on **global** admin role_grants only — `has_scoped_role(_, _, null)`
93
- // rejects scoped admin role_grants. Without this, a `{role: 'admin', scope_id: scope_X}`
94
- // role_grant would let the holder offer any admin-grant-path role without holding it
95
- // themselves, escalating scoped admin to global authority over the offer surface.
96
- if (has_scoped_role(auth, ROLE_ADMIN, null))
97
- return true;
98
- return has_scoped_role(auth, input.role, null);
75
+ const default_authorize = async (auth, _input, _deps, _ctx) => {
76
+ // Admin-only conferral: the caller must hold a *global* `admin` grant. Holding
77
+ // the offered role itself confers no power to offer it (no holder-propagation —
78
+ // see participation-gates.md Decision 6). The global check (scope_id null) stops
79
+ // a scoped `admin` escalating into global authority over the offer surface.
80
+ // Reads the in-memory `auth.role_grants` snapshot; no DB roundtrip.
81
+ return has_scoped_role(auth, ROLE_ADMIN, null);
99
82
  };
100
83
  // -- Action factory ---------------------------------------------------------
101
84
  /**
102
- * Create the seven role-grant-offer RPC actions (six offer-lifecycle methods
103
- * plus `role_grant_revoke`).
85
+ * Create the eight role-grant-offer RPC actions (six offer-lifecycle methods
86
+ * plus `role_grant_revoke` and the immediate `role_grant_assign`).
104
87
  *
105
88
  * @param deps - `RouteFactoryDeps` (`log`, `audit`, …) plus optional `notification_sender` for WS fan-out — when absent, WS fan-out is silently skipped (DB-only side effects still happen). Consumers wiring `BackendWebsocketTransport` assign its instance directly (the transport's `send_to_account` signature accepts the broader `JsonrpcMessageFromServerToClient`, which is contravariantly compatible)
106
89
  * @param options - role schema, default TTL, authorization override
@@ -464,6 +447,77 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
464
447
  }
465
448
  return { ok: true, revoked: true };
466
449
  };
450
+ // The immediate admin-only conferral path — the consent-free sibling of
451
+ // `role_grant_offer_create`. Admin-gated at the dispatcher (`roles:
452
+ // ['admin']`), it re-checks admin-grantability (so keeper / CLI-only roles
453
+ // can't be web-assigned), resolves the target actor, writes the idempotent
454
+ // grant, and emits a `role_grant_create` audit row — the same event the
455
+ // offer-accept and self-service paths emit. No WS notification in v1 (the
456
+ // grantee picks the capability up on its next authenticated request).
457
+ const assign_handler = async (input, ctx) => {
458
+ const auth = ctx.auth;
459
+ // Admin-grant-path gate — the same registry check the offer/revoke gates
460
+ // use; keeper / daemon-scoped roles stay CLI-only. Denial is
461
+ // forensic-audited (`role_grant_id` omitted — no grant was created).
462
+ if (!role_has_grant_path(role_specs, input.role, GRANT_PATH_ADMIN)) {
463
+ audit.emit_role_grant_target(ctx, auth, {
464
+ event_type: 'role_grant_create',
465
+ outcome: 'failure',
466
+ target_account_id: input.to_account_id,
467
+ target_actor_id: input.to_actor_id ?? null,
468
+ metadata: { role: input.role, scope_id: input.scope_id ?? null },
469
+ });
470
+ throw jsonrpc_errors.forbidden('role not web-grantable', {
471
+ reason: ERROR_ROLE_NOT_WEB_GRANTABLE,
472
+ });
473
+ }
474
+ // Resolve the grant's target actor. A supplied `to_actor_id` must be an
475
+ // active actor of `to_account_id`; otherwise the account's sole active
476
+ // actor is used (a multi-actor account must name one). A missing /
477
+ // actorless account 404-masks. Inlined (not a helper) so the
478
+ // actor-account-mismatch throw stays in the handler body the
479
+ // error_reasons drift scan reads.
480
+ const actors = await query_active_actors_by_account(ctx, input.to_account_id);
481
+ let target_actor_id;
482
+ if (input.to_actor_id != null) {
483
+ if (!actors.some((a) => a.id === input.to_actor_id)) {
484
+ throw jsonrpc_errors.invalid_params('to_actor_id does not belong to to_account_id', {
485
+ reason: ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH,
486
+ });
487
+ }
488
+ target_actor_id = input.to_actor_id;
489
+ }
490
+ else if (actors.length === 1) {
491
+ target_actor_id = actors[0].id;
492
+ }
493
+ else if (actors.length === 0) {
494
+ throw jsonrpc_errors.not_found('account');
495
+ }
496
+ else {
497
+ throw jsonrpc_errors.invalid_params('to_actor_id is required for a multi-actor account');
498
+ }
499
+ // Idempotent write — re-assigning an already-held grant returns the
500
+ // existing row. `scope_kind` stays null: `scope_id` alone is the v1
501
+ // discriminator. `granted_by` is the assigning admin.
502
+ const grant = await query_create_role_grant(ctx, {
503
+ actor_id: target_actor_id,
504
+ role: input.role,
505
+ scope_kind: null,
506
+ scope_id: input.scope_id ?? null,
507
+ granted_by: auth.actor.id,
508
+ });
509
+ audit.emit_role_grant_target(ctx, auth, {
510
+ event_type: 'role_grant_create',
511
+ target_account_id: input.to_account_id,
512
+ target_actor_id,
513
+ metadata: {
514
+ role: grant.role,
515
+ role_grant_id: grant.id,
516
+ scope_id: grant.scope_id,
517
+ },
518
+ });
519
+ return { ok: true, role_grant_id: grant.id };
520
+ };
467
521
  return [
468
522
  rpc_action(role_grant_offer_create_action_spec, create_handler),
469
523
  rpc_action(role_grant_offer_accept_action_spec, accept_handler),
@@ -472,5 +526,6 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
472
526
  rpc_action(role_grant_offer_list_action_spec, list_handler),
473
527
  rpc_action(role_grant_offer_history_action_spec, history_handler),
474
528
  rpc_action(role_grant_revoke_action_spec, revoke_handler),
529
+ rpc_action(role_grant_assign_action_spec, assign_handler),
475
530
  ];
476
531
  };
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. Nullable `created_by` / `updated_by`
55
- FKs to `actor` (NULL = system origin). GIN on `data` / `refs`; global
56
- partial-unique on `path`.
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`, `_list_by_data_kind / _list_by_creator`, the
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 `ref` filter narrows by `cell.refs`), and
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
@@ -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
- * - `idx_cell_data`: shape-driven queries (`data ? 'kind'`, `data @> ...`).
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;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,4aAYtB,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CAYtC,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"}
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"}
@@ -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
- * - `idx_cell_data`: shape-driven queries (`data ? 'kind'`, `data @> ...`).
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
- /** Input for `query_cell_create`. `refs` is derived from `data`. */
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 whose `data.kind` matches the given value, newest first.
157
- * Uses the `idx_cell_data` GIN index (`data @> ...`).
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 - `data.kind` value to match (e.g. `'collection'`, `'entry'`)
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 query_cell_list_by_data_kind: (deps: QueryDeps, kind: string, options?: Pick<CellListOptions, "limit" | "offset">) => Promise<Array<CellRow>>;
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 `data.kind = ?` via `data @> {"kind": ?}` (uses `idx_cell_data`). */
225
- data_kind?: string;
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 `data_kind` /
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":"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"}
1
+ {"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
@@ -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($2::cell_visibility, 'private'::cell_visibility), $3, $4::text[], $5)
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 whose `data.kind` matches the given value, newest first.
175
- * Uses the `idx_cell_data` GIN index (`data @> ...`).
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 - `data.kind` value to match (e.g. `'collection'`, `'entry'`)
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 query_cell_list_by_data_kind = async (deps, kind, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
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 data @> $1::jsonb
185
+ WHERE kind = $1
185
186
  AND deleted_at IS NULL
186
187
  ORDER BY created_at DESC
187
- LIMIT $2 OFFSET $3`, [JSON.stringify({ kind }), options?.limit ?? null, options?.offset ?? 0]);
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.data_kind ?? null,
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.data @> jsonb_build_object('kind', $2::text))
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.data @> jsonb_build_object('kind', $2::text))
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)
@@ -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
- `account_lifecycle` / `fact_serving` / `ready` / `account_status` /
901
- `oversized_reject_closes_connection` / `peer_request` — each has a `test_if`
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
- Both gate `true` on TS + Rust (cells run on both, no `.skip`). Cross-process
1194
- wiring is `src/test/cross_backend/cell.cross.test.ts` (both suites); the
1195
- in-process legs (plain `gro test`) are `src/test/auth/cell_crud_parity.db.test.ts`
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;CAC/B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAWpC,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"}
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;AAsD9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAGzD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IA6VlF,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"}