@fuzdev/fuz_app 0.102.0 → 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 (43) hide show
  1. package/dist/auth/CLAUDE.md +15 -6
  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/db/CLAUDE.md +8 -5
  12. package/dist/db/cell_ddl.d.ts +11 -2
  13. package/dist/db/cell_ddl.d.ts.map +1 -1
  14. package/dist/db/cell_ddl.js +13 -1
  15. package/dist/db/cell_queries.d.ts +14 -8
  16. package/dist/db/cell_queries.d.ts.map +1 -1
  17. package/dist/db/cell_queries.js +12 -11
  18. package/dist/testing/CLAUDE.md +28 -7
  19. package/dist/testing/cross_backend/capabilities.d.ts +14 -0
  20. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  21. package/dist/testing/cross_backend/capabilities.js +3 -0
  22. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  23. package/dist/testing/cross_backend/cell_crud.js +55 -13
  24. package/dist/testing/cross_backend/cell_gated_create.d.ts +4 -0
  25. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -0
  26. package/dist/testing/cross_backend/cell_gated_create.js +71 -0
  27. package/dist/testing/cross_backend/cell_grant_role.js +4 -4
  28. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  29. package/dist/testing/cross_backend/cell_relations.js +30 -24
  30. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  31. package/dist/testing/cross_backend/default_backend_configs.js +7 -0
  32. package/dist/testing/cross_backend/expected_schema.json +1 -0
  33. package/dist/testing/cross_backend/fact_serving.js +2 -2
  34. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  35. package/dist/testing/cross_backend/full_spine_mount.js +6 -1
  36. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  37. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
  38. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +17 -0
  39. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -0
  40. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +36 -0
  41. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  42. package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
  43. package/package.json +1 -1
@@ -44,7 +44,7 @@ import { jsonrpc_errors, dev_only } from '../http/jsonrpc_errors.js';
44
44
  import { is_pg_unique_violation } from '../db/pg_error.js';
45
45
  import { has_role } from './request_context.js';
46
46
  import { ROLE_ADMIN } from './role_schema.js';
47
- import { cell_create_action_spec, cell_get_action_spec, cell_update_action_spec, cell_delete_action_spec, cell_list_action_spec, cell_clone_action_spec, ERROR_CELL_NOT_FOUND, ERROR_CELL_PATH_ADMIN_ONLY, ERROR_CELL_PATH_TAKEN, ERROR_CELL_VISIBILITY_MANAGE_ONLY, ERROR_CELL_GET_REQUIRES_ID_OR_PATH, ERROR_CELL_CLONE_KIND_MISMATCH, ERROR_CELL_LIST_CREATED_BY_REQUIRES_AUTH, ERROR_CELL_LIST_SHARED_WITH_REQUIRES_AUTH, CELL_LIST_LIMIT_DEFAULT, CELL_RELATIONS_BUNDLE_LIMIT, } from './cell_action_specs.js';
47
+ import { cell_create_action_spec, cell_get_action_spec, cell_update_action_spec, cell_delete_action_spec, cell_list_action_spec, cell_clone_action_spec, ERROR_CELL_NOT_FOUND, ERROR_CELL_PATH_ADMIN_ONLY, ERROR_CELL_PATH_TAKEN, ERROR_CELL_VISIBILITY_MANAGE_ONLY, ERROR_CELL_GET_REQUIRES_ID_OR_PATH, ERROR_CELL_KIND_IN_DATA, ERROR_CELL_KIND_EMPTY, ERROR_CELL_LIST_CREATED_BY_REQUIRES_AUTH, ERROR_CELL_LIST_SHARED_WITH_REQUIRES_AUTH, CELL_LIST_LIMIT_DEFAULT, CELL_RELATIONS_BUNDLE_LIMIT, } from './cell_action_specs.js';
48
48
  import { query_cell_create, query_cell_get, query_cell_get_by_path, query_cell_update, query_cell_delete, query_cell_list, query_cell_load_many, } from '../db/cell_queries.js';
49
49
  import { query_cell_grant_list_for_cell, query_cell_grants_for_caller_in_cells, } from '../db/cell_grant_queries.js';
50
50
  import { query_cell_field_list_for_source, query_cell_field_set } from '../db/cell_field_queries.js';
