@fuzdev/fuz_app 0.102.0 → 0.104.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 (49) hide show
  1. package/dist/auth/CLAUDE.md +37 -10
  2. package/dist/auth/cell_action_specs.d.ts +261 -35
  3. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  4. package/dist/auth/cell_action_specs.js +102 -9
  5. package/dist/auth/cell_actions.d.ts +51 -0
  6. package/dist/auth/cell_actions.d.ts.map +1 -1
  7. package/dist/auth/cell_actions.js +168 -22
  8. package/dist/auth/cell_audit_events.d.ts +1 -1
  9. package/dist/auth/cell_audit_events.d.ts.map +1 -1
  10. package/dist/auth/cell_audit_events.js +3 -2
  11. package/dist/auth/cell_audit_metadata.d.ts +15 -0
  12. package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
  13. package/dist/auth/cell_audit_metadata.js +11 -0
  14. package/dist/auth/cell_data_schema.d.ts +14 -16
  15. package/dist/auth/cell_data_schema.d.ts.map +1 -1
  16. package/dist/auth/cell_data_schema.js +14 -16
  17. package/dist/db/CLAUDE.md +19 -6
  18. package/dist/db/cell_ddl.d.ts +24 -2
  19. package/dist/db/cell_ddl.d.ts.map +1 -1
  20. package/dist/db/cell_ddl.js +33 -1
  21. package/dist/db/cell_queries.d.ts +59 -8
  22. package/dist/db/cell_queries.d.ts.map +1 -1
  23. package/dist/db/cell_queries.js +50 -12
  24. package/dist/testing/CLAUDE.md +37 -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 +28 -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 +213 -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/expected_schema.json +4 -0
  39. package/dist/testing/cross_backend/fact_serving.js +2 -2
  40. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  41. package/dist/testing/cross_backend/full_spine_mount.js +9 -1
  42. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  43. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
  44. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +22 -0
  45. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -0
  46. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +75 -0
  47. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  48. package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
  49. package/package.json +1 -1
@@ -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"}
@@ -27,7 +27,12 @@ import '../assert_dev_env.js';
27
27
  * - `path` write by a non-admin → 403 (`cell_path_admin_only`), on both create
28
28
  * and update (even by the owner);
29
29
  * - `cell_get` with neither `id` nor `path` → `invalid_params`;
30
- * - null-auth `cell_list` with `created_by` → `invalid_params`.
30
+ * - null-auth `cell_list` with `created_by` → `invalid_params`;
31
+ * - a denied caller (non-editor update / non-admin path create) carrying a
32
+ * `kind` inside `data` still gets the authz error (404 / 403), never the
33
+ * kind error — authz runs before the input-shape checks, on both spines;
34
+ * - an empty-string `kind` is rejected on create (`cell_kind_empty`) and is
35
+ * accepted as a list filter that matches nothing.
31
36
  *
32
37
  * The visibility-manage-tier 403 (`cell_visibility_manage_only`) needs a
33
38
  * non-owner editor, which only a `cell_grant` can produce, so it lives in
@@ -54,7 +59,7 @@ export const describe_cell_crud_cross_tests = (options) => {
54
59
  const t = fixture.fresh_transport();
55
60
  const owner_headers = owner.create_session_headers();
56
61
  // create — default visibility, owner-stamped, no grants
57
- const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note', label: 'hi' } }, owner_headers), CellCreateOutput);
62
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: { label: 'hi' } }, owner_headers), CellCreateOutput);
58
63
  assert.strictEqual(created.cell.visibility, 'private');
59
64
  assert.strictEqual(created.cell.created_by, owner.actor.id);
60
65
  assert.strictEqual(created.cell.updated_by, null);
@@ -70,7 +75,7 @@ export const describe_cell_crud_cross_tests = (options) => {
70
75
  assert.strictEqual(got.can_edit, true);
71
76
  assert.strictEqual(got.can_grant, true); // owner is manage-tier
72
77
  // update data + flip to public (owner is manage-tier, so visibility write is allowed)
73
- const updated = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { kind: 'note', label: 'hi2' }, visibility: 'public' }, owner_headers), CellUpdateOutput);
78
+ const updated = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { label: 'hi2' }, visibility: 'public' }, owner_headers), CellUpdateOutput);
74
79
  assert.strictEqual(updated.cell.visibility, 'public');
