@fuzdev/fuz_app 0.67.0 → 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 (168) 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/cross_backend/spawn_backend.d.ts.map +1 -1
  153. package/dist/testing/cross_backend/spawn_backend.js +31 -3
  154. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
  155. package/dist/testing/cross_backend/testing_server_bun.js +29 -2
  156. package/dist/testing/entities.d.ts.map +1 -1
  157. package/dist/testing/entities.js +4 -0
  158. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  159. package/dist/testing/ws_round_trip.js +4 -0
  160. package/dist/ui/AdminAccounts.svelte +58 -0
  161. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  162. package/dist/ui/admin_accounts_state.svelte.d.ts +30 -2
  163. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  164. package/dist/ui/admin_accounts_state.svelte.js +45 -1
  165. package/dist/ui/admin_rpc_adapters.d.ts +6 -2
  166. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  167. package/dist/ui/admin_rpc_adapters.js +5 -1
  168. package/package.json +4 -2
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Filesystem-backed `FactExternalFetcher`.
3
+ *
4
+ * Resolves `file:<shard>/<rest>` external URLs against a configured
5
+ * facts directory. The URL shape is the canonical relative-path scheme:
6
+ * 2 hex chars for the shard subdir + 62 hex chars for the rest of the
7
+ * blake3 hash (64 hex chars total). Files live at
8
+ * `<facts_dir>/<shard>/<rest>` after the writer atomically temp+renames
9
+ * them in.
10
+ *
11
+ * **Pre-filter regex**: `^file:[0-9a-f]{2}/[0-9a-f]{62}$`. A `..`
12
+ * segment can't match (`.` isn't in `[0-9a-f]`); neither can absolute
13
+ * paths, query strings, or any non-hex character. Defense-in-depth in
14
+ * front of `path.join` — the fetcher never trusts the URL came from a
15
+ * fact row, even though it always does in practice.
16
+ *
17
+ * The fetcher does NOT verify hash content — `PgFactStore.get` calls
18
+ * `fact_hash_verify(hash, bytes)` after the fetch and returns null on
19
+ * mismatch.
20
+ *
21
+ * Runtime: uses `node:fs/promises` + `node:fs` `createReadStream` so
22
+ * the same code works under Deno (via node compat) and vitest.
23
+ *
24
+ * @module
25
+ */
26
+ import type { FactExternalFetcher } from '../db/fact_store.js';
27
+ /** Construction options. */
28
+ export interface FileFactFetcherOptions {
29
+ /**
30
+ * Absolute path to the facts directory. Files resolve to
31
+ * `<facts_dir>/<shard>/<rest>`.
32
+ */
33
+ facts_dir: string;
34
+ }
35
+ /**
36
+ * Build a `FactExternalFetcher` that resolves `file:` URLs against the
37
+ * filesystem. Throws on a malformed URL before touching the disk so
38
+ * `PgFactStore.get` logs the warning + returns null without an I/O
39
+ * round-trip on bad data.
40
+ */
41
+ export declare const create_file_fact_fetcher: (options: FileFactFetcherOptions) => FactExternalFetcher;
42
+ //# sourceMappingURL=file_fact_fetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file_fact_fetcher.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/file_fact_fetcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAOH,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAG7D,4BAA4B;AAC5B,MAAM,WAAW,sBAAsB;IACtC;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,sBAAsB,KAAG,mBA0B1E,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Filesystem-backed `FactExternalFetcher`.
3
+ *
4
+ * Resolves `file:<shard>/<rest>` external URLs against a configured
5
+ * facts directory. The URL shape is the canonical relative-path scheme:
6
+ * 2 hex chars for the shard subdir + 62 hex chars for the rest of the
7
+ * blake3 hash (64 hex chars total). Files live at
8
+ * `<facts_dir>/<shard>/<rest>` after the writer atomically temp+renames
9
+ * them in.
10
+ *
11
+ * **Pre-filter regex**: `^file:[0-9a-f]{2}/[0-9a-f]{62}$`. A `..`
12
+ * segment can't match (`.` isn't in `[0-9a-f]`); neither can absolute
13
+ * paths, query strings, or any non-hex character. Defense-in-depth in
14
+ * front of `path.join` — the fetcher never trusts the URL came from a
15
+ * fact row, even though it always does in practice.
16
+ *
17
+ * The fetcher does NOT verify hash content — `PgFactStore.get` calls
18
+ * `fact_hash_verify(hash, bytes)` after the fetch and returns null on
19
+ * mismatch.
20
+ *
21
+ * Runtime: uses `node:fs/promises` + `node:fs` `createReadStream` so
22
+ * the same code works under Deno (via node compat) and vitest.
23
+ *
24
+ * @module
25
+ */
26
+ import { readFile } from 'node:fs/promises';
27
+ import { createReadStream } from 'node:fs';
28
+ import { Readable } from 'node:stream';
29
+ import { join } from 'node:path';
30
+ import { parse_file_fact_url } from './file_fact_url.js';
31
+ /**
32
+ * Build a `FactExternalFetcher` that resolves `file:` URLs against the
33
+ * filesystem. Throws on a malformed URL before touching the disk so
34
+ * `PgFactStore.get` logs the warning + returns null without an I/O
35
+ * round-trip on bad data.
36
+ */
37
+ export const create_file_fact_fetcher = (options) => {
38
+ const { facts_dir } = options;
39
+ const resolve_path = (url) => {
40
+ const parsed = parse_file_fact_url(url);
41
+ if (!parsed) {
42
+ throw new Error(`invalid file fact url: ${url}`);
43
+ }
44
+ return join(facts_dir, parsed.shard, parsed.rest);
45
+ };
46
+ return {
47
+ fetch_bytes: async (url) => {
48
+ const path = resolve_path(url);
49
+ const buf = await readFile(path);
50
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
51
+ },
52
+ // `Promise.resolve().then(...)` (rather than `async`) funnels any
53
+ // `resolve_path` throw into a rejection without an unused `await`.
54
+ fetch_stream: (url) => Promise.resolve().then(() => {
55
+ const path = resolve_path(url);
56
+ const node_stream = createReadStream(path);
57
+ return Readable.toWeb(node_stream);
58
+ }),
59
+ };
60
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Canonical filesystem-fact URL shape.
3
+ *
4
+ * `external_url` on the generic `facts` row is `string | null` because the
5
+ * `FactStore` interface stays federation-friendly (future
6
+ * `https://...` / `s3://...` shapes). Filesystem-minted URLs are exactly
7
+ * `file:<shard>/<rest>` where `<shard>` is the first 2 hex chars of the
8
+ * blake3 digest and `<rest>` the remaining 62 — files land at
9
+ * `<facts_dir>/<shard>/<rest>` after the writer atomically temp+renames
10
+ * them in.
11
+ *
12
+ * Centralizing the regex keeps the shape in one place: the `serve_fact_route`
13
+ * defense-in-depth check and the `file_fact_fetcher` resolver both validate
14
+ * against it, so federation work that loosens or extends the shape (e.g.,
15
+ * admitting `https://`) updates one place, not several.
16
+ *
17
+ * Defense-in-depth: a `..` segment can't match (`.` isn't in `[0-9a-f]`),
18
+ * neither can absolute paths, query strings, or any non-hex character.
19
+ * Used in front of `path.join` so the resolver never trusts the URL came
20
+ * from a fact row, even though it always does in practice.
21
+ *
22
+ * @module
23
+ */
24
+ import { z } from 'zod';
25
+ /** Anchored, capture-group form: `^file:(<shard>)/(<rest>)$`. */
26
+ export declare const FILE_FACT_URL_PATTERN: RegExp;
27
+ /**
28
+ * Branded URL form. Construct only via `parse_file_fact_url` or
29
+ * `mint_file_fact_url` — those are the validated boundaries. Direct
30
+ * string literals don't satisfy the brand.
31
+ */
32
+ export declare const FileFactUrl: z.core.$ZodBranded<z.ZodString, "FileFactUrl", "out">;
33
+ export type FileFactUrl = z.infer<typeof FileFactUrl>;
34
+ /** Type guard. Useful when discriminating a `string | null` column. */
35
+ export declare const is_file_fact_url: (s: string) => s is FileFactUrl;
36
+ /**
37
+ * Validate a string against the canonical shape. Returns the branded URL
38
+ * plus its parsed parts, or `null` on shape mismatch — callers decide
39
+ * whether that's a 404 (read), a skip (GC), or a hard reject (write).
40
+ */
41
+ export declare const parse_file_fact_url: (url: string) => {
42
+ url: FileFactUrl;
43
+ shard: string;
44
+ rest: string;
45
+ } | null;
46
+ /**
47
+ * Construct a canonical `file:<shard>/<rest>` URL. The writer side
48
+ * (`server/fact_write.ts`) assembles the shape from a freshly-computed
49
+ * hash; this helper centralizes the literal so a future shape change is
50
+ * a single edit.
51
+ */
52
+ export declare const mint_file_fact_url: (shard: string, rest: string) => FileFactUrl;
53
+ //# sourceMappingURL=file_fact_url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file_fact_url.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/file_fact_url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,QAAyC,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,WAAW,uDAA+D,CAAC;AACxF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,KAAG,CAAC,IAAI,WAA4C,CAAC;AAE/F;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,MAAM,KACT;IAAC,GAAG,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAAG,IAIpD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,WAC1B,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Canonical filesystem-fact URL shape.
3
+ *
4
+ * `external_url` on the generic `facts` row is `string | null` because the
5
+ * `FactStore` interface stays federation-friendly (future
6
+ * `https://...` / `s3://...` shapes). Filesystem-minted URLs are exactly
7
+ * `file:<shard>/<rest>` where `<shard>` is the first 2 hex chars of the
8
+ * blake3 digest and `<rest>` the remaining 62 — files land at
9
+ * `<facts_dir>/<shard>/<rest>` after the writer atomically temp+renames
10
+ * them in.
11
+ *
12
+ * Centralizing the regex keeps the shape in one place: the `serve_fact_route`
13
+ * defense-in-depth check and the `file_fact_fetcher` resolver both validate
14
+ * against it, so federation work that loosens or extends the shape (e.g.,
15
+ * admitting `https://`) updates one place, not several.
16
+ *
17
+ * Defense-in-depth: a `..` segment can't match (`.` isn't in `[0-9a-f]`),
18
+ * neither can absolute paths, query strings, or any non-hex character.
19
+ * Used in front of `path.join` so the resolver never trusts the URL came
20
+ * from a fact row, even though it always does in practice.
21
+ *
22
+ * @module
23
+ */
24
+ import { z } from 'zod';
25
+ /** Anchored, capture-group form: `^file:(<shard>)/(<rest>)$`. */
26
+ export const FILE_FACT_URL_PATTERN = /^file:([0-9a-f]{2})\/([0-9a-f]{62})$/;
27
+ /**
28
+ * Branded URL form. Construct only via `parse_file_fact_url` or
29
+ * `mint_file_fact_url` — those are the validated boundaries. Direct
30
+ * string literals don't satisfy the brand.
31
+ */
32
+ export const FileFactUrl = z.string().regex(FILE_FACT_URL_PATTERN).brand('FileFactUrl');
33
+ /** Type guard. Useful when discriminating a `string | null` column. */
34
+ export const is_file_fact_url = (s) => FILE_FACT_URL_PATTERN.test(s);
35
+ /**
36
+ * Validate a string against the canonical shape. Returns the branded URL
37
+ * plus its parsed parts, or `null` on shape mismatch — callers decide
38
+ * whether that's a 404 (read), a skip (GC), or a hard reject (write).
39
+ */
40
+ export const parse_file_fact_url = (url) => {
41
+ const m = FILE_FACT_URL_PATTERN.exec(url);
42
+ if (!m)
43
+ return null;
44
+ return { url: url, shard: m[1], rest: m[2] };
45
+ };
46
+ /**
47
+ * Construct a canonical `file:<shard>/<rest>` URL. The writer side
48
+ * (`server/fact_write.ts`) assembles the shape from a freshly-computed
49
+ * hash; this helper centralizes the literal so a future shape change is
50
+ * a single edit.
51
+ */
52
+ export const mint_file_fact_url = (shard, rest) => `file:${shard}/${rest}`;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `GET /api/facts/:hash` — content-addressed fact serving.
3
+ *
4
+ * Resolves a fact hash to the bytes referenced by at least one viewable
5
+ * cell. Embedded facts stream from the `facts.bytes` PG column;
6
+ * external facts (filesystem-backed `file:<shard>/<rest>` URLs) either
7
+ * return an `X-Accel-Redirect` header pointing into nginx's internal
8
+ * facts location (production) or stream from disk via the filesystem
9
+ * `FactExternalFetcher` (dev / tests). The runtime mode is selected by
10
+ * the optional `x_accel_redirect_prefix` factory option — set in prod,
11
+ * unset in dev.
12
+ *
13
+ * REST, not RPC: binary responses don't fit the JSON-RPC envelope.
14
+ *
15
+ * ## Authorization
16
+ *
17
+ * Auth is `{account: 'none', actor: 'none'}` — the dispatcher's
18
+ * authorization phase is skipped for pure-public routes, so this handler
19
+ * builds the `RequestContext` itself from `c.var.account_id` (populated
20
+ * by the `/api/*` session middleware) by resolving the caller's single
21
+ * actor and loading their role_grants. Unauthed callers pass through
22
+ * with `req_ctx: null`. Viewers are admitted via `can_view_cell` over
23
+ * **every** active cell that references the hash. Multi-actor accounts
24
+ * fall through with `req_ctx: null` — there's no `acting?` slot on a
25
+ * pure-public route, so multi-actor callers are treated as anonymous
26
+ * (admitted only by the public-visibility branch). A fact is viewable iff
27
+ * at least one referencing cell admits the caller; unauthenticated callers
28
+ * are admitted only via a referencing cell with `cell.visibility ===
29
+ * 'public'`. Facts with no referencing active cell are unreachable here —
30
+ * orphan-fact GC reaps them separately.
31
+ *
32
+ * 404 is the universal "not viewable" response: missing fact, missing
33
+ * referencing cell, all referencing cells private to other actors. We
34
+ * deliberately don't distinguish 403 from 404 — the existence of a
35
+ * private hash should not leak through the public surface.
36
+ *
37
+ * ## Defense-in-depth
38
+ *
39
+ * The `external_url` regex is re-validated before issuing
40
+ * `X-Accel-Redirect` even though `PgFactStore.put_ref` only writes
41
+ * `file:<shard>/<rest>`-shaped URLs. A future row-injection bug
42
+ * upstream would otherwise hand nginx an attacker-controlled path.
43
+ *
44
+ * @module
45
+ */
46
+ import type { Logger } from '@fuzdev/fuz_util/log.js';
47
+ import { type RouteSpec } from '../http/route_spec.js';
48
+ import type { AppDeps } from '../auth/deps.js';
49
+ export interface CreateServeFactRouteSpecOptions {
50
+ /**
51
+ * App deps reference. Currently unused at handler time (cell + fact
52
+ * tables are read via `RouteContext.db`); kept on the factory
53
+ * signature for symmetry with sibling route factories and to give
54
+ * future role_grant-scoped viewer extensions somewhere to read other
55
+ * deps without changing the public shape.
56
+ */
57
+ deps: AppDeps;
58
+ /** Absolute path of the facts directory. Used for the dev/test streaming path. */
59
+ facts_dir: string;
60
+ /**
61
+ * When set, external facts return an `X-Accel-Redirect` pointing at
62
+ * `${prefix}<shard>/<rest>` — nginx's internal facts location serves
63
+ * the bytes. When unset, external facts stream from
64
+ * `<facts_dir>/<shard>/<rest>` directly. Production sets this (e.g.
65
+ * `/_facts/`); tests + dev leave it unset.
66
+ */
67
+ x_accel_redirect_prefix?: string;
68
+ log: Logger;
69
+ }
70
+ /**
71
+ * Build the `GET /api/facts/:hash` `RouteSpec`.
72
+ *
73
+ * Pure-public auth — the handler builds the per-request `RequestContext`
74
+ * from `c.var.account_id` and enforces visibility per-fact via the
75
+ * cell-walk above.
76
+ */
77
+ export declare const create_serve_fact_route_spec: (options: CreateServeFactRouteSpecOptions) => RouteSpec;
78
+ //# sourceMappingURL=serve_fact_route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAKH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAOpD,OAAO,EAAmB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEvE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAoB7C,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SAgIF,CAAC"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * `GET /api/facts/:hash` — content-addressed fact serving.
3
+ *
4
+ * Resolves a fact hash to the bytes referenced by at least one viewable
5
+ * cell. Embedded facts stream from the `facts.bytes` PG column;
6
+ * external facts (filesystem-backed `file:<shard>/<rest>` URLs) either
7
+ * return an `X-Accel-Redirect` header pointing into nginx's internal
8
+ * facts location (production) or stream from disk via the filesystem
9
+ * `FactExternalFetcher` (dev / tests). The runtime mode is selected by
10
+ * the optional `x_accel_redirect_prefix` factory option — set in prod,
11
+ * unset in dev.
12
+ *
13
+ * REST, not RPC: binary responses don't fit the JSON-RPC envelope.
14
+ *
15
+ * ## Authorization
16
+ *
17
+ * Auth is `{account: 'none', actor: 'none'}` — the dispatcher's
18
+ * authorization phase is skipped for pure-public routes, so this handler
19
+ * builds the `RequestContext` itself from `c.var.account_id` (populated
20
+ * by the `/api/*` session middleware) by resolving the caller's single
21
+ * actor and loading their role_grants. Unauthed callers pass through
22
+ * with `req_ctx: null`. Viewers are admitted via `can_view_cell` over
23
+ * **every** active cell that references the hash. Multi-actor accounts
24
+ * fall through with `req_ctx: null` — there's no `acting?` slot on a
25
+ * pure-public route, so multi-actor callers are treated as anonymous
26
+ * (admitted only by the public-visibility branch). A fact is viewable iff
27
+ * at least one referencing cell admits the caller; unauthenticated callers
28
+ * are admitted only via a referencing cell with `cell.visibility ===
29
+ * 'public'`. Facts with no referencing active cell are unreachable here —
30
+ * orphan-fact GC reaps them separately.
31
+ *
32
+ * 404 is the universal "not viewable" response: missing fact, missing
33
+ * referencing cell, all referencing cells private to other actors. We
34
+ * deliberately don't distinguish 403 from 404 — the existence of a
35
+ * private hash should not leak through the public surface.
36
+ *
37
+ * ## Defense-in-depth
38
+ *
39
+ * The `external_url` regex is re-validated before issuing
40
+ * `X-Accel-Redirect` even though `PgFactStore.put_ref` only writes
41
+ * `file:<shard>/<rest>`-shaped URLs. A future row-injection bug
42
+ * upstream would otherwise hand nginx an attacker-controlled path.
43
+ *
44
+ * @module
45
+ */
46
+ import { createReadStream } from 'node:fs';
47
+ import { Readable } from 'node:stream';
48
+ import { join } from 'node:path';
49
+ import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.js';
50
+ import { z } from 'zod';
51
+ import { build_request_context } from '../auth/request_context.js';
52
+ import { ACCOUNT_ID_KEY } from '../hono_context.js';
53
+ import { query_actors_by_account } from '../auth/account_queries.js';
54
+ import { get_route_params } from '../http/route_spec.js';
55
+ import { ERROR_INVALID_ROUTE_PARAMS } from '../http/error_schemas.js';
56
+ import { query_get_fact, query_get_fact_meta } from '../db/fact_queries.js';
57
+ import { query_cell_list_by_ref } from '../db/cell_queries.js';
58
+ import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
59
+ import { can_view_cell } from '../auth/cell_authorize.js';
60
+ import { parse_file_fact_url } from './file_fact_url.js';
61
+ /** `Cache-Control` for fact responses — 5 min revocation window. */
62
+ const CACHE_CONTROL = 'private, max-age=300';
63
+ /**
64
+ * Path-param schema. Matching the canonical fact-hash form here pushes
65
+ * malformed-hash 400s through the framework's standard params-validation
66
+ * error shape (`ERROR_INVALID_ROUTE_PARAMS`), which the round-trip
67
+ * validator expects.
68
+ */
69
+ const params_schema = z.strictObject({
70
+ hash: FactHashSchema,
71
+ });
72
+ /**
73
+ * Build the `GET /api/facts/:hash` `RouteSpec`.
74
+ *
75
+ * Pure-public auth — the handler builds the per-request `RequestContext`
76
+ * from `c.var.account_id` and enforces visibility per-fact via the
77
+ * cell-walk above.
78
+ */
79
+ export const create_serve_fact_route_spec = (options) => {
80
+ const { facts_dir, x_accel_redirect_prefix, log } = options;
81
+ return {
82
+ method: 'GET',
83
+ path: '/api/facts/:hash',
84
+ auth: { account: 'none', actor: 'none' },
85
+ description: 'Serve content-addressed fact bytes. 404 unless at least one referencing cell admits the caller via can_view_cell.',
86
+ params: params_schema,
87
+ input: z.null(),
88
+ // The body is a binary stream; no JSON output schema applies.
89
+ output: z.null(),
90
+ errors: {
91
+ // Tighten the auto-derived 400 from `ApiError` (`error: string`) to
92
+ // the actual literal emitted by `create_params_validation`, so
93
+ // `assert_error_schema_tightness` reads the surface as specific
94
+ // rather than generic.
95
+ 400: z.looseObject({
96
+ error: z.literal(ERROR_INVALID_ROUTE_PARAMS),
97
+ issues: z.array(z.unknown()),
98
+ }),
99
+ },
100
+ handler: async (c, route) => {
101
+ const { hash } = get_route_params(c);
102
+ const meta = await query_get_fact_meta({ db: route.db }, hash);
103
+ if (!meta) {
104
+ return c.body(null, 404);
105
+ }
106
+ // Pure-public route — dispatcher skips the authorization phase, so
107
+ // build the `RequestContext` here from the session-middleware-set
108
+ // account id. Multi-actor accounts fall through with `null` (no
109
+ // `acting?` slot on a public route to disambiguate); single-actor
110
+ // accounts resolve their actor and role_grants for owner / grant /
111
+ // admin admission paths.
112
+ const account_id = c.get(ACCOUNT_ID_KEY);
113
+ let req_ctx = null;
114
+ if (account_id) {
115
+ const actors = await query_actors_by_account({ db: route.db }, account_id);
116
+ if (actors.length === 1) {
117
+ req_ctx = await build_request_context({ db: route.db }, account_id, actors[0].id);
118
+ }
119
+ }
120
+ // `include_grant_count: false` — the authz walk only reads
121
+ // `can_view_cell`-relevant fields, so skip the per-row grant
122
+ // COUNT subquery. Cheap to begin with; even cheaper now.
123
+ const referencing_cells = await query_cell_list_by_ref({ db: route.db }, hash, {
124
+ include_grant_count: false,
125
+ });
126
+ // Sequential walk with early break on first admit — preserves
127
+ // the original `.some()` short-circuit. Unauthenticated callers
128
+ // skip the grant fetch entirely since no grant can admit a null
129
+ // req_ctx (the only admit path is the public-visibility branch
130
+ // in `can_view_cell`, which doesn't need grants). Authenticated
131
+ // callers eat one `cell_grant` lookup per referencing cell up
132
+ // to the first admit; acceptable at MVP fact-serve scale.
133
+ // TODO: if profiling shows this hot, batch grants in one query
134
+ // across all referencing cells, or push the predicate into SQL.
135
+ let viewable = false;
136
+ for (const cell of referencing_cells) {
137
+ const grants = req_ctx
138
+ ? await query_cell_grant_list_for_cell({ db: route.db }, cell.id)
139
+ : null;
140
+ if (can_view_cell(req_ctx, cell, grants)) {
141
+ viewable = true;
142
+ break;
143
+ }
144
+ }
145
+ if (!viewable) {
146
+ // 404 (not 403) so existence of private hashes doesn't leak
147
+ // through the public surface. Same response shape as a
148
+ // genuinely missing fact.
149
+ return c.body(null, 404);
150
+ }
151
+ const content_type = meta.content_type ?? 'application/octet-stream';
152
+ const size = String(meta.size);
153
+ if (meta.external_url === null) {
154
+ // Embedded — bytes live in the PG row.
155
+ const row = await query_get_fact({ db: route.db }, hash);
156
+ if (!row || row.bytes === null) {
157
+ // Race: meta said embedded but bytes vanished. Treat as not-found.
158
+ log.warn(`serve_fact: embedded bytes missing for ${hash} (meta said embedded, row=${row ? 'present' : 'null'})`);
159
+ return c.body(null, 404);
160
+ }
161
+ const bytes = to_uint8(row.bytes);
162
+ return c.body(bytes, 200, {
163
+ 'Content-Type': content_type,
164
+ 'Content-Length': size,
165
+ 'Cache-Control': CACHE_CONTROL,
166
+ });
167
+ }
168
+ // External — defense-in-depth re-validate the URL before trusting it
169
+ // to address the filesystem.
170
+ const parsed = parse_file_fact_url(meta.external_url);
171
+ if (!parsed) {
172
+ log.error(`serve_fact: rejecting malformed external_url for ${hash}: ${meta.external_url}`);
173
+ return c.body(null, 404);
174
+ }
175
+ const { shard, rest } = parsed;
176
+ if (x_accel_redirect_prefix !== undefined) {
177
+ // Production: hand off to nginx via the internal facts location.
178
+ return c.body(null, 200, {
179
+ 'Content-Type': content_type,
180
+ 'Content-Length': size,
181
+ 'Cache-Control': CACHE_CONTROL,
182
+ 'X-Accel-Redirect': `${x_accel_redirect_prefix}${shard}/${rest}`,
183
+ });
184
+ }
185
+ // Dev / tests: stream from disk. `createReadStream` errors land on the
186
+ // returned ReadableStream, which Hono surfaces as a 500 to the client.
187
+ const file_path = join(facts_dir, shard, rest);
188
+ const node_stream = createReadStream(file_path);
189
+ const web_stream = Readable.toWeb(node_stream);
190
+ return c.body(web_stream, 200, {
191
+ 'Content-Type': content_type,
192
+ 'Content-Length': size,
193
+ 'Cache-Control': CACHE_CONTROL,
194
+ });
195
+ },
196
+ };
197
+ };
198
+ /**
199
+ * Coerce whatever the driver returns for BYTEA into a `Uint8Array`. Same
200
+ * shape as the helper in `PgFactStore.get` — `pg` returns a `Buffer`
201
+ * (`Uint8Array` subclass), `pglite` already returns `Uint8Array`.
202
+ */
203
+ const to_uint8 = (value) => value instanceof Uint8Array && value.constructor === Uint8Array
204
+ ? value
205
+ : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
@@ -149,7 +149,7 @@ Key module-scope values:
149
149
 
