@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,247 @@
1
+ /**
2
+ * Cell PG schema.
3
+ *
4
+ * The universal content primitive: a single `cell` table whose `data` JSONB
5
+ * is interpreted by view shape. Parent→child membership and named
6
+ * relationships live in two sibling tables — `cell_item` (ordered
7
+ * children, fractional-indexing keyed) and `cell_field` (named edges).
8
+ * `refs text[]` carries `blake3:` fact hashes auto-extracted from `data`
9
+ * by application code on every write.
10
+ *
11
+ * Soft delete via `deleted_at`. Most indexes are partial on
12
+ * `deleted_at IS NULL` so active-cell queries skip tombstones.
13
+ *
14
+ * `path` is the global namespace axis — a partial unique index enforces
15
+ * uniqueness across all active rows (PostgreSQL UNIQUE constraints don't
16
+ * support WHERE clauses, so it's expressed as a partial unique index). It
17
+ * additionally filters on `deleted_at IS NULL` so a soft-deleted cell
18
+ * doesn't block reuse of its path. Path writes are admin-only at the
19
+ * action layer; user-namespaced paths are a future extension.
20
+ *
21
+ * **Ownership columns** (`created_by`, `updated_by`) are nullable FKs to
22
+ * `actor`: NULL = system origin (well-known cells, daemon/agent cells).
23
+ * The non-admin authz path treats NULL `created_by` as admin-only via an
24
+ * explicit equality check (`auth/cell_authorize.ts`).
25
+ *
26
+ * **Timestamp naming** (`created_at`, `updated_at`) aligns with fuz_app's
27
+ * `_at`-everywhere convention used by `account`, `actor`, `audit_log`,
28
+ * `role_grant`, etc.
29
+ *
30
+ * **Single-migration shape**: `cell_v0` creates the canonical
31
+ * cell + cell_grant + cell_field + cell_item layout in one shot from the
32
+ * live exported constants.
33
+ *
34
+ * @module
35
+ */
36
+ /**
37
+ * `cell_visibility` enum — access-control axis for a cell. Lives as a
38
+ * top-level column (not inside `data`) because visibility is access
39
+ * control, not content metadata. `cell_grant` is the other ACL surface;
40
+ * keeping visibility as a peer column (not a JSON field) co-locates
41
+ * access-control state and lets the planner reason about it directly.
42
+ *
43
+ * Ships with two states (`'private'`, `'public'`); a third (unlisted /
44
+ * public-link) folds in via `ALTER TYPE` when public-link sharing lands.
45
+ *
46
+ * Wrapped in a `DO` block so the migration can replay idempotently —
47
+ * `CREATE TYPE` has no `IF NOT EXISTS` variant in PostgreSQL.
48
+ */
49
+ export const CELL_VISIBILITY_TYPE = `
50
+ DO $$ BEGIN
51
+ CREATE TYPE cell_visibility AS ENUM ('private', 'public');
52
+ EXCEPTION WHEN duplicate_object THEN NULL;
53
+ END $$`;
54
+ /**
55
+ * `cell` table — universal content primitive: identity + content only.
56
+ * Parent→child membership lives in `cell_item`; named relations live in
57
+ * `cell_field`. Includes the `created_by` / `updated_by` ownership columns.
58
+ *
59
+ * `visibility` is the access-control axis — `private` (default) is
60
+ * restricted to admin / owner / `cell_grant`-admitted callers; `public`
61
+ * admits everyone, including unauthenticated visitors. Lives as a
62
+ * top-level column so the auth predicate reads off the row directly
63
+ * rather than reaching into `data`.
64
+ *
65
+ * `path` is the global namespace axis (no tenant/hub scoping) — globally
66
+ * unique on active rows via `idx_cell_path_unique`.
67
+ */
68
+ export const CELL_SCHEMA = `
69
+ CREATE TABLE IF NOT EXISTS cell (
70
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
71
+ data JSONB NOT NULL,
72
+ visibility cell_visibility NOT NULL DEFAULT 'private',
73
+ path TEXT,
74
+ refs TEXT[],
75
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
76
+ updated_at TIMESTAMPTZ,
77
+ deleted_at TIMESTAMPTZ,
78
+ created_by UUID REFERENCES actor(id) ON DELETE SET NULL,
79
+ updated_by UUID REFERENCES actor(id) ON DELETE SET NULL
80
+ )`;
81
+ /**
82
+ * Cell indexes — all active-only, partial on `deleted_at IS NULL`.
83
+ *
84
+ * - `idx_cell_active`: active-cell list/scan ordered by creation.
85
+ * - `idx_cell_path_unique`: global `path` uniqueness + read-side path
86
+ * lookup. Partial on path + active so reused paths after soft delete are
87
+ * allowed.
88
+ * - `idx_cell_data`: shape-driven queries (`data ? 'kind'`, `data @> ...`).
89
+ * - `idx_cell_refs`: cells-by-fact discovery (cross-cell reference graph).
90
+ * - `idx_cell_created_by`: "cells this actor created" queries.
91
+ *
92
+ * Parent↔child membership and named relations live in sibling tables;
93
+ * see `CELL_ITEM_INDEXES` / `CELL_FIELD_INDEXES` below.
94
+ */
95
+ export const CELL_INDEXES = [
96
+ `CREATE INDEX IF NOT EXISTS idx_cell_active ON cell(created_at)
97
+ WHERE deleted_at IS NULL`,
98
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_cell_path_unique
99
+ ON cell(path)
100
+ WHERE path IS NOT NULL AND deleted_at IS NULL`,
101
+ `CREATE INDEX IF NOT EXISTS idx_cell_data ON cell USING gin(data)
102
+ WHERE deleted_at IS NULL`,
103
+ `CREATE INDEX IF NOT EXISTS idx_cell_refs ON cell USING gin(refs)
104
+ WHERE refs IS NOT NULL AND deleted_at IS NULL`,
105
+ `CREATE INDEX IF NOT EXISTS idx_cell_created_by ON cell(created_by)
106
+ WHERE deleted_at IS NULL`,
107
+ ];
108
+ /**
109
+ * `cell_grant` table — resource-side ACL for cells. Each row admits a
110
+ * principal (actor or `(role, scope_id)`) at a `level` (`viewer` or
111
+ * `editor`). Owner is implicit (`cell.created_by`); the table never carries
112
+ * owner rows.
113
+ *
114
+ * The single-principal arm is actor-grain (`actor_id` FK); the other arm is
115
+ * role-shaped (`(role, scope_id)`). The CHECK enforces exactly one arm.
116
+ */
117
+ export const CELL_GRANT_SCHEMA = `
118
+ CREATE TABLE IF NOT EXISTS cell_grant (
119
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
120
+ cell_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,
121
+ level TEXT NOT NULL CHECK (level IN ('viewer', 'editor')),
122
+ actor_id UUID REFERENCES actor(id) ON DELETE CASCADE,
123
+ role TEXT,
124
+ scope_id UUID,
125
+ granted_by UUID REFERENCES actor(id) ON DELETE SET NULL,
126
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
127
+ CHECK (
128
+ (actor_id IS NOT NULL AND role IS NULL AND scope_id IS NULL) OR
129
+ (actor_id IS NULL AND role IS NOT NULL)
130
+ )
131
+ )`;
132
+ /**
133
+ * `cell_grant` indexes.
134
+ *
135
+ * - `idx_cell_grant_cell`: forward lookup ("who has access to this cell?").
136
+ * - `idx_cell_grant_actor`: reverse lookup ("which cells does this actor have access to?").
137
+ * - `idx_cell_grant_role_scope`: reverse lookup for role-shaped principals.
138
+ * - `idx_cell_grant_unique_actor`: prevents duplicate actor-shaped grants for the same cell.
139
+ * Re-granting updates `level` via UPSERT on this index.
140
+ * - `idx_cell_grant_unique_role_scope`: same, for role-shaped grants.
141
+ * `NULLS NOT DISTINCT` so two rows with the same `(cell_id, role)` and
142
+ * `scope_id IS NULL` collide — without it, default NULL-distinct
143
+ * semantics would let duplicate null-scope role grants slip past the
144
+ * re-share UPSERT path. Requires PostgreSQL 15+ (pglite tracks PG 16).
145
+ */
146
+ export const CELL_GRANT_INDEXES = [
147
+ `CREATE INDEX IF NOT EXISTS idx_cell_grant_cell ON cell_grant(cell_id)`,
148
+ `CREATE INDEX IF NOT EXISTS idx_cell_grant_actor
149
+ ON cell_grant(actor_id) WHERE actor_id IS NOT NULL`,
150
+ `CREATE INDEX IF NOT EXISTS idx_cell_grant_role_scope
151
+ ON cell_grant(role, scope_id) WHERE role IS NOT NULL`,
152
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_cell_grant_unique_actor
153
+ ON cell_grant(cell_id, actor_id) WHERE actor_id IS NOT NULL`,
154
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_cell_grant_unique_role_scope
155
+ ON cell_grant(cell_id, role, scope_id) NULLS NOT DISTINCT
156
+ WHERE role IS NOT NULL`,
157
+ ];
158
+ /**
159
+ * `cell_field` table — named relation (`(source_id, name) → target_id`).
160
+ * One target per name per source — JSON-object keys are unique. Multiplicity
161
+ * is expressed by composition (`foo.tags = collection_cell` whose `items[]`
162
+ * are the tags), not by allowing duplicate `(source_id, name)` rows.
163
+ */
164
+ export const CELL_FIELD_SCHEMA = `
165
+ CREATE TABLE IF NOT EXISTS cell_field (
166
+ source_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,
167
+ name TEXT NOT NULL,
168
+ target_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,
169
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
170
+ PRIMARY KEY (source_id, name)
171
+ )`;
172
+ /**
173
+ * `cell_field` indexes.
174
+ *
175
+ * - PK on `(source_id, name)` covers forward lookup ("what does this cell
176
+ * point to via field X?") and the per-source fields list.
177
+ * - `idx_cell_field_target` covers reverse lookup ("which cells link to
178
+ * this target?").
179
+ *
180
+ * Soft-delete is filtered by JOIN at the read boundary; no partial indexes
181
+ * here on `deleted_at` (would force index churn on cell soft-delete
182
+ * toggles, and the join filter is sufficient).
183
+ */
184
+ export const CELL_FIELD_INDEXES = [
185
+ `CREATE INDEX IF NOT EXISTS idx_cell_field_target ON cell_field(target_id)`,
186
+ ];
187
+ /**
188
+ * `cell_item` table — ordered child membership keyed by an opaque
189
+ * fractional-indexing string. `(parent_id, position)` PK enforces one cell
190
+ * per slot; the same `child_id` may appear at multiple positions (the
191
+ * primitive is JSON-array-shaped — ordered multiset, not set). Domain
192
+ * dedup rules ride on top in helpers.
193
+ */
194
+ export const CELL_ITEM_SCHEMA = `
195
+ CREATE TABLE IF NOT EXISTS cell_item (
196
+ parent_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,
197
+ position TEXT NOT NULL,
198
+ child_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,
199
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
200
+ PRIMARY KEY (parent_id, position)
201
+ )`;
202
+ /**
203
+ * `cell_item` indexes.
204
+ *
205
+ * - PK on `(parent_id, position)` covers ordered scans for the per-parent
206
+ * items list (`SELECT ... ORDER BY position`).
207
+ * - `idx_cell_item_child` covers reverse lookup ("which parents contain
208
+ * this child?").
209
+ */
210
+ export const CELL_ITEM_INDEXES = [
211
+ `CREATE INDEX IF NOT EXISTS idx_cell_item_parent_position ON cell_item(parent_id, position)`,
212
+ `CREATE INDEX IF NOT EXISTS idx_cell_item_child ON cell_item(child_id)`,
213
+ ];
214
+ /** Tables created by `CELL_MIGRATION_NS`, in drop order (children first). */
215
+ export const CELL_DROP_TABLES = ['cell_field', 'cell_item', 'cell_grant', 'cell'];
216
+ /** Cell migrations. */
217
+ export const CELL_MIGRATIONS = [
218
+ {
219
+ name: 'cell_v0',
220
+ up: async (db) => {
221
+ await db.query(CELL_VISIBILITY_TYPE);
222
+ await db.query(CELL_SCHEMA);
223
+ for (const sql of CELL_INDEXES) {
224
+ await db.query(sql);
225
+ }
226
+ await db.query(CELL_GRANT_SCHEMA);
227
+ for (const sql of CELL_GRANT_INDEXES) {
228
+ await db.query(sql);
229
+ }
230
+ await db.query(CELL_FIELD_SCHEMA);
231
+ for (const sql of CELL_FIELD_INDEXES) {
232
+ await db.query(sql);
233
+ }
234
+ await db.query(CELL_ITEM_SCHEMA);
235
+ for (const sql of CELL_ITEM_INDEXES) {
236
+ await db.query(sql);
237
+ }
238
+ },
239
+ },
240
+ ];
241
+ /** Namespace identifier for cell migrations. */
242
+ export const CELL_MIGRATION_NAMESPACE = 'fuz_cell';
243
+ /** Migration namespace consumed by `run_migrations`. */
244
+ export const CELL_MIGRATION_NS = {
245
+ namespace: CELL_MIGRATION_NAMESPACE,
246
+ migrations: CELL_MIGRATIONS,
247
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Raw queries against the `cell_field` table.
3
+ *
4
+ * Named-relation primitive: each row is `(source_id, name) → target_id`,
5
+ * a JSON-object-shaped edge from one cell to another. `(source_id, name)`
6
+ * is unique — one target per name per source. Multiplicity by composition
7
+ * (target a collection cell whose `items[]` are the multi-valued tags),
8
+ * not by allowing duplicate field rows.
9
+ *
10
+ * Reads filter both endpoints by `cell.deleted_at IS NULL` so relations
11
+ * dangling off a soft-deleted cell don't surface to the live graph.
12
+ *
13
+ * `query_cell_field_set` upserts on the `(source_id, name)` PK so
14
+ * re-pointing a name updates `target_id` in place — JSON-object semantics
15
+ * (`obj.foo = bar` overwrites whatever was there).
16
+ *
17
+ * @module
18
+ */
19
+ import type { QueryDeps } from './query_deps.js';
20
+ import type { Uuid } from '@fuzdev/fuz_util/id.js';
21
+ /** Row shape returned by `cell_field` SELECTs. */
22
+ export interface CellFieldRow {
23
+ source_id: Uuid;
24
+ name: string;
25
+ target_id: Uuid;
26
+ created_at: Date;
27
+ }
28
+ /** Input for `query_cell_field_set`. */
29
+ export interface CellFieldSetQueryInput {
30
+ source_id: Uuid;
31
+ name: string;
32
+ target_id: Uuid;
33
+ }
34
+ /**
35
+ * Insert or update a field row.
36
+ *
37
+ * UPSERT on `(source_id, name)` — re-setting the same name updates
38
+ * `target_id` and bumps `created_at` (timestamp reflects last write).
39
+ * Idempotent at the row level: caller can re-issue with the same input
40
+ * without checking existence first.
41
+ *
42
+ * @param deps - query deps
43
+ * @param input - source, name, target
44
+ * @returns the inserted-or-updated row
45
+ * @mutates `cell_field` - inserts or updates one row
46
+ */
47
+ export declare const query_cell_field_set: (deps: QueryDeps, input: CellFieldSetQueryInput) => Promise<CellFieldRow>;
48
+ /**
49
+ * Fetch one field row by primary key.
50
+ *
51
+ * Does NOT JOIN cell — the caller decides whether to filter by
52
+ * `deleted_at`. Used by handlers that need the row's current target_id
53
+ * for audit envelopes before issuing the delete.
54
+ *
55
+ * @param deps - query deps
56
+ * @param source_id - source cell id
57
+ * @param name - field name
58
+ * @returns the row or `null` when not found
59
+ */
60
+ export declare const query_cell_field_get: (deps: QueryDeps, source_id: Uuid, name: string) => Promise<CellFieldRow | null>;
61
+ /**
62
+ * Delete a field row by primary key. Returns the deleted row so callers
63
+ * can audit the prior target_id without a pre-fetch.
64
+ *
65
+ * @returns the deleted row, or `null` when no row matched (idempotent
66
+ * delete: a 200 response is correct even when nothing was deleted)
67
+ * @mutates `cell_field` - deletes one row
68
+ */
69
+ export declare const query_cell_field_delete: (deps: QueryDeps, source_id: Uuid, name: string) => Promise<CellFieldRow | null>;
70
+ /**
71
+ * Forward fields list (`source.fields[]`).
72
+ *
73
+ * Filters target by `deleted_at IS NULL` so relations to tombstoned cells
74
+ * don't surface; the source filter is the caller's responsibility (gated
75
+ * upstream by `can_view_cell(source)`).
76
+ *
77
+ * @param deps - query deps
78
+ * @param source_id - source cell id
79
+ * @returns matching rows, oldest first by name (lex order)
80
+ */
81
+ export declare const query_cell_field_list_for_source: (deps: QueryDeps, source_id: Uuid, options?: {
82
+ limit?: number;
83
+ name_after?: string;
84
+ }) => Promise<Array<CellFieldRow>>;
85
+ /**
86
+ * Reverse fields list (`target.upfields[]`).
87
+ *
88
+ * Returns rows whose `target_id = $1`, joined to `cell` on `source_id` so
89
+ * relations from tombstoned sources don't surface. The caller-side
90
+ * authz filter (per-source `can_view_cell`) runs after the SQL fetch
91
+ * — see the 2-layer authz contract on `cell_field_list({target_id})`.
92
+ *
93
+ * Bounded by `limit` (the wire `cell_field_list` cap) so a heavily
94
+ * inbound-linked target can't force an unbounded fetch + per-source authz
95
+ * pass on the public, IP-rate-limited reverse endpoint.
96
+ *
97
+ * @param deps - query deps
98
+ * @param target_id - target cell id
99
+ * @param options - `limit` caps the row count
100
+ * @returns matching rows, oldest first by source created_at
101
+ */
102
+ export declare const query_cell_field_list_for_target: (deps: QueryDeps, target_id: Uuid, options?: {
103
+ limit?: number;
104
+ }) => Promise<Array<CellFieldRow>>;
105
+ //# sourceMappingURL=cell_field_queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_field_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_field_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAGjD,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,OAAO,sBAAsB,KAC3B,OAAO,CAAC,YAAY,CAUtB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,MAAM,MAAM,KACV,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAC,KAC7C,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAa7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAC,KACxB,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAS5B,CAAC"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Raw queries against the `cell_field` table.
3
+ *
4
+ * Named-relation primitive: each row is `(source_id, name) → target_id`,
5
+ * a JSON-object-shaped edge from one cell to another. `(source_id, name)`
6
+ * is unique — one target per name per source. Multiplicity by composition
7
+ * (target a collection cell whose `items[]` are the multi-valued tags),
8
+ * not by allowing duplicate field rows.
9
+ *
10
+ * Reads filter both endpoints by `cell.deleted_at IS NULL` so relations
11
+ * dangling off a soft-deleted cell don't surface to the live graph.
12
+ *
13
+ * `query_cell_field_set` upserts on the `(source_id, name)` PK so
14
+ * re-pointing a name updates `target_id` in place — JSON-object semantics
15
+ * (`obj.foo = bar` overwrites whatever was there).
16
+ *
17
+ * @module
18
+ */
19
+ import { assert_row } from './assert_row.js';
20
+ /**
21
+ * Insert or update a field row.
22
+ *
23
+ * UPSERT on `(source_id, name)` — re-setting the same name updates
24
+ * `target_id` and bumps `created_at` (timestamp reflects last write).
25
+ * Idempotent at the row level: caller can re-issue with the same input
26
+ * without checking existence first.
27
+ *
28
+ * @param deps - query deps
29
+ * @param input - source, name, target
30
+ * @returns the inserted-or-updated row
31
+ * @mutates `cell_field` - inserts or updates one row
32
+ */
33
+ export const query_cell_field_set = async (deps, input) => {
34
+ const row = await deps.db.query_one(`INSERT INTO cell_field (source_id, name, target_id)
35
+ VALUES ($1, $2, $3)
36
+ ON CONFLICT (source_id, name)
37
+ DO UPDATE SET target_id = EXCLUDED.target_id, created_at = NOW()
38
+ RETURNING *`, [input.source_id, input.name, input.target_id]);
39
+ return assert_row(row, 'INSERT INTO cell_field');
40
+ };
41
+ /**
42
+ * Fetch one field row by primary key.
43
+ *
44
+ * Does NOT JOIN cell — the caller decides whether to filter by
45
+ * `deleted_at`. Used by handlers that need the row's current target_id
46
+ * for audit envelopes before issuing the delete.
47
+ *
48
+ * @param deps - query deps
49
+ * @param source_id - source cell id
50
+ * @param name - field name
51
+ * @returns the row or `null` when not found
52
+ */
53
+ export const query_cell_field_get = async (deps, source_id, name) => {
54
+ const row = await deps.db.query_one(`SELECT * FROM cell_field WHERE source_id = $1 AND name = $2`, [source_id, name]);
55
+ return row ?? null;
56
+ };
57
+ /**
58
+ * Delete a field row by primary key. Returns the deleted row so callers
59
+ * can audit the prior target_id without a pre-fetch.
60
+ *
61
+ * @returns the deleted row, or `null` when no row matched (idempotent
62
+ * delete: a 200 response is correct even when nothing was deleted)
63
+ * @mutates `cell_field` - deletes one row
64
+ */
65
+ export const query_cell_field_delete = async (deps, source_id, name) => {
66
+ const row = await deps.db.query_one(`DELETE FROM cell_field WHERE source_id = $1 AND name = $2 RETURNING *`, [source_id, name]);
67
+ return row ?? null;
68
+ };
69
+ /**
70
+ * Forward fields list (`source.fields[]`).
71
+ *
72
+ * Filters target by `deleted_at IS NULL` so relations to tombstoned cells
73
+ * don't surface; the source filter is the caller's responsibility (gated
74
+ * upstream by `can_view_cell(source)`).
75
+ *
76
+ * @param deps - query deps
77
+ * @param source_id - source cell id
78
+ * @returns matching rows, oldest first by name (lex order)
79
+ */
80
+ export const query_cell_field_list_for_source = async (deps, source_id, options) => {
81
+ const limit = options?.limit ?? null;
82
+ const name_after = options?.name_after ?? null;
83
+ return deps.db.query(`SELECT f.* FROM cell_field f
84
+ JOIN cell t ON t.id = f.target_id
85
+ WHERE f.source_id = $1
86
+ AND t.deleted_at IS NULL
87
+ AND ($3::text IS NULL OR f.name > $3)
88
+ ORDER BY f.name ASC
89
+ LIMIT $2`, [source_id, limit, name_after]);
90
+ };
91
+ /**
92
+ * Reverse fields list (`target.upfields[]`).
93
+ *
94
+ * Returns rows whose `target_id = $1`, joined to `cell` on `source_id` so
95
+ * relations from tombstoned sources don't surface. The caller-side
96
+ * authz filter (per-source `can_view_cell`) runs after the SQL fetch
97
+ * — see the 2-layer authz contract on `cell_field_list({target_id})`.
98
+ *
99
+ * Bounded by `limit` (the wire `cell_field_list` cap) so a heavily
100
+ * inbound-linked target can't force an unbounded fetch + per-source authz
101
+ * pass on the public, IP-rate-limited reverse endpoint.
102
+ *
103
+ * @param deps - query deps
104
+ * @param target_id - target cell id
105
+ * @param options - `limit` caps the row count
106
+ * @returns matching rows, oldest first by source created_at
107
+ */
108
+ export const query_cell_field_list_for_target = async (deps, target_id, options) => deps.db.query(`SELECT f.* FROM cell_field f
109
+ JOIN cell s ON s.id = f.source_id
110
+ WHERE f.target_id = $1
111
+ AND s.deleted_at IS NULL
112
+ ORDER BY f.created_at ASC
113
+ LIMIT $2`, [target_id, options?.limit ?? null]);
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Raw queries against the `cell_grant` table.
3
+ *
4
+ * Resource-side ACL for cells: each row admits a principal at a `level`
5
+ * (`viewer` | `editor`). Principal is discriminated by which columns are
6
+ * set — `actor_id` (single actor) xor `(role, scope_id?)` (any holder
7
+ * of a matching role_grant). Owner is implicit on `cell.created_by` and never
8
+ * appears in this table.
9
+ *
10
+ * Convention: `deps: QueryDeps` first, no audit side effects, mutations
11
+ * return the affected row (or `null` for not-found).
12
+ *
13
+ * `query_cell_grant_create` upserts on the relevant partial unique index so
14
+ * re-granting the same principal updates `level` rather than producing
15
+ * duplicate rows. The two principal shapes use different indexes:
16
+ *
17
+ * - Actor-shaped: `idx_cell_grant_unique_actor` on `(cell_id, actor_id)`.
18
+ * - Role-shaped: `idx_cell_grant_unique_role_scope` on `(cell_id, role, scope_id)`
19
+ * with `NULLS NOT DISTINCT` so two `(role, NULL)` grants on the same cell
20
+ * collide.
21
+ *
22
+ * @module
23
+ */
24
+ import type { QueryDeps } from './query_deps.js';
25
+ import type { Uuid } from '@fuzdev/fuz_util/id.js';
26
+ import type { CellGrantLevel } from '../auth/cell_grant_action_specs.js';
27
+ /** Row shape returned by `cell_grant` SELECTs. */
28
+ export interface CellGrantRow {
29
+ id: Uuid;
30
+ cell_id: Uuid;
31
+ level: CellGrantLevel;
32
+ actor_id: Uuid | null;
33
+ role: string | null;
34
+ scope_id: Uuid | null;
35
+ granted_by: Uuid | null;
36
+ created_at: Date;
37
+ }
38
+ /**
39
+ * Discriminated principal input for `query_cell_grant_create`. Wire
40
+ * and query shapes are aligned — actor-shaped principals carry a
41
+ * pre-resolved `actor_id`; pickers run `actor_search` to convert a
42
+ * typed name to an id upstream of the handler.
43
+ */
44
+ export type CellGrantPrincipalQueryInput = {
45
+ kind: 'actor';
46
+ actor_id: Uuid;
47
+ } | {
48
+ kind: 'role';
49
+ role: string;
50
+ scope_id: Uuid | null;
51
+ };
52
+ /** Input for `query_cell_grant_create`. */
53
+ export interface CellGrantCreateQueryInput {
54
+ cell_id: Uuid;
55
+ level: CellGrantLevel;
56
+ principal: CellGrantPrincipalQueryInput;
57
+ granted_by: Uuid | null;
58
+ }
59
+ /**
60
+ * Insert a grant, or update the existing row's `level` + `granted_by` when
61
+ * one already exists for the same `(cell_id, principal)` pair.
62
+ *
63
+ * Idempotent re-share: caller doesn't need to check existence first. The
64
+ * UPSERT path runs even when the existing row's level matches — handlers
65
+ * reading the row's prior state for audit ("create vs. update") must do
66
+ * so before this call.
67
+ *
68
+ * @param deps - query deps
69
+ * @param input - cell, level, principal, grantor
70
+ * @returns the inserted-or-updated row
71
+ * @mutates `cell_grant` - inserts or updates one row
72
+ */
73
+ export declare const query_cell_grant_create: (deps: QueryDeps, input: CellGrantCreateQueryInput) => Promise<CellGrantRow>;
74
+ /**
75
+ * Fetch a grant by id.
76
+ *
77
+ * @param deps - query deps
78
+ * @param grant_id - grant id
79
+ * @returns the row or `null` when not found
80
+ */
81
+ export declare const query_cell_grant_get: (deps: QueryDeps, grant_id: Uuid) => Promise<CellGrantRow | null>;
82
+ /**
83
+ * Delete a grant by id, returning the deleted row.
84
+ *
85
+ * Returning the row lets the caller audit the principal + level after the
86
+ * delete and (for self-revoke) recompute `still_admitted` against the
87
+ * remaining grants on the cell without a second fetch.
88
+ *
89
+ * @param deps - query deps
90
+ * @param grant_id - grant id
91
+ * @returns the deleted row or `null` when no row matched
92
+ * @mutates `cell_grant` - deletes one row
93
+ */
94
+ export declare const query_cell_grant_delete: (deps: QueryDeps, grant_id: Uuid) => Promise<CellGrantRow | null>;
95
+ /**
96
+ * List all grants on a cell, oldest first.
97
+ *
98
+ * Used by `cell_grant_list` (RPC) and by handlers that need grants
99
+ * alongside the cell row for the authorize predicate.
100
+ *
101
+ * @param deps - query deps
102
+ * @param cell_id - cell id
103
+ * @returns matching rows
104
+ */
105
+ export declare const query_cell_grant_list_for_cell: (deps: QueryDeps, cell_id: Uuid) => Promise<Array<CellGrantRow>>;
106
+ /**
107
+ * List all grants across a set of cells, ordered by cell then creation.
108
+ * Used by the strict relation-read filter to test `can_view_cell` per
109
+ * target in memory — the caller groups the flat result by `cell_id`.
110
+ * Returns **every** grant on each cell (not caller-filtered), because
111
+ * `can_view_cell` needs the full grant list to decide admission.
112
+ *
113
+ * @param deps - query deps
114
+ * @param cell_ids - cells to fetch grants for (duplicates are harmless)
115
+ * @returns matching grant rows (group by `cell_id` caller-side)
116
+ */
117
+ export declare const query_cell_grant_list_for_cells: (deps: QueryDeps, cell_ids: ReadonlyArray<Uuid>) => Promise<Array<CellGrantRow>>;
118
+ /**
119
+ * Load grants that admit the caller (by actor or role-scoped role_grants) across
120
+ * multiple cells. Used to enrich `cell_list` responses with context about what
121
+ * granted access. Returns grants for the given cells that match the caller's
122
+ * identity or role_grant set.
123
+ *
124
+ * @param cell_ids - cells to fetch grants for
125
+ * @param caller_actor_id - actor id of the caller (null for unauth)
126
+ * @param role_grant_roles - active role_grant roles (parallel array)
127
+ * @param role_grant_scope_ids - active role_grant scope ids (parallel array, parallel to roles)
128
+ * @returns matching grants (may include grants the caller doesn't match; caller's
129
+ * list handler must filter when returning to the API)
130
+ */
131
+ export declare const query_cell_grants_for_caller_in_cells: (deps: QueryDeps, cell_ids: Array<Uuid>, caller_actor_id: Uuid | null, role_grant_roles: Array<string>, role_grant_scope_ids: Array<Uuid | null>) => Promise<Array<CellGrantRow>>;
132
+ //# sourceMappingURL=cell_grant_queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_grant_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_grant_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,oCAAoC,CAAC;AAGvE,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,MAAM,4BAA4B,GACrC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAC,GAC/B;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAA;CAAC,CAAC;AAEvD,2CAA2C;AAC3C,MAAM,WAAW,yBAAyB;IACzC,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,SAAS,EAAE,4BAA4B,CAAC;IACxC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,OAAO,yBAAyB,KAC9B,OAAO,CAAC,YAAY,CAsBtB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,UAAU,IAAI,KACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAK7B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,UAAU,IAAI,KACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAM7B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,SAAS,IAAI,KACX,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAM5B,CAAC;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,UAAU,aAAa,CAAC,IAAI,CAAC,KAC3B,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAQ7B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,UAAU,KAAK,CAAC,IAAI,CAAC,EACrB,iBAAiB,IAAI,GAAG,IAAI,EAC5B,kBAAkB,KAAK,CAAC,MAAM,CAAC,EAC/B,sBAAsB,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,KACtC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAmB7B,CAAC"}