@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-item RPC specs — declarative contract for the four ordered-child
|
|
3
|
+
* verbs (`insert` / `move` / `delete` / `list`).
|
|
4
|
+
*
|
|
5
|
+
* `(parent_id, position) → child_id` rows. `position` is opaque text
|
|
6
|
+
* (fractional-indexing key); the wire validates the alphabet
|
|
7
|
+
* (`^[0-9A-Za-z]+$`) and length, the lex-ordering invariant is the
|
|
8
|
+
* client's contract.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
14
|
+
import { FRACTIONAL_INDEX_LENGTH_MAX, FRACTIONAL_INDEX_REGEX, } from '@fuzdev/fuz_util/fractional_index.js';
|
|
15
|
+
import { ActingActor } from '../http/auth_shape.js';
|
|
16
|
+
// -- Error reasons ----------------------------------------------------------
|
|
17
|
+
/** Error reason — `cell_item_list` got neither `parent_id` nor `child_id`. */
|
|
18
|
+
export const ERROR_CELL_ITEM_LIST_REQUIRES_PARENT_OR_CHILD = 'cell_item_list_requires_parent_or_child';
|
|
19
|
+
/**
|
|
20
|
+
* Error reason — `(parent_id, position)` collision on `cell_item_insert`
|
|
21
|
+
* or `cell_item_move`. Surfaces when two clients computed the same
|
|
22
|
+
* fractional-indexing key (rare given helper-side jitter; the safety
|
|
23
|
+
* net for the residual race). Client refreshes its bracket and retries.
|
|
24
|
+
*/
|
|
25
|
+
export const ERROR_CELL_ITEM_POSITION_TAKEN = 'cell_item_position_taken';
|
|
26
|
+
// -- Shared schemas ---------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Position grammar — base62 fractional-indexing key. Wire enforces
|
|
29
|
+
* non-empty, alphabet only, and the helper's `FRACTIONAL_INDEX_LENGTH_MAX`
|
|
30
|
+
* cap (well above realistic lengths even for hundreds of consecutive
|
|
31
|
+
* front-inserts; set high to avoid arbitrary cliffs). Lex ordering is the
|
|
32
|
+
* contract; the no-trailing-`'0'` invariant lives in the helper, not the
|
|
33
|
+
* wire.
|
|
34
|
+
*/
|
|
35
|
+
export const CellItemPosition = z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(FRACTIONAL_INDEX_LENGTH_MAX)
|
|
39
|
+
.regex(FRACTIONAL_INDEX_REGEX)
|
|
40
|
+
.brand('CellItemPosition');
|
|
41
|
+
/**
|
|
42
|
+
* Wire-format for a `cell_item` row.
|
|
43
|
+
*
|
|
44
|
+
* `position` is branded `CellItemPosition` so consumers that round-trip
|
|
45
|
+
* the value back into a `position_after` / `position` input field don't
|
|
46
|
+
* need a cast at every call site. Wire ingress is validated by the
|
|
47
|
+
* `CellItemPosition` Zod schema (alphabet + length); wire egress trusts
|
|
48
|
+
* the DB CHECK constraint that backs `cell_item.position`, so the
|
|
49
|
+
* server-side `to_item_json` casts a raw string from `CellItemRow`.
|
|
50
|
+
*/
|
|
51
|
+
export const ItemJson = z.strictObject({
|
|
52
|
+
parent_id: Uuid,
|
|
53
|
+
position: CellItemPosition,
|
|
54
|
+
child_id: Uuid,
|
|
55
|
+
created_at: z.string(),
|
|
56
|
+
});
|
|
57
|
+
// -- cell_item_insert -------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Input for `cell_item_insert`. Caller computes `position` via
|
|
60
|
+
* `fractional_index_between(prev, next)` (`@fuzdev/fuz_util/fractional_index.js`)
|
|
61
|
+
* client-side. Returns `cell_item_position_taken` on `(parent_id,
|
|
62
|
+
* position)` unique violation; client refreshes bracket and retries.
|
|
63
|
+
*/
|
|
64
|
+
export const CellItemInsertInput = z.strictObject({
|
|
65
|
+
parent_id: Uuid.meta({ description: 'Cell to insert into.' }),
|
|
66
|
+
child_id: Uuid.meta({ description: 'Cell to insert as a child.' }),
|
|
67
|
+
position: CellItemPosition.meta({
|
|
68
|
+
description: 'Fractional-indexing key. Client-computed via `fractional_index_between`.',
|
|
69
|
+
}),
|
|
70
|
+
acting: ActingActor,
|
|
71
|
+
});
|
|
72
|
+
export const CellItemInsertOutput = z.strictObject({ item: ItemJson });
|
|
73
|
+
// -- cell_item_move ---------------------------------------------------------
|
|
74
|
+
/**
|
|
75
|
+
* Input for `cell_item_move`. Move within the same parent (cross-parent
|
|
76
|
+
* moves are a future extension).
|
|
77
|
+
*/
|
|
78
|
+
export const CellItemMoveInput = z.strictObject({
|
|
79
|
+
parent_id: Uuid.meta({ description: 'Parent cell.' }),
|
|
80
|
+
position: CellItemPosition.meta({ description: 'Current position of the row to move.' }),
|
|
81
|
+
new_position: CellItemPosition.meta({ description: 'New fractional-indexing key.' }),
|
|
82
|
+
acting: ActingActor,
|
|
83
|
+
});
|
|
84
|
+
export const CellItemMoveOutput = z.strictObject({ item: ItemJson });
|
|
85
|
+
// -- cell_item_delete -------------------------------------------------------
|
|
86
|
+
/** Input for `cell_item_delete`. Idempotent on the slot key. */
|
|
87
|
+
export const CellItemDeleteInput = z.strictObject({
|
|
88
|
+
parent_id: Uuid.meta({ description: 'Parent cell.' }),
|
|
89
|
+
position: CellItemPosition.meta({ description: 'Slot to delete.' }),
|
|
90
|
+
acting: ActingActor,
|
|
91
|
+
});
|
|
92
|
+
export const CellItemDeleteOutput = z.strictObject({
|
|
93
|
+
ok: z.literal(true),
|
|
94
|
+
deleted: z.boolean(),
|
|
95
|
+
});
|
|
96
|
+
// -- cell_item_list ---------------------------------------------------------
|
|
97
|
+
/**
|
|
98
|
+
* Input for `cell_item_list`. Pass `parent_id` for forward items or
|
|
99
|
+
* `child_id` for reverse lists — exactly one. Reverse listing has 2-layer
|
|
100
|
+
* authz (child view-check gates the call; per-parent view-check filters
|
|
101
|
+
* the rows).
|
|
102
|
+
*
|
|
103
|
+
* Forward listing supports cursor pagination via `position_after`
|
|
104
|
+
* (return rows with `position > position_after`). The reverse listing
|
|
105
|
+
* doesn't paginate (the result set is small in practice — number of
|
|
106
|
+
* parents containing a given child).
|
|
107
|
+
*/
|
|
108
|
+
export const CellItemListInput = z
|
|
109
|
+
.strictObject({
|
|
110
|
+
parent_id: Uuid.optional().meta({
|
|
111
|
+
description: 'List forward items whose parent is this cell.',
|
|
112
|
+
}),
|
|
113
|
+
child_id: Uuid.optional().meta({
|
|
114
|
+
description: 'List reverse parents whose child is this cell.',
|
|
115
|
+
}),
|
|
116
|
+
position_after: CellItemPosition.optional().meta({
|
|
117
|
+
description: 'Cursor for forward pagination — return rows whose position > this.',
|
|
118
|
+
}),
|
|
119
|
+
limit: z.number().int().positive().max(500).optional().meta({
|
|
120
|
+
description: 'Page size cap (max 500). Omit for unbounded — explicit list calls escape the bundled `cell_get` cap.',
|
|
121
|
+
}),
|
|
122
|
+
acting: ActingActor,
|
|
123
|
+
})
|
|
124
|
+
.refine((v) => Boolean(v.parent_id) !== Boolean(v.child_id), {
|
|
125
|
+
message: ERROR_CELL_ITEM_LIST_REQUIRES_PARENT_OR_CHILD,
|
|
126
|
+
});
|
|
127
|
+
export const CellItemListOutput = z.strictObject({
|
|
128
|
+
items: z.array(ItemJson),
|
|
129
|
+
});
|
|
130
|
+
// -- Action specs -----------------------------------------------------------
|
|
131
|
+
export const cell_item_insert_action_spec = {
|
|
132
|
+
method: 'cell_item_insert',
|
|
133
|
+
kind: 'request_response',
|
|
134
|
+
initiator: 'frontend',
|
|
135
|
+
auth: { account: 'required', actor: 'required' },
|
|
136
|
+
side_effects: true,
|
|
137
|
+
input: CellItemInsertInput,
|
|
138
|
+
output: CellItemInsertOutput,
|
|
139
|
+
async: true,
|
|
140
|
+
description: 'Insert a cell as an ordered child at `position` under `parent`. Caller must be able to edit `parent` and view `child`. Returns `cell_item_position_taken` on `(parent_id, position)` unique violation; client refreshes bracket and retries.',
|
|
141
|
+
};
|
|
142
|
+
export const cell_item_move_action_spec = {
|
|
143
|
+
method: 'cell_item_move',
|
|
144
|
+
kind: 'request_response',
|
|
145
|
+
initiator: 'frontend',
|
|
146
|
+
auth: { account: 'required', actor: 'required' },
|
|
147
|
+
side_effects: true,
|
|
148
|
+
input: CellItemMoveInput,
|
|
149
|
+
output: CellItemMoveOutput,
|
|
150
|
+
async: true,
|
|
151
|
+
description: 'Move an item within its parent to a new position. Caller must be able to edit `parent`. Returns `cell_item_position_taken` on the new-position unique violation.',
|
|
152
|
+
};
|
|
153
|
+
export const cell_item_delete_action_spec = {
|
|
154
|
+
method: 'cell_item_delete',
|
|
155
|
+
kind: 'request_response',
|
|
156
|
+
initiator: 'frontend',
|
|
157
|
+
auth: { account: 'required', actor: 'required' },
|
|
158
|
+
side_effects: true,
|
|
159
|
+
input: CellItemDeleteInput,
|
|
160
|
+
output: CellItemDeleteOutput,
|
|
161
|
+
async: true,
|
|
162
|
+
description: 'Delete the item at `(parent, position)`. Idempotent — `deleted: false` when no row matched. Caller must be able to edit `parent`.',
|
|
163
|
+
};
|
|
164
|
+
export const cell_item_list_action_spec = {
|
|
165
|
+
method: 'cell_item_list',
|
|
166
|
+
kind: 'request_response',
|
|
167
|
+
initiator: 'frontend',
|
|
168
|
+
auth: { account: 'optional', actor: 'optional' },
|
|
169
|
+
side_effects: false,
|
|
170
|
+
input: CellItemListInput,
|
|
171
|
+
output: CellItemListOutput,
|
|
172
|
+
async: true,
|
|
173
|
+
rate_limit: 'ip',
|
|
174
|
+
description: 'List forward items (pass `parent_id`) or reverse parents (pass `child_id`). Forward listing filters children to those the caller may view (strict target-visibility). Reverse listing has 2-layer authz: gate on `can_view_cell(child)` first (404 otherwise), then filter rows by per-parent `can_view_cell`. Per-IP rate-limited — symmetric with `cell_get` to bound public-surface id-walking.',
|
|
175
|
+
};
|
|
176
|
+
/** All cell_item action specs — composed into `all_cell_action_specs`. */
|
|
177
|
+
export const all_cell_item_action_specs = [
|
|
178
|
+
cell_item_insert_action_spec,
|
|
179
|
+
cell_item_move_action_spec,
|
|
180
|
+
cell_item_delete_action_spec,
|
|
181
|
+
cell_item_list_action_spec,
|
|
182
|
+
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-item RPC handlers.
|
|
3
|
+
*
|
|
4
|
+
* Four `request_response` actions bound to the specs in
|
|
5
|
+
* `./cell_item_action_specs.ts`:
|
|
6
|
+
*
|
|
7
|
+
* - `cell_item_insert` — admin / owner / editor-grant on `parent` may
|
|
8
|
+
* insert; `child` must be view-admitted. Returns
|
|
9
|
+
* `cell_item_position_taken` on `(parent_id, position)` unique
|
|
10
|
+
* violation; client refreshes bracket and retries.
|
|
11
|
+
* - `cell_item_move` — admin / owner / editor-grant on `parent`. Same
|
|
12
|
+
* collision-error shape as insert.
|
|
13
|
+
* - `cell_item_delete` — admin / owner / editor-grant on `parent`.
|
|
14
|
+
* Idempotent: `deleted: false` when no row matched.
|
|
15
|
+
* - `cell_item_list` — bidirectional. Forward (pass `parent_id`) is
|
|
16
|
+
* gated on `can_view_cell(parent)` and filters children to those the
|
|
17
|
+
* caller may view (strict target-visibility, batched). Reverse (pass
|
|
18
|
+
* `child_id`) has 2-layer authz: gate on `can_view_cell(child)`, then
|
|
19
|
+
* filter rows by `can_view_cell(parent)`.
|
|
20
|
+
*
|
|
21
|
+
* IDOR-mask 404s on cell-miss / cell-unviewable, mirroring the existence-
|
|
22
|
+
* leak guards in `cell_actions.ts`.
|
|
23
|
+
*
|
|
24
|
+
* Audit events `cell_item_insert` / `cell_item_move` / `cell_item_delete`
|
|
25
|
+
* carry IDs only — see `./cell_item_audit_metadata.ts`.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { type RpcAction } from '../actions/action_rpc.js';
|
|
30
|
+
import type { RouteFactoryDeps } from './deps.js';
|
|
31
|
+
import { type ItemJson } from './cell_item_action_specs.js';
|
|
32
|
+
import { type CellItemRow } from '../db/cell_item_queries.js';
|
|
33
|
+
export type CellItemActionDeps = Pick<RouteFactoryDeps, 'log' | 'audit'>;
|
|
34
|
+
export declare const to_item_json: (row: CellItemRow) => ItemJson;
|
|
35
|
+
/** Create the four `cell_item_*` RPC actions. */
|
|
36
|
+
export declare const create_cell_item_actions: (deps: CellItemActionDeps) => Array<RpcAction>;
|
|
37
|
+
//# sourceMappingURL=cell_item_actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_item_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_item_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAeN,KAAK,QAAQ,EACb,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAMN,KAAK,WAAW,EAChB,MAAM,4BAA4B,CAAC;AAOpC,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC;AAEzE,eAAO,MAAM,YAAY,GAAI,KAAK,WAAW,KAAG,QAS9C,CAAC;AAOH,iDAAiD;AACjD,eAAO,MAAM,wBAAwB,GAAI,MAAM,kBAAkB,KAAG,KAAK,CAAC,SAAS,CA2KlF,CAAC"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-item RPC handlers.
|
|
3
|
+
*
|
|
4
|
+
* Four `request_response` actions bound to the specs in
|
|
5
|
+
* `./cell_item_action_specs.ts`:
|
|
6
|
+
*
|
|
7
|
+
* - `cell_item_insert` — admin / owner / editor-grant on `parent` may
|
|
8
|
+
* insert; `child` must be view-admitted. Returns
|
|
9
|
+
* `cell_item_position_taken` on `(parent_id, position)` unique
|
|
10
|
+
* violation; client refreshes bracket and retries.
|
|
11
|
+
* - `cell_item_move` — admin / owner / editor-grant on `parent`. Same
|
|
12
|
+
* collision-error shape as insert.
|
|
13
|
+
* - `cell_item_delete` — admin / owner / editor-grant on `parent`.
|
|
14
|
+
* Idempotent: `deleted: false` when no row matched.
|
|
15
|
+
* - `cell_item_list` — bidirectional. Forward (pass `parent_id`) is
|
|
16
|
+
* gated on `can_view_cell(parent)` and filters children to those the
|
|
17
|
+
* caller may view (strict target-visibility, batched). Reverse (pass
|
|
18
|
+
* `child_id`) has 2-layer authz: gate on `can_view_cell(child)`, then
|
|
19
|
+
* filter rows by `can_view_cell(parent)`.
|
|
20
|
+
*
|
|
21
|
+
* IDOR-mask 404s on cell-miss / cell-unviewable, mirroring the existence-
|
|
22
|
+
* leak guards in `cell_actions.ts`.
|
|
23
|
+
*
|
|
24
|
+
* Audit events `cell_item_insert` / `cell_item_move` / `cell_item_delete`
|
|
25
|
+
* carry IDs only — see `./cell_item_audit_metadata.ts`.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { rpc_action, } from '../actions/action_rpc.js';
|
|
30
|
+
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
31
|
+
import { is_pg_unique_violation } from '../db/pg_error.js';
|
|
32
|
+
import { cell_item_insert_action_spec, cell_item_move_action_spec, cell_item_delete_action_spec, cell_item_list_action_spec, ERROR_CELL_ITEM_POSITION_TAKEN, } from './cell_item_action_specs.js';
|
|
33
|
+
import { ERROR_CELL_NOT_FOUND } from './cell_action_specs.js';
|
|
34
|
+
import { can_view_cell, can_edit_cell } from './cell_authorize.js';
|
|
35
|
+
import { filter_visible_target_ids } from './cell_relation_visibility.js';
|
|
36
|
+
import { query_cell_get } from '../db/cell_queries.js';
|
|
37
|
+
import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
|
|
38
|
+
import { query_cell_item_insert, query_cell_item_move, query_cell_item_delete, query_cell_item_list_for_parent, query_cell_item_list_for_child, } from '../db/cell_item_queries.js';
|
|
39
|
+
export const to_item_json = (row) => ({
|
|
40
|
+
parent_id: row.parent_id,
|
|
41
|
+
// `cell.position` is a DB-shape string; brand at the wire boundary so
|
|
42
|
+
// consumers can round-trip it without casting (the DB CHECK constraint
|
|
43
|
+
// is the runtime validator on egress, the Zod brand is the validator
|
|
44
|
+
// on ingress).
|
|
45
|
+
position: row.position,
|
|
46
|
+
child_id: row.child_id,
|
|
47
|
+
created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString(),
|
|
48
|
+
});
|
|
49
|
+
const position_taken_error = () => jsonrpc_errors.invalid_params('cell_item position taken', {
|
|
50
|
+
reason: ERROR_CELL_ITEM_POSITION_TAKEN,
|
|
51
|
+
});
|
|
52
|
+
/** Create the four `cell_item_*` RPC actions. */
|
|
53
|
+
export const create_cell_item_actions = (deps) => {
|
|
54
|
+
const insert_handler = async (input, ctx) => {
|
|
55
|
+
const auth = ctx.auth;
|
|
56
|
+
const parent = await query_cell_get(ctx, input.parent_id);
|
|
57
|
+
if (!parent) {
|
|
58
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
59
|
+
}
|
|
60
|
+
const parent_grants = await query_cell_grant_list_for_cell(ctx, parent.id);
|
|
61
|
+
if (!can_edit_cell(auth, parent, parent_grants)) {
|
|
62
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
63
|
+
}
|
|
64
|
+
const child = await query_cell_get(ctx, input.child_id);
|
|
65
|
+
if (!child) {
|
|
66
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
67
|
+
}
|
|
68
|
+
// Child must be view-admitted — otherwise insert leaks existence.
|
|
69
|
+
const child_grants = await query_cell_grant_list_for_cell(ctx, child.id);
|
|
70
|
+
if (!can_view_cell(auth, child, child_grants)) {
|
|
71
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
72
|
+
}
|
|
73
|
+
let row;
|
|
74
|
+
try {
|
|
75
|
+
row = await query_cell_item_insert(ctx, {
|
|
76
|
+
parent_id: input.parent_id,
|
|
77
|
+
position: input.position,
|
|
78
|
+
child_id: input.child_id,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (is_pg_unique_violation(err))
|
|
83
|
+
throw position_taken_error();
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
deps.audit.emit(ctx, {
|
|
87
|
+
event_type: 'cell_item_insert',
|
|
88
|
+
actor_id: auth.actor.id,
|
|
89
|
+
account_id: auth.account.id,
|
|
90
|
+
ip: ctx.client_ip,
|
|
91
|
+
metadata: {
|
|
92
|
+
parent_id: row.parent_id,
|
|
93
|
+
position: row.position,
|
|
94
|
+
child_id: row.child_id,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return { item: to_item_json(row) };
|
|
98
|
+
};
|
|
99
|
+
const move_handler = async (input, ctx) => {
|
|
100
|
+
const auth = ctx.auth;
|
|
101
|
+
const parent = await query_cell_get(ctx, input.parent_id);
|
|
102
|
+
if (!parent) {
|
|
103
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
104
|
+
}
|
|
105
|
+
const parent_grants = await query_cell_grant_list_for_cell(ctx, parent.id);
|
|
106
|
+
if (!can_edit_cell(auth, parent, parent_grants)) {
|
|
107
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
108
|
+
}
|
|
109
|
+
let row;
|
|
110
|
+
try {
|
|
111
|
+
row = await query_cell_item_move(ctx, input.parent_id, input.position, input.new_position);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (is_pg_unique_violation(err))
|
|
115
|
+
throw position_taken_error();
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
if (!row) {
|
|
119
|
+
// Source row missing — raced with deleter. 404 covers the gap.
|
|
120
|
+
throw jsonrpc_errors.not_found('cell_item', { reason: ERROR_CELL_NOT_FOUND });
|
|
121
|
+
}
|
|
122
|
+
deps.audit.emit(ctx, {
|
|
123
|
+
event_type: 'cell_item_move',
|
|
124
|
+
actor_id: auth.actor.id,
|
|
125
|
+
account_id: auth.account.id,
|
|
126
|
+
ip: ctx.client_ip,
|
|
127
|
+
metadata: {
|
|
128
|
+
parent_id: row.parent_id,
|
|
129
|
+
position_old: input.position,
|
|
130
|
+
position_new: row.position,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return { item: to_item_json(row) };
|
|
134
|
+
};
|
|
135
|
+
const delete_handler = async (input, ctx) => {
|
|
136
|
+
const auth = ctx.auth;
|
|
137
|
+
const parent = await query_cell_get(ctx, input.parent_id);
|
|
138
|
+
if (!parent) {
|
|
139
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
140
|
+
}
|
|
141
|
+
const parent_grants = await query_cell_grant_list_for_cell(ctx, parent.id);
|
|
142
|
+
if (!can_edit_cell(auth, parent, parent_grants)) {
|
|
143
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
144
|
+
}
|
|
145
|
+
const deleted = await query_cell_item_delete(ctx, input.parent_id, input.position);
|
|
146
|
+
if (deleted) {
|
|
147
|
+
deps.audit.emit(ctx, {
|
|
148
|
+
event_type: 'cell_item_delete',
|
|
149
|
+
actor_id: auth.actor.id,
|
|
150
|
+
account_id: auth.account.id,
|
|
151
|
+
ip: ctx.client_ip,
|
|
152
|
+
metadata: {
|
|
153
|
+
parent_id: deleted.parent_id,
|
|
154
|
+
position: deleted.position,
|
|
155
|
+
child_id: deleted.child_id,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return { ok: true, deleted: deleted !== null };
|
|
160
|
+
};
|
|
161
|
+
const list_handler = async (input, ctx) => {
|
|
162
|
+
const auth = ctx.auth;
|
|
163
|
+
// Forward listing: gate on can_view_cell(parent), then filter the
|
|
164
|
+
// children to those the caller may view (strict target-visibility).
|
|
165
|
+
if (input.parent_id !== undefined) {
|
|
166
|
+
const parent = await query_cell_get(ctx, input.parent_id);
|
|
167
|
+
if (!parent) {
|
|
168
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
169
|
+
}
|
|
170
|
+
const parent_grants = auth ? await query_cell_grant_list_for_cell(ctx, parent.id) : null;
|
|
171
|
+
if (!can_view_cell(auth, parent, parent_grants)) {
|
|
172
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
173
|
+
}
|
|
174
|
+
const rows = await query_cell_item_list_for_parent(ctx, parent.id, {
|
|
175
|
+
limit: input.limit,
|
|
176
|
+
position_after: input.position_after,
|
|
177
|
+
});
|
|
178
|
+
const visible_children = await filter_visible_target_ids(ctx, auth, rows.map((r) => r.child_id));
|
|
179
|
+
return { items: rows.filter((r) => visible_children.has(r.child_id)).map(to_item_json) };
|
|
180
|
+
}
|
|
181
|
+
// Reverse listing: 2-layer authz. First, can_view_cell(child).
|
|
182
|
+
const child = await query_cell_get(ctx, input.child_id);
|
|
183
|
+
if (!child) {
|
|
184
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
185
|
+
}
|
|
186
|
+
const child_grants = auth ? await query_cell_grant_list_for_cell(ctx, child.id) : null;
|
|
187
|
+
if (!can_view_cell(auth, child, child_grants)) {
|
|
188
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
189
|
+
}
|
|
190
|
+
// Then filter rows by per-parent can_view_cell. Batched (not N+1):
|
|
191
|
+
// one bulk visibility filter over all parent ids, same as the forward
|
|
192
|
+
// branch. Bounded by `limit` at the query so a heavily inbound-linked
|
|
193
|
+
// child can't force an unbounded fetch on this public endpoint.
|
|
194
|
+
const rows = await query_cell_item_list_for_child(ctx, child.id, { limit: input.limit });
|
|
195
|
+
const visible_parents = await filter_visible_target_ids(ctx, auth, rows.map((r) => r.parent_id));
|
|
196
|
+
return { items: rows.filter((r) => visible_parents.has(r.parent_id)).map(to_item_json) };
|
|
197
|
+
};
|
|
198
|
+
return [
|
|
199
|
+
rpc_action(cell_item_insert_action_spec, insert_handler),
|
|
200
|
+
rpc_action(cell_item_move_action_spec, move_handler),
|
|
201
|
+
rpc_action(cell_item_delete_action_spec, delete_handler),
|
|
202
|
+
rpc_action(cell_item_list_action_spec, list_handler),
|
|
203
|
+
];
|
|
204
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-log metadata schemas for `cell_item_insert` / `_move` / `_delete`.
|
|
3
|
+
*
|
|
4
|
+
* IDs only (positions are opaque text, not user-derived; safe to log).
|
|
5
|
+
* Apps register these via `extra_events:` on `create_audit_log_config`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
/** Metadata envelope for `cell_item_insert`. */
|
|
11
|
+
export declare const CellItemInsertAuditMetadata: z.ZodObject<{
|
|
12
|
+
parent_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
13
|
+
position: z.ZodString;
|
|
14
|
+
child_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
15
|
+
}, z.core.$loose>;
|
|
16
|
+
export type CellItemInsertAuditMetadata = z.infer<typeof CellItemInsertAuditMetadata>;
|
|
17
|
+
/**
|
|
18
|
+
* Metadata envelope for `cell_item_move`. Carries both old and new
|
|
19
|
+
* position so the audit trail shows the reorder without a join back to
|
|
20
|
+
* the live row.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CellItemMoveAuditMetadata: z.ZodObject<{
|
|
23
|
+
parent_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
24
|
+
position_old: z.ZodString;
|
|
25
|
+
position_new: z.ZodString;
|
|
26
|
+
}, z.core.$loose>;
|
|
27
|
+
export type CellItemMoveAuditMetadata = z.infer<typeof CellItemMoveAuditMetadata>;
|
|
28
|
+
/** Metadata envelope for `cell_item_delete`. */
|
|
29
|
+
export declare const CellItemDeleteAuditMetadata: z.ZodObject<{
|
|
30
|
+
parent_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
31
|
+
position: z.ZodString;
|
|
32
|
+
child_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
33
|
+
}, z.core.$loose>;
|
|
34
|
+
export type CellItemDeleteAuditMetadata = z.infer<typeof CellItemDeleteAuditMetadata>;
|
|
35
|
+
//# sourceMappingURL=cell_item_audit_metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_item_audit_metadata.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_item_audit_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,gDAAgD;AAChD,eAAO,MAAM,2BAA2B;;;;iBAItC,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEtF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;;;iBAIpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,gDAAgD;AAChD,eAAO,MAAM,2BAA2B;;;;iBAItC,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-log metadata schemas for `cell_item_insert` / `_move` / `_delete`.
|
|
3
|
+
*
|
|
4
|
+
* IDs only (positions are opaque text, not user-derived; safe to log).
|
|
5
|
+
* Apps register these via `extra_events:` on `create_audit_log_config`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
11
|
+
/** Metadata envelope for `cell_item_insert`. */
|
|
12
|
+
export const CellItemInsertAuditMetadata = z.looseObject({
|
|
13
|
+
parent_id: Uuid,
|
|
14
|
+
position: z.string(),
|
|
15
|
+
child_id: Uuid,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Metadata envelope for `cell_item_move`. Carries both old and new
|
|
19
|
+
* position so the audit trail shows the reorder without a join back to
|
|
20
|
+
* the live row.
|
|
21
|
+
*/
|
|
22
|
+
export const CellItemMoveAuditMetadata = z.looseObject({
|
|
23
|
+
parent_id: Uuid,
|
|
24
|
+
position_old: z.string(),
|
|
25
|
+
position_new: z.string(),
|
|
26
|
+
});
|
|
27
|
+
/** Metadata envelope for `cell_item_delete`. */
|
|
28
|
+
export const CellItemDeleteAuditMetadata = z.looseObject({
|
|
29
|
+
parent_id: Uuid,
|
|
30
|
+
position: z.string(),
|
|
31
|
+
child_id: Uuid,
|
|
32
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict relation-read visibility filter.
|
|
3
|
+
*
|
|
4
|
+
* Forward relation reads (the `cell_get` bundle, forward `cell_field_list`
|
|
5
|
+
* / `cell_item_list`, the deep-clone walk) must not surface edges whose
|
|
6
|
+
* target the caller cannot view — otherwise an editor of a public parent
|
|
7
|
+
* could enumerate private children by id, or a public cell could leak the
|
|
8
|
+
* existence of private linked cells. This helper bulk-loads the target
|
|
9
|
+
* cells + their grants and runs `can_view_cell` per target in memory,
|
|
10
|
+
* returning the set of viewable target ids. Batched (two queries for the
|
|
11
|
+
* whole id-set) to avoid the N+1 a naive per-row check would cause.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
16
|
+
import type { QueryDeps } from '../db/query_deps.js';
|
|
17
|
+
import type { RequestContext } from './request_context.js';
|
|
18
|
+
/**
|
|
19
|
+
* Return the subset of `target_ids` the caller may view.
|
|
20
|
+
*
|
|
21
|
+
* Soft-deleted targets and ids with no matching cell are absent from the
|
|
22
|
+
* result (treated as not-viewable). Grants are loaded only for
|
|
23
|
+
* authenticated callers — `null` auth admits solely via the public
|
|
24
|
+
* branch of `can_view_cell`, so the grant load is skipped entirely.
|
|
25
|
+
*
|
|
26
|
+
* @param deps - query deps
|
|
27
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
28
|
+
* @param target_ids - candidate cell ids (duplicates are harmless)
|
|
29
|
+
* @returns the set of ids the caller may view
|
|
30
|
+
*/
|
|
31
|
+
export declare const filter_visible_target_ids: (deps: QueryDeps, auth: RequestContext | null, target_ids: ReadonlyArray<Uuid>) => Promise<Set<Uuid>>;
|
|
32
|
+
//# sourceMappingURL=cell_relation_visibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_relation_visibility.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_relation_visibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAKzD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,MAAM,cAAc,GAAG,IAAI,EAC3B,YAAY,aAAa,CAAC,IAAI,CAAC,KAC7B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAwBnB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict relation-read visibility filter.
|
|
3
|
+
*
|
|
4
|
+
* Forward relation reads (the `cell_get` bundle, forward `cell_field_list`
|
|
5
|
+
* / `cell_item_list`, the deep-clone walk) must not surface edges whose
|
|
6
|
+
* target the caller cannot view — otherwise an editor of a public parent
|
|
7
|
+
* could enumerate private children by id, or a public cell could leak the
|
|
8
|
+
* existence of private linked cells. This helper bulk-loads the target
|
|
9
|
+
* cells + their grants and runs `can_view_cell` per target in memory,
|
|
10
|
+
* returning the set of viewable target ids. Batched (two queries for the
|
|
11
|
+
* whole id-set) to avoid the N+1 a naive per-row check would cause.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { can_view_cell } from './cell_authorize.js';
|
|
16
|
+
import { query_cell_load_many } from '../db/cell_queries.js';
|
|
17
|
+
import { query_cell_grant_list_for_cells } from '../db/cell_grant_queries.js';
|
|
18
|
+
/**
|
|
19
|
+
* Return the subset of `target_ids` the caller may view.
|
|
20
|
+
*
|
|
21
|
+
* Soft-deleted targets and ids with no matching cell are absent from the
|
|
22
|
+
* result (treated as not-viewable). Grants are loaded only for
|
|
23
|
+
* authenticated callers — `null` auth admits solely via the public
|
|
24
|
+
* branch of `can_view_cell`, so the grant load is skipped entirely.
|
|
25
|
+
*
|
|
26
|
+
* @param deps - query deps
|
|
27
|
+
* @param auth - request context, or `null` for unauthenticated callers
|
|
28
|
+
* @param target_ids - candidate cell ids (duplicates are harmless)
|
|
29
|
+
* @returns the set of ids the caller may view
|
|
30
|
+
*/
|
|
31
|
+
export const filter_visible_target_ids = async (deps, auth, target_ids) => {
|
|
32
|
+
const visible = new Set();
|
|
33
|
+
if (target_ids.length === 0)
|
|
34
|
+
return visible;
|
|
35
|
+
const unique = [...new Set(target_ids)];
|
|
36
|
+
const cells = await query_cell_load_many(deps, unique);
|
|
37
|
+
// Grants only matter for authenticated callers — null auth admits via
|
|
38
|
+
// the public branch alone, so skip the grant load entirely.
|
|
39
|
+
const grants_by_cell = new Map();
|
|
40
|
+
if (auth) {
|
|
41
|
+
const grant_rows = await query_cell_grant_list_for_cells(deps, unique);
|
|
42
|
+
for (const g of grant_rows) {
|
|
43
|
+
let list = grants_by_cell.get(g.cell_id);
|
|
44
|
+
if (list === undefined) {
|
|
45
|
+
list = [];
|
|
46
|
+
grants_by_cell.set(g.cell_id, list);
|
|
47
|
+
}
|
|
48
|
+
list.push(g);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const cell of cells) {
|
|
52
|
+
const grants = auth ? (grants_by_cell.get(cell.id) ?? []) : null;
|
|
53
|
+
if (can_view_cell(auth, cell, grants))
|
|
54
|
+
visible.add(cell.id);
|
|
55
|
+
}
|
|
56
|
+
return visible;
|
|
57
|
+
};
|
package/dist/auth/deps.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import type { Logger } from '@fuzdev/fuz_util/log.js';
|
|
11
|
+
import type { FactStore } from '@fuzdev/fuz_util/fact_store.js';
|
|
11
12
|
import type { Keyring } from './keyring.js';
|
|
12
13
|
import type { PasswordHashDeps } from './password.js';
|
|
13
14
|
import type { Db } from '../db/db.js';
|
|
@@ -42,6 +43,14 @@ export interface AppDeps {
|
|
|
42
43
|
* is no pool slot on the handler context.
|
|
43
44
|
*/
|
|
44
45
|
audit: AuditEmitter;
|
|
46
|
+
/**
|
|
47
|
+
* Optional content-addressed byte store. Present only on backends that
|
|
48
|
+
* serve binary content (facts) — minimal consumers leave it unset. The
|
|
49
|
+
* consumer constructs a `PgFactStore` (`db/fact_store.ts`) wired to a
|
|
50
|
+
* `file_fact_fetcher` (`server/file_fact_fetcher.ts`) at its own backend
|
|
51
|
+
* assembly and assigns it here; `create_app_backend` stays facts-agnostic.
|
|
52
|
+
*/
|
|
53
|
+
fact_store?: FactStore;
|
|
45
54
|
}
|
|
46
55
|
/**
|
|
47
56
|
* Capabilities for route spec factories.
|
package/dist/auth/deps.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gCAAgC,CAAC;AAE9D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACvB,+DAA+D;IAC/D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,2BAA2B;IAC3B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,yBAAyB;IACzB,EAAE,EAAE,EAAE,CAAC;IACP,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC"}
|
|
@@ -134,6 +134,36 @@ export declare const query_role_grant_has_role: (deps: QueryDeps, actor_id: stri
|
|
|
134
134
|
* @returns `true` if any actor on the account has an active global role_grant for `role`
|
|
135
135
|
*/
|
|
136
136
|
export declare const query_account_has_global_role: (deps: QueryDeps, account_id: string, role: string) => Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* Like `query_account_has_global_role`, but only counts the grant when the
|
|
139
|
+
* **account itself is active** (`deleted_at IS NULL`). Used by the last-admin
|
|
140
|
+
* branch of the removability guard: a soft-deleted admin can't log in and is
|
|
141
|
+
* excluded from `query_count_active_accounts_with_global_role`, so the guard
|
|
142
|
+
* must use the same active-account predicate when testing whether the *target*
|
|
143
|
+
* is an admin — otherwise removing an already-tombstoned admin is falsely
|
|
144
|
+
* blocked as `cannot_delete_last_admin` even though it can't lower the active
|
|
145
|
+
* count. The keeper branch deliberately uses the unconditional
|
|
146
|
+
* `query_account_has_global_role` (a keeper is never removable regardless of
|
|
147
|
+
* tombstone state).
|
|
148
|
+
*
|
|
149
|
+
* @param deps - query dependencies
|
|
150
|
+
* @param account_id - the account to check
|
|
151
|
+
* @param role - the role to check for (e.g. `ROLE_ADMIN`)
|
|
152
|
+
* @returns `true` if the account is active and any of its actors holds an active global `role` grant
|
|
153
|
+
*/
|
|
154
|
+
export declare const query_account_has_active_global_role: (deps: QueryDeps, account_id: string, role: string) => Promise<boolean>;
|
|
155
|
+
/**
|
|
156
|
+
* Count **active** accounts (`deleted_at IS NULL`) holding an active global
|
|
157
|
+
* role_grant for `role`. Used by the last-admin guard on `account_delete` /
|
|
158
|
+
* `account_purge`: a soft-deleted account's admin grant isn't revoked, so a
|
|
159
|
+
* plain grant count would include tombstoned (unusable) admins — this joins
|
|
160
|
+
* `account` and excludes them, counting only admins that can actually log in.
|
|
161
|
+
*
|
|
162
|
+
* @param deps - query dependencies
|
|
163
|
+
* @param role - the role to count (e.g. `ROLE_ADMIN`)
|
|
164
|
+
* @returns the number of distinct active accounts with an active global `role` grant
|
|
165
|
+
*/
|
|
166
|
+
export declare const query_count_active_accounts_with_global_role: (deps: QueryDeps, role: string) => Promise<number>;
|
|
137
167
|
/**
|
|
138
168
|
* List all role_grants for an actor (including revoked/expired).
|
|
139
169
|
*/
|