150
150
  - `stub_password_deps` — `PasswordHashDeps` hashing via `stub_hash_${password}` and verifying by equality. Deterministic, no Argon2 cost — use for every test not specifically exercising password hashing.
151
151
  - `TEST_COOKIE_SECRET` — 64-hex-char deterministic cookie secret. Produces a valid `Keyring` via `create_validated_keyring`. Never used in production — the stub guard plus fixed value is the contract.
152
- - `fallback_pglite_factory` — module-level PGlite factory `create_test_app_server` uses when no `db` is passed. Reuses the WASM cache via `create_pglite_factory`.
152
+ - `fallback_pglite_factory` — module-level auth-only PGlite factory `create_test_app_server` uses when no `db` is passed. Reuses the WASM cache via `create_pglite_factory`. When `migration_namespaces` is supplied, a memoized sibling factory (keyed by namespace set) migrating `[auth_migration_ns, ...migration_namespaces]` is used instead — same shared WASM, extra tables.
153
153
 
154
154
  Two helpers share the "insert account + actor + roles + API token + session +
155
155
  cookie" flow, split by intent:
@@ -167,7 +167,7 @@ pre-keeper, lock unflipped), use `create_test_app_for_bootstrap` — pair with
167
167
  Types:
168
168
 
169
169
  - `TestAppServer extends AppBackend` — adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`.
170
- - `TestAppServerOptions` — `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `audit_factory`. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, wrap with `emit_decorator` (via `create_emit_ordering_audit_factory`), or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename.
170
+ - `TestAppServerOptions` — `session_options` (required), optional `db`, `db_type`, `migration_namespaces`, `password`, `username`, `password_value`, `roles`, `audit_factory`. `migration_namespaces` runs extra namespaces after auth in the auto-created PGlite (mirrors `create_app_backend`); mutually exclusive with `db` (caller-migrated) — passing both throws. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, wrap with `emit_decorator` (via `create_emit_ordering_audit_factory`), or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename.
171
171
  - `CreateTestAppOptions extends TestAppServerOptions` — adds `create_route_specs` (required), `rpc_endpoints?: RpcEndpointsSuiteOption` (top-level only — single source of truth, symmetric with the suite-level option), `bootstrap?: BootstrapServerOptions` (top-level only — same precedent as `rpc_endpoints`), and `app_options?: SuiteAppOptions` (`Partial<AppServerOptions>` excluding the five fields the helper manages: `backend`, `session_options`, `create_route_specs`, `rpc_endpoints`, `bootstrap`).