75
80
  assert.strictEqual(updated.cell.updated_by, owner.actor.id);
76
81
  // list includes it
@@ -97,7 +102,7 @@ export const describe_cell_crud_cross_tests = (options) => {
97
102
  const ref_b = `blake3:${'b'.repeat(64)}`;
98
103
  const ref_c = `blake3:${'c'.repeat(64)}`;
99
104
  const sorted = [ref_a, ref_b, ref_c];
100
- const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note', cover: ref_c, nested: { attachments: [ref_b, ref_a, ref_b] } } }, owner_headers), CellCreateOutput);
105
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: { cover: ref_c, nested: { attachments: [ref_b, ref_a, ref_b] } } }, owner_headers), CellCreateOutput);
101
106
  assert.deepStrictEqual(created.cell.refs, sorted, 'create did not return sorted refs');
102
107
  // Read it back — the stored column round-trips sorted too.
103
108
  const got = expect_output(await cross_rpc_call(t, rpc_path, 'cell_get', { id: created.cell.id }, owner_headers), CellGetOutput);
@@ -111,8 +116,8 @@ export const describe_cell_crud_cross_tests = (options) => {
111
116
  const owner = await fixture.create_account({ username: 'cell_anon_owner' });
112
117
  const owner_headers = owner.create_session_headers();
113
118
  const authed = fixture.fresh_transport();
114
- const priv = expect_output(await cross_rpc_call(authed, rpc_path, 'cell_create', { data: { kind: 'note' }, visibility: 'private' }, owner_headers), CellCreateOutput).cell;
115
- const pub = expect_output(await cross_rpc_call(authed, rpc_path, 'cell_create', { data: { kind: 'note' }, visibility: 'public' }, owner_headers), CellCreateOutput).cell;
119
+ const priv = expect_output(await cross_rpc_call(authed, rpc_path, 'cell_create', { kind: 'note', data: {}, visibility: 'private' }, owner_headers), CellCreateOutput).cell;
120
+ const pub = expect_output(await cross_rpc_call(authed, rpc_path, 'cell_create', { kind: 'note', data: {}, visibility: 'public' }, owner_headers), CellCreateOutput).cell;
116
121
  const anon = fixture.fresh_transport({ origin: null });
117
122
  const anon_pub = await cross_rpc_call(anon, rpc_path, 'cell_get', { id: pub.id }, {});
118
123
  assert.ok(anon_pub.ok, `anon could not read public cell: ${JSON.stringify(anon_pub.error)}`);
@@ -129,11 +134,11 @@ export const describe_cell_crud_cross_tests = (options) => {
129
134
  const owner = await fixture.create_account({ username: 'cell_idor_owner' });
130
135
  const other = await fixture.create_account({ username: 'cell_idor_other' });
131
136
  const t = fixture.fresh_transport();
132
- const priv = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner.create_session_headers()), CellCreateOutput).cell;
137
+ const priv = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner.create_session_headers()), CellCreateOutput).cell;
133
138
  const other_headers = other.create_session_headers();
134
139
  const read = await cross_rpc_call(t, rpc_path, 'cell_get', { id: priv.id }, other_headers);
135
140
  assert.strictEqual(error_reason(read), 'cell_not_found');
136
- const edit = await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id: priv.id, data: { kind: 'note', label: 'x' } }, other_headers);
141
+ const edit = await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id: priv.id, data: { label: 'x' } }, other_headers);
137
142
  assert.ok(!edit.ok, 'non-owner edited a private cell');
138
143
  assert.strictEqual(error_reason(edit), 'cell_not_found');
139
144
  const del = await cross_rpc_call(t, rpc_path, 'cell_delete', { cell_id: priv.id }, other_headers);
