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