@fuzdev/fuz_app 0.67.1 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/CLAUDE.md +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 +168 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +146 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +218 -4
- 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/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/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 +58 -5
- package/dist/testing/app_server.d.ts +12 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +36 -2
- 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 +76 -0
- package/dist/testing/cross_backend/capabilities.d.ts +31 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +3 -0
- package/dist/testing/cross_backend/cell_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_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/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +6 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +5 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +4 -0
- 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 +58 -0
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts +30 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +45 -1
- 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/package.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
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 const CellData = z.looseObject({
|
|
39
|
+
kind: z.string().optional(),
|
|
40
|
+
label: z.string().optional(),
|
|
41
|
+
summary: z.string().optional(),
|
|
42
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-field RPC specs — declarative contract for the three named-relation
|
|
3
|
+
* verbs (`set` / `delete` / `list`).
|
|
4
|
+
*
|
|
5
|
+
* `(source_id, name) → target_id` edges modeled after JSON object
|
|
6
|
+
* key/value pairs. One target per `(source_id, name)` pair: re-setting a
|
|
7
|
+
* name overwrites the prior target. `cell_field_list` is bidirectional:
|
|
8
|
+
* pass `source_id` for forward fields, `target_id` for reverse upfields
|
|
9
|
+
* (the latter has 2-layer authz — see handler).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
/** Error reason — `cell_field_list` got neither `source_id` nor `target_id`. */
|
|
15
|
+
export declare const ERROR_CELL_FIELD_LIST_REQUIRES_SOURCE_OR_TARGET: "cell_field_list_requires_source_or_target";
|
|
16
|
+
/**
|
|
17
|
+
* Field name grammar — fuz snake_case identifier convention. Anchored
|
|
18
|
+
* `^[a-z][a-z0-9_]{0,63}$`: leading letter, alphanumeric + underscore
|
|
19
|
+
* trailing, 64-char cap. No reserved names yet.
|
|
20
|
+
*/
|
|
21
|
+
export declare const CELL_FIELD_NAME_REGEX: RegExp;
|
|
22
|
+
export declare const CellFieldName: z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">;
|
|
23
|
+
export type CellFieldName = z.infer<typeof CellFieldName>;
|
|
24
|
+
/** Wire-format for a `cell_field` row. ISO `created_at`, branded UUIDs. */
|
|
25
|
+
export declare const FieldJson: z.ZodObject<{
|
|
26
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
27
|
+
name: z.ZodString;
|
|
28
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
29
|
+
created_at: z.ZodString;
|
|
30
|
+
}, z.core.$strict>;
|
|
31
|
+
export type FieldJson = z.infer<typeof FieldJson>;
|
|
32
|
+
/**
|
|
33
|
+
* Input for `cell_field_set`. UPSERT on `(source_id, name)` — re-issuing
|
|
34
|
+
* the same input updates `target_id` and bumps `created_at`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const CellFieldSetInput: z.ZodObject<{
|
|
37
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
38
|
+
name: z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">;
|
|
39
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
40
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
41
|
+
}, z.core.$strict>;
|
|
42
|
+
export type CellFieldSetInput = z.infer<typeof CellFieldSetInput>;
|
|
43
|
+
export declare const CellFieldSetOutput: z.ZodObject<{
|
|
44
|
+
field: z.ZodObject<{
|
|
45
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
46
|
+
name: z.ZodString;
|
|
47
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
48
|
+
created_at: z.ZodString;
|
|
49
|
+
}, z.core.$strict>;
|
|
50
|
+
}, z.core.$strict>;
|
|
51
|
+
export type CellFieldSetOutput = z.infer<typeof CellFieldSetOutput>;
|
|
52
|
+
/**
|
|
53
|
+
* Input for `cell_field_delete`. Idempotent: a successful response is
|
|
54
|
+
* returned even when no row matched.
|
|
55
|
+
*/
|
|
56
|
+
export declare const CellFieldDeleteInput: z.ZodObject<{
|
|
57
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
58
|
+
name: z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">;
|
|
59
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
60
|
+
}, z.core.$strict>;
|
|
61
|
+
export type CellFieldDeleteInput = z.infer<typeof CellFieldDeleteInput>;
|
|
62
|
+
export declare const CellFieldDeleteOutput: z.ZodObject<{
|
|
63
|
+
ok: z.ZodLiteral<true>;
|
|
64
|
+
deleted: z.ZodBoolean;
|
|
65
|
+
}, z.core.$strict>;
|
|
66
|
+
export type CellFieldDeleteOutput = z.infer<typeof CellFieldDeleteOutput>;
|
|
67
|
+
/**
|
|
68
|
+
* Input for `cell_field_list`. Pass `source_id` for forward fields or
|
|
69
|
+
* `target_id` for reverse upfields — exactly one (the schema rejects
|
|
70
|
+
* both / neither). Reverse listing has 2-layer authz (target view-check
|
|
71
|
+
* gates the call; per-source view-check filters the rows).
|
|
72
|
+
*
|
|
73
|
+
* Forward listing supports cursor pagination via `name_after` (return
|
|
74
|
+
* rows whose `name > name_after` lex). The reverse listing doesn't
|
|
75
|
+
* paginate (the result set is small in practice — number of sources
|
|
76
|
+
* pointing at a given target).
|
|
77
|
+
*/
|
|
78
|
+
export declare const CellFieldListInput: z.ZodObject<{
|
|
79
|
+
source_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
80
|
+
target_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
81
|
+
name_after: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">>;
|
|
82
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
83
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
84
|
+
}, z.core.$strict>;
|
|
85
|
+
export type CellFieldListInput = z.infer<typeof CellFieldListInput>;
|
|
86
|
+
export declare const CellFieldListOutput: z.ZodObject<{
|
|
87
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
88
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
89
|
+
name: z.ZodString;
|
|
90
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
91
|
+
created_at: z.ZodString;
|
|
92
|
+
}, z.core.$strict>>;
|
|
93
|
+
}, z.core.$strict>;
|
|
94
|
+
export type CellFieldListOutput = z.infer<typeof CellFieldListOutput>;
|
|
95
|
+
export declare const cell_field_set_action_spec: {
|
|
96
|
+
method: string;
|
|
97
|
+
kind: "request_response";
|
|
98
|
+
initiator: "frontend";
|
|
99
|
+
auth: {
|
|
100
|
+
account: "required";
|
|
101
|
+
actor: "required";
|
|
102
|
+
};
|
|
103
|
+
side_effects: true;
|
|
104
|
+
input: z.ZodObject<{
|
|
105
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
106
|
+
name: z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">;
|
|
107
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
108
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
109
|
+
}, z.core.$strict>;
|
|
110
|
+
output: z.ZodObject<{
|
|
111
|
+
field: z.ZodObject<{
|
|
112
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
113
|
+
name: z.ZodString;
|
|
114
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
115
|
+
created_at: z.ZodString;
|
|
116
|
+
}, z.core.$strict>;
|
|
117
|
+
}, z.core.$strict>;
|
|
118
|
+
async: true;
|
|
119
|
+
description: string;
|
|
120
|
+
};
|
|
121
|
+
export declare const cell_field_delete_action_spec: {
|
|
122
|
+
method: string;
|
|
123
|
+
kind: "request_response";
|
|
124
|
+
initiator: "frontend";
|
|
125
|
+
auth: {
|
|
126
|
+
account: "required";
|
|
127
|
+
actor: "required";
|
|
128
|
+
};
|
|
129
|
+
side_effects: true;
|
|
130
|
+
input: z.ZodObject<{
|
|
131
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
132
|
+
name: z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">;
|
|
133
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
134
|
+
}, z.core.$strict>;
|
|
135
|
+
output: z.ZodObject<{
|
|
136
|
+
ok: z.ZodLiteral<true>;
|
|
137
|
+
deleted: z.ZodBoolean;
|
|
138
|
+
}, z.core.$strict>;
|
|
139
|
+
async: true;
|
|
140
|
+
description: string;
|
|
141
|
+
};
|
|
142
|
+
export declare const cell_field_list_action_spec: {
|
|
143
|
+
method: string;
|
|
144
|
+
kind: "request_response";
|
|
145
|
+
initiator: "frontend";
|
|
146
|
+
auth: {
|
|
147
|
+
account: "optional";
|
|
148
|
+
actor: "optional";
|
|
149
|
+
};
|
|
150
|
+
side_effects: false;
|
|
151
|
+
input: z.ZodObject<{
|
|
152
|
+
source_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
153
|
+
target_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
154
|
+
name_after: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">>;
|
|
155
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
156
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
157
|
+
}, z.core.$strict>;
|
|
158
|
+
output: z.ZodObject<{
|
|
159
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
160
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
161
|
+
name: z.ZodString;
|
|
162
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
163
|
+
created_at: z.ZodString;
|
|
164
|
+
}, z.core.$strict>>;
|
|
165
|
+
}, z.core.$strict>;
|
|
166
|
+
async: true;
|
|
167
|
+
rate_limit: "ip";
|
|
168
|
+
description: string;
|
|
169
|
+
};
|
|
170
|
+
/** All cell_field action specs — composed into `all_cell_action_specs`. */
|
|
171
|
+
export declare const all_cell_field_action_specs: readonly [{
|
|
172
|
+
method: string;
|
|
173
|
+
kind: "request_response";
|
|
174
|
+
initiator: "frontend";
|
|
175
|
+
auth: {
|
|
176
|
+
account: "required";
|
|
177
|
+
actor: "required";
|
|
178
|
+
};
|
|
179
|
+
side_effects: true;
|
|
180
|
+
input: z.ZodObject<{
|
|
181
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
182
|
+
name: z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">;
|
|
183
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
184
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
185
|
+
}, z.core.$strict>;
|
|
186
|
+
output: z.ZodObject<{
|
|
187
|
+
field: z.ZodObject<{
|
|
188
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
189
|
+
name: z.ZodString;
|
|
190
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
191
|
+
created_at: z.ZodString;
|
|
192
|
+
}, z.core.$strict>;
|
|
193
|
+
}, z.core.$strict>;
|
|
194
|
+
async: true;
|
|
195
|
+
description: string;
|
|
196
|
+
}, {
|
|
197
|
+
method: string;
|
|
198
|
+
kind: "request_response";
|
|
199
|
+
initiator: "frontend";
|
|
200
|
+
auth: {
|
|
201
|
+
account: "required";
|
|
202
|
+
actor: "required";
|
|
203
|
+
};
|
|
204
|
+
side_effects: true;
|
|
205
|
+
input: z.ZodObject<{
|
|
206
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
207
|
+
name: z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">;
|
|
208
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
209
|
+
}, z.core.$strict>;
|
|
210
|
+
output: z.ZodObject<{
|
|
211
|
+
ok: z.ZodLiteral<true>;
|
|
212
|
+
deleted: z.ZodBoolean;
|
|
213
|
+
}, z.core.$strict>;
|
|
214
|
+
async: true;
|
|
215
|
+
description: string;
|
|
216
|
+
}, {
|
|
217
|
+
method: string;
|
|
218
|
+
kind: "request_response";
|
|
219
|
+
initiator: "frontend";
|
|
220
|
+
auth: {
|
|
221
|
+
account: "optional";
|
|
222
|
+
actor: "optional";
|
|
223
|
+
};
|
|
224
|
+
side_effects: false;
|
|
225
|
+
input: z.ZodObject<{
|
|
226
|
+
source_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
227
|
+
target_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
228
|
+
name_after: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "CellFieldName", "out">>;
|
|
229
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
230
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
231
|
+
}, z.core.$strict>;
|
|
232
|
+
output: z.ZodObject<{
|
|
233
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
234
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
235
|
+
name: z.ZodString;
|
|
236
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
237
|
+
created_at: z.ZodString;
|
|
238
|
+
}, z.core.$strict>>;
|
|
239
|
+
}, z.core.$strict>;
|
|
240
|
+
async: true;
|
|
241
|
+
rate_limit: "ip";
|
|
242
|
+
description: string;
|
|
243
|
+
}];
|
|
244
|
+
//# sourceMappingURL=cell_field_action_specs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_field_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_field_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAQtB,gFAAgF;AAChF,eAAO,MAAM,+CAA+C,EAC3D,2CAAoD,CAAC;AAItD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAA2B,CAAC;AAC9D,eAAO,MAAM,aAAa,yDAAiE,CAAC;AAC5F,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,2EAA2E;AAC3E,eAAO,MAAM,SAAS;;;;;kBAKpB,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAIlD;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;kBAO5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,kBAAkB;;;;;;;kBAAqC,CAAC;AACrE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAIpE;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,eAAO,MAAM,qBAAqB;;;kBAGhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAI1E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB;;;;;;kBAoB5B,CAAC;AACJ,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;AAItE,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAEtC,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;CAWL,CAAC;AAEtC,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;CAYH,CAAC;AAEtC,2EAA2E;AAC3E,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAI9B,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-field RPC specs — declarative contract for the three named-relation
|
|
3
|
+
* verbs (`set` / `delete` / `list`).
|
|
4
|
+
*
|
|
5
|
+
* `(source_id, name) → target_id` edges modeled after JSON object
|
|
6
|
+
* key/value pairs. One target per `(source_id, name)` pair: re-setting a
|
|
7
|
+
* name overwrites the prior target. `cell_field_list` is bidirectional:
|
|
8
|
+
* pass `source_id` for forward fields, `target_id` for reverse upfields
|
|
9
|
+
* (the latter has 2-layer authz — see handler).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
15
|
+
import { ActingActor } from '../http/auth_shape.js';
|
|
16
|
+
// -- Error reasons ----------------------------------------------------------
|
|
17
|
+
/** Error reason — `cell_field_list` got neither `source_id` nor `target_id`. */
|
|
18
|
+
export const ERROR_CELL_FIELD_LIST_REQUIRES_SOURCE_OR_TARGET = 'cell_field_list_requires_source_or_target';
|
|
19
|
+
// -- Shared schemas ---------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Field name grammar — fuz snake_case identifier convention. Anchored
|
|
22
|
+
* `^[a-z][a-z0-9_]{0,63}$`: leading letter, alphanumeric + underscore
|
|
23
|
+
* trailing, 64-char cap. No reserved names yet.
|
|
24
|
+
*/
|
|
25
|
+
export const CELL_FIELD_NAME_REGEX = /^[a-z][a-z0-9_]{0,63}$/;
|
|
26
|
+
export const CellFieldName = z.string().regex(CELL_FIELD_NAME_REGEX).brand('CellFieldName');
|
|
27
|
+
/** Wire-format for a `cell_field` row. ISO `created_at`, branded UUIDs. */
|
|
28
|
+
export const FieldJson = z.strictObject({
|
|
29
|
+
source_id: Uuid,
|
|
30
|
+
name: z.string(),
|
|
31
|
+
target_id: Uuid,
|
|
32
|
+
created_at: z.string(),
|
|
33
|
+
});
|
|
34
|
+
// -- cell_field_set ---------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* Input for `cell_field_set`. UPSERT on `(source_id, name)` — re-issuing
|
|
37
|
+
* the same input updates `target_id` and bumps `created_at`.
|
|
38
|
+
*/
|
|
39
|
+
export const CellFieldSetInput = z.strictObject({
|
|
40
|
+
source_id: Uuid.meta({ description: 'Cell whose field to set.' }),
|
|
41
|
+
name: CellFieldName.meta({
|
|
42
|
+
description: 'Field name. snake_case identifier; max 64 chars.',
|
|
43
|
+
}),
|
|
44
|
+
target_id: Uuid.meta({ description: 'Cell the field points at.' }),
|
|
45
|
+
acting: ActingActor,
|
|
46
|
+
});
|
|
47
|
+
export const CellFieldSetOutput = z.strictObject({ field: FieldJson });
|
|
48
|
+
// -- cell_field_delete ------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Input for `cell_field_delete`. Idempotent: a successful response is
|
|
51
|
+
* returned even when no row matched.
|
|
52
|
+
*/
|
|
53
|
+
export const CellFieldDeleteInput = z.strictObject({
|
|
54
|
+
source_id: Uuid.meta({ description: 'Cell whose field to delete.' }),
|
|
55
|
+
name: CellFieldName.meta({ description: 'Field name to delete.' }),
|
|
56
|
+
acting: ActingActor,
|
|
57
|
+
});
|
|
58
|
+
export const CellFieldDeleteOutput = z.strictObject({
|
|
59
|
+
ok: z.literal(true),
|
|
60
|
+
deleted: z.boolean(),
|
|
61
|
+
});
|
|
62
|
+
// -- cell_field_list --------------------------------------------------------
|
|
63
|
+
/**
|
|
64
|
+
* Input for `cell_field_list`. Pass `source_id` for forward fields or
|
|
65
|
+
* `target_id` for reverse upfields — exactly one (the schema rejects
|
|
66
|
+
* both / neither). Reverse listing has 2-layer authz (target view-check
|
|
67
|
+
* gates the call; per-source view-check filters the rows).
|
|
68
|
+
*
|
|
69
|
+
* Forward listing supports cursor pagination via `name_after` (return
|
|
70
|
+
* rows whose `name > name_after` lex). The reverse listing doesn't
|
|
71
|
+
* paginate (the result set is small in practice — number of sources
|
|
72
|
+
* pointing at a given target).
|
|
73
|
+
*/
|
|
74
|
+
export const CellFieldListInput = z
|
|
75
|
+
.strictObject({
|
|
76
|
+
source_id: Uuid.optional().meta({
|
|
77
|
+
description: 'List forward fields whose source is this cell.',
|
|
78
|
+
}),
|
|
79
|
+
target_id: Uuid.optional().meta({
|
|
80
|
+
description: 'List reverse upfields whose target is this cell.',
|
|
81
|
+
}),
|
|
82
|
+
name_after: CellFieldName.optional().meta({
|
|
83
|
+
description: 'Cursor for forward pagination — return rows whose name > this lex. Forward only.',
|
|
84
|
+
}),
|
|
85
|
+
limit: z.number().int().positive().max(500).optional().meta({
|
|
86
|
+
description: 'Page size cap (max 500). Forward only. Omit for unbounded — explicit list calls escape the bundled `cell_get` cap.',
|
|
87
|
+
}),
|
|
88
|
+
acting: ActingActor,
|
|
89
|
+
})
|
|
90
|
+
.refine((v) => Boolean(v.source_id) !== Boolean(v.target_id), {
|
|
91
|
+
message: ERROR_CELL_FIELD_LIST_REQUIRES_SOURCE_OR_TARGET,
|
|
92
|
+
});
|
|
93
|
+
export const CellFieldListOutput = z.strictObject({
|
|
94
|
+
fields: z.array(FieldJson),
|
|
95
|
+
});
|
|
96
|
+
// -- Action specs -----------------------------------------------------------
|
|
97
|
+
export const cell_field_set_action_spec = {
|
|
98
|
+
method: 'cell_field_set',
|
|
99
|
+
kind: 'request_response',
|
|
100
|
+
initiator: 'frontend',
|
|
101
|
+
auth: { account: 'required', actor: 'required' },
|
|
102
|
+
side_effects: true,
|
|
103
|
+
input: CellFieldSetInput,
|
|
104
|
+
output: CellFieldSetOutput,
|
|
105
|
+
async: true,
|
|
106
|
+
description: 'Set a named relation `(source.name) → target`. UPSERT on `(source_id, name)`: re-pointing replaces in place. Caller must be able to edit `source` and view `target`; both gate via `can_edit_cell` / `can_view_cell`.',
|
|
107
|
+
};
|
|
108
|
+
export const cell_field_delete_action_spec = {
|
|
109
|
+
method: 'cell_field_delete',
|
|
110
|
+
kind: 'request_response',
|
|
111
|
+
initiator: 'frontend',
|
|
112
|
+
auth: { account: 'required', actor: 'required' },
|
|
113
|
+
side_effects: true,
|
|
114
|
+
input: CellFieldDeleteInput,
|
|
115
|
+
output: CellFieldDeleteOutput,
|
|
116
|
+
async: true,
|
|
117
|
+
description: 'Delete a named relation. Idempotent — `deleted: false` when no row matched. Caller must be able to edit `source`.',
|
|
118
|
+
};
|
|
119
|
+
export const cell_field_list_action_spec = {
|
|
120
|
+
method: 'cell_field_list',
|
|
121
|
+
kind: 'request_response',
|
|
122
|
+
initiator: 'frontend',
|
|
123
|
+
auth: { account: 'optional', actor: 'optional' },
|
|
124
|
+
side_effects: false,
|
|
125
|
+
input: CellFieldListInput,
|
|
126
|
+
output: CellFieldListOutput,
|
|
127
|
+
async: true,
|
|
128
|
+
rate_limit: 'ip',
|
|
129
|
+
description: 'List forward fields (pass `source_id`) or reverse upfields (pass `target_id`). Forward listing filters targets to those the caller may view (strict target-visibility). Reverse listing has 2-layer authz: gate on `can_view_cell(target)` first (404 otherwise), then filter rows by per-source `can_view_cell`. Per-IP rate-limited — symmetric with `cell_get` to bound public-surface id-walking.',
|
|
130
|
+
};
|
|
131
|
+
/** All cell_field action specs — composed into `all_cell_action_specs`. */
|
|
132
|
+
export const all_cell_field_action_specs = [
|
|
133
|
+
cell_field_set_action_spec,
|
|
134
|
+
cell_field_delete_action_spec,
|
|
135
|
+
cell_field_list_action_spec,
|
|
136
|
+
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-field RPC handlers.
|
|
3
|
+
*
|
|
4
|
+
* Three `request_response` actions bound to the specs in
|
|
5
|
+
* `./cell_field_action_specs.ts`:
|
|
6
|
+
*
|
|
7
|
+
* - `cell_field_set` — admin / owner / editor-grant on `source` may set;
|
|
8
|
+
* `target` must be view-admitted (so a caller can't link to a cell they
|
|
9
|
+
* couldn't otherwise see). Idempotent UPSERT on `(source_id, name)`.
|
|
10
|
+
* - `cell_field_delete` — admin / owner / editor-grant on `source`.
|
|
11
|
+
* Idempotent: `deleted: false` when no row matched.
|
|
12
|
+
* - `cell_field_list` — bidirectional. Forward (pass `source_id`) is
|
|
13
|
+
* gated on `can_view_cell(source)` and filters targets to those the
|
|
14
|
+
* caller may view (strict target-visibility, batched). Reverse (pass
|
|
15
|
+
* `target_id`) has 2-layer authz: gate on `can_view_cell(target)`
|
|
16
|
+
* first, then filter rows by `can_view_cell(source)`.
|
|
17
|
+
*
|
|
18
|
+
* IDOR-mask 404s on cell-miss / cell-unviewable, mirroring the existence-
|
|
19
|
+
* leak guards in `cell_actions.ts` / `cell_grant_actions.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Audit events `cell_field_set` / `cell_field_delete` carry IDs only —
|
|
22
|
+
* see `./cell_field_audit_metadata.ts`.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { type RpcAction } from '../actions/action_rpc.js';
|
|
27
|
+
import type { RouteFactoryDeps } from './deps.js';
|
|
28
|
+
import { type FieldJson } from './cell_field_action_specs.js';
|
|
29
|
+
import { type CellFieldRow } from '../db/cell_field_queries.js';
|
|
30
|
+
export type CellFieldActionDeps = Pick<RouteFactoryDeps, 'log' | 'audit'>;
|
|
31
|
+
export declare const to_field_json: (row: CellFieldRow) => FieldJson;
|
|
32
|
+
/** Create the three `cell_field_*` RPC actions. */
|
|
33
|
+
export declare const create_cell_field_actions: (deps: CellFieldActionDeps) => Array<RpcAction>;
|
|
34
|
+
//# sourceMappingURL=cell_field_actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_field_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_field_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAUN,KAAK,SAAS,EACd,MAAM,8BAA8B,CAAC;AAMtC,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,6BAA6B,CAAC;AAMrC,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC;AAE1E,eAAO,MAAM,aAAa,GAAI,KAAK,YAAY,KAAG,SAKhD,CAAC;AAEH,mDAAmD;AACnD,eAAO,MAAM,yBAAyB,GAAI,MAAM,mBAAmB,KAAG,KAAK,CAAC,SAAS,CAmIpF,CAAC"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-field RPC handlers.
|
|
3
|
+
*
|
|
4
|
+
* Three `request_response` actions bound to the specs in
|
|
5
|
+
* `./cell_field_action_specs.ts`:
|
|
6
|
+
*
|
|
7
|
+
* - `cell_field_set` — admin / owner / editor-grant on `source` may set;
|
|
8
|
+
* `target` must be view-admitted (so a caller can't link to a cell they
|
|
9
|
+
* couldn't otherwise see). Idempotent UPSERT on `(source_id, name)`.
|
|
10
|
+
* - `cell_field_delete` — admin / owner / editor-grant on `source`.
|
|
11
|
+
* Idempotent: `deleted: false` when no row matched.
|
|
12
|
+
* - `cell_field_list` — bidirectional. Forward (pass `source_id`) is
|
|
13
|
+
* gated on `can_view_cell(source)` and filters targets to those the
|
|
14
|
+
* caller may view (strict target-visibility, batched). Reverse (pass
|
|
15
|
+
* `target_id`) has 2-layer authz: gate on `can_view_cell(target)`
|
|
16
|
+
* first, then filter rows by `can_view_cell(source)`.
|
|
17
|
+
*
|
|
18
|
+
* IDOR-mask 404s on cell-miss / cell-unviewable, mirroring the existence-
|
|
19
|
+
* leak guards in `cell_actions.ts` / `cell_grant_actions.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Audit events `cell_field_set` / `cell_field_delete` carry IDs only —
|
|
22
|
+
* see `./cell_field_audit_metadata.ts`.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { rpc_action, } from '../actions/action_rpc.js';
|
|
27
|
+
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
28
|
+
import { cell_field_set_action_spec, cell_field_delete_action_spec, cell_field_list_action_spec, } from './cell_field_action_specs.js';
|
|
29
|
+
import { ERROR_CELL_NOT_FOUND } from './cell_action_specs.js';
|
|
30
|
+
import { can_view_cell, can_edit_cell } from './cell_authorize.js';
|
|
31
|
+
import { filter_visible_target_ids } from './cell_relation_visibility.js';
|
|
32
|
+
import { query_cell_get } from '../db/cell_queries.js';
|
|
33
|
+
import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
|
|
34
|
+
import { query_cell_field_set, query_cell_field_delete, query_cell_field_list_for_source, query_cell_field_list_for_target, } from '../db/cell_field_queries.js';
|
|
35
|
+
export const to_field_json = (row) => ({
|
|
36
|
+
source_id: row.source_id,
|
|
37
|
+
name: row.name,
|
|
38
|
+
target_id: row.target_id,
|
|
39
|
+
created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString(),
|
|
40
|
+
});
|
|
41
|
+
/** Create the three `cell_field_*` RPC actions. */
|
|
42
|
+
export const create_cell_field_actions = (deps) => {
|
|
43
|
+
const set_handler = async (input, ctx) => {
|
|
44
|
+
const auth = ctx.auth;
|
|
45
|
+
const source = await query_cell_get(ctx, input.source_id);
|
|
46
|
+
if (!source) {
|
|
47
|
+
// IDOR mask: same code as cell_get's miss/unviewable.
|
|
48
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
49
|
+
}
|
|
50
|
+
const source_grants = await query_cell_grant_list_for_cell(ctx, source.id);
|
|
51
|
+
if (!can_edit_cell(auth, source, source_grants)) {
|
|
52
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
53
|
+
}
|
|
54
|
+
const target = await query_cell_get(ctx, input.target_id);
|
|
55
|
+
if (!target) {
|
|
56
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
57
|
+
}
|
|
58
|
+
// Target must be view-admitted — otherwise a caller could probe for
|
|
59
|
+
// the existence of private cells by trying to point a field at them
|
|
60
|
+
// (and observe whether the call 404s vs. succeeds).
|
|
61
|
+
const target_grants = await query_cell_grant_list_for_cell(ctx, target.id);
|
|
62
|
+
if (!can_view_cell(auth, target, target_grants)) {
|
|
63
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
64
|
+
}
|
|
65
|
+
const row = await query_cell_field_set(ctx, {
|
|
66
|
+
source_id: input.source_id,
|
|
67
|
+
name: input.name,
|
|
68
|
+
target_id: input.target_id,
|
|
69
|
+
});
|
|
70
|
+
deps.audit.emit(ctx, {
|
|
71
|
+
event_type: 'cell_field_set',
|
|
72
|
+
actor_id: auth.actor.id,
|
|
73
|
+
account_id: auth.account.id,
|
|
74
|
+
ip: ctx.client_ip,
|
|
75
|
+
metadata: {
|
|
76
|
+
source_id: row.source_id,
|
|
77
|
+
name: row.name,
|
|
78
|
+
target_id: row.target_id,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return { field: to_field_json(row) };
|
|
82
|
+
};
|
|
83
|
+
const delete_handler = async (input, ctx) => {
|
|
84
|
+
const auth = ctx.auth;
|
|
85
|
+
const source = await query_cell_get(ctx, input.source_id);
|
|
86
|
+
if (!source) {
|
|
87
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
88
|
+
}
|
|
89
|
+
const source_grants = await query_cell_grant_list_for_cell(ctx, source.id);
|
|
90
|
+
if (!can_edit_cell(auth, source, source_grants)) {
|
|
91
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
92
|
+
}
|
|
93
|
+
const deleted = await query_cell_field_delete(ctx, input.source_id, input.name);
|
|
94
|
+
if (deleted) {
|
|
95
|
+
deps.audit.emit(ctx, {
|
|
96
|
+
event_type: 'cell_field_delete',
|
|
97
|
+
actor_id: auth.actor.id,
|
|
98
|
+
account_id: auth.account.id,
|
|
99
|
+
ip: ctx.client_ip,
|
|
100
|
+
metadata: {
|
|
101
|
+
source_id: deleted.source_id,
|
|
102
|
+
name: deleted.name,
|
|
103
|
+
target_id: deleted.target_id,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, deleted: deleted !== null };
|
|
108
|
+
};
|
|
109
|
+
const list_handler = async (input, ctx) => {
|
|
110
|
+
const auth = ctx.auth;
|
|
111
|
+
// Forward listing: gate on can_view_cell(source), then filter the
|
|
112
|
+
// targets to those the caller may view (strict target-visibility).
|
|
113
|
+
if (input.source_id !== undefined) {
|
|
114
|
+
const source = await query_cell_get(ctx, input.source_id);
|
|
115
|
+
if (!source) {
|
|
116
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
117
|
+
}
|
|
118
|
+
const source_grants = auth ? await query_cell_grant_list_for_cell(ctx, source.id) : null;
|
|
119
|
+
if (!can_view_cell(auth, source, source_grants)) {
|
|
120
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
121
|
+
}
|
|
122
|
+
const rows = await query_cell_field_list_for_source(ctx, source.id, {
|
|
123
|
+
limit: input.limit,
|
|
124
|
+
name_after: input.name_after,
|
|
125
|
+
});
|
|
126
|
+
const visible_targets = await filter_visible_target_ids(ctx, auth, rows.map((r) => r.target_id));
|
|
127
|
+
return { fields: rows.filter((r) => visible_targets.has(r.target_id)).map(to_field_json) };
|
|
128
|
+
}
|
|
129
|
+
// Reverse listing: 2-layer authz. First, can_view_cell(target).
|
|
130
|
+
// Without this, the count of returned rows leaks "at least N
|
|
131
|
+
// viewable cells link to this private target."
|
|
132
|
+
const target = await query_cell_get(ctx, input.target_id);
|
|
133
|
+
if (!target) {
|
|
134
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
135
|
+
}
|
|
136
|
+
const target_grants = auth ? await query_cell_grant_list_for_cell(ctx, target.id) : null;
|
|
137
|
+
if (!can_view_cell(auth, target, target_grants)) {
|
|
138
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
139
|
+
}
|
|
140
|
+
// Then filter rows by per-source can_view_cell. Batched (not N+1):
|
|
141
|
+
// one bulk visibility filter over all source ids, same as the forward
|
|
142
|
+
// branch. Bounded by `limit` at the query so a heavily inbound-linked
|
|
143
|
+
// target can't force an unbounded fetch on this public endpoint.
|
|
144
|
+
const rows = await query_cell_field_list_for_target(ctx, target.id, { limit: input.limit });
|
|
145
|
+
const visible_sources = await filter_visible_target_ids(ctx, auth, rows.map((r) => r.source_id));
|
|
146
|
+
return { fields: rows.filter((r) => visible_sources.has(r.source_id)).map(to_field_json) };
|
|
147
|
+
};
|
|
148
|
+
return [
|
|
149
|
+
rpc_action(cell_field_set_action_spec, set_handler),
|
|
150
|
+
rpc_action(cell_field_delete_action_spec, delete_handler),
|
|
151
|
+
rpc_action(cell_field_list_action_spec, list_handler),
|
|
152
|
+
];
|
|
153
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-log metadata schemas for `cell_field_set` / `cell_field_delete`.
|
|
3
|
+
*
|
|
4
|
+
* IDs only — same discipline as the cell + cell_grant envelopes (audit
|
|
5
|
+
* logs store references, not denormalized strings). Apps register these
|
|
6
|
+
* via `extra_events:` on `create_audit_log_config`.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
/**
|
|
12
|
+
* Metadata envelope for `cell_field_set`. Emitted on every successful
|
|
13
|
+
* create OR update path (UPSERT on `(source_id, name)`); the audit
|
|
14
|
+
* reader correlates create-vs-update via repeated `(source_id, name)`
|
|
15
|
+
* if needed.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CellFieldSetAuditMetadata: z.ZodObject<{
|
|
18
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
19
|
+
name: z.ZodString;
|
|
20
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
21
|
+
}, z.core.$loose>;
|
|
22
|
+
export type CellFieldSetAuditMetadata = z.infer<typeof CellFieldSetAuditMetadata>;
|
|
23
|
+
/** Metadata envelope for `cell_field_delete`. */
|
|
24
|
+
export declare const CellFieldDeleteAuditMetadata: z.ZodObject<{
|
|
25
|
+
source_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
26
|
+
name: z.ZodString;
|
|
27
|
+
target_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
28
|
+
}, z.core.$loose>;
|
|
29
|
+
export type CellFieldDeleteAuditMetadata = z.infer<typeof CellFieldDeleteAuditMetadata>;
|
|
30
|
+
//# sourceMappingURL=cell_field_audit_metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_field_audit_metadata.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_field_audit_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;;iBAIpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,iDAAiD;AACjD,eAAO,MAAM,4BAA4B;;;;iBAIvC,CAAC;AACH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
|