@fuzdev/fuz_app 0.67.1 → 0.69.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/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +10 -3
- package/dist/auth/CLAUDE.md +99 -5
- package/dist/auth/account_queries.d.ts +87 -4
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +107 -17
- package/dist/auth/account_schema.d.ts +19 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +8 -0
- package/dist/auth/admin_action_specs.d.ts +170 -3
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +148 -4
- package/dist/auth/admin_actions.d.ts +4 -14
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +246 -40
- package/dist/auth/audit_log_ddl.d.ts +10 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +13 -4
- package/dist/auth/audit_log_schema.d.ts +34 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +73 -0
- package/dist/auth/auth_ddl.d.ts +2 -2
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +10 -2
- package/dist/auth/cell_action_specs.d.ts +1295 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_action_specs.js +397 -0
- package/dist/auth/cell_actions.d.ts +63 -0
- package/dist/auth/cell_actions.d.ts.map +1 -0
- package/dist/auth/cell_actions.js +546 -0
- package/dist/auth/cell_audit_action_specs.d.ts +131 -0
- package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_audit_action_specs.js +70 -0
- package/dist/auth/cell_audit_actions.d.ts +18 -0
- package/dist/auth/cell_audit_actions.d.ts.map +1 -0
- package/dist/auth/cell_audit_actions.js +59 -0
- package/dist/auth/cell_audit_events.d.ts +28 -0
- package/dist/auth/cell_audit_events.d.ts.map +1 -0
- package/dist/auth/cell_audit_events.js +42 -0
- package/dist/auth/cell_audit_metadata.d.ts +48 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_audit_metadata.js +46 -0
- package/dist/auth/cell_authorize.d.ts +88 -0
- package/dist/auth/cell_authorize.d.ts.map +1 -0
- package/dist/auth/cell_authorize.js +172 -0
- package/dist/auth/cell_data_schema.d.ts +44 -0
- package/dist/auth/cell_data_schema.d.ts.map +1 -0
- package/dist/auth/cell_data_schema.js +42 -0
- package/dist/auth/cell_field_action_specs.d.ts +244 -0
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_field_action_specs.js +136 -0
- package/dist/auth/cell_field_actions.d.ts +34 -0
- package/dist/auth/cell_field_actions.d.ts.map +1 -0
- package/dist/auth/cell_field_actions.js +153 -0
- package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
- package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_field_audit_metadata.js +28 -0
- package/dist/auth/cell_grant_action_specs.d.ts +333 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_grant_action_specs.js +148 -0
- package/dist/auth/cell_grant_actions.d.ts +50 -0
- package/dist/auth/cell_grant_actions.d.ts.map +1 -0
- package/dist/auth/cell_grant_actions.js +208 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_grant_audit_metadata.js +54 -0
- package/dist/auth/cell_item_action_specs.d.ts +331 -0
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_item_action_specs.js +182 -0
- package/dist/auth/cell_item_actions.d.ts +37 -0
- package/dist/auth/cell_item_actions.d.ts.map +1 -0
- package/dist/auth/cell_item_actions.js +204 -0
- package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
- package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_item_audit_metadata.js +32 -0
- package/dist/auth/cell_relation_visibility.d.ts +32 -0
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
- package/dist/auth/cell_relation_visibility.js +57 -0
- package/dist/auth/deps.d.ts +9 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.d.ts +30 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +54 -0
- package/dist/auth/signup_routes.d.ts +0 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +9 -3
- package/dist/auth/standard_rpc_actions.d.ts +5 -5
- package/dist/auth/standard_rpc_actions.js +4 -4
- package/dist/db/CLAUDE.md +118 -0
- package/dist/db/cell_audit_queries.d.ts +26 -0
- package/dist/db/cell_audit_queries.d.ts.map +1 -0
- package/dist/db/cell_audit_queries.js +53 -0
- package/dist/db/cell_ddl.d.ts +151 -0
- package/dist/db/cell_ddl.d.ts.map +1 -0
- package/dist/db/cell_ddl.js +247 -0
- package/dist/db/cell_field_queries.d.ts +105 -0
- package/dist/db/cell_field_queries.d.ts.map +1 -0
- package/dist/db/cell_field_queries.js +113 -0
- package/dist/db/cell_grant_queries.d.ts +132 -0
- package/dist/db/cell_grant_queries.d.ts.map +1 -0
- package/dist/db/cell_grant_queries.js +145 -0
- package/dist/db/cell_history_ddl.d.ts +38 -0
- package/dist/db/cell_history_ddl.d.ts.map +1 -0
- package/dist/db/cell_history_ddl.js +61 -0
- package/dist/db/cell_item_queries.d.ts +107 -0
- package/dist/db/cell_item_queries.d.ts.map +1 -0
- package/dist/db/cell_item_queries.js +119 -0
- package/dist/db/cell_queries.d.ts +327 -0
- package/dist/db/cell_queries.d.ts.map +1 -0
- package/dist/db/cell_queries.js +431 -0
- package/dist/db/fact_ddl.d.ts +38 -0
- package/dist/db/fact_ddl.d.ts.map +1 -0
- package/dist/db/fact_ddl.js +71 -0
- package/dist/db/fact_queries.d.ts +140 -0
- package/dist/db/fact_queries.d.ts.map +1 -0
- package/dist/db/fact_queries.js +161 -0
- package/dist/db/fact_store.d.ts +112 -0
- package/dist/db/fact_store.d.ts.map +1 -0
- package/dist/db/fact_store.js +225 -0
- package/dist/server/app_server.d.ts +1 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +1 -5
- package/dist/server/env.d.ts +2 -0
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +6 -0
- package/dist/server/fact_write.d.ts +32 -0
- package/dist/server/fact_write.d.ts.map +1 -0
- package/dist/server/fact_write.js +56 -0
- package/dist/server/file_fact_fetcher.d.ts +42 -0
- package/dist/server/file_fact_fetcher.d.ts.map +1 -0
- package/dist/server/file_fact_fetcher.js +60 -0
- package/dist/server/file_fact_url.d.ts +53 -0
- package/dist/server/file_fact_url.d.ts.map +1 -0
- package/dist/server/file_fact_url.js +52 -0
- package/dist/server/serve_fact_route.d.ts +78 -0
- package/dist/server/serve_fact_route.d.ts.map +1 -0
- package/dist/server/serve_fact_route.js +205 -0
- package/dist/testing/CLAUDE.md +142 -6
- package/dist/testing/app_server.d.ts +46 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +67 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +67 -1
- package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/account_lifecycle.js +144 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +10 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -0
- package/dist/testing/cross_backend/actor_lookup.js +83 -0
- package/dist/testing/cross_backend/actor_search.d.ts +6 -0
- package/dist/testing/cross_backend/actor_search.d.ts.map +1 -0
- package/dist/testing/cross_backend/actor_search.js +92 -0
- package/dist/testing/cross_backend/app_settings.d.ts +6 -0
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -0
- package/dist/testing/cross_backend/app_settings.js +95 -0
- package/dist/testing/cross_backend/backend_config.d.ts +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +29 -7
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +3 -1
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
- package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_crud.js +168 -0
- package/dist/testing/cross_backend/cell_grant_role.d.ts +8 -0
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_grant_role.js +102 -0
- package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_relations.js +229 -0
- package/dist/testing/cross_backend/conformance_case.d.ts +144 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_case.js +132 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +46 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_table.js +199 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +6 -2
- package/dist/testing/cross_backend/default_spine_surface.d.ts +17 -9
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +20 -12
- package/dist/testing/cross_backend/origin.d.ts +10 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin.js +73 -0
- package/dist/testing/cross_backend/setup.d.ts +22 -40
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +39 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +90 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +91 -3
- package/dist/testing/cross_backend/xfail.d.ts +15 -0
- package/dist/testing/cross_backend/xfail.d.ts.map +1 -0
- package/dist/testing/cross_backend/xfail.js +37 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +4 -0
- package/dist/testing/integration.d.ts +2 -3
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +20 -85
- package/dist/testing/rate_limiting.d.ts +1 -1
- package/dist/testing/rpc_helpers.d.ts +3 -3
- package/dist/testing/sse_round_trip.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +0 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +4 -0
- package/dist/ui/AdminAccounts.svelte +84 -35
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +21 -23
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +17 -26
- package/dist/ui/OpenSignupToggle.svelte +2 -5
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.d.ts +9 -10
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +7 -17
- package/dist/ui/admin_accounts_state.svelte.d.ts +41 -20
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +52 -22
- package/dist/ui/admin_invites_state.svelte.d.ts +8 -11
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +7 -16
- package/dist/ui/admin_rpc_adapters.d.ts +6 -2
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +5 -1
- package/dist/ui/admin_sessions_state.svelte.d.ts +6 -10
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +4 -14
- package/dist/ui/app_settings_state.svelte.d.ts +8 -12
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +6 -16
- package/dist/ui/audit_log_state.svelte.d.ts +9 -8
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +8 -20
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_audit_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAMtB;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;kBAO7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,2EAA2E;AAC3E,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAEhD,eAAO,MAAM,kBAAkB;;;kBAG7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;kBAE9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWH,CAAC;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAyC,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cell_audit_list` RPC — per-cell audit timeline.
|
|
3
|
+
*
|
|
4
|
+
* Returns audit-log rows whose metadata names this cell on any of the
|
|
5
|
+
* `(cell_id, source_id, parent_id, child_id, target_id, new_id)` keys
|
|
6
|
+
* used by the cell-domain event types. The handler 404-masks for
|
|
7
|
+
* callers who are not in the cell's manage tier (`can_manage_cell` =
|
|
8
|
+
* admin / owner) — the timeline reveals who-touched-the-cell, so it is
|
|
9
|
+
* gated above `can_view_cell`.
|
|
10
|
+
*
|
|
11
|
+
* Read-only; no audit side effect. Returns the most-recent
|
|
12
|
+
* `CELL_AUDIT_LIST_DEFAULT_LIMIT` events; pagination is intentionally
|
|
13
|
+
* not on the wire yet — the only consumer renders a single page. Add
|
|
14
|
+
* `{before, limit}` input + `{next_before}` output together when a
|
|
15
|
+
* paginating consumer surfaces.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
21
|
+
import { ActingActor } from '../http/auth_shape.js';
|
|
22
|
+
/**
|
|
23
|
+
* Wire shape for a single cell-audit row. Narrower than
|
|
24
|
+
* `AuditLogEventJson` — `account_id` and `target_account_id` are
|
|
25
|
+
* deliberately omitted so this verb does NOT surface the actor↔account
|
|
26
|
+
* join. `target_actor_id` and `metadata` are dropped too: `target_actor_id`
|
|
27
|
+
* is NULL for every cell-domain event (the grant recipient lives inside
|
|
28
|
+
* `metadata.principal` on grant rows, not on the audit-log top-level
|
|
29
|
+
* field); `metadata` is unread by the timeline UI.
|
|
30
|
+
*
|
|
31
|
+
* `ip` is also omitted: it is PII about the actors who touched the cell,
|
|
32
|
+
* and even at the manage tier this per-cell timeline has no need for it
|
|
33
|
+
* (admins reach the full `audit_log` surface, which carries `ip`, through
|
|
34
|
+
* the admin audit verbs). Keeping it off this wire avoids leaking
|
|
35
|
+
* collaborators' IPs to a cell's owner.
|
|
36
|
+
*
|
|
37
|
+
* All omitted fields can be re-added under a richer admin-only
|
|
38
|
+
* event-detail view later — keep the wire surface honest about what
|
|
39
|
+
* consumers use.
|
|
40
|
+
*/
|
|
41
|
+
export const CellAuditEventJson = z.strictObject({
|
|
42
|
+
id: Uuid,
|
|
43
|
+
seq: z.number(),
|
|
44
|
+
event_type: z.string(),
|
|
45
|
+
outcome: z.enum(['success', 'failure']),
|
|
46
|
+
actor_id: Uuid.nullable(),
|
|
47
|
+
created_at: z.string(),
|
|
48
|
+
});
|
|
49
|
+
/** Page size for `cell_audit_list`. Single page at MVP; no cursor wire. */
|
|
50
|
+
export const CELL_AUDIT_LIST_DEFAULT_LIMIT = 50;
|
|
51
|
+
export const CellAuditListInput = z.strictObject({
|
|
52
|
+
cell_id: Uuid.meta({ description: 'Cell whose audit trail to fetch.' }),
|
|
53
|
+
acting: ActingActor,
|
|
54
|
+
});
|
|
55
|
+
export const CellAuditListOutput = z.strictObject({
|
|
56
|
+
events: z.array(CellAuditEventJson),
|
|
57
|
+
});
|
|
58
|
+
export const cell_audit_list_action_spec = {
|
|
59
|
+
method: 'cell_audit_list',
|
|
60
|
+
kind: 'request_response',
|
|
61
|
+
initiator: 'frontend',
|
|
62
|
+
auth: { account: 'required', actor: 'required' },
|
|
63
|
+
side_effects: false,
|
|
64
|
+
input: CellAuditListInput,
|
|
65
|
+
output: CellAuditListOutput,
|
|
66
|
+
async: true,
|
|
67
|
+
description: 'List the most-recent audit events referencing the cell on any of `cell_id`, `source_id`, `parent_id`, `child_id`, `target_id`, or `new_id` metadata keys. Manage-tier only (admin / owner) — the timeline reveals who touched the cell, so viewers and editors get the IDOR-mask 404. 404 on miss or unauthorized — same shape as `cell_get` so private-cell existence does not leak.',
|
|
68
|
+
};
|
|
69
|
+
/** Registry export to compose into `all_cell_action_specs`. */
|
|
70
|
+
export const all_cell_audit_action_specs = [cell_audit_list_action_spec];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cell_audit_list` handler — per-cell audit timeline read.
|
|
3
|
+
*
|
|
4
|
+
* Authz is **manage-tier** (`can_manage_cell` = admin / owner), NOT
|
|
5
|
+
* `can_view_cell`. The timeline surfaces the `actor_id` of every account
|
|
6
|
+
* that touched the cell (including via `cell_field` / `cell_item` / clone
|
|
7
|
+
* edges where the cell is the target / child / source); exposing that to
|
|
8
|
+
* mere viewers — or, for a public cell, to any authenticated caller —
|
|
9
|
+
* leaks who-touched-what. Gating to admin / owner mirrors
|
|
10
|
+
* `cell_grant_list` ("the audit trail is the manager's to read"). Misses +
|
|
11
|
+
* unauthorized reads both 404 with `cell_not_found` — private-cell
|
|
12
|
+
* existence stays masked. No audit side effect (read-only).
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { type RpcAction } from '../actions/action_rpc.js';
|
|
17
|
+
export declare const create_cell_audit_actions: () => Array<RpcAction>;
|
|
18
|
+
//# sourceMappingURL=cell_audit_actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_audit_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAsC,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAgC7F,eAAO,MAAM,yBAAyB,QAAO,KAAK,CAAC,SAAS,CA0B3D,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cell_audit_list` handler — per-cell audit timeline read.
|
|
3
|
+
*
|
|
4
|
+
* Authz is **manage-tier** (`can_manage_cell` = admin / owner), NOT
|
|
5
|
+
* `can_view_cell`. The timeline surfaces the `actor_id` of every account
|
|
6
|
+
* that touched the cell (including via `cell_field` / `cell_item` / clone
|
|
7
|
+
* edges where the cell is the target / child / source); exposing that to
|
|
8
|
+
* mere viewers — or, for a public cell, to any authenticated caller —
|
|
9
|
+
* leaks who-touched-what. Gating to admin / owner mirrors
|
|
10
|
+
* `cell_grant_list` ("the audit trail is the manager's to read"). Misses +
|
|
11
|
+
* unauthorized reads both 404 with `cell_not_found` — private-cell
|
|
12
|
+
* existence stays masked. No audit side effect (read-only).
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { rpc_action } from '../actions/action_rpc.js';
|
|
17
|
+
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
18
|
+
import { cell_audit_list_action_spec, CELL_AUDIT_LIST_DEFAULT_LIMIT, } from './cell_audit_action_specs.js';
|
|
19
|
+
import { ERROR_CELL_NOT_FOUND } from './cell_action_specs.js';
|
|
20
|
+
import { query_cell_get } from '../db/cell_queries.js';
|
|
21
|
+
import { query_audit_log_list_by_cell } from '../db/cell_audit_queries.js';
|
|
22
|
+
import { can_manage_cell } from './cell_authorize.js';
|
|
23
|
+
/**
|
|
24
|
+
* Project a DB row onto the narrowed wire shape. `account_id` /
|
|
25
|
+
* `target_account_id` / `target_actor_id` / `metadata` / `ip` are
|
|
26
|
+
* deliberately omitted — see `CellAuditEventJson` docstring for the
|
|
27
|
+
* rationale.
|
|
28
|
+
*/
|
|
29
|
+
const to_cell_audit_event_json = (row) => ({
|
|
30
|
+
id: row.id,
|
|
31
|
+
seq: row.seq,
|
|
32
|
+
event_type: row.event_type,
|
|
33
|
+
outcome: row.outcome,
|
|
34
|
+
actor_id: row.actor_id,
|
|
35
|
+
created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString(),
|
|
36
|
+
});
|
|
37
|
+
export const create_cell_audit_actions = () => {
|
|
38
|
+
const handler = async (input, ctx) => {
|
|
39
|
+
const auth = ctx.auth;
|
|
40
|
+
const cell = await query_cell_get(ctx, input.cell_id);
|
|
41
|
+
if (!cell) {
|
|
42
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
43
|
+
}
|
|
44
|
+
// Manage-tier gate (admin / owner). A populated timeline leaks both
|
|
45
|
+
// the existence of the cell and the actor IDs that touched it (incl.
|
|
46
|
+
// across `cell_field` / `cell_item` / clone edges), so viewers — and
|
|
47
|
+
// any authed caller on a public cell — must NOT read it. Non-managers
|
|
48
|
+
// get the same 404 as a non-viewer on `cell_get` (IDOR mask). Grants
|
|
49
|
+
// aren't consulted (manage tier is owner/admin only).
|
|
50
|
+
if (!can_manage_cell(auth, cell)) {
|
|
51
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
52
|
+
}
|
|
53
|
+
const rows = await query_audit_log_list_by_cell(ctx, cell.id, {
|
|
54
|
+
limit: CELL_AUDIT_LIST_DEFAULT_LIMIT,
|
|
55
|
+
});
|
|
56
|
+
return { events: rows.map(to_cell_audit_event_json) };
|
|
57
|
+
};
|
|
58
|
+
return [rpc_action(cell_audit_list_action_spec, handler)];
|
|
59
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical cell-layer audit event registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps every cell-domain `event_type` a cell handler emits to its
|
|
5
|
+
* metadata schema. Consumers register the whole bundle in one shot via
|
|
6
|
+
* `create_audit_log_config({extra_events: {...cell_audit_events, ...}})`
|
|
7
|
+
* so the cell handlers' `deps.audit.emit(...)` calls validate against the
|
|
8
|
+
* extended registry. Spreading lets an app fold its own event types in
|
|
9
|
+
* alongside.
|
|
10
|
+
*
|
|
11
|
+
* Aggregator module by design — not a compat shim. The per-event metadata
|
|
12
|
+
* schemas live in their own files (`cell_audit_metadata.ts`,
|
|
13
|
+
* `cell_grant_audit_metadata.ts`, `cell_field_audit_metadata.ts`,
|
|
14
|
+
* `cell_item_audit_metadata.ts`); this module is the single registration
|
|
15
|
+
* surface that keeps the keys in lockstep with the handlers.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import type { z } from 'zod';
|
|
20
|
+
/**
|
|
21
|
+
* Cell-layer `event_type → metadata schema` map for `extra_events`.
|
|
22
|
+
*
|
|
23
|
+
* Covers the six generic cell verbs' mutation events plus the grant /
|
|
24
|
+
* field / item relation events. Read-only verbs (`cell_get`, `cell_list`,
|
|
25
|
+
* `cell_*_list`, `cell_audit_list`) emit nothing and are absent here.
|
|
26
|
+
*/
|
|
27
|
+
export declare const cell_audit_events: Readonly<Record<string, z.ZodType>>;
|
|
28
|
+
//# sourceMappingURL=cell_audit_events.d.ts.map
|
|
@@ -0,0 +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;AAiB3B;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAYjE,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical cell-layer audit event registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps every cell-domain `event_type` a cell handler emits to its
|
|
5
|
+
* metadata schema. Consumers register the whole bundle in one shot via
|
|
6
|
+
* `create_audit_log_config({extra_events: {...cell_audit_events, ...}})`
|
|
7
|
+
* so the cell handlers' `deps.audit.emit(...)` calls validate against the
|
|
8
|
+
* extended registry. Spreading lets an app fold its own event types in
|
|
9
|
+
* alongside.
|
|
10
|
+
*
|
|
11
|
+
* Aggregator module by design — not a compat shim. The per-event metadata
|
|
12
|
+
* schemas live in their own files (`cell_audit_metadata.ts`,
|
|
13
|
+
* `cell_grant_audit_metadata.ts`, `cell_field_audit_metadata.ts`,
|
|
14
|
+
* `cell_item_audit_metadata.ts`); this module is the single registration
|
|
15
|
+
* surface that keeps the keys in lockstep with the handlers.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { CellAuditMetadata, CellCloneAuditMetadata } from './cell_audit_metadata.js';
|
|
20
|
+
import { CellGrantCreateAuditMetadata, CellGrantRevokeAuditMetadata, } from './cell_grant_audit_metadata.js';
|
|
21
|
+
import { CellFieldSetAuditMetadata, CellFieldDeleteAuditMetadata, } from './cell_field_audit_metadata.js';
|
|
22
|
+
import { CellItemInsertAuditMetadata, CellItemMoveAuditMetadata, CellItemDeleteAuditMetadata, } from './cell_item_audit_metadata.js';
|
|
23
|
+
/**
|
|
24
|
+
* Cell-layer `event_type → metadata schema` map for `extra_events`.
|
|
25
|
+
*
|
|
26
|
+
* Covers the six generic cell verbs' mutation events plus the grant /
|
|
27
|
+
* field / item relation events. Read-only verbs (`cell_get`, `cell_list`,
|
|
28
|
+
* `cell_*_list`, `cell_audit_list`) emit nothing and are absent here.
|
|
29
|
+
*/
|
|
30
|
+
export const cell_audit_events = {
|
|
31
|
+
cell_create: CellAuditMetadata,
|
|
32
|
+
cell_update: CellAuditMetadata,
|
|
33
|
+
cell_delete: CellAuditMetadata,
|
|
34
|
+
cell_clone: CellCloneAuditMetadata,
|
|
35
|
+
cell_grant_create: CellGrantCreateAuditMetadata,
|
|
36
|
+
cell_grant_revoke: CellGrantRevokeAuditMetadata,
|
|
37
|
+
cell_field_set: CellFieldSetAuditMetadata,
|
|
38
|
+
cell_field_delete: CellFieldDeleteAuditMetadata,
|
|
39
|
+
cell_item_insert: CellItemInsertAuditMetadata,
|
|
40
|
+
cell_item_move: CellItemMoveAuditMetadata,
|
|
41
|
+
cell_item_delete: CellItemDeleteAuditMetadata,
|
|
42
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-log metadata schemas for the cell layer's mutation events.
|
|
3
|
+
*
|
|
4
|
+
* Apps register these via `extra_events:` on `create_audit_log_config`
|
|
5
|
+
* alongside any app-defined event types.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
/**
|
|
11
|
+
* Shared metadata envelope for cell mutations. `kind` and `path` are
|
|
12
|
+
* captured at emit-time so the audit-log viewer can show useful context
|
|
13
|
+
* for soft-deleted rows even after the cell snapshot is gone. Relation
|
|
14
|
+
* membership is tracked independently via the `cell_item_*` /
|
|
15
|
+
* `cell_field_*` per-row audit events.
|
|
16
|
+
*
|
|
17
|
+
* Loose object: per-kind handlers may extend the metadata without spec
|
|
18
|
+
* churn.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CellAuditMetadata: z.ZodObject<{
|
|
21
|
+
cell_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
22
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
23
|
+
path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
24
|
+
}, z.core.$loose>;
|
|
25
|
+
export type CellAuditMetadata = z.infer<typeof CellAuditMetadata>;
|
|
26
|
+
/**
|
|
27
|
+
* Metadata envelope for `cell_clone`.
|
|
28
|
+
*
|
|
29
|
+
* `source_id` and `new_id` capture the parent → clone edge. `deep` flags
|
|
30
|
+
* whether children were walked. `item_count` reports the number of
|
|
31
|
+
* children actually cloned (post-skip). `kind` is captured at emit-time so
|
|
32
|
+
* an operator can filter the audit log by source shape (e.g., "every
|
|
33
|
+
* collection clone").
|
|
34
|
+
*
|
|
35
|
+
* No skipped-child count is recorded: surfacing how many children the
|
|
36
|
+
* caller couldn't view would leak the source's hidden-child count to the
|
|
37
|
+
* cloner (who owns — and can audit — the clone). Non-viewable children are
|
|
38
|
+
* dropped silently (D8).
|
|
39
|
+
*/
|
|
40
|
+
export declare const CellCloneAuditMetadata: z.ZodObject<{
|
|
41
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
42
|
+
new_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
43
|
+
deep: z.ZodBoolean;
|
|
44
|
+
item_count: z.ZodNumber;
|
|
45
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, z.core.$loose>;
|
|
47
|
+
export type CellCloneAuditMetadata = z.infer<typeof CellCloneAuditMetadata>;
|
|
48
|
+
//# sourceMappingURL=cell_audit_metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_audit_metadata.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB;;;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-log metadata schemas for the cell layer's mutation events.
|
|
3
|
+
*
|
|
4
|
+
* Apps register these via `extra_events:` on `create_audit_log_config`
|
|
5
|
+
* alongside any app-defined event types.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
11
|
+
/**
|
|
12
|
+
* Shared metadata envelope for cell mutations. `kind` and `path` are
|
|
13
|
+
* captured at emit-time so the audit-log viewer can show useful context
|
|
14
|
+
* for soft-deleted rows even after the cell snapshot is gone. Relation
|
|
15
|
+
* membership is tracked independently via the `cell_item_*` /
|
|
16
|
+
* `cell_field_*` per-row audit events.
|
|
17
|
+
*
|
|
18
|
+
* Loose object: per-kind handlers may extend the metadata without spec
|
|
19
|
+
* churn.
|
|
20
|
+
*/
|
|
21
|
+
export const CellAuditMetadata = z.looseObject({
|
|
22
|
+
cell_id: Uuid,
|
|
23
|
+
kind: z.string().optional(),
|
|
24
|
+
path: z.string().nullable().optional(),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Metadata envelope for `cell_clone`.
|
|
28
|
+
*
|
|
29
|
+
* `source_id` and `new_id` capture the parent → clone edge. `deep` flags
|
|
30
|
+
* whether children were walked. `item_count` reports the number of
|
|
31
|
+
* children actually cloned (post-skip). `kind` is captured at emit-time so
|
|
32
|
+
* an operator can filter the audit log by source shape (e.g., "every
|
|
33
|
+
* collection clone").
|
|
34
|
+
*
|
|
35
|
+
* No skipped-child count is recorded: surfacing how many children the
|
|
36
|
+
* caller couldn't view would leak the source's hidden-child count to the
|
|
37
|
+
* cloner (who owns — and can audit — the clone). Non-viewable children are
|
|
38
|
+
* dropped silently (D8).
|
|
39
|
+
*/
|
|
40
|
+
export const CellCloneAuditMetadata = z.looseObject({
|
|
41
|
+
source_id: Uuid,
|
|
42
|
+
new_id: Uuid,
|
|
43
|
+
deep: z.boolean(),
|
|
44
|
+
item_count: z.number().int().nonnegative(),
|
|
45
|
+
kind: z.string().optional(),
|
|
46
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell view + edit + manage authorization helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions over `(auth, cell, grants)`. No DB I/O; the caller is
|
|
5
|
+
* responsible for loading the cell row and the cell's grant list out-of-
|
|
6
|
+
* band, then handing both to the predicate.
|
|
7
|
+
*
|
|
8
|
+
* Unauthenticated callers pass `null` for `auth` — public pages can
|
|
9
|
+
* resolve `cell.visibility === 'public'` cells without a session, and
|
|
10
|
+
* that branch is the only path that admits them. Callers may pass `null`
|
|
11
|
+
* for `grants` in that case (and any case where they know they don't
|
|
12
|
+
* need to consider grants); `grant_admits` short-circuits on null.
|
|
13
|
+
*
|
|
14
|
+
* Three tiers:
|
|
15
|
+
*
|
|
16
|
+
* - `can_view_cell` — admin / public / owner / any `viewer`+ grant.
|
|
17
|
+
* - `can_edit_cell` — admin / owner / `editor` grant. NULL `created_by`
|
|
18
|
+
* is admin-only (system-origin defense-in-depth).
|
|
19
|
+
* - `can_manage_cell` — admin / owner only. Not delegable, not a grant
|
|
20
|
+
* level. Gates the manage tier: `visibility` writes and all grant
|
|
21
|
+
* management (`cell_grant_create` / `_list` / `_revoke`).
|
|
22
|
+
*
|
|
23
|
+
* Grant-admit branches: per-`actor_id` grants admit the matching
|
|
24
|
+
* actor; `(role, scope_id?)` grants admit any holder of an active
|
|
25
|
+
* role_grant matching those literals (`scope_id IS NULL` admits any-scope
|
|
26
|
+
* role_grant). The grant's `level` (`viewer` / `editor`) gates which
|
|
27
|
+
* predicate it satisfies.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { type RequestContext } from './request_context.js';
|
|
32
|
+
import type { CellRow } from '../db/cell_queries.js';
|
|
33
|
+
import type { CellGrantRow } from '../db/cell_grant_queries.js';
|
|
34
|
+
/**
|
|
35
|
+
* View authorization for a cell.
|
|
36
|
+
*
|
|
37
|
+
* - Admin: always allowed.
|
|
38
|
+
* - `cell.visibility === 'public'`: allowed for everyone, including
|
|
39
|
+
* unauthenticated callers (e.g. a public landing cell).
|
|
40
|
+
* - Owner (`cell.created_by === auth.actor.id`): allowed.
|
|
41
|
+
* - Any active grant on the cell admits the caller (actor-shaped:
|
|
42
|
+
* match on actor_id; role-shaped: match on `(role, scope_id?)`
|
|
43
|
+
* against an active role_grant).
|
|
44
|
+
* - Otherwise: false.
|
|
45
|
+
*
|
|
46
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
47
|
+
* @param cell - the cell row
|
|
48
|
+
* @param grants - the cell's grant list, or `null` to skip the grant branch
|
|
49
|
+
* @returns whether the caller may view the cell
|
|
50
|
+
*/
|
|
51
|
+
export declare const can_view_cell: (auth: RequestContext | null, cell: CellRow, grants: ReadonlyArray<CellGrantRow> | null) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Edit authorization for a cell.
|
|
54
|
+
*
|
|
55
|
+
* Unauthenticated callers can never edit. Admin always allowed.
|
|
56
|
+
*
|
|
57
|
+
* IMPORTANT: the `cell.created_by === null` branch is **explicit
|
|
58
|
+
* defense-in-depth**. NULL `created_by` means system origin (well-known
|
|
59
|
+
* cells seeded by migration, future daemon/agent cells). Non-admin edits
|
|
60
|
+
* MUST be denied — editor-level grants do NOT bypass this guard,
|
|
61
|
+
* because system cells are policy-controlled at admin level. Do NOT
|
|
62
|
+
* collapse into a single equality check that would silently return
|
|
63
|
+
* `false` for NULL via JS equality semantics — the explicit branch
|
|
64
|
+
* survives refactors and reads as a load-bearing security property.
|
|
65
|
+
*
|
|
66
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
67
|
+
* @param cell - the cell row
|
|
68
|
+
* @param grants - the cell's grant list, or `null` to skip the grant branch
|
|
69
|
+
* @returns whether the caller may edit the cell
|
|
70
|
+
*/
|
|
71
|
+
export declare const can_edit_cell: (auth: RequestContext | null, cell: CellRow, grants: ReadonlyArray<CellGrantRow> | null) => boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Manage authorization for a cell — `admin || owner`.
|
|
74
|
+
*
|
|
75
|
+
* The implicit tier above editor: gates `visibility` writes and all grant
|
|
76
|
+
* management (`cell_grant_create` / `_list` / `_revoke`). NOT delegable and
|
|
77
|
+
* NOT a grant level — an editor-grant holder is never a manager. Grants are
|
|
78
|
+
* not consulted.
|
|
79
|
+
*
|
|
80
|
+
* NULL `created_by` (system origin) has no owner, so manage falls to
|
|
81
|
+
* admin only — the explicit NULL guard lives in `is_owner`.
|
|
82
|
+
*
|
|
83
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
84
|
+
* @param cell - the cell row
|
|
85
|
+
* @returns whether the caller is in the manage tier for the cell
|
|
86
|
+
*/
|
|
87
|
+
export declare const can_manage_cell: (auth: RequestContext | null, cell: CellRow) => boolean;
|
|
88
|
+
//# sourceMappingURL=cell_authorize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_authorize.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_authorize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAW,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAInE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AA+D9D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,GACzB,MAAM,cAAc,GAAG,IAAI,EAC3B,MAAM,OAAO,EACb,QAAQ,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,KACxC,OAMF,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,GACzB,MAAM,cAAc,GAAG,IAAI,EAC3B,MAAM,OAAO,EACb,QAAQ,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,KACxC,OAUF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,cAAc,GAAG,IAAI,EAAE,MAAM,OAAO,KAAG,OAI5E,CAAC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell view + edit + manage authorization helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions over `(auth, cell, grants)`. No DB I/O; the caller is
|
|
5
|
+
* responsible for loading the cell row and the cell's grant list out-of-
|
|
6
|
+
* band, then handing both to the predicate.
|
|
7
|
+
*
|
|
8
|
+
* Unauthenticated callers pass `null` for `auth` — public pages can
|
|
9
|
+
* resolve `cell.visibility === 'public'` cells without a session, and
|
|
10
|
+
* that branch is the only path that admits them. Callers may pass `null`
|
|
11
|
+
* for `grants` in that case (and any case where they know they don't
|
|
12
|
+
* need to consider grants); `grant_admits` short-circuits on null.
|
|
13
|
+
*
|
|
14
|
+
* Three tiers:
|
|
15
|
+
*
|
|
16
|
+
* - `can_view_cell` — admin / public / owner / any `viewer`+ grant.
|
|
17
|
+
* - `can_edit_cell` — admin / owner / `editor` grant. NULL `created_by`
|
|
18
|
+
* is admin-only (system-origin defense-in-depth).
|
|
19
|
+
* - `can_manage_cell` — admin / owner only. Not delegable, not a grant
|
|
20
|
+
* level. Gates the manage tier: `visibility` writes and all grant
|
|
21
|
+
* management (`cell_grant_create` / `_list` / `_revoke`).
|
|
22
|
+
*
|
|
23
|
+
* Grant-admit branches: per-`actor_id` grants admit the matching
|
|
24
|
+
* actor; `(role, scope_id?)` grants admit any holder of an active
|
|
25
|
+
* role_grant matching those literals (`scope_id IS NULL` admits any-scope
|
|
26
|
+
* role_grant). The grant's `level` (`viewer` / `editor`) gates which
|
|
27
|
+
* predicate it satisfies.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { has_role } from './request_context.js';
|
|
32
|
+
import { is_role_grant_active } from './account_schema.js';
|
|
33
|
+
import { ROLE_ADMIN } from './role_schema.js';
|
|
34
|
+
const cell_is_public = (cell) => cell.visibility === 'public';
|
|
35
|
+
/**
|
|
36
|
+
* Whether any grant admits `auth` at the given level.
|
|
37
|
+
*
|
|
38
|
+
* `null` grants short-circuits to false — callers who skipped the load
|
|
39
|
+
* (e.g. unauthenticated requests, where no grant could admit anyway)
|
|
40
|
+
* pass null instead of allocating an empty array. Authenticated
|
|
41
|
+
* callers that loaded a list pass it as-is; an empty array is
|
|
42
|
+
* semantically distinct ("loaded, no grants exist") and walks to false
|
|
43
|
+
* via the empty `for...of`.
|
|
44
|
+
*
|
|
45
|
+
* Actor-shaped grants match `auth.actor.id`. Role-shaped grants walk
|
|
46
|
+
* `auth.role_grants` for an active role_grant with matching role and either a
|
|
47
|
+
* matching `scope_id` or `g.scope_id IS NULL` (any scope). The active-
|
|
48
|
+
* role_grant recheck via `is_role_grant_active` is belt-and-suspenders for
|
|
49
|
+
* long-lived requests crossing an expiration boundary; middleware
|
|
50
|
+
* already filtered to active role_grants at request entry.
|
|
51
|
+
*/
|
|
52
|
+
const grant_admits = (auth, grants, required_level) => {
|
|
53
|
+
if (grants === null)
|
|
54
|
+
return false;
|
|
55
|
+
const now = new Date();
|
|
56
|
+
for (const g of grants) {
|
|
57
|
+
// Editor-required filters out viewer-level grants up front.
|
|
58
|
+
if (required_level === 'editor' && g.level !== 'editor')
|
|
59
|
+
continue;
|
|
60
|
+
if (g.actor_id !== null) {
|
|
61
|
+
// Actor-shaped grants only match a resolved acting actor. Account-
|
|
62
|
+
// grain auth (no `acting` declared on input → `actor: null`) cannot
|
|
63
|
+
// match an actor-id grant by definition.
|
|
64
|
+
if (auth.actor !== null && g.actor_id === auth.actor.id)
|
|
65
|
+
return true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Role-shaped principal. The CHECK constraint on cell_grant
|
|
69
|
+
// guarantees `role IS NOT NULL` here (actor_id xor role).
|
|
70
|
+
const matched = auth.role_grants.some((p) => p.role === g.role &&
|
|
71
|
+
(g.scope_id === null || p.scope_id === g.scope_id) &&
|
|
72
|
+
is_role_grant_active(p, now));
|
|
73
|
+
if (matched)
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Whether `auth` owns `cell` (the implicit manage tier below admin).
|
|
80
|
+
*
|
|
81
|
+
* Owner is the `cell.created_by` actor field — not a role, not a
|
|
82
|
+
* delegable grant level. NULL `created_by` (system origin) is never
|
|
83
|
+
* owned by anyone; the explicit `created_by !== null` guard keeps that
|
|
84
|
+
* a load-bearing property rather than relying on JS equality returning
|
|
85
|
+
* false for NULL.
|
|
86
|
+
*/
|
|
87
|
+
const is_owner = (auth, cell) => auth.actor !== null && cell.created_by !== null && cell.created_by === auth.actor.id;
|
|
88
|
+
/**
|
|
89
|
+
* View authorization for a cell.
|
|
90
|
+
*
|
|
91
|
+
* - Admin: always allowed.
|
|
92
|
+
* - `cell.visibility === 'public'`: allowed for everyone, including
|
|
93
|
+
* unauthenticated callers (e.g. a public landing cell).
|
|
94
|
+
* - Owner (`cell.created_by === auth.actor.id`): allowed.
|
|
95
|
+
* - Any active grant on the cell admits the caller (actor-shaped:
|
|
96
|
+
* match on actor_id; role-shaped: match on `(role, scope_id?)`
|
|
97
|
+
* against an active role_grant).
|
|
98
|
+
* - Otherwise: false.
|
|
99
|
+
*
|
|
100
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
101
|
+
* @param cell - the cell row
|
|
102
|
+
* @param grants - the cell's grant list, or `null` to skip the grant branch
|
|
103
|
+
* @returns whether the caller may view the cell
|
|
104
|
+
*/
|
|
105
|
+
export const can_view_cell = (auth, cell, grants) => {
|
|
106
|
+
if (auth && has_role(auth, ROLE_ADMIN))
|
|
107
|
+
return true;
|
|
108
|
+
if (cell_is_public(cell))
|
|
109
|
+
return true;
|
|
110
|
+
if (auth && is_owner(auth, cell))
|
|
111
|
+
return true;
|
|
112
|
+
if (auth && grant_admits(auth, grants, 'viewer'))
|
|
113
|
+
return true;
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Edit authorization for a cell.
|
|
118
|
+
*
|
|
119
|
+
* Unauthenticated callers can never edit. Admin always allowed.
|
|
120
|
+
*
|
|
121
|
+
* IMPORTANT: the `cell.created_by === null` branch is **explicit
|
|
122
|
+
* defense-in-depth**. NULL `created_by` means system origin (well-known
|
|
123
|
+
* cells seeded by migration, future daemon/agent cells). Non-admin edits
|
|
124
|
+
* MUST be denied — editor-level grants do NOT bypass this guard,
|
|
125
|
+
* because system cells are policy-controlled at admin level. Do NOT
|
|
126
|
+
* collapse into a single equality check that would silently return
|
|
127
|
+
* `false` for NULL via JS equality semantics — the explicit branch
|
|
128
|
+
* survives refactors and reads as a load-bearing security property.
|
|
129
|
+
*
|
|
130
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
131
|
+
* @param cell - the cell row
|
|
132
|
+
* @param grants - the cell's grant list, or `null` to skip the grant branch
|
|
133
|
+
* @returns whether the caller may edit the cell
|
|
134
|
+
*/
|
|
135
|
+
export const can_edit_cell = (auth, cell, grants) => {
|
|
136
|
+
if (!auth)
|
|
137
|
+
return false;
|
|
138
|
+
if (has_role(auth, ROLE_ADMIN))
|
|
139
|
+
return true;
|
|
140
|
+
if (cell.created_by === null)
|
|
141
|
+
return false; // explicit: NULL = admin-only
|
|
142
|
+
// Owner check requires a resolved acting actor — account-grain auth
|
|
143
|
+
// (`actor: null`) cannot match an actor-id `created_by`. Grant-admit
|
|
144
|
+
// fallback below handles the actor-grain editor-grant path.
|
|
145
|
+
if (is_owner(auth, cell))
|
|
146
|
+
return true;
|
|
147
|
+
if (grant_admits(auth, grants, 'editor'))
|
|
148
|
+
return true;
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Manage authorization for a cell — `admin || owner`.
|
|
153
|
+
*
|
|
154
|
+
* The implicit tier above editor: gates `visibility` writes and all grant
|
|
155
|
+
* management (`cell_grant_create` / `_list` / `_revoke`). NOT delegable and
|
|
156
|
+
* NOT a grant level — an editor-grant holder is never a manager. Grants are
|
|
157
|
+
* not consulted.
|
|
158
|
+
*
|
|
159
|
+
* NULL `created_by` (system origin) has no owner, so manage falls to
|
|
160
|
+
* admin only — the explicit NULL guard lives in `is_owner`.
|
|
161
|
+
*
|
|
162
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
163
|
+
* @param cell - the cell row
|
|
164
|
+
* @returns whether the caller is in the manage tier for the cell
|
|
165
|
+
*/
|
|
166
|
+
export const can_manage_cell = (auth, cell) => {
|
|
167
|
+
if (!auth)
|
|
168
|
+
return false;
|
|
169
|
+
if (has_role(auth, ROLE_ADMIN))
|
|
170
|
+
return true;
|
|
171
|
+
return is_owner(auth, cell);
|
|
172
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic cell-data base schema.
|
|
3
|
+
*
|
|
4
|
+
* The wire-side shape every cell consumer can read without per-kind
|
|
5
|
+
* knowledge: a permissive bag with three universally-relevant typed
|
|
6
|
+
* fields. Per-kind schemas extend this and narrow `kind` to a literal.
|
|
7
|
+
*
|
|
8
|
+
* Loose object: arbitrary additional fields pass through unvalidated,
|
|
9
|
+
* preserving the "unknown kinds ship without RPC churn" property. Per-kind
|
|
10
|
+
* shape enforcement is opt-in via the `validate_data` deps slot — see
|
|
11
|
+
* `cell_actions.ts`.
|
|
12
|
+
*
|
|
13
|
+
* **Discipline**: a field joins `CellData` only when at least two
|
|
14
|
+
* consumers in different domains read it generically. `kind` (editor
|
|
15
|
+
* dispatch + sub-API registry), `label` (list/index rendering), and
|
|
16
|
+
* `summary` (card subtitle + share-target description) meet this bar.
|
|
17
|
+
* Future candidates require evidence of generic usage — otherwise they
|
|
18
|
+
* stay per-kind.
|
|
19
|
+
*
|
|
20
|
+
* **Visibility is not in here.** Access control is a peer of `cell_grant`,
|
|
21
|
+
* not content metadata — `cell.visibility` lives as a top-level column on
|
|
22
|
+
* `CellJson` and `CellRow` (the `CellVisibility` enum is defined in
|
|
23
|
+
* `cell_action_specs.ts` next to the wire fields that use it), and is
|
|
24
|
+
* enforced by `can_view_cell` reading the column directly (no JSON dive).
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
/**
|
|
30
|
+
* Base cell-data shape. All fields optional; loose mode admits arbitrary
|
|
31
|
+
* 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.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CellData: z.ZodObject<{
|
|
39
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
40
|
+
label: z.ZodOptional<z.ZodString>;
|
|
41
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
42
|
+
}, z.core.$loose>;
|
|
43
|
+
export type CellData = z.infer<typeof CellData>;
|
|
44
|
+
//# sourceMappingURL=cell_data_schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_data_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_data_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ;;;;iBAInB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC"}
|