@@ -144,7 +149,7 @@ export const describe_cell_crud_cross_tests = (options) => {
144
149
  const fixture = await setup_test();
145
150
  const owner = await fixture.create_account({ username: 'cell_admin_owner' });
146
151
  const t = fixture.fresh_transport();
147
- const priv = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner.create_session_headers()), CellCreateOutput).cell;
152
+ const priv = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner.create_session_headers()), CellCreateOutput).cell;
148
153
  // `fixture` is the bootstrapped keeper (holds ROLE_ADMIN).
149
154
  const admin_read = expect_output(await cross_rpc_call(t, rpc_path, 'cell_get', { id: priv.id }, fixture.create_session_headers()), CellGetOutput);
150
155
  assert.strictEqual(admin_read.cell.id, priv.id);
@@ -154,8 +159,8 @@ export const describe_cell_crud_cross_tests = (options) => {
154
159
  const t = fixture.fresh_transport();
155
160
  // `path` writes are admin-only; the keeper is admin.
156
161
  const admin_headers = fixture.create_session_headers();
157
- expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' }, path: 'parity/dup' }, admin_headers), CellCreateOutput);
158
- const dup = await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' }, path: 'parity/dup' }, admin_headers);
162
+ expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {}, path: 'parity/dup' }, admin_headers), CellCreateOutput);
163
+ const dup = await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {}, path: 'parity/dup' }, admin_headers);
159
164
  assert.ok(!dup.ok, 'duplicate path was accepted');
160
165
  assert.strictEqual(error_reason(dup), 'cell_path_taken');
161
166
  });
@@ -164,11 +169,11 @@ export const describe_cell_crud_cross_tests = (options) => {
164
169
  const owner = await fixture.create_account({ username: 'cell_path_owner' });
165
170
  const t = fixture.fresh_transport();
166
171
  const owner_headers = owner.create_session_headers();
167
- const create_with_path = await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' }, path: 'parity/forbidden' }, owner_headers);
172
+ const create_with_path = await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {}, path: 'parity/forbidden' }, owner_headers);
168
173
  assert.ok(!create_with_path.ok, 'non-admin set a path on create');
169
174
  assert.strictEqual(error_reason(create_with_path), 'cell_path_admin_only');
170
175
  // Even owning the cell, a non-admin cannot write `path` on update.
171
- const owned = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_headers), CellCreateOutput).cell;
176
+ const owned = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner_headers), CellCreateOutput).cell;
172
177
  const update_path = await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id: owned.id, path: 'parity/owned' }, owner_headers);
173
178
  assert.ok(!update_path.ok, 'non-admin set a path on update');
174
179
  assert.strictEqual(error_reason(update_path), 'cell_path_admin_only');
@@ -187,5 +192,42 @@ export const describe_cell_crud_cross_tests = (options) => {
187
192
  assert.ok(!bad.ok, 'anon created_by filter accepted');
188
193
  assert.strictEqual(error_reason(bad), 'cell_list_created_by_requires_auth');
189
194
  });