172
172
  - `TestAccount` — `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`.
173
173
  - `TestApp` — `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`.
@@ -799,7 +799,11 @@ source of truth for wire-shape conformance.
799
799
 
800
800
  - `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
801
801
  `TestAccountFixture` / `CreateTestAccountOptions` types,
802
- `default_in_process_setup(options)` (wraps `create_test_app`), and
802
+ `default_in_process_setup(options)` (wraps `create_test_app`; pass
803
+ `migration_namespaces` for suites needing tables beyond the auth-only
804
+ default — the cell parity suite passes `[CELL_MIGRATION_NS]`, and
805
+ `create_test_app` provisions a per-test fresh db migrating
806
+ `[auth_migration_ns, ...migration_namespaces]`), and
803
807
  `default_in_process_suite_options(options)` (emits the full Tier 1 suite
804
808
  options bag: the `{setup_test, surface_source, capabilities}` triple plus
805
809
  `session_options` / `create_route_specs` / `rpc_endpoints` pass-through;
@@ -842,8 +846,16 @@ source of truth for wire-shape conformance.
842
846
 
843
847
  - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
844
848
  (`bearer_auth` / `trusted_proxy` / `login_rate_limit` / `ws` / `sse` /
845
- `in_process_only`), `test_if(cond, name, fn)` for capability-gated cases,
846
- and `in_process_capabilities` preset.
849
+ `cell_crud` / `cell_relations` / `account_lifecycle` / `in_process_only`),
850
+ `test_if(cond, name, fn)`
851
+ for capability-gated cases, and `in_process_capabilities` preset. `cell_crud`
852
+ gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
853
+ parity suite — both `true` on every backend that live-mounts the full cell
854
+ surface (TS spine binary, in-process app, Rust stub). A backend mounting only
855
+ plain CRUD would declare `cell_crud: true, cell_relations: false`.
856
+ `account_lifecycle` gates `describe_account_lifecycle_cross_tests` (the
857
+ `account_delete` / `account_undelete` / `account_purge` parity suite) — also
858
+ off the declared surface like cells, `true` on every spine.
847
859
 
848
860
  ### `cross_backend/standard.ts` — `describe_standard_cross_process_tests`
849
861
 
@@ -925,6 +937,47 @@ only — wire from a `*.cross.test.ts`. fuz_app's own wiring is
925
937
  `src/test/cross_backend/sse.cross.test.ts`; only the TS spines advertise
926
938
  `sse` (they wire `audit_log_sse`), so the Rust `spine_stub` cases `.skip`.
927
939
 
940
+ ### `cross_backend/cell_crud.ts` + `cell_relations.ts` — cell parity suites
941
+
942
+ The cell-layer parity coverage is split across two sibling suites. Cells
943
+ can't ride the generic `describe_rpc_round_trip_tests` (stateful verbs need a
944
+ real id threaded across calls; `cell_get` has a top-level `.refine()`), so —
945
+ like ws/sse — the full cell surface **live-mounts** on the spine RPC path but
946
+ stays **off** `create_spine_surface_spec`, and these dedicated suites are the
947
+ cell validators (`describe_standard_cross_process_tests`' generic round-trip
948
+ never sees them). Both parse every success response against the verb's Zod
949
+ **output** schema, so a TS↔Rust envelope drift fails the suite — not just a
950
+ payload-field drift. Call-site primitives (`rpc_call` / `error_reason` /
951
+ `expect_output` + the shared `CellCrossTestOptions`) live in
952
+ `cross_backend/cell_cross_helpers.ts`.
953
+
954
+ - **`describe_cell_crud_cross_tests`** (gates on `capabilities.cell_crud`) —
955
+ the create → get → update → delete → list lifecycle threading the id, plus
956
+ the CRUD authz matrix (owner CRUD; anon-public-only / private-404; non-owner
957
+ edit/read/delete → 404 IDOR mask; admin reaches any; dup active `path` → 409;
958
+ `path` write by non-admin → 403 on create + update; `cell_get` with no
959
+ id/path → `invalid_params`; null-auth `cell_list` `created_by` →
960
+ `invalid_params`).
961
+ - **`describe_cell_relations_cross_tests`** (gates on
962
+ `capabilities.cell_relations`) — the verbs beyond CRUD: grant lifecycle
963
+ (actor-shaped editor grant enables edit, manage-tier `cell_grant_list`,
964
+ revoke), the now-reachable `cell_visibility_manage_only` 403 (editor-grant
965
+ holder can't flip visibility), field set / forward+reverse list / idempotent
966
+ delete, item insert / ordered forward+reverse list / move / idempotent
967
+ delete, clone shallow (shares edges) vs deep (clones children), and
968
+ manage-tier `cell_audit_list` (owner reads the timeline; a viewer-grant
969
+ holder who can `cell_get` still gets the IDOR 404). Only **actor-shaped**
970
+ grants are exercised — role-shaped principals need a closed role registry the
971
+ Rust spine deliberately lacks.
972
+
973
+ Both gate `true` on TS + Rust (cells run on both, no `.skip`). Cross-process
974
+ wiring is `src/test/cross_backend/cell.cross.test.ts` (both suites); the
975
+ in-process legs (plain `gro test`) are `src/test/auth/cell_crud_parity.db.test.ts`
976
+
977
+ - `cell_relations_parity.db.test.ts`, sharing the full-surface
978
+ `create_cell_parity_setup` (`cell_parity_helpers.ts`) which mounts every cell
979
+ verb and registers `cell_audit_events` through the audit factory.
980
+
928
981
  ### Cross-process plumbing (consumed by `*.cross.test.ts` suites)
929
982
 
930
983
  - `testing/cross_backend/backend_config.ts` — `BackendConfig` +
@@ -18,6 +18,7 @@ import { type Keyring } from '../auth/keyring.js';
18
18
  import type { Db, DbType } from '../db/db.js';
19
19
  import type { PasswordHashDeps } from '../auth/password.js';
20
20
  import { type SessionOptions } from '../auth/session_cookie.js';
21
+ import { type MigrationNamespace } from '../db/migrate.js';
21
22
  import { type AppBackend, type AuditFactory } from '../server/app_backend.js';
22
23
  import { type AppServerOptions, type AppServerContext, type BootstrapServerOptions, type BootstrapLiveOptions } from '../server/app_server.js';
23
24
  import type { AppSurface, AppSurfaceSpec } from '../http/surface.js';
@@ -146,6 +147,17 @@ export interface TestAppServerOptions {
146
147
  db?: Db;
147
148
  /** Database driver type — only used when `db` is provided. Default: `'pglite-memory'`. */
148
149
  db_type?: DbType;
150
+ /**
151
+ * Extra migration namespaces run after the builtin auth namespace in the
152
+ * auto-created in-memory PGlite, mirroring `create_app_backend`'s
153
+ * `migration_namespaces`. For suites whose backend needs tables beyond
154
+ * auth — the cell parity suite passes `[CELL_MIGRATION_NS]`. The harness
155
+ * builds + caches a fresh-per-test factory migrating
156
+ * `[auth_migration_ns, ...migration_namespaces]`; the reset-on-`create`
157
+ * gives the same fresh-db isolation as the auth-only default. Mutually
158
+ * exclusive with `db` (which assumes the caller already migrated).
159
+ */
160
+ migration_namespaces?: ReadonlyArray<MigrationNamespace>;
149
161
  /** Password implementation. Default: `stub_password_deps`. Pass `argon2_password_deps` for tests that exercise login. */
150
162
  password?: PasswordHashDeps;
151
163
  /** Username for the bootstrapped account. Default: `'keeper'`. */