@@ -66,10 +66,25 @@ const to_iso_nullable = (value) => {
66
66
  * write is unambiguously a path collision.
67
67
  */
68
68
  const path_taken_error = () => jsonrpc_errors.conflict('cell.path is already taken', { reason: ERROR_CELL_PATH_TAKEN });
69
+ /**
70
+ * Reject a `kind` key supplied inside `data`. `kind` is the top-level
71
+ * `cell.kind` column (write-once capability axis), so a copy inside the
72
+ * loose `data` blob would be a second source of truth that silently
73
+ * diverges on update. Fail-loud `invalid_params` at every wire-`data`
74
+ * boundary (create / update / clone-patch).
75
+ */
76
+ const reject_kind_in_data = (data) => {
77
+ if (Object.hasOwn(data, 'kind')) {
78
+ throw jsonrpc_errors.invalid_params('cell.data must not contain `kind` (it is a top-level field)', {
79
+ reason: ERROR_CELL_KIND_IN_DATA,
80
+ });
81
+ }
82
+ };
69
83
  export const to_cell_json = (row) => ({
70
84
  id: row.id,
71
85
  path: row.path,
72
86
  data: row.data,
87
+ kind: row.kind,
73
88
  visibility: row.visibility,
74
89
  refs: row.refs,
75
90
  created_by: row.created_by,
@@ -92,14 +107,14 @@ const emit_cell_audit = (ctx, event_type, row, deps, auth) => {
92
107
  ip: ctx.client_ip,
93
108
  metadata: {
94
109
  cell_id: row.id,
95
- kind: row.data.kind,
110
+ kind: row.kind ?? undefined,
96
111
  path: row.path,
97
112
  },
98
113
  });
99
114
  };
100
115
  /** Create the six generic cell RPC actions. */
101
116
  export const create_cell_actions = (deps) => {
102
- const { validate_data } = deps;
117
+ const { validate_data, authorize_create } = deps;
103
118
  /**
104
119
  * Run the optional `validate_data` deps callback and convert any thrown
105
120
  * `ZodError` into the standard `invalid_params` JSON-RPC error so per-
@@ -129,9 +144,28 @@ export const create_cell_actions = (deps) => {
129
144
  reason: ERROR_CELL_PATH_ADMIN_ONLY,
130
145
  });
131
146
  }
147
+ // `kind` is the top-level column — reject a stray copy inside `data`,
148
+ // and reject an empty `kind` (a tag tags nothing — `null` is the typeless
149
+ // state), so `kind` stays a clean `null | non-empty-string`. Both run
150
+ // after the path-admin gate so an unauthorized caller sees that first.
151
+ reject_kind_in_data(input.data);
152
+ if (input.kind === '') {
153
+ throw jsonrpc_errors.invalid_params('cell.kind must not be empty (use null for a typeless cell)', { reason: ERROR_CELL_KIND_EMPTY });
154
+ }
132
155
  // Per-kind shape validation (sub-API). Unknown kinds pass through;
133
156
  // known kinds with malformed payloads surface as invalid_params.
134
157
  const validated_data = validate_data_or_throw(input.data);
158
+ const kind = input.kind ?? null;
159
+ // Creation authorizer (opt-in): may this actor create this kind? Runs
160
+ // after shape validation, before the insert. Deny → 404 IDOR mask (the
161
+ // established cell-layer deny shape — a gated kind is indistinguishable
162
+ // from a missing resource). Receives the **raw** `input.data` (not the
163
+ // `validate_data`-normalized value) to match the Rust twin, so a
164
+ // consumer with a transforming validator can't diverge TS↔Rust.
165
+ if (authorize_create &&
166
+ !(await authorize_create(auth, { kind, data: input.data, scope_id: null }))) {
167
+ throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
168
+ }
135
169
  let row;
136
170
  try {
137
171
  row = await query_cell_create(ctx, {
@@ -141,6 +175,7 @@ export const create_cell_actions = (deps) => {
141
175
  // extend `Json`. The DB column is JSONB; the cast trusts the
142
176
  // schema-validated runtime shape.
143
177
  data: validated_data,
178
+ kind,
144
179
  visibility: input.visibility,
145
180
  path: input.path ?? null,
146
181
  created_by: auth.actor.id,
@@ -254,7 +289,11 @@ export const create_cell_actions = (deps) => {
254
289
  // Per-kind shape validation when `data` is supplied. Patch-only —
255
290
  // we don't validate the existing row's data on update (validation
256
291
  // is for incoming patches). `data` writes fully replace, so the
257
- // patch IS the post-update state.
292
+ // patch IS the post-update state. A stray `kind` inside it is rejected
293
+ // (kind is the write-once top-level column — `cell_update` cannot
294
+ // change it, structurally: it is not even a field on `CellUpdateInput`).
295
+ if (input.data !== undefined)
296
+ reject_kind_in_data(input.data);
258
297
  const validated_data = input.data !== undefined ? validate_data_or_throw(input.data) : undefined;
259
298
  let updated;
260
299
  try {
@@ -312,23 +351,13 @@ export const create_cell_actions = (deps) => {
312
351
  * server-stamped provenance fields.
313
352
  */
314
353
  const clone_one_cell_row = async (ctx, source, auth, options) => {
354
+ // A patch is wire-supplied `data` — reject a stray `kind` (kind is the
355
+ // immutable top-level column; the clone inherits `source.kind`).
356
+ if (options.patch_data !== undefined)
357
+ reject_kind_in_data(options.patch_data);
315
358
  // Both `source.data` and `options.patch_data` are CellData (loose
316
359
  // objects). Patch-last shallow merge composes cleanly.
317
360
  const merged_data = options.patch_data !== undefined ? { ...source.data, ...options.patch_data } : source.data;
318
- // Block cross-kind patches before per-kind shape validation: a
319
- // kind-A → kind-B patch can pass `validate_data` coincidentally
320
- // (loose shapes accept overlapping fields) and produce an
321
- // incoherent result. Reject when the merged kind diverges from the
322
- // source's kind. Both undefined or equal kinds pass through;
323
- // one-sided undefined is permissive (caller patching unrelated
324
- // fields on a typeless source).
325
- if (source.data.kind !== undefined &&
326
- merged_data.kind !== undefined &&
327
- source.data.kind !== merged_data.kind) {
328
- throw jsonrpc_errors.invalid_params('cell_clone cannot change kind via with_data_patch', {
329
- reason: ERROR_CELL_CLONE_KIND_MISMATCH,
330
- });
331
- }
332
361
  // Per-kind shape validation runs on the merged result (sub-API).
333
362
  // Source rows are validated on their original create; the patch
334
363
  // could violate the kind shape (e.g., remove a required field).
@@ -336,6 +365,9 @@ export const create_cell_actions = (deps) => {
336
365
  return query_cell_create(ctx, {
337
366
  // Boundary cast (see `create_handler`).
338
367
  data: validated_data,
368
+ // Clone inherits the source's kind verbatim — kind is fixed at
369
+ // birth and a `with_data_patch` can't reach the column.
370
+ kind: source.kind,
339
371
  visibility: source.visibility,
340
372
  path: null, // admin-only paths cannot auto-clone
341
373
  created_by: auth.actor.id,
@@ -463,7 +495,7 @@ export const create_cell_actions = (deps) => {
463
495
  // — emit directly rather than threading it through `emit_cell_audit`.
464
496
  // `kind` is read from `source.data` (not the cloned row) so the
465
497
  // audit trail attributes the clone to the source's shape.
466
- const source_kind = source.data.kind;
498
+ const source_kind = source.kind ?? undefined;
467
499
  const cloned_child_count = deep ? cloneable_items.length : 0;
468
500
  deps.audit.emit(ctx, {
469
501
  event_type: 'cell_clone',
@@ -505,7 +537,7 @@ export const create_cell_actions = (deps) => {
505
537
  const caller_actor_id = auth?.actor?.id ?? null;
506
538
  const rows = await query_cell_list(ctx, {
507
539
  ids: input.ids,
508
- data_kind: input.data_kind,
540
+ kind: input.kind,
509
541
  visibility: input.visibility,
510
542
  ref: input.ref,
511
543
  created_by: input.created_by,
@@ -11,17 +11,19 @@
11
11
  * `auth/cell_actions.ts`.
12
12
  *
13
13
  * **Discipline**: a field joins `CellData` only when at least two
14
- * consumers in different domains read it generically. `kind` (editor
15
- * dispatch + sub-API registry), `label` (list/index rendering), and
16
- * `summary` (card subtitle + share-target description) meet this bar.
17
- * Future candidates require evidence of generic usage — otherwise they
18
- * stay per-kind.
14
+ * consumers in different domains read it generically. `label` (list/index
15
+ * rendering) and `summary` (card subtitle + share-target description) meet
16
+ * this bar. Future candidates require evidence of generic usage — otherwise
17
+ * they stay per-kind.
19
18
  *
20
- * **Visibility is not in here.** Access control is a peer of `cell_grant`,
21
- * not content metadata — `cell.visibility` lives as a top-level column on
22
- * `CellJson` and `CellRow` (the `CellVisibility` enum is defined in
23
- * `auth/cell_action_specs.ts` next to the wire fields that use it), and is
24
- * enforced by `can_view_cell` reading the column directly (no JSON dive).
19
+ * **`kind` is not in here, and is rejected if present.** Like `visibility`,
20
+ * a cell's `kind` is a top-level column (`cell.kind` on `CellJson` /
21
+ * `CellRow`), not content metadata — it is the capability / identity axis a
22
+ * creation authorizer gates on, write-once at birth. A stray `kind` key
23
+ * inside `data` is a fail-loud `ERROR_CELL_KIND_IN_DATA` at the create /
24
+ * update / clone-patch boundary (single source of truth). Access control
25
+ * (`cell.visibility`) is likewise a peer column, enforced by `can_view_cell`
26
+ * reading the column directly (no JSON dive).
25
27
  *
26
28
  * @module
27
29
  */
@@ -29,14 +31,10 @@ import { z } from 'zod';
29
31
  /**
30
32
  * Base cell-data shape. All fields optional; loose mode admits arbitrary
31
33
  * additional keys so apps can attach metadata or stage new kinds without
32
- * touching the wire schema.
33
- *
34
- * `kind` is optional because cells without a registered kind are valid —
35
- * admin-curated content, in-development types, or unknown shapes pass
36
- * through. Known kinds get richer validation via the per-kind sub-API.
34
+ * touching the wire schema. `kind` lives on the top-level `cell.kind`
35
+ * column, not here (see the module doc).
37
36
  */
38
37
  export declare const CellData: z.ZodObject<{
39
- kind: z.ZodOptional<z.ZodString>;
40
38
  label: z.ZodOptional<z.ZodString>;
41
39
  summary: z.ZodOptional<z.ZodString>;
42
40
  }, z.core.$loose>;
@@ -1 +1 @@
1
- {"version":3,"file":"cell_data_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_data_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ;;;;iBAInB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC"}
1
+ {"version":3,"file":"cell_data_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_data_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;GAKG;AACH,eAAO,MAAM,QAAQ;;;iBAGnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC"}
@@ -11,17 +11,19 @@
11
11
  * `auth/cell_actions.ts`.
12
12
  *
13
13
  * **Discipline**: a field joins `CellData` only when at least two
14
- * consumers in different domains read it generically. `kind` (editor
15
- * dispatch + sub-API registry), `label` (list/index rendering), and
16
- * `summary` (card subtitle + share-target description) meet this bar.
17
- * Future candidates require evidence of generic usage — otherwise they
18
- * stay per-kind.
14
+ * consumers in different domains read it generically. `label` (list/index
15
+ * rendering) and `summary` (card subtitle + share-target description) meet
16
+ * this bar. Future candidates require evidence of generic usage — otherwise
17
+ * they stay per-kind.
19
18
  *
20
- * **Visibility is not in here.** Access control is a peer of `cell_grant`,
21
- * not content metadata — `cell.visibility` lives as a top-level column on
22
- * `CellJson` and `CellRow` (the `CellVisibility` enum is defined in
23
- * `auth/cell_action_specs.ts` next to the wire fields that use it), and is
24
- * enforced by `can_view_cell` reading the column directly (no JSON dive).
19
+ * **`kind` is not in here, and is rejected if present.** Like `visibility`,
20
+ * a cell's `kind` is a top-level column (`cell.kind` on `CellJson` /
21
+ * `CellRow`), not content metadata — it is the capability / identity axis a
22
+ * creation authorizer gates on, write-once at birth. A stray `kind` key
23
+ * inside `data` is a fail-loud `ERROR_CELL_KIND_IN_DATA` at the create /
24
+ * update / clone-patch boundary (single source of truth). Access control
25
+ * (`cell.visibility`) is likewise a peer column, enforced by `can_view_cell`
26
+ * reading the column directly (no JSON dive).
25
27
  *
26
28
  * @module
27
29
  */
@@ -29,14 +31,10 @@ import { z } from 'zod';
29
31
  /**
30
32
  * Base cell-data shape. All fields optional; loose mode admits arbitrary
31
33
  * additional keys so apps can attach metadata or stage new kinds without
32
- * touching the wire schema.
33
- *
34
- * `kind` is optional because cells without a registered kind are valid —
35
- * admin-curated content, in-development types, or unknown shapes pass
36
- * through. Known kinds get richer validation via the per-kind sub-API.
34
+ * touching the wire schema. `kind` lives on the top-level `cell.kind`
35
+ * column, not here (see the module doc).
37
36
  */
38
37
  export const CellData = z.looseObject({
39
- kind: z.string().optional(),
40
38
  label: z.string().optional(),
41
39
  summary: z.string().optional(),
42
40
  });
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
@@ -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"}