195
+ test_if(capabilities.cell_crud, 'authz before input-shape: a non-editor update with kind-in-data → 404, not the kind error', async () => {
196
+ const fixture = await setup_test();
197
+ const owner = await fixture.create_account({ username: 'cell_order_owner' });
198
+ const other = await fixture.create_account({ username: 'cell_order_other' });
199
+ const t = fixture.fresh_transport();
200
+ const priv = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner.create_session_headers()), CellCreateOutput).cell;
201
+ // `can_edit` (404 IDOR mask) must fire before `reject_kind_in_data`
202
+ // on both spines — a denied caller can't tell a malformed payload
203
+ // from a missing cell.
204
+ const edit = await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id: priv.id, data: { kind: 'post' } }, other.create_session_headers());
205
+ assert.ok(!edit.ok, 'non-editor reached the kind check');
206
+ assert.strictEqual(error_reason(edit), 'cell_not_found');
207
+ });
208
+ test_if(capabilities.cell_crud, 'authz before input-shape: a non-admin create with path + kind-in-data → 403, not the kind error', async () => {
209
+ const fixture = await setup_test();
210
+ const owner = await fixture.create_account({ username: 'cell_order_path' });
211
+ const t = fixture.fresh_transport();
212
+ // The path-admin gate (403) fires before `reject_kind_in_data`.
213
+ const bad = await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' }, path: 'parity/order' }, owner.create_session_headers());
214
+ assert.ok(!bad.ok, 'non-admin path write was not refused first');
215
+ assert.strictEqual(error_reason(bad), 'cell_path_admin_only');
216
+ });
217
+ test_if(capabilities.cell_crud, 'empty kind: rejected on create (cell_kind_empty); accepted as a filter that matches nothing', async () => {
218
+ const fixture = await setup_test();
219
+ const owner = await fixture.create_account({ username: 'cell_kind_empty' });
220
+ const t = fixture.fresh_transport();
221
+ const owner_headers = owner.create_session_headers();
222
+ const empty = await cross_rpc_call(t, rpc_path, 'cell_create', { kind: '', data: {} }, owner_headers);
223
+ assert.ok(!empty.ok, 'an empty kind was stored');
224
+ assert.strictEqual(error_reason(empty), 'cell_kind_empty');
225
+ // The list filter accepts '' on both spines (TS dropped the
226
+ // min-length Rust never had) and matches nothing — no cell carries
227
+ // an empty kind.
228
+ const note = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner_headers), CellCreateOutput).cell;
229
+ const listed = expect_output(await cross_rpc_call(t, rpc_path, 'cell_list', { kind: '' }, owner_headers), CellListOutput);
230
+ assert.ok(!listed.cells.some((c) => c.id === note.id), 'empty-kind filter matched a non-empty-kind cell');
231
+ });
190
232
  });
191
233
  };
