@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.
Files changed (164) hide show
  1. package/dist/auth/CLAUDE.md +99 -5
  2. package/dist/auth/account_queries.d.ts +87 -4
  3. package/dist/auth/account_queries.d.ts.map +1 -1
  4. package/dist/auth/account_queries.js +107 -17
  5. package/dist/auth/account_schema.d.ts +19 -0
  6. package/dist/auth/account_schema.d.ts.map +1 -1
  7. package/dist/auth/account_schema.js +8 -0
  8. package/dist/auth/admin_action_specs.d.ts +168 -0
  9. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  10. package/dist/auth/admin_action_specs.js +146 -1
  11. package/dist/auth/admin_actions.d.ts.map +1 -1
  12. package/dist/auth/admin_actions.js +218 -4
  13. package/dist/auth/audit_log_ddl.d.ts +10 -1
  14. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  15. package/dist/auth/audit_log_ddl.js +13 -4
  16. package/dist/auth/audit_log_schema.d.ts +34 -1
  17. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  18. package/dist/auth/audit_log_schema.js +73 -0
  19. package/dist/auth/auth_ddl.d.ts +2 -2
  20. package/dist/auth/auth_ddl.d.ts.map +1 -1
  21. package/dist/auth/auth_ddl.js +10 -2
  22. package/dist/auth/cell_action_specs.d.ts +1295 -0
  23. package/dist/auth/cell_action_specs.d.ts.map +1 -0
  24. package/dist/auth/cell_action_specs.js +397 -0
  25. package/dist/auth/cell_actions.d.ts +63 -0
  26. package/dist/auth/cell_actions.d.ts.map +1 -0
  27. package/dist/auth/cell_actions.js +546 -0
  28. package/dist/auth/cell_audit_action_specs.d.ts +131 -0
  29. package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
  30. package/dist/auth/cell_audit_action_specs.js +70 -0
  31. package/dist/auth/cell_audit_actions.d.ts +18 -0
  32. package/dist/auth/cell_audit_actions.d.ts.map +1 -0
  33. package/dist/auth/cell_audit_actions.js +59 -0
  34. package/dist/auth/cell_audit_events.d.ts +28 -0
  35. package/dist/auth/cell_audit_events.d.ts.map +1 -0
  36. package/dist/auth/cell_audit_events.js +42 -0
  37. package/dist/auth/cell_audit_metadata.d.ts +48 -0
  38. package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
  39. package/dist/auth/cell_audit_metadata.js +46 -0
  40. package/dist/auth/cell_authorize.d.ts +88 -0
  41. package/dist/auth/cell_authorize.d.ts.map +1 -0
  42. package/dist/auth/cell_authorize.js +172 -0
  43. package/dist/auth/cell_data_schema.d.ts +44 -0
  44. package/dist/auth/cell_data_schema.d.ts.map +1 -0
  45. package/dist/auth/cell_data_schema.js +42 -0
  46. package/dist/auth/cell_field_action_specs.d.ts +244 -0
  47. package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
  48. package/dist/auth/cell_field_action_specs.js +136 -0
  49. package/dist/auth/cell_field_actions.d.ts +34 -0
  50. package/dist/auth/cell_field_actions.d.ts.map +1 -0
  51. package/dist/auth/cell_field_actions.js +153 -0
  52. package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
  53. package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
  54. package/dist/auth/cell_field_audit_metadata.js +28 -0
  55. package/dist/auth/cell_grant_action_specs.d.ts +333 -0
  56. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
  57. package/dist/auth/cell_grant_action_specs.js +148 -0
  58. package/dist/auth/cell_grant_actions.d.ts +50 -0
  59. package/dist/auth/cell_grant_actions.d.ts.map +1 -0
  60. package/dist/auth/cell_grant_actions.js +208 -0
  61. package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
  62. package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
  63. package/dist/auth/cell_grant_audit_metadata.js +54 -0
  64. package/dist/auth/cell_item_action_specs.d.ts +331 -0
  65. package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
  66. package/dist/auth/cell_item_action_specs.js +182 -0
  67. package/dist/auth/cell_item_actions.d.ts +37 -0
  68. package/dist/auth/cell_item_actions.d.ts.map +1 -0
  69. package/dist/auth/cell_item_actions.js +204 -0
  70. package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
  71. package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
  72. package/dist/auth/cell_item_audit_metadata.js +32 -0
  73. package/dist/auth/cell_relation_visibility.d.ts +32 -0
  74. package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
  75. package/dist/auth/cell_relation_visibility.js +57 -0
  76. package/dist/auth/deps.d.ts +9 -0
  77. package/dist/auth/deps.d.ts.map +1 -1
  78. package/dist/auth/role_grant_queries.d.ts +30 -0
  79. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  80. package/dist/auth/role_grant_queries.js +54 -0
  81. package/dist/db/CLAUDE.md +118 -0
  82. package/dist/db/cell_audit_queries.d.ts +26 -0
  83. package/dist/db/cell_audit_queries.d.ts.map +1 -0
  84. package/dist/db/cell_audit_queries.js +53 -0
  85. package/dist/db/cell_ddl.d.ts +151 -0
  86. package/dist/db/cell_ddl.d.ts.map +1 -0
  87. package/dist/db/cell_ddl.js +247 -0
  88. package/dist/db/cell_field_queries.d.ts +105 -0
  89. package/dist/db/cell_field_queries.d.ts.map +1 -0
  90. package/dist/db/cell_field_queries.js +113 -0
  91. package/dist/db/cell_grant_queries.d.ts +132 -0
  92. package/dist/db/cell_grant_queries.d.ts.map +1 -0
  93. package/dist/db/cell_grant_queries.js +145 -0
  94. package/dist/db/cell_history_ddl.d.ts +38 -0
  95. package/dist/db/cell_history_ddl.d.ts.map +1 -0
  96. package/dist/db/cell_history_ddl.js +61 -0
  97. package/dist/db/cell_item_queries.d.ts +107 -0
  98. package/dist/db/cell_item_queries.d.ts.map +1 -0
  99. package/dist/db/cell_item_queries.js +119 -0
  100. package/dist/db/cell_queries.d.ts +327 -0
  101. package/dist/db/cell_queries.d.ts.map +1 -0
  102. package/dist/db/cell_queries.js +431 -0
  103. package/dist/db/fact_ddl.d.ts +38 -0
  104. package/dist/db/fact_ddl.d.ts.map +1 -0
  105. package/dist/db/fact_ddl.js +71 -0
  106. package/dist/db/fact_queries.d.ts +140 -0
  107. package/dist/db/fact_queries.d.ts.map +1 -0
  108. package/dist/db/fact_queries.js +161 -0
  109. package/dist/db/fact_store.d.ts +112 -0
  110. package/dist/db/fact_store.d.ts.map +1 -0
  111. package/dist/db/fact_store.js +225 -0
  112. package/dist/server/env.d.ts +2 -0
  113. package/dist/server/env.d.ts.map +1 -1
  114. package/dist/server/env.js +6 -0
  115. package/dist/server/fact_write.d.ts +32 -0
  116. package/dist/server/fact_write.d.ts.map +1 -0
  117. package/dist/server/fact_write.js +56 -0
  118. package/dist/server/file_fact_fetcher.d.ts +42 -0
  119. package/dist/server/file_fact_fetcher.d.ts.map +1 -0
  120. package/dist/server/file_fact_fetcher.js +60 -0
  121. package/dist/server/file_fact_url.d.ts +53 -0
  122. package/dist/server/file_fact_url.d.ts.map +1 -0
  123. package/dist/server/file_fact_url.js +52 -0
  124. package/dist/server/serve_fact_route.d.ts +78 -0
  125. package/dist/server/serve_fact_route.d.ts.map +1 -0
  126. package/dist/server/serve_fact_route.js +205 -0
  127. package/dist/testing/CLAUDE.md +58 -5
  128. package/dist/testing/app_server.d.ts +12 -0
  129. package/dist/testing/app_server.d.ts.map +1 -1
  130. package/dist/testing/app_server.js +36 -2
  131. package/dist/testing/audit_completeness.d.ts.map +1 -1
  132. package/dist/testing/audit_completeness.js +67 -1
  133. package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
  134. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
  135. package/dist/testing/cross_backend/account_lifecycle.js +76 -0
  136. package/dist/testing/cross_backend/capabilities.d.ts +31 -0
  137. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/capabilities.js +3 -0
  139. package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
  140. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
  141. package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
  142. package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
  143. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
  144. package/dist/testing/cross_backend/cell_crud.js +168 -0
  145. package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
  146. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/cell_relations.js +229 -0
  148. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/default_backend_configs.js +6 -0
  150. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/setup.js +5 -0
  152. package/dist/testing/entities.d.ts.map +1 -1
  153. package/dist/testing/entities.js +4 -0
  154. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  155. package/dist/testing/ws_round_trip.js +4 -0
  156. package/dist/ui/AdminAccounts.svelte +58 -0
  157. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  158. package/dist/ui/admin_accounts_state.svelte.d.ts +30 -2
  159. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  160. package/dist/ui/admin_accounts_state.svelte.js +45 -1
  161. package/dist/ui/admin_rpc_adapters.d.ts +6 -2
  162. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  163. package/dist/ui/admin_rpc_adapters.js +5 -1
  164. 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
+ };
@@ -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.
@@ -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;AAEpD,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;CACpB;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,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
  */