@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.
- package/dist/auth/CLAUDE.md +37 -10
- package/dist/auth/cell_action_specs.d.ts +261 -35
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +102 -9
- package/dist/auth/cell_actions.d.ts +51 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +168 -22
- package/dist/auth/cell_audit_events.d.ts +1 -1
- package/dist/auth/cell_audit_events.d.ts.map +1 -1
- package/dist/auth/cell_audit_events.js +3 -2
- package/dist/auth/cell_audit_metadata.d.ts +15 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
- package/dist/auth/cell_audit_metadata.js +11 -0
- 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 +19 -6
- package/dist/db/cell_ddl.d.ts +24 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +33 -1
- package/dist/db/cell_queries.d.ts +59 -8
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +50 -12
- package/dist/testing/CLAUDE.md +37 -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 +28 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_gated_create.js +213 -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 +4 -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 +9 -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 +22 -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 +75 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAMtB,OAAO,EAEN,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAE3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEN,0BAA0B,EAC1B,6BAA6B,EAC7B,2BAA2B,EAE3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEN,4BAA4B,EAC5B,0BAA0B,EAC1B,4BAA4B,EAC5B,0BAA0B,EAE1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAEN,2BAA2B,EAC3B,MAAM,8BAA8B,CAAC;AAGtC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc;;;EAAgC,CAAC;AAC5D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAO5D,OAAO,EAAC,6BAA6B,EAAE,6BAA6B,EAAE,2BAA2B,EAAC,CAAC;AACnG,OAAO,EAAC,0BAA0B,EAAE,6BAA6B,EAAE,2BAA2B,EAAC,CAAC;AAChG,OAAO,EACN,4BAA4B,EAC5B,0BAA0B,EAC1B,4BAA4B,EAC5B,0BAA0B,GAC1B,CAAC;AACF,OAAO,EAAC,2BAA2B,EAAC,CAAC;AAIrC,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,EAAG,gBAAyB,CAAC;AAE9D,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAC;AAE1E;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,EAAG,iBAA0B,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,EAAG,6BAAsC,CAAC;AAExF,6EAA6E;AAC7E,eAAO,MAAM,kCAAkC,EAAG,8BAAuC,CAAC;AAE1F;;;;;GAKG;AACH,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"cell_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_action_specs.ts"],"names":[],"mappings":"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"}
|
|
@@ -64,12 +64,42 @@ export const ERROR_CELL_VISIBILITY_MANAGE_ONLY = 'cell_visibility_manage_only';
|
|
|
64
64
|
/** Error reason — input shape for `cell_get` lacked both `id` and `path`. */
|
|
65
65
|
export const ERROR_CELL_GET_REQUIRES_ID_OR_PATH = 'cell_get_requires_id_or_path';
|
|
66
66
|
/**
|
|
67
|
-
* Error reason —
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* patch
|
|
67
|
+
* Error reason — a `kind` key was supplied inside `data`. `kind` is a
|
|
68
|
+
* top-level column (`cell.kind`), not content metadata; accepting it inside
|
|
69
|
+
* `data` would create two sources of truth. Rejected fail-loud at the
|
|
70
|
+
* create / update / clone-patch boundary so the column stays canonical.
|
|
71
71
|
*/
|
|
72
|
-
export const
|
|
72
|
+
export const ERROR_CELL_KIND_IN_DATA = 'cell_kind_in_data';
|
|
73
|
+
/**
|
|
74
|
+
* Error reason — `cell_create` was given an empty-string `kind`. `kind` is a
|
|
75
|
+
* non-empty capability tag or absent (`null` = typeless cell); `""` (a tag
|
|
76
|
+
* that tags nothing) is rejected fail-loud so `kind` stays a clean
|
|
77
|
+
* `null | non-empty-string` (twin of the Rust `ERROR_CELL_KIND_EMPTY`).
|
|
78
|
+
*/
|
|
79
|
+
export const ERROR_CELL_KIND_EMPTY = 'cell_kind_empty';
|
|
80
|
+
/**
|
|
81
|
+
* Error reason — the parent-aware `CellCreateAuthorize` denied the create for a
|
|
82
|
+
* **viewable** parent (or a root creation): "you see it, you can't contribute
|
|
83
|
+
* here." A **403** `forbidden`, distinct from the create-path **404** mask used
|
|
84
|
+
* when the parent itself isn't viewable (twin of the Rust
|
|
85
|
+
* `ERROR_CELL_CREATE_FORBIDDEN`).
|
|
86
|
+
*/
|
|
87
|
+
export const ERROR_CELL_CREATE_FORBIDDEN = 'cell_create_forbidden';
|
|
88
|
+
/**
|
|
89
|
+
* Error reason — `cell_moderate` caller can view the contribution but lacks
|
|
90
|
+
* moderation authority over its governing root (not admin / not the root's
|
|
91
|
+
* manager). A **403** `forbidden`. The author can view their own pending
|
|
92
|
+
* contribution, so they reach this gate — and are denied here, which is the
|
|
93
|
+
* anti-self-approval guard (twin of the Rust `ERROR_CELL_MODERATE_FORBIDDEN`).
|
|
94
|
+
*/
|
|
95
|
+
export const ERROR_CELL_MODERATE_FORBIDDEN = 'cell_moderate_forbidden';
|
|
96
|
+
/**
|
|
97
|
+
* Error reason — `cell_moderate` target has no governing root (`root_id` is
|
|
98
|
+
* null): it's a root or an unparented cell, not a gated contribution, so there
|
|
99
|
+
* is nothing to moderate. A **400** `invalid_params` (twin of the Rust
|
|
100
|
+
* `ERROR_CELL_NOT_A_CONTRIBUTION`).
|
|
101
|
+
*/
|
|
102
|
+
export const ERROR_CELL_NOT_A_CONTRIBUTION = 'cell_not_a_contribution';
|
|
73
103
|
/**
|
|
74
104
|
* Error reason — null-auth `cell_list` caller passed a `created_by`
|
|
75
105
|
* filter. The filter is a soft account-id enumeration vector ("does
|
|
@@ -136,10 +166,22 @@ export const CellJson = z.strictObject({
|
|
|
136
166
|
id: Uuid,
|
|
137
167
|
path: CellPath.nullable(),
|
|
138
168
|
data: CellData,
|
|
169
|
+
kind: z.string().nullable().meta({
|
|
170
|
+
description: 'Capability / identity tag — a write-once top-level column (peer to `visibility` / `path`), not a field inside `data`. `null` for a typeless cell. The discriminator a creation authorizer gates on; fixed at birth.',
|
|
171
|
+
}),
|
|
139
172
|
visibility: CellVisibility.meta({
|
|
140
173
|
description: "Access-control tag. `'public'` admits everyone (including unauthenticated visitors); `'private'` (default) admits admin / owner / `cell_grant`-admitted callers. Top-level column, not inside `data`.",
|
|
141
174
|
}),
|
|
142
175
|
refs: z.array(FactHashSchema).nullable(),
|
|
176
|
+
parent_id: Uuid.nullable().meta({
|
|
177
|
+
description: 'Immediate container in the directory tree (the directory this cell lives in). `null` = a root (parentless). Set once at create, immutable in v1.',
|
|
178
|
+
}),
|
|
179
|
+
root_id: Uuid.nullable().meta({
|
|
180
|
+
description: 'Governing root of the directory subtree, denormalized for flat-subtree queries (`root_id = parent.root_id ?? parent.id`). `null` for a root. Set once at create, immutable in v1.',
|
|
181
|
+
}),
|
|
182
|
+
moderation: z.string().nullable().meta({
|
|
183
|
+
description: "Approval-lifecycle marker for a gated contribution (`'pending'` / `'approved'` / `'rejected'`); `null` = unmoderated. A control field with a non-author writer — peer to `visibility`, never inside `data` (set by the create authorizer's verdict, transitioned only by `cell_moderate`).",
|
|
184
|
+
}),
|
|
143
185
|
created_by: Uuid.nullable(),
|
|
144
186
|
updated_by: Uuid.nullable(),
|
|
145
187
|
created_at: z.string(),
|
|
@@ -157,7 +199,10 @@ export const CellJson = z.strictObject({
|
|
|
157
199
|
*/
|
|
158
200
|
export const CellCreateInput = z.strictObject({
|
|
159
201
|
data: CellData.meta({
|
|
160
|
-
description: 'Cell
|
|
202
|
+
description: 'Cell content. Base fields (label / summary) typed; extras loose. A `kind` key here is rejected (`ERROR_CELL_KIND_IN_DATA`) — kind is the top-level field. Per-kind shape is sub-API.',
|
|
203
|
+
}),
|
|
204
|
+
kind: z.string().nullish().meta({
|
|
205
|
+
description: 'Capability / identity tag, written to the top-level `cell.kind` column (not `data`). Write-once — fixed at birth, not updatable. Non-empty when present (an empty string is rejected `ERROR_CELL_KIND_EMPTY`); omit / null for a typeless cell. A creation authorizer (if mounted) gates on this.',
|
|
161
206
|
}),
|
|
162
207
|
visibility: CellVisibility.optional().meta({
|
|
163
208
|
description: "Access-control tag. Top-level column (not in `data`). Default `'private'` when omitted.",
|
|
@@ -165,6 +210,9 @@ export const CellCreateInput = z.strictObject({
|
|
|
165
210
|
path: CellPath.nullish().meta({
|
|
166
211
|
description: 'Admin-only named lookup alias. Globally unique on active rows.',
|
|
167
212
|
}),
|
|
213
|
+
parent_id: Uuid.nullish().meta({
|
|
214
|
+
description: 'Immediate container — the directory this cell is created in. Omit / null = a root (parentless). The handler resolves `root_id` from it and gates the create through the parent-aware authorizer; `root_id` / `moderation` are handler-derived, never wire input.',
|
|
215
|
+
}),
|
|
168
216
|
acting: ActingActor,
|
|
169
217
|
});
|
|
170
218
|
export const CellCreateOutput = z.strictObject({ cell: CellJson });
|
|
@@ -245,7 +293,7 @@ export const CellDeleteOutput = z.strictObject({
|
|
|
245
293
|
* `shared_with: 'me'` narrows to cells that admit the caller via a
|
|
246
294
|
* `cell_grant` row (actor-shaped or role-shaped principal) AND that
|
|
247
295
|
* the caller does not own. Authenticated only; combine with
|
|
248
|
-
* `
|
|
296
|
+
* `kind` / `path_prefix` etc. to scope further. Combining with
|
|
249
297
|
* `created_by: <my-actor-id>` produces an empty result by definition
|
|
250
298
|
* (owner is implicit, never appears as a grant principal); we don't
|
|
251
299
|
* reject the combination at the schema layer because SQL emptiness is
|
|
@@ -258,7 +306,9 @@ export const CellListInput = z
|
|
|
258
306
|
.max(CELL_LIST_LIMIT_MAX)
|
|
259
307
|
.optional()
|
|
260
308
|
.meta({ description: 'Batch-fetch by id. Visibility predicate still applies.' }),
|
|
261
|
-
|
|
309
|
+
kind: z.string().optional().meta({
|
|
310
|
+
description: 'Match `cell.kind = ?`. An empty string matches nothing — no cell carries an empty kind.',
|
|
311
|
+
}),
|
|
262
312
|
visibility: CellVisibility.optional().meta({
|
|
263
313
|
description: "Match `cell.visibility = ?`. The SQL-side auth-narrow already filters to public-or-admitted; this is an additional narrowing filter (e.g. `visibility: 'public'` on the discovery feed so authed callers don't see their own private entries mixed in).",
|
|
264
314
|
}),
|
|
@@ -267,6 +317,12 @@ export const CellListInput = z
|
|
|
267
317
|
path_prefix: CellPath.optional().meta({
|
|
268
318
|
description: 'Match cells whose path starts with this.',
|
|
269
319
|
}),
|
|
320
|
+
root_id: Uuid.optional().meta({
|
|
321
|
+
description: 'Scope to a directory subtree by governing root (`cell.root_id = ?`) — the feed + mod-queue key.',
|
|
322
|
+
}),
|
|
323
|
+
moderation: z.string().optional().meta({
|
|
324
|
+
description: "Filter by moderation lifecycle marker (`'pending'` drives the moderation queue).",
|
|
325
|
+
}),
|
|
270
326
|
shared_with: z.literal('me').optional().meta({
|
|
271
327
|
description: 'Narrow to cells admitting the caller via `cell_grant`, excluding cells the caller owns. Self-only — a `Uuid` form would need cross-actor role_grant loading.',
|
|
272
328
|
}),
|
|
@@ -305,6 +361,30 @@ export const CellCloneInput = z.strictObject({
|
|
|
305
361
|
acting: ActingActor,
|
|
306
362
|
});
|
|
307
363
|
export const CellCloneOutput = z.strictObject({ cell: CellJson });
|
|
364
|
+
// -- cell_moderate ----------------------------------------------------------
|
|
365
|
+
/**
|
|
366
|
+
* The terminal transition a `cell_moderate` call applies to a gated
|
|
367
|
+
* contribution's `moderation` lifecycle. The born state (`'pending'`) is set
|
|
368
|
+
* by the create authorizer, never on the wire; the verb only moves it to a
|
|
369
|
+
* terminal state. `'approved'` publishes (also flips `visibility → 'public'`);
|
|
370
|
+
* `'rejected'` leaves it private.
|
|
371
|
+
*/
|
|
372
|
+
export const CellModerationDecision = z.enum(['approved', 'rejected']);
|
|
373
|
+
/**
|
|
374
|
+
* Input for `cell_moderate` — the `pending → approved | rejected` transition,
|
|
375
|
+
* gated on moderation authority over the **governing root** (admin / root
|
|
376
|
+
* owner in v1), not the contribution (which the author manages, and could
|
|
377
|
+
* otherwise self-approve). 404 when the target isn't viewable; 403 when it is
|
|
378
|
+
* but the caller isn't a root manager (the author lands here).
|
|
379
|
+
*/
|
|
380
|
+
export const CellModerateInput = z.strictObject({
|
|
381
|
+
cell_id: Uuid.meta({ description: 'Contribution to moderate.' }),
|
|
382
|
+
moderation: CellModerationDecision.meta({
|
|
383
|
+
description: 'Terminal decision: `approved` (publishes, visibility → public) or `rejected`.',
|
|
384
|
+
}),
|
|
385
|
+
acting: ActingActor,
|
|
386
|
+
});
|
|
387
|
+
export const CellModerateOutput = z.strictObject({ cell: CellJson });
|
|
308
388
|
// -- Action specs -----------------------------------------------------------
|
|
309
389
|
export const cell_create_action_spec = {
|
|
310
390
|
method: 'cell_create',
|
|
@@ -376,9 +456,21 @@ export const cell_clone_action_spec = {
|
|
|
376
456
|
rate_limit: 'account',
|
|
377
457
|
description: 'Clone a cell (optionally deep). New owner is the caller; `path` is always nulled. Provenance recorded only in the `cell_clone` audit row. Per-account rate-limited — `deep: true` walks `cell_item` rows and fans out, so unbounded clone is a write-amplification vector.',
|
|
378
458
|
};
|
|
459
|
+
export const cell_moderate_action_spec = {
|
|
460
|
+
method: 'cell_moderate',
|
|
461
|
+
kind: 'request_response',
|
|
462
|
+
initiator: 'frontend',
|
|
463
|
+
auth: { account: 'required', actor: 'required' },
|
|
464
|
+
side_effects: true,
|
|
465
|
+
input: CellModerateInput,
|
|
466
|
+
output: CellModerateOutput,
|
|
467
|
+
async: true,
|
|
468
|
+
rate_limit: 'account',
|
|
469
|
+
description: 'Moderate a gated contribution (`pending → approved | rejected`). Gated on moderation authority over the governing root (admin / root owner), not the contribution — so the author cannot self-approve. Approval flips `moderation → approved` + `visibility → public`; rejection sets `moderation → rejected`. 404 on an unviewable target, 403 for a viewable target whose caller is not a root manager.',
|
|
470
|
+
};
|
|
379
471
|
/**
|
|
380
472
|
* All cell-layer action specs — composed by app registries. Bundles the
|
|
381
|
-
*
|
|
473
|
+
* seven generic verbs (this module), the three `cell_grant_*` specs, the
|
|
382
474
|
* three `cell_field_*` specs, the four `cell_item_*` specs, and the
|
|
383
475
|
* `cell_audit_list` spec so codegen + UI clients see a single cell
|
|
384
476
|
* namespace.
|
|
@@ -390,6 +482,7 @@ export const all_cell_action_specs = [
|
|
|
390
482
|
cell_delete_action_spec,
|
|
391
483
|
cell_list_action_spec,
|
|
392
484
|
cell_clone_action_spec,
|
|
485
|
+
cell_moderate_action_spec,
|
|
393
486
|
...all_cell_grant_action_specs,
|
|
394
487
|
...all_cell_field_action_specs,
|
|
395
488
|
...all_cell_item_action_specs,
|
|
@@ -39,10 +39,57 @@
|
|
|
39
39
|
* @module
|
|
40
40
|
*/
|
|
41
41
|
import { type RpcAction } from '../actions/action_rpc.js';
|
|
42
|
+
import { type RequestActorContext } from './request_context.js';
|
|
42
43
|
import type { RouteFactoryDeps } from './deps.js';
|
|
44
|
+
import type { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
43
45
|
import { type CellJson } from './cell_action_specs.js';
|
|
44
46
|
import { type CellRow } from '../db/cell_queries.js';
|
|
45
47
|
import type { CellData } from './cell_data_schema.js';
|
|
48
|
+
/**
|
|
49
|
+
* Input to a `CellCreateAuthorize` callback — the TS twin of the Rust
|
|
50
|
+
* `CellCreateAuthorizeInput`. **Parent-aware**: it carries the directory
|
|
51
|
+
* context (`parent_id` / the handler-resolved `root_id`) so the authorizer can
|
|
52
|
+
* resolve the governing root's policy.
|
|
53
|
+
*/
|
|
54
|
+
export interface CellCreateAuthorizeInput {
|
|
55
|
+
/** The cell's `kind` (the top-level `cell.kind` value); `null` for a typeless cell. */
|
|
56
|
+
kind: string | null;
|
|
57
|
+
/** The cell `data` (kind-free — a `kind` key is rejected upstream). For richer M3-era policies (e.g. content pre-screen). */
|
|
58
|
+
data: CellData;
|
|
59
|
+
/** The immediate container the create targets. `null` = a root creation; otherwise a contribution under that parent. */
|
|
60
|
+
parent_id: Uuid | null;
|
|
61
|
+
/** The governing root of the directory subtree, resolved by the handler from the parent (`parent.root_id ?? parent.id`). `null` for a root creation. */
|
|
62
|
+
root_id: Uuid | null;
|
|
63
|
+
/** The governing root's `data` — the handler reads it in-tx (when an authorizer is mounted) and hands it over, so a directory-aware authorizer resolves `root.data.policy[kind]` **without a DB read of its own** (pure predicate; reading in-tx avoids the single-connection PGlite deadlock a separate handle would hit). `null` for a root creation, or when no authorizer is mounted. */
|
|
64
|
+
root_data: CellData | null;
|
|
65
|
+
/** Target scope — designed-in for M2 space-scoping; **always `null` in v1** (cells carry no scope column). */
|
|
66
|
+
scope_id: Uuid | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* An authorizer's decision for a `cell_create` — the TS twin of the Rust
|
|
70
|
+
* `Verdict`. Folds the moderation outcome into the authority decision (one
|
|
71
|
+
* policy resolution, not two): `{allow: false}` denies (the handler surfaces a
|
|
72
|
+
* 403 `forbidden` for a viewable parent / a root creation), `{allow: true,
|
|
73
|
+
* moderation_required}` admits — `true` → born `pending` + private, `false` →
|
|
74
|
+
* born `approved` at the author's visibility.
|
|
75
|
+
*/
|
|
76
|
+
export type CellCreateVerdict = {
|
|
77
|
+
allow: false;
|
|
78
|
+
} | {
|
|
79
|
+
allow: true;
|
|
80
|
+
moderation_required: boolean;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Opt-in, parent-aware creation authorizer — the TS twin of the Rust
|
|
84
|
+
* `CellCreateAuthorize` trait. Gates both roots (`parent_id = null`) and
|
|
85
|
+
* contributions; answers "may *this actor* create *this kind* here?" and
|
|
86
|
+
* returns a `CellCreateVerdict`. Runs in `cell_create` after `validate_data`
|
|
87
|
+
* and after the handler resolves `root_id` from the parent (an unviewable
|
|
88
|
+
* parent already 404-masks before this runs). Omitted = today's open create
|
|
89
|
+
* (all consumers untouched). Async-capable (DB / policy calls) — a DB-backed
|
|
90
|
+
* impl closes over its own `db` (the create handler stays unaware).
|
|
91
|
+
*/
|
|
92
|
+
export type CellCreateAuthorize = (auth: RequestActorContext, input: CellCreateAuthorizeInput) => CellCreateVerdict | Promise<CellCreateVerdict>;
|
|
46
93
|
/**
|
|
47
94
|
* Dependencies for `create_cell_actions`.
|
|
48
95
|
*
|
|
@@ -53,9 +100,13 @@ import type { CellData } from './cell_data_schema.js';
|
|
|
53
100
|
* `invalid_params` JSON-RPC error so per-kind validation failures
|
|
54
101
|
* surface to clients with code -32602 (not -32603 / internal). When
|
|
55
102
|
* omitted, payloads pass through as-is.
|
|
103
|
+
*
|
|
104
|
+
* `authorize_create` is the optional creation-gate hook (see
|
|
105
|
+
* `CellCreateAuthorize`). When omitted, create is open (today's behavior).
|
|
56
106
|
*/
|
|
57
107
|
export type CellActionDeps = Pick<RouteFactoryDeps, 'log' | 'audit'> & {
|
|
58
108
|
validate_data?: (data: CellData) => CellData;
|
|
109
|
+
authorize_create?: CellCreateAuthorize;
|
|
59
110
|
};
|
|
60
111
|
export declare const to_cell_json: (row: CellRow) => CellJson;
|
|
61
112
|
/** Create the six generic cell RPC actions. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAW,KAAK,mBAAmB,EAAC,MAAM,sBAAsB,CAAC;AAExE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAoCN,KAAK,QAAQ,EAEb,MAAM,wBAAwB,CAAC;AAChC,OAAO,EASN,KAAK,OAAO,EACZ,MAAM,uBAAuB,CAAC;AAsB/B,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,uBAAuB,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACxC,uFAAuF;IACvF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,6HAA6H;IAC7H,IAAI,EAAE,QAAQ,CAAC;IACf,wHAAwH;IACxH,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,wJAAwJ;IACxJ,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,6XAA6X;IAC7X,SAAS,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,8GAA8G;IAC9G,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAC,KAAK,EAAE,KAAK,CAAA;CAAC,GAAG;IAAC,KAAK,EAAE,IAAI,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAC,CAAC;AAE7F;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,CACjC,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,wBAAwB,KAC3B,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG;IACtE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,QAAQ,CAAC;IAC7C,gBAAgB,CAAC,EAAE,mBAAmB,CAAC;CACvC,CAAC;AAkCF,eAAO,MAAM,YAAY,GAAI,KAAK,OAAO,KAAG,QAgB1C,CAAC;AA2BH,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB,GAAI,MAAM,cAAc,KAAG,KAAK,CAAC,SAAS,CAinBzE,CAAC"}
|
|
@@ -44,8 +44,8 @@ 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,
|
|
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';
|
|
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, cell_moderate_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_CREATE_FORBIDDEN, ERROR_CELL_MODERATE_FORBIDDEN, ERROR_CELL_NOT_A_CONTRIBUTION, 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
|
+
import { query_cell_create, query_cell_get, query_cell_get_by_path, query_cell_update, query_cell_delete, query_cell_set_moderation, 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';
|
|
51
51
|
import { query_cell_item_insert, query_cell_item_list_for_parent, } from '../db/cell_item_queries.js';
|
|
@@ -66,12 +66,30 @@ 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,
|
|
90
|
+
parent_id: row.parent_id,
|
|
91
|
+
root_id: row.root_id,
|
|
92
|
+
moderation: row.moderation,
|
|
75
93
|
created_by: row.created_by,
|
|
76
94
|
updated_by: row.updated_by,
|
|
77
95
|
created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString(),
|
|
@@ -92,14 +110,14 @@ const emit_cell_audit = (ctx, event_type, row, deps, auth) => {
|
|
|
92
110
|
ip: ctx.client_ip,
|
|
93
111
|
metadata: {
|
|
94
112
|
cell_id: row.id,
|
|
95
|
-
kind: row.
|
|
113
|
+
kind: row.kind ?? undefined,
|
|
96
114
|
path: row.path,
|
|
97
115
|
},
|
|
98
116
|
});
|
|
99
117
|
};
|
|
100
118
|
/** Create the six generic cell RPC actions. */
|
|
101
119
|
export const create_cell_actions = (deps) => {
|
|
102
|
-
const { validate_data } = deps;
|
|
120
|
+
const { validate_data, authorize_create } = deps;
|
|
103
121
|
/**
|
|
104
122
|
* Run the optional `validate_data` deps callback and convert any thrown
|
|
105
123
|
* `ZodError` into the standard `invalid_params` JSON-RPC error so per-
|
|
@@ -129,9 +147,88 @@ export const create_cell_actions = (deps) => {
|
|
|
129
147
|
reason: ERROR_CELL_PATH_ADMIN_ONLY,
|
|
130
148
|
});
|
|
131
149
|
}
|
|
150
|
+
// `kind` is the top-level column — reject a stray copy inside `data`,
|
|
151
|
+
// and reject an empty `kind` (a tag tags nothing — `null` is the typeless
|
|
152
|
+
// state), so `kind` stays a clean `null | non-empty-string`. Both run
|
|
153
|
+
// after the path-admin gate so an unauthorized caller sees that first.
|
|
154
|
+
reject_kind_in_data(input.data);
|
|
155
|
+
if (input.kind === '') {
|
|
156
|
+
throw jsonrpc_errors.invalid_params('cell.kind must not be empty (use null for a typeless cell)', { reason: ERROR_CELL_KIND_EMPTY });
|
|
157
|
+
}
|
|
132
158
|
// Per-kind shape validation (sub-API). Unknown kinds pass through;
|
|
133
159
|
// known kinds with malformed payloads surface as invalid_params.
|
|
134
160
|
const validated_data = validate_data_or_throw(input.data);
|
|
161
|
+
const kind = input.kind ?? null;
|
|
162
|
+
// Directory tree: resolve the governing `root_id` from the parent (and
|
|
163
|
+
// the root's `data` for the authorizer's policy read). Runs whether or
|
|
164
|
+
// not an authorizer is mounted — `root_id` is written regardless, and an
|
|
165
|
+
// unviewable parent 404-masks the attempt (the same IDOR mask the read
|
|
166
|
+
// path uses; never reveals a hidden container). A root creation
|
|
167
|
+
// (`parent_id` null/absent) has `root_id = null`. `root_data` is read
|
|
168
|
+
// **in-tx** (only when an authorizer is mounted), reusing the parent row
|
|
169
|
+
// when the parent *is* the root, else one read by `root_id` — so the
|
|
170
|
+
// authorizer stays pure + the read rides the handler's connection (no
|
|
171
|
+
// separate-handle deadlock on the single-connection PGlite).
|
|
172
|
+
const parent_id = input.parent_id ?? null;
|
|
173
|
+
let root_id = null;
|
|
174
|
+
let root_data = null;
|
|
175
|
+
if (parent_id !== null) {
|
|
176
|
+
const parent = await query_cell_get(ctx, parent_id);
|
|
177
|
+
if (!parent)
|
|
178
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
179
|
+
const parent_grants = await query_cell_grant_list_for_cell(ctx, parent.id);
|
|
180
|
+
if (!can_view_cell(auth, parent, parent_grants)) {
|
|
181
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
182
|
+
}
|
|
183
|
+
root_id = parent.root_id ?? parent.id;
|
|
184
|
+
if (authorize_create) {
|
|
185
|
+
root_data =
|
|
186
|
+
root_id === parent.id
|
|
187
|
+
? parent.data
|
|
188
|
+
: ((await query_cell_get(ctx, root_id))?.data ?? null);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Parent-aware creation authorizer (opt-in). Runs after shape validation
|
|
192
|
+
// and after the parent-resolve (a hidden parent already 404-masked).
|
|
193
|
+
// `{allow: false}` for a *viewable* parent / a root creation is a 403
|
|
194
|
+
// `forbidden` (not the 404 mask); an `{allow: true}` verdict folds the
|
|
195
|
+
// moderation outcome. Receives the **raw** `input.data` (not the
|
|
196
|
+
// `validate_data`-normalized value) to match the Rust twin, so a consumer
|
|
197
|
+
// with a transforming validator can't diverge TS↔Rust. `scope_id` is
|
|
198
|
+
// `null` in v1. Without an authorizer, create is open: `moderation` stays
|
|
199
|
+
// null and the author's visibility holds.
|
|
200
|
+
let moderation = null;
|
|
201
|
+
let visibility = input.visibility;
|
|
202
|
+
if (authorize_create) {
|
|
203
|
+
const verdict = await authorize_create(auth, {
|
|
204
|
+
kind,
|
|
205
|
+
data: input.data,
|
|
206
|
+
parent_id,
|
|
207
|
+
root_id,
|
|
208
|
+
root_data,
|
|
209
|
+
scope_id: null,
|
|
210
|
+
});
|
|
211
|
+
if (!verdict.allow) {
|
|
212
|
+
throw jsonrpc_errors.forbidden('cell creation is not permitted here', {
|
|
213
|
+
reason: ERROR_CELL_CREATE_FORBIDDEN,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// Moderation is a *contribution* concept — only stamp it for a create
|
|
217
|
+
// with a parent. A root / unparented create stays `moderation = null`
|
|
218
|
+
// at the author's visibility even under a mounted authorizer (there is
|
|
219
|
+
// no container to be moderated under).
|
|
220
|
+
if (parent_id !== null) {
|
|
221
|
+
if (verdict.moderation_required) {
|
|
222
|
+
// Born pending: stays private until `cell_moderate` approves it.
|
|
223
|
+
moderation = 'pending';
|
|
224
|
+
visibility = 'private';
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Born approved + live immediately at the author's visibility.
|
|
228
|
+
moderation = 'approved';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
135
232
|
let row;
|
|
136
233
|
try {
|
|
137
234
|
row = await query_cell_create(ctx, {
|
|
@@ -141,8 +238,12 @@ export const create_cell_actions = (deps) => {
|
|
|
141
238
|
// extend `Json`. The DB column is JSONB; the cast trusts the
|
|
142
239
|
// schema-validated runtime shape.
|
|
143
240
|
data: validated_data,
|
|
144
|
-
|
|
241
|
+
kind,
|
|
242
|
+
visibility,
|
|
145
243
|
path: input.path ?? null,
|
|
244
|
+
parent_id,
|
|
245
|
+
root_id,
|
|
246
|
+
moderation,
|
|
146
247
|
created_by: auth.actor.id,
|
|
147
248
|
});
|
|
148
249
|
}
|
|
@@ -254,7 +355,11 @@ export const create_cell_actions = (deps) => {
|
|
|
254
355
|
// Per-kind shape validation when `data` is supplied. Patch-only —
|
|
255
356
|
// we don't validate the existing row's data on update (validation
|
|
256
357
|
// is for incoming patches). `data` writes fully replace, so the
|
|
257
|
-
// patch IS the post-update state.
|
|
358
|
+
// patch IS the post-update state. A stray `kind` inside it is rejected
|
|
359
|
+
// (kind is the write-once top-level column — `cell_update` cannot
|
|
360
|
+
// change it, structurally: it is not even a field on `CellUpdateInput`).
|
|
361
|
+
if (input.data !== undefined)
|
|
362
|
+
reject_kind_in_data(input.data);
|
|
258
363
|
const validated_data = input.data !== undefined ? validate_data_or_throw(input.data) : undefined;
|
|
259
364
|
let updated;
|
|
260
365
|
try {
|
|
@@ -299,6 +404,51 @@ export const create_cell_actions = (deps) => {
|
|
|
299
404
|
emit_cell_audit(ctx, 'cell_delete', existing, deps, auth);
|
|
300
405
|
return { ok: true, deleted: true };
|
|
301
406
|
};
|
|
407
|
+
const moderate_handler = async (input, ctx) => {
|
|
408
|
+
const auth = ctx.auth;
|
|
409
|
+
const existing = await query_cell_get(ctx, input.cell_id);
|
|
410
|
+
// 404 covers miss + unviewable so a pending (private) contribution
|
|
411
|
+
// doesn't leak existence to a non-viewer.
|
|
412
|
+
if (!existing)
|
|
413
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
414
|
+
const grants = await query_cell_grant_list_for_cell(ctx, existing.id);
|
|
415
|
+
if (!can_view_cell(auth, existing, grants)) {
|
|
416
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
417
|
+
}
|
|
418
|
+
// Only a contribution (with a governing root) is moderatable.
|
|
419
|
+
if (existing.root_id === null) {
|
|
420
|
+
throw jsonrpc_errors.invalid_params('cell is not a contribution (no governing root to moderate under)', { reason: ERROR_CELL_NOT_A_CONTRIBUTION });
|
|
421
|
+
}
|
|
422
|
+
// Authority is over the governing ROOT (admin / root owner), not the
|
|
423
|
+
// contribution — the author manages the contribution and could otherwise
|
|
424
|
+
// self-approve. A viewable-but-unauthorized caller (incl. the author) → 403.
|
|
425
|
+
const root = await query_cell_get(ctx, existing.root_id);
|
|
426
|
+
if (!root)
|
|
427
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
428
|
+
if (!can_manage_cell(auth, root)) {
|
|
429
|
+
throw jsonrpc_errors.forbidden('cell_moderate requires moderation authority over the governing root', { reason: ERROR_CELL_MODERATE_FORBIDDEN });
|
|
430
|
+
}
|
|
431
|
+
const approved = input.moderation === 'approved';
|
|
432
|
+
const updated = await query_cell_set_moderation(ctx, input.cell_id, input.moderation, {
|
|
433
|
+
set_visibility_public: approved,
|
|
434
|
+
updated_by: auth.actor.id,
|
|
435
|
+
});
|
|
436
|
+
// Raced with a deleter between the authz check and the UPDATE.
|
|
437
|
+
if (!updated)
|
|
438
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
439
|
+
deps.audit.emit(ctx, {
|
|
440
|
+
event_type: 'cell_moderate',
|
|
441
|
+
actor_id: auth.actor.id,
|
|
442
|
+
account_id: auth.account.id,
|
|
443
|
+
ip: ctx.client_ip,
|
|
444
|
+
metadata: {
|
|
445
|
+
cell_id: updated.id,
|
|
446
|
+
root_id: existing.root_id,
|
|
447
|
+
moderation: input.moderation,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
return { cell: to_cell_json(updated) };
|
|
451
|
+
};
|
|
302
452
|
/**
|
|
303
453
|
* Insert one cloned cell row owned by the caller.
|
|
304
454
|
*
|
|
@@ -312,23 +462,13 @@ export const create_cell_actions = (deps) => {
|
|
|
312
462
|
* server-stamped provenance fields.
|
|
313
463
|
*/
|
|
314
464
|
const clone_one_cell_row = async (ctx, source, auth, options) => {
|
|
465
|
+
// A patch is wire-supplied `data` — reject a stray `kind` (kind is the
|
|
466
|
+
// immutable top-level column; the clone inherits `source.kind`).
|
|
467
|
+
if (options.patch_data !== undefined)
|
|
468
|
+
reject_kind_in_data(options.patch_data);
|
|
315
469
|
// Both `source.data` and `options.patch_data` are CellData (loose
|
|
316
470
|
// objects). Patch-last shallow merge composes cleanly.
|
|
317
471
|
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
472
|
// Per-kind shape validation runs on the merged result (sub-API).
|
|
333
473
|
// Source rows are validated on their original create; the patch
|
|
334
474
|
// could violate the kind shape (e.g., remove a required field).
|
|
@@ -336,6 +476,9 @@ export const create_cell_actions = (deps) => {
|
|
|
336
476
|
return query_cell_create(ctx, {
|
|
337
477
|
// Boundary cast (see `create_handler`).
|
|
338
478
|
data: validated_data,
|
|
479
|
+
// Clone inherits the source's kind verbatim — kind is fixed at
|
|
480
|
+
// birth and a `with_data_patch` can't reach the column.
|
|
481
|
+
kind: source.kind,
|
|
339
482
|
visibility: source.visibility,
|
|
340
483
|
path: null, // admin-only paths cannot auto-clone
|
|
341
484
|
created_by: auth.actor.id,
|
|
@@ -463,7 +606,7 @@ export const create_cell_actions = (deps) => {
|
|
|
463
606
|
// — emit directly rather than threading it through `emit_cell_audit`.
|
|
464
607
|
// `kind` is read from `source.data` (not the cloned row) so the
|
|
465
608
|
// audit trail attributes the clone to the source's shape.
|
|
466
|
-
const source_kind = source.
|
|
609
|
+
const source_kind = source.kind ?? undefined;
|
|
467
610
|
const cloned_child_count = deep ? cloneable_items.length : 0;
|
|
468
611
|
deps.audit.emit(ctx, {
|
|
469
612
|
event_type: 'cell_clone',
|
|
@@ -505,11 +648,13 @@ export const create_cell_actions = (deps) => {
|
|
|
505
648
|
const caller_actor_id = auth?.actor?.id ?? null;
|
|
506
649
|
const rows = await query_cell_list(ctx, {
|
|
507
650
|
ids: input.ids,
|
|
508
|
-
|
|
651
|
+
kind: input.kind,
|
|
509
652
|
visibility: input.visibility,
|
|
510
653
|
ref: input.ref,
|
|
511
654
|
created_by: input.created_by,
|
|
512
655
|
path_prefix: input.path_prefix,
|
|
656
|
+
root_id: input.root_id,
|
|
657
|
+
moderation: input.moderation,
|
|
513
658
|
viewer_actor_id: caller_actor_id,
|
|
514
659
|
viewer_is_admin: auth ? has_role(auth, ROLE_ADMIN) : false,
|
|
515
660
|
caller_actor_id,
|
|
@@ -540,5 +685,6 @@ export const create_cell_actions = (deps) => {
|
|
|
540
685
|
rpc_action(cell_delete_action_spec, delete_handler),
|
|
541
686
|
rpc_action(cell_list_action_spec, list_handler),
|
|
542
687
|
rpc_action(cell_clone_action_spec, clone_handler),
|
|
688
|
+
rpc_action(cell_moderate_action_spec, moderate_handler),
|
|
543
689
|
];
|
|
544
690
|
};
|
|
@@ -20,7 +20,7 @@ import type { z } from 'zod';
|
|
|
20
20
|
/**
|
|
21
21
|
* Cell-layer `event_type → metadata schema` map for `extra_events`.
|
|
22
22
|
*
|
|
23
|
-
* Covers the
|
|
23
|
+
* Covers the seven generic cell verbs' mutation events plus the grant /
|
|
24
24
|
* field / item relation events. Read-only verbs (`cell_get`, `cell_list`,
|
|
25
25
|
* `cell_*_list`, `cell_audit_list`) emit nothing and are absent here.
|
|
26
26
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_audit_events.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_audit_events.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAqB3B;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAajE,CAAC"}
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
|
-
import { CellAuditMetadata, CellCloneAuditMetadata } from './cell_audit_metadata.js';
|
|
19
|
+
import { CellAuditMetadata, CellCloneAuditMetadata, CellModerateAuditMetadata, } from './cell_audit_metadata.js';
|
|
20
20
|
import { CellGrantCreateAuditMetadata, CellGrantRevokeAuditMetadata, } from './cell_grant_audit_metadata.js';
|
|
21
21
|
import { CellFieldSetAuditMetadata, CellFieldDeleteAuditMetadata, } from './cell_field_audit_metadata.js';
|
|
22
22
|
import { CellItemInsertAuditMetadata, CellItemMoveAuditMetadata, CellItemDeleteAuditMetadata, } from './cell_item_audit_metadata.js';
|
|
23
23
|
/**
|
|
24
24
|
* Cell-layer `event_type → metadata schema` map for `extra_events`.
|
|
25
25
|
*
|
|
26
|
-
* Covers the
|
|
26
|
+
* Covers the seven generic cell verbs' mutation events plus the grant /
|
|
27
27
|
* field / item relation events. Read-only verbs (`cell_get`, `cell_list`,
|
|
28
28
|
* `cell_*_list`, `cell_audit_list`) emit nothing and are absent here.
|
|
29
29
|
*/
|
|
@@ -32,6 +32,7 @@ export const cell_audit_events = {
|
|
|
32
32
|
cell_update: CellAuditMetadata,
|
|
33
33
|
cell_delete: CellAuditMetadata,
|
|
34
34
|
cell_clone: CellCloneAuditMetadata,
|
|
35
|
+
cell_moderate: CellModerateAuditMetadata,
|
|
35
36
|
cell_grant_create: CellGrantCreateAuditMetadata,
|
|
36
37
|
cell_grant_revoke: CellGrantRevokeAuditMetadata,
|
|
37
38
|
cell_field_set: CellFieldSetAuditMetadata,
|
|
@@ -45,4 +45,19 @@ export declare const CellCloneAuditMetadata: z.ZodObject<{
|
|
|
45
45
|
kind: z.ZodOptional<z.ZodString>;
|
|
46
46
|
}, z.core.$loose>;
|
|
47
47
|
export type CellCloneAuditMetadata = z.infer<typeof CellCloneAuditMetadata>;
|
|
48
|
+
/**
|
|
49
|
+
* Metadata envelope for `cell_moderate`. `cell_id` is the moderated
|
|
50
|
+
* contribution; `root_id` is its governing root (the container whose
|
|
51
|
+
* moderation authority gated the call); `moderation` is the terminal decision
|
|
52
|
+
* applied (`'approved'` | `'rejected'`).
|
|
53
|
+
*/
|
|
54
|
+
export declare const CellModerateAuditMetadata: z.ZodObject<{
|
|
55
|
+
cell_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
56
|
+
root_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
57
|
+
moderation: z.ZodEnum<{
|
|
58
|
+
rejected: "rejected";
|
|
59
|
+
approved: "approved";
|
|
60
|
+
}>;
|
|
61
|
+
}, z.core.$loose>;
|
|
62
|
+
export type CellModerateAuditMetadata = z.infer<typeof CellModerateAuditMetadata>;
|
|
48
63
|
//# sourceMappingURL=cell_audit_metadata.d.ts.map
|