@@ -0,0 +1,28 @@
1
+ import '../assert_dev_env.js';
2
+ import type { RpcPathCrossSuiteOptions } from './setup.js';
3
+ export declare const describe_cell_gated_create_cross_tests: (options: RpcPathCrossSuiteOptions) => void;
4
+ /**
5
+ * Cross-backend parity for the **`cell_moderate` verb** (the `pending →
6
+ * approved | rejected` transition) — root-authority-gated.
7
+ *
8
+ * Builds a moderated public space (admin) + a `participant`'s `post` (born
9
+ * `pending` + private under the `moderation_required: true` policy), then
10
+ * proves both spines agree:
11
+ *
12
+ * - **a root manager (admin) approves** → `moderation: 'approved'` +
13
+ * `visibility: 'public'` (the contribution goes live).
14
+ * - **the author cannot self-approve** → `403 cell_moderate_forbidden` (the
15
+ * author can *view* their own pending cell, so they reach the gate, and are
16
+ * denied — moderation authority is over the governing root, not the
17
+ * contribution).
18
+ * - **a non-viewer is 404-masked** → a stranger who can't see the pending
19
+ * contribution gets `cell_not_found`, never learns it exists.
20
+ * - **reject** → `moderation: 'rejected'`, visibility stays private.
21
+ *
22
+ * Gated on `capabilities.cell_gated_create` — a `pending` contribution only
23
+ * exists when the directory authorizer is mounted (reference spine binaries).
24
+ *
25
+ * `$lib`-free by contract (relative specifiers only).
26
+ */
27
+ export declare const describe_cell_moderate_cross_tests: (options: RpcPathCrossSuiteOptions) => void;
28
+ //# sourceMappingURL=cell_gated_create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiD9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAkBzD,eAAO,MAAM,sCAAsC,GAAI,SAAS,wBAAwB,KAAG,IAwP1F,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kCAAkC,GAAI,SAAS,wBAAwB,KAAG,IAwHtF,CAAC"}
@@ -0,0 +1,213 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend parity suite for the **parent-aware cell-creation authorizer**
4
+ * (`CellCreateAuthorize`) — the directory model.
5
+ *
6
+ * The authorizer adds no method, column, or wire shape, so the schema-snapshot
7
+ * and action-manifest parity gates are **blind** to a TS↔Rust authorizer
8
+ * divergence (the authorizer in the wrong phase, a different deny shape, the
9
+ * 404/403 split, the moderation outcome). This behavioral cross case is the
10
+ * only gate that catches one — proven *here in fuz_app*, against both reference
11
+ * spines.
12
+ *
13
+ * Both spines mount the same directory-model policy (the TS spine binary via
14
+ * `create_test_cell_gated_create_authorize`, the Rust `testing_spine_stub` via
15
+ * `TestCellGatedCreateAuthorize`): admin bypasses; a non-admin creating a
16
+ * `kind: 'space'` **root** is denied (admin-only); a **contribution** under a
17
+ * space is gated by the space's `data.policy[kind] = {min_role, moderation_required}`,
18
+ * with the moderation outcome folded into the verdict; plain parentless creates
19
+ * stay open. The suite asserts all spines agree on:
20
+ *
21
+ * - **root-create admin-only** — a non-admin `space` root → **403**
22
+ * `cell_create_forbidden`; admin → succeeds.
23
+ * - **contribution gated by the root's policy** — under a public space, an
24
+ * unauthorized stranger → **403**; a `participant` → succeeds.
25
+ * - **moderation per `moderation_required`** — a `participant`'s `post`
26
+ * (`moderation_required: true`) is born `moderation: 'pending'` + private; a
27
+ * `react` (`moderation_required: false`) is born `moderation: 'approved'` at
28
+ * the author's visibility.
29
+ * - **404 on a hidden parent vs 403 on a visible one** — a contribution under a
30
+ * **private** space the caller can't view → **404** `cell_not_found` (the
31
+ * parent is masked); under a **public** space → **403** (you see it, you
32
+ * can't contribute).
33
+ *
34
+ * Gated on `capabilities.cell_gated_create` — `true` only on the reference
35
+ * spine binaries that mount the policy, so it skips for generic consumers and
36
+ * the in-process default app (the authorizer hook's in-process coverage is the
37
+ * standalone `auth/cell_create_authorize.db.test.ts`).
38
+ *
39
+ * `$lib`-free by contract (relative specifiers only).
40
+ *
41
+ * @module
42
+ */
43
+ import { describe, assert } from 'vitest';
44
+ import { CellCreateOutput, CellModerateOutput } from '../../auth/cell_action_specs.js';
45
+ import { test_if } from './capabilities.js';
46
+ import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
47
+ import { SPINE_RPC_PATH } from './spine_surface_constants.js';
48
+ import { SPACE_CELL_KIND, PARTICIPATION_ROLE } from './test_cell_gated_create_authorize.js';
49
+ /**
50
+ * A space `data.policy` mirroring the test ladder: gated `post` (moderated) +
51
+ * `react` (live) + `comment` (live). `comment` exists so the deep-case test can
52
+ * create a comment under a post (`root_id !== parent_id`) and prove the policy
53
+ * resolves from the governing root (the space), not the immediate parent.
54
+ */
55
+ const SPACE_POLICY = {
56
+ policy: {
57
+ post: { min_role: PARTICIPATION_ROLE, moderation_required: true },
58
+ react: { min_role: PARTICIPATION_ROLE, moderation_required: false },
59
+ comment: { min_role: PARTICIPATION_ROLE, moderation_required: false },
60
+ },
61
+ };
62
+ export const describe_cell_gated_create_cross_tests = (options) => {
63
+ const { setup_test, capabilities } = options;
64
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
65
+ describe('cell_gated_create authorizer parity (directory model)', () => {
66
+ test_if(capabilities.cell_gated_create, 'a non-admin is denied a `space` root (admin-only), admin succeeds', async () => {
67
+ const fixture = await setup_test();
68
+ const stranger = await fixture.create_account({ username: 'space_stranger' });
69
+ const denied = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {} }, stranger.create_session_headers());
70
+ assert.ok(!denied.ok, 'a non-admin must not create a space root');
71
+ assert.strictEqual(error_reason(denied), 'cell_create_forbidden');
72
+ const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
73
+ assert.strictEqual(space.cell.kind, SPACE_CELL_KIND, 'admin creates the space root');
74
+ assert.strictEqual(space.cell.parent_id, null, 'a root is parentless');
75
+ assert.strictEqual(space.cell.root_id, null, 'a root has no governing root');
76
+ assert.strictEqual(space.cell.moderation, null, 'a root carries no moderation marker even under a mounted authorizer (moderation is a contribution concept)');
77
+ });
78
+ test_if(capabilities.cell_gated_create, 'a contribution under a public space: unauthorized → 403, participant → allowed', async () => {
79
+ const fixture = await setup_test();
80
+ const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
81
+ // A non-participant sees the public space but can't contribute → 403.
82
+ const stranger = await fixture.create_account({ username: 'post_stranger' });
83
+ const forbidden = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, stranger.create_session_headers());
84
+ assert.ok(!forbidden.ok, 'a non-participant must not post in the space');
85
+ assert.strictEqual(error_reason(forbidden), 'cell_create_forbidden');
86
+ // A participant may post; the contribution carries the directory edges.
87
+ const participant = await fixture.create_account({
88
+ username: 'post_participant',
89
+ roles: [PARTICIPATION_ROLE],
90
+ });
91
+ const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, participant.create_session_headers()), CellCreateOutput);
92
+ assert.strictEqual(post.cell.parent_id, space.cell.id, 'parent edge set');
93
+ assert.strictEqual(post.cell.root_id, space.cell.id, 'root resolved to the space');
94
+ });
95
+ test_if(capabilities.cell_gated_create, 'moderation is set per `moderation_required` (post → pending+private, react → approved)', async () => {
96
+ const fixture = await setup_test();
97
+ const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
98
+ const participant = await fixture.create_account({
99
+ username: 'mod_participant',
100
+ roles: [PARTICIPATION_ROLE],
101
+ });
102
+ // `post` requires moderation → born pending + forced private.
103
+ const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id, visibility: 'public' }, participant.create_session_headers()), CellCreateOutput);
104
+ assert.strictEqual(post.cell.moderation, 'pending', 'gated post is born pending');
105
+ assert.strictEqual(post.cell.visibility, 'private', 'a pending contribution is forced private until approved');
106
+ // `react` does not require moderation → born approved at the author's visibility.
107
+ const react = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'react', data: {}, parent_id: space.cell.id, visibility: 'public' }, participant.create_session_headers()), CellCreateOutput);
108
+ assert.strictEqual(react.cell.moderation, 'approved', 'unmoderated kind is born approved');
109
+ assert.strictEqual(react.cell.visibility, 'public', "the author's visibility is honored");
110
+ });
111
+ test_if(capabilities.cell_gated_create, 'a contribution under a hidden (private) space is 404-masked (not 403)', async () => {
112
+ const fixture = await setup_test();
113
+ // A private space — only admin/owner can view it.
114
+ const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'private' }, fixture.create_session_headers()), CellCreateOutput);
115
+ const stranger = await fixture.create_account({ username: 'hidden_stranger' });
116
+ const masked = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, stranger.create_session_headers());
117
+ assert.ok(!masked.ok, 'a stranger must not contribute under a hidden space');
118
+ assert.strictEqual(error_reason(masked), 'cell_not_found', 'a hidden parent 404-masks the attempt (never reveals it exists)');
119
+ });
120
+ test_if(capabilities.cell_gated_create, 'a deep contribution (comment under a post) resolves policy from the governing root, not the parent', async () => {
121
+ const fixture = await setup_test();
122
+ const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
123
+ const participant = await fixture.create_account({
124
+ username: 'deep_participant',
125
+ roles: [PARTICIPATION_ROLE],
126
+ });
127
+ // A post lives directly under the space — the shallow case
128
+ // (`root_id === parent_id`), so the handler reuses the parent row as
129
+ // `root_data`. The post is gated `moderation_required: true`, so it's
130
+ // born pending + private; the author can still view their own pending
131
+ // cell, so it's a valid parent for the comment below.
132
+ const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, participant.create_session_headers()), CellCreateOutput);
133
+ assert.strictEqual(post.cell.root_id, space.cell.id, 'a post roots at the space');
134
+ // A comment lives under the post — the DEEP case. Here
135
+ // `root_id !== parent_id`, so the handler must read the governing
136
+ // root (the space) *through* the post (the separate in-tx root read),
137
+ // and the authorizer must resolve `policy.comment` from the SPACE's
138
+ // data, not the post's. This is the only case that exercises the
139
+ // deep-read branch on both spines.
140
+ const comment = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'comment', data: {}, parent_id: post.cell.id }, participant.create_session_headers()), CellCreateOutput);
141
+ assert.strictEqual(comment.cell.parent_id, post.cell.id, 'the immediate parent is the post');
142
+ assert.strictEqual(comment.cell.root_id, space.cell.id, 'the governing root resolves to the space (deep read through the post), not the post');
143
+ assert.strictEqual(comment.cell.moderation, 'approved', 'the comment policy (moderation_required: false) resolved from the space, not the post');
144
+ });
145
+ });
146
+ };
147
+ /**
148
+ * Cross-backend parity for the **`cell_moderate` verb** (the `pending →
149
+ * approved | rejected` transition) — root-authority-gated.
150
+ *
151
+ * Builds a moderated public space (admin) + a `participant`'s `post` (born
152
+ * `pending` + private under the `moderation_required: true` policy), then
153
+ * proves both spines agree:
154
+ *
155
+ * - **a root manager (admin) approves** → `moderation: 'approved'` +
156
+ * `visibility: 'public'` (the contribution goes live).
157
+ * - **the author cannot self-approve** → `403 cell_moderate_forbidden` (the
158
+ * author can *view* their own pending cell, so they reach the gate, and are
159
+ * denied — moderation authority is over the governing root, not the
160
+ * contribution).
161
+ * - **a non-viewer is 404-masked** → a stranger who can't see the pending
162
+ * contribution gets `cell_not_found`, never learns it exists.
163
+ * - **reject** → `moderation: 'rejected'`, visibility stays private.
164
+ *
165
+ * Gated on `capabilities.cell_gated_create` — a `pending` contribution only
166
+ * exists when the directory authorizer is mounted (reference spine binaries).
167
+ *
168
+ * `$lib`-free by contract (relative specifiers only).
169
+ */
170
+ export const describe_cell_moderate_cross_tests = (options) => {
171
+ const { setup_test, capabilities } = options;
172
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
173
+ // Build an admin-owned public space (moderated `post` policy) + a
174
+ // participant's pending post under it.
175
+ const setup_pending_post = async () => {
176
+ const fixture = await setup_test();
177
+ const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
178
+ const author = await fixture.create_account({
179
+ username: 'moderate_author',
180
+ roles: [PARTICIPATION_ROLE],
181
+ });
182
+ const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, author.create_session_headers()), CellCreateOutput);
183
+ return { fixture, author, post };
184
+ };
185
+ describe('cell_moderate verb parity (root-authority-gated)', () => {
186
+ test_if(capabilities.cell_gated_create, 'a root manager (admin) approves a pending post → approved + public', async () => {
187
+ const { fixture, post } = await setup_pending_post();
188
+ assert.strictEqual(post.cell.moderation, 'pending', 'the post is born pending');
189
+ const moderated = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, fixture.create_session_headers()), CellModerateOutput);
190
+ assert.strictEqual(moderated.cell.moderation, 'approved', 'approval sets the marker');
191
+ assert.strictEqual(moderated.cell.visibility, 'public', 'approval publishes the post');
192
+ });
193
+ test_if(capabilities.cell_gated_create, 'the author (non-manager) cannot moderate their own pending post → 403', async () => {
194
+ const { fixture, author, post } = await setup_pending_post();
195
+ const denied = await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, author.create_session_headers());
196
+ assert.ok(!denied.ok, 'the author must not self-approve');
197
+ assert.strictEqual(error_reason(denied), 'cell_moderate_forbidden');
198
+ });
199
+ test_if(capabilities.cell_gated_create, 'a non-viewer is 404-masked from moderating a pending post', async () => {
200
+ const { fixture, post } = await setup_pending_post();
201
+ const stranger = await fixture.create_account({ username: 'moderate_stranger' });
202
+ const masked = await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, stranger.create_session_headers());
203
+ assert.ok(!masked.ok, 'a non-viewer must not moderate');
204
+ assert.strictEqual(error_reason(masked), 'cell_not_found', 'a non-viewer never learns the pending post exists');
205
+ });
206
+ test_if(capabilities.cell_gated_create, 'a root manager rejects a pending post → rejected, stays private', async () => {
207
+ const { fixture, post } = await setup_pending_post();
208
+ const moderated = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'rejected' }, fixture.create_session_headers()), CellModerateOutput);
209
+ assert.strictEqual(moderated.cell.moderation, 'rejected', 'rejection sets the marker');
210
+ assert.strictEqual(moderated.cell.visibility, 'private', 'rejection leaves the post private');
211
+ });
212
+ });
213
+ };
@@ -59,7 +59,7 @@ export const describe_cell_grant_role_cross_tests = (options) => {
59
59
  const stranger = await fixture.create_account({ username: 'cell_role_stranger' });
60
60
  // Owner creates a private cell (default visibility) and grants
61
61
  // view access to anyone holding CELL_EDITOR_ROLE.
62
- const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
62
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner_h), CellCreateOutput);
63
63
  const cell_id = created.cell.id;
64
64
  expect_output(await cross_rpc_call(t, rpc_path, 'cell_grant_create', { cell_id, level: 'viewer', principal: { kind: 'role', role: CELL_EDITOR_ROLE } }, owner_h), CellGrantCreateOutput);
65
65
  // Holder of the role is admitted through the role-shaped grant.
@@ -76,7 +76,7 @@ export const describe_cell_grant_role_cross_tests = (options) => {
76
76
  const t = fixture.transport;
77
77
  const owner = await fixture.create_account({ username: 'cell_unknown_role_owner' });
78
78
  const owner_h = owner.create_session_headers();
79
- const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
79
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner_h), CellCreateOutput);
80
80
  const denied = await cross_rpc_call(t, rpc_path, 'cell_grant_create', {
81
81
  cell_id: created.cell.id,
82
82
  level: 'viewer',
@@ -92,10 +92,10 @@ export const describe_cell_grant_role_cross_tests = (options) => {
92
92
  const owner_h = owner.create_session_headers();
93
93
  const holder = fixture.extra_accounts[CELL_ROLE_HOLDER_USERNAME];
94
94
  assert.ok(holder, `fixture must seed the ${CELL_ROLE_HOLDER_USERNAME} extra account`);
95
- const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
95
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, owner_h), CellCreateOutput);
96
96
  const cell_id = created.cell.id;
97
97
  expect_output(await cross_rpc_call(t, rpc_path, 'cell_grant_create', { cell_id, level: 'editor', principal: { kind: 'role', role: CELL_EDITOR_ROLE } }, owner_h), CellGrantCreateOutput);
98
- const edited = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { kind: 'note', label: 'by role editor' } }, holder.create_session_headers()), CellUpdateOutput);
98
+ const edited = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { label: 'by role editor' } }, holder.create_session_headers()), CellUpdateOutput);
99
99
  assert.strictEqual(edited.cell.updated_by, holder.actor.id, 'edit attributed to the holder');
100
100
  });
101
101
  });
@@ -1 +1 @@
1
- {"version":3,"file":"cell_relations.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_relations.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAqF9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAsFzD,eAAO,MAAM,mCAAmC,GAAI,SAAS,wBAAwB,KAAG,IAyvBvF,CAAC"}
1
+ {"version":3,"file":"cell_relations.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_relations.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAqF9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAsFzD,eAAO,MAAM,mCAAmC,GAAI,SAAS,wBAAwB,KAAG,IA+vBvF,CAAC"}