@fuzdev/fuz_app 0.143.0 → 0.144.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 (49) hide show
  1. package/dist/auth/CLAUDE.md +8 -0
  2. package/dist/auth/cell_action_specs.d.ts +13 -0
  3. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  4. package/dist/auth/cell_action_specs.js +13 -0
  5. package/dist/auth/cell_actions.d.ts +7 -0
  6. package/dist/auth/cell_actions.d.ts.map +1 -1
  7. package/dist/auth/cell_actions.js +30 -0
  8. package/dist/auth/cell_ref_admission.d.ts +51 -0
  9. package/dist/auth/cell_ref_admission.d.ts.map +1 -0
  10. package/dist/auth/cell_ref_admission.js +69 -0
  11. package/dist/auth/deps.d.ts +6 -3
  12. package/dist/auth/deps.d.ts.map +1 -1
  13. package/dist/db/CLAUDE.md +34 -21
  14. package/dist/db/cell_ddl.d.ts +3 -2
  15. package/dist/db/cell_ddl.d.ts.map +1 -1
  16. package/dist/db/cell_ddl.js +3 -2
  17. package/dist/db/cell_queries.d.ts +31 -6
  18. package/dist/db/cell_queries.d.ts.map +1 -1
  19. package/dist/db/cell_queries.js +45 -18
  20. package/dist/db/fact_ddl.d.ts +29 -4
  21. package/dist/db/fact_ddl.d.ts.map +1 -1
  22. package/dist/db/fact_ddl.js +43 -4
  23. package/dist/db/fact_holder_queries.d.ts +66 -0
  24. package/dist/db/fact_holder_queries.d.ts.map +1 -0
  25. package/dist/db/fact_holder_queries.js +75 -0
  26. package/dist/db/fact_queries.d.ts +9 -7
  27. package/dist/db/fact_queries.d.ts.map +1 -1
  28. package/dist/db/fact_store.d.ts +65 -9
  29. package/dist/db/fact_store.d.ts.map +1 -1
  30. package/dist/db/fact_store.js +49 -20
  31. package/dist/server/serve_fact_route.d.ts +24 -4
  32. package/dist/server/serve_fact_route.d.ts.map +1 -1
  33. package/dist/server/serve_fact_route.js +24 -4
  34. package/dist/testing/CLAUDE.md +25 -1
  35. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  36. package/dist/testing/cross_backend/cell_crud.js +6 -3
  37. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  38. package/dist/testing/cross_backend/cell_gated_create.js +25 -16
  39. package/dist/testing/cross_backend/cell_ref_admission.d.ts +4 -0
  40. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -0
  41. package/dist/testing/cross_backend/cell_ref_admission.js +268 -0
  42. package/dist/testing/cross_backend/expected_schema.json +66 -10
  43. package/dist/testing/cross_backend/fact_serving.d.ts +19 -1
  44. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  45. package/dist/testing/cross_backend/fact_serving.js +31 -9
  46. package/dist/testing/cross_backend/testing_reset_actions.d.ts +6 -2
  47. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  48. package/dist/testing/cross_backend/testing_reset_actions.js +13 -3
  49. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  /**
2
- * PG-backed `FactStore` implementation.
2
+ * PG-backed `HeldFactStore` implementation.
3
3
  *
4
4
  * Wraps the raw queries in `db/fact_queries.ts` with the lifecycle the
5
- * `FactStore` interface promises:
5
+ * `HeldFactStore` interface promises:
6
6
  *
7
7
  * - sync hash on `put`; `put_stream` hashes as it streams
8
8
  * - idempotent insert (`ON CONFLICT DO NOTHING` in the queries layer)
@@ -33,18 +33,32 @@
33
33
  * `external_url` has any other shape, and any external row on a store with no
34
34
  * disk CAS configured, as unavailable: a warning and `null`.
35
35
  *
36
+ * ## Holders
37
+ *
38
+ * Every put names its holder (`HeldFactPutOptions.holder`): the actor whose
39
+ * request carried the bytes, or `null` for a system put (seeds, server-side
40
+ * imports). A named holder gets a `fact_holder` row on every put — a
41
+ * deduplicated put included, since bringing identical bytes is possession. The
42
+ * `fact` row, its refs, and the holder row commit in one transaction. Cell ref
43
+ * admission (`auth/cell_ref_admission.ts`) reads the holder rows. The holder is
44
+ * a spine concept, so `PgFactStore` implements `HeldFactStore` — the
45
+ * `@fuzdev/fuz_util` `FactStore` reads plus puts that take `HeldFactPutOptions` —
46
+ * rather than the backend-agnostic `FactStore`, whose puts name no actor.
47
+ *
36
48
  * @module
37
49
  */
38
50
  import { to_error_message } from '@fuzdev/fuz_util/error.ts';
39
51
  import { fact_hash_bytes, fact_hash_verify, fact_hash_extract_refs } from '@fuzdev/fuz_util/fact_hash.ts';
40
52
  import { query_delete_fact, query_get_fact, query_get_fact_meta, query_get_fact_refs, query_has_fact, query_put_fact, query_put_fact_refs } from './fact_queries.js';
53
+ import { query_put_fact_holder } from './fact_holder_queries.js';
41
54
  import { is_file_fact_url } from './file_fact_url.js';
42
55
  import { read_fact_bytes_from_disk, stream_fact_to_disk, write_fact_bytes_to_disk } from './fact_disk_storage.js';
43
56
  /** Default embedded-vs-referenced cutoff (1 MiB). */
44
57
  export const FACT_EMBEDDED_THRESHOLD_DEFAULT = 1024 * 1024;
45
58
  /**
46
- * PG-backed `FactStore`. Delegates to `db/fact_queries.ts` for I/O and adds
47
- * the lifecycle layer described in the module doc.
59
+ * PG-backed fact store. Delegates to `db/fact_queries.ts` for I/O and adds
60
+ * the lifecycle layer described in the module doc. Implements `HeldFactStore`:
61
+ * the `@fuzdev/fuz_util` `FactStore` reads, puts taking `HeldFactPutOptions`.
48
62
  */
49
63
  export class PgFactStore {
50
64
  #deps;
@@ -65,7 +79,10 @@ export class PgFactStore {
65
79
  * are configured) at `<facts_dir>/<shard>/<rest>` with a `file:` URL. Oversize
66
80
  * without a disk root throws: the store has nowhere to put bytes it cannot
67
81
  * embed. Idempotent — `ON CONFLICT DO NOTHING` + content-addressed disk
68
- * filenames make a re-write a no-op.
82
+ * filenames make a re-write a no-op. `options.holder`, when set, is recorded
83
+ * as a holder on a fresh insert and a dedup hit alike.
84
+ *
85
+ * @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
69
86
  */
70
87
  async put(bytes, options) {
71
88
  const hash = fact_hash_bytes(bytes);
@@ -82,16 +99,13 @@ export class PgFactStore {
82
99
  row_bytes = bytes;
83
100
  row_external_url = null;
84
101
  }
85
- const inserted = await query_put_fact(this.#deps, {
102
+ await this.#insert_row({
86
103
  hash,
87
104
  bytes: row_bytes,
88
105
  external_url: row_external_url,
89
- content_type: options?.content_type ?? null,
106
+ content_type: options.content_type ?? null,
90
107
  size: bytes.length
91
- });
92
- if (inserted) {
93
- await query_put_fact_refs(this.#deps, hash, resolve_refs(bytes, options));
94
- }
108
+ }, resolve_refs(bytes, options), options.holder);
95
109
  return hash;
96
110
  }
97
111
  /**
@@ -109,7 +123,9 @@ export class PgFactStore {
109
123
  *
110
124
  * Requires `fs` (and, for the over-threshold spill, `disk_root`) to be
111
125
  * configured. The streaming twin of `put`; mirrors the Rust
112
- * `FactStore::put_stream`.
126
+ * `FactStore::put_stream`. Records `options.holder` the way `put` does.
127
+ *
128
+ * @mutates fact_holder - records `options.holder` (when non-null) in the transaction that writes the `fact` row
113
129
  */
114
130
  async put_stream(stream, max_bytes, options) {
115
131
  if (this.#fs === undefined) {
@@ -118,18 +134,31 @@ export class PgFactStore {
118
134
  const streamed = await stream_fact_to_disk(this.#fs, this.#disk_root, stream, max_bytes, this.#embedded_threshold);
119
135
  const row_bytes = streamed.placement.kind === 'embedded' ? streamed.placement.bytes : null;
120
136
  const row_external_url = streamed.placement.kind === 'disk' ? streamed.placement.external_url : null;
121
- const inserted = await query_put_fact(this.#deps, {
137
+ await this.#insert_row({
122
138
  hash: streamed.hash,
123
139
  bytes: row_bytes,
124
140
  external_url: row_external_url,
125
- content_type: options?.content_type ?? null,
141
+ content_type: options.content_type ?? null,
126
142
  size: streamed.size
127
- });
128
- if (inserted && options?.refs && options.refs.length > 0) {
129
- await query_put_fact_refs(this.#deps, streamed.hash, options.refs);
130
- }
143
+ }, options.refs ?? [], options.holder);
131
144
  return { hash: streamed.hash, sha256: streamed.sha256, size: streamed.size };
132
145
  }
146
+ /**
147
+ * Insert the `fact` row, then — in the same transaction — its refs (fresh
148
+ * insert only) and the holder row (every put, a dedup hit included). Shared
149
+ * by `put` and `put_stream`; twin of the Rust `PgFactStore::insert_row`.
150
+ */
151
+ async #insert_row(row, refs, holder) {
152
+ await this.#deps.db.transaction(async (db) => {
153
+ const inserted = await query_put_fact({ db }, row);
154
+ if (inserted && refs.length > 0) {
155
+ await query_put_fact_refs({ db }, row.hash, refs);
156
+ }
157
+ if (holder !== null) {
158
+ await query_put_fact_holder({ db }, holder, row.hash);
159
+ }
160
+ });
161
+ }
133
162
  /**
134
163
  * Retrieve bytes. Embedded reads return PG bytes directly. An external
135
164
  * read comes from the disk CAS and nowhere else: the bytes at the row's
@@ -219,9 +248,9 @@ export class PgFactStore {
219
248
  * from JSON content; otherwise no refs.
220
249
  */
221
250
  const resolve_refs = (bytes, options) => {
222
- if (options?.refs !== undefined)
251
+ if (options.refs !== undefined)
223
252
  return options.refs;
224
- if (options?.content_type !== 'application/json')
253
+ if (options.content_type !== 'application/json')
225
254
  return [];
226
255
  let value;
227
256
  try {
@@ -41,6 +41,25 @@
41
41
  * hash. B publishing identical bytes from B's public cell makes them
42
42
  * readable *via B's cell* — it never touches A's private reference.
43
43
  *
44
+ * The edge is only as safe as what may create it. A cell's `refs` derive
45
+ * from its `data`, and any account that can write a cell writes its `data` —
46
+ * so this route alone would make a known hash a read capability: name it in
47
+ * a cell you own, flip the cell public, and the bytes serve to the world,
48
+ * while a 200 against a 404 says whether the fact exists. What closes that is
49
+ * **possession-gated ref admission** on the cell verbs
50
+ * (`auth/cell_ref_admission.ts`): a non-admin `cell_create` / `cell_update` /
51
+ * `cell_clone` may introduce a ref only to a fact its acting actor holds —
52
+ * one it put the bytes of, recorded in `fact_holder` on every put, a
53
+ * deduplicated put included — and is refused `403 cell_ref_not_held`
54
+ * otherwise, the same answer whether or not a fact is stored. Refs a cell
55
+ * already carries stay admitted through updates and clones, so a viewer who
56
+ * clones a cell carries its refs: what a viewer can read through a cell, it
57
+ * can republish — no more than downloading the bytes and uploading them
58
+ * again. A serve is a snapshot of the edge at read time; revoking someone's
59
+ * access to a cell does not withdraw refs they already wrote elsewhere.
60
+ * Writers that bypass the verbs (seeds, raw inserts) are trusted and not
61
+ * admitted.
62
+ *
44
63
  * The bare-hash route is **admin-only**: an admin's reach already spans
45
64
  * every cell, so serving by bare hash grants no escalation. Non-admin
46
65
  * callers are rejected at the auth phase and never reach the handler.
@@ -64,10 +83,11 @@
64
83
  * don't distinguish 403 from 404 — neither the existence of a fact nor the
65
84
  * existence of a cell→fact edge should leak through the public surface.
66
85
  *
67
- * Content-addressed serving of inline `blake3:` images (a markdown doc cell
68
- * with embedded image refs) works through this model: the referencing cell
69
- * is the doc cell, so serving goes view-doc-cell → doc-cell-refs-include-hash
70
- * → serve.
86
+ * A ref is a `data` string value that is exactly a fact hash
87
+ * (`derive_cell_refs`) — a hash inside prose or markup is not one. So a doc
88
+ * cell that shows images stores each image's hash as its own string value
89
+ * (an attachment list, say) beside the text that places them; serving then
90
+ * goes view-doc-cell → doc-cell-refs-include-hash → serve.
71
91
  *
72
92
  * ## Untrusted-content hardening
73
93
  *
@@ -1 +1 @@
1
- {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAetD,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAO/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkEjD,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAkHD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SAuDF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SA2CF,CAAC"}
1
+ {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkHG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAetD,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAO/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkEjD,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAkHD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SAuDF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SA2CF,CAAC"}
@@ -41,6 +41,25 @@
41
41
  * hash. B publishing identical bytes from B's public cell makes them
42
42
  * readable *via B's cell* — it never touches A's private reference.
43
43
  *
44
+ * The edge is only as safe as what may create it. A cell's `refs` derive
45
+ * from its `data`, and any account that can write a cell writes its `data` —
46
+ * so this route alone would make a known hash a read capability: name it in
47
+ * a cell you own, flip the cell public, and the bytes serve to the world,
48
+ * while a 200 against a 404 says whether the fact exists. What closes that is
49
+ * **possession-gated ref admission** on the cell verbs
50
+ * (`auth/cell_ref_admission.ts`): a non-admin `cell_create` / `cell_update` /
51
+ * `cell_clone` may introduce a ref only to a fact its acting actor holds —
52
+ * one it put the bytes of, recorded in `fact_holder` on every put, a
53
+ * deduplicated put included — and is refused `403 cell_ref_not_held`
54
+ * otherwise, the same answer whether or not a fact is stored. Refs a cell
55
+ * already carries stay admitted through updates and clones, so a viewer who
56
+ * clones a cell carries its refs: what a viewer can read through a cell, it
57
+ * can republish — no more than downloading the bytes and uploading them
58
+ * again. A serve is a snapshot of the edge at read time; revoking someone's
59
+ * access to a cell does not withdraw refs they already wrote elsewhere.
60
+ * Writers that bypass the verbs (seeds, raw inserts) are trusted and not
61
+ * admitted.
62
+ *
44
63
  * The bare-hash route is **admin-only**: an admin's reach already spans
45
64
  * every cell, so serving by bare hash grants no escalation. Non-admin
46
65
  * callers are rejected at the auth phase and never reach the handler.
@@ -64,10 +83,11 @@
64
83
  * don't distinguish 403 from 404 — neither the existence of a fact nor the
65
84
  * existence of a cell→fact edge should leak through the public surface.
66
85
  *
67
- * Content-addressed serving of inline `blake3:` images (a markdown doc cell
68
- * with embedded image refs) works through this model: the referencing cell
69
- * is the doc cell, so serving goes view-doc-cell → doc-cell-refs-include-hash
70
- * → serve.
86
+ * A ref is a `data` string value that is exactly a fact hash
87
+ * (`derive_cell_refs`) — a hash inside prose or markup is not one. So a doc
88
+ * cell that shows images stores each image's hash as its own string value
89
+ * (an attachment list, say) beside the text that places them; serving then
90
+ * goes view-doc-cell → doc-cell-refs-include-hash → serve.
71
91
  *
72
92
  * ## Untrusted-content hardening
73
93
  *
@@ -968,7 +968,9 @@ source of truth for wire-shape conformance.
968
968
  gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference +
969
969
  admin-only bare-hash fact-serving parity suite); like cells it stays off the
970
970
  declared surface and is `true` on every spine that mounts the serve routes +
971
- the `_testing_put_fact` seeder. `ready` gates `describe_ready_cross_tests`
971
+ the `_testing_put_fact` seeder (whose optional `holder` records a
972
+ `fact_holder` row, so a non-admin can reference what it seeds) — it also
973
+ gates `describe_cell_ref_admission_cross_tests`. `ready` gates `describe_ready_cross_tests`
972
974
  (anonymous `GET /ready` → `200 {ready: true}` on a clean spine bootstrap);
973
975
  like cells/sse the `/ready` deploy gate stays off the declared surface, `true`
974
976
  on every spine that live-mounts it over the shared `expected_schema.json`.
@@ -1640,6 +1642,28 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1640
1642
  complete action list plus its own spec. Daemon-token-gated like its siblings;
1641
1643
  the Rust mirror is `fuz_testing::create_testing_action_manifest_action_spec`.
1642
1644
 
1645
+ ### Cell ref admission parity — `cross_backend/cell_ref_admission.ts`
1646
+
1647
+ `describe_cell_ref_admission_cross_tests({setup_test, capabilities, rpc_path?})`
1648
+ — possession-gated ref admission over real HTTP: a non-admin `cell_create`
1649
+ naming a stored fact it never put and one naming an unstored hash get one
1650
+ identical `403 cell_ref_not_held` (status + error body); a system-put fact is
1651
+ refused too; the uploader's public cell serves the bytes to anyone; a second
1652
+ actor putting identical bytes becomes a holder; an editor keeps refs another
1653
+ writer introduced and is refused adding (or restoring a dropped) unheld one; a
1654
+ shallow and a deep clone carry each row's source refs, while a patch adding an
1655
+ unheld ref or moving a child's ref onto the root is refused; the keeper (a
1656
+ global admin) is ungated; a grantee who read a fact through a granted cell
1657
+ can't name its hash in a cell of its own, before or after the revoke; the ref
1658
+ grammar is exact (prose, markup, `blake3:zz`, uppercase, object keys are not
1659
+ refs); and a hidden cell's update, a create under a hidden parent, and a clone
1660
+ of a hidden source answer `404` whatever `data` names. Facts are put through
1661
+ `cross_put_fact` (exported by `fact_serving.ts`), whose `holder` maps onto
1662
+ `_testing_put_fact`'s optional `holder` key. Gated on
1663
+ `capabilities.fact_serving`; cross-process only. fuz_app's own wiring is
1664
+ `src/test/cross_backend/cell_ref_admission.cross.test.ts`; the in-process
1665
+ cases are `src/test/auth/cell_ref_admission{,.fact_less}.db.test.ts`.
1666
+
1643
1667
  ### Token-scope surface parity — `cross_backend/token_scope_surface.ts`
1644
1668
 
1645
1669
  `describe_token_scope_surface_cross_tests({setup_test, capabilities, rpc_path?,
@@ -1 +1 @@
1
- {"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IA6fF,CAAC"}
1
+ {"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IAggBF,CAAC"}
@@ -100,14 +100,17 @@ export const describe_cell_crud_cross_tests = (options) => {
100
100
  });
101
101
  test_if(capabilities.cell_crud, 'refs are deduped and lexicographically sorted (deterministic, cross-impl-identical)', async () => {
102
102
  const fixture = await setup_test();
103
- const owner = await fixture.create_account({ username: 'cell_refs_owner' });
104
103
  const t = fixture.fresh_transport();
105
- const owner_headers = owner.create_session_headers();
104
+ // The keeper — a global admin, whom ref admission doesn't gate — so
105
+ // fabricated hashes nobody holds are writable refs (a non-admin
106
+ // writing them is `cell_ref_not_held`, the admission suite's
107
+ // subject), and the case runs on backends without the fact chain.
108
+ const owner_headers = fixture.create_session_headers();
106
109
  // Three valid distinct fact hashes that sort a < b < c, embedded in
107
110
  // `data` in a deliberately unsorted, nested, duplicated order. Both
108
111
  // spines must derive `refs` as the sorted [a, b, c] regardless of
109
112
  // traversal order or `HashSet` iteration — the byte-identical
110
- // cross-backend contract (Rust `extract_refs` ↔ TS `derive_refs`).
113
+ // cross-backend contract (Rust `extract_refs` ↔ TS `derive_cell_refs`).
111
114
  const ref_a = `blake3:${'a'.repeat(64)}`;
112
115
  const ref_b = `blake3:${'b'.repeat(64)}`;
113
116
  const ref_c = `blake3:${'c'.repeat(64)}`;
@@ -1 +1 @@
1
- {"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoE9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAqB1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAuTF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,IA25BF,CAAC"}
1
+ {"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoE9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAqB1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAuTF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,IAs6BF,CAAC"}
@@ -241,37 +241,48 @@ export const describe_cell_gated_create_cross_tests = (options) => {
241
241
  export const describe_cell_moderate_cross_tests = (options) => {
242
242
  const { setup_test, capabilities } = options;
243
243
  const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
244
+ // Seed `content` as an embedded fact the author holds (as their upload
245
+ // would) — a non-admin cell may reference only a fact its writer holds —
246
+ // and return the post `data` referencing it, `{}` for no fact.
247
+ const author_post_data = async (fixture, author, post_fact_content) => {
248
+ if (post_fact_content === undefined)
249
+ return { post_data: {}, hash: null };
250
+ const { hash } = expect_output(await cross_rpc_call(
251
+ // origin-free — the daemon-token middleware discards the credential beside an `Origin`
252
+ fixture.fresh_transport({ origin: null }), rpc_path, '_testing_put_fact', { content: post_fact_content, holder: author.actor.id }, fixture.create_daemon_token_headers()), TestingPutFactOutput);
253
+ return { post_data: { cover: hash }, hash };
254
+ };
244
255
  // Build an admin-owned public space (moderated `post` policy) + a
245
- // participant's pending post under it.
246
- const setup_pending_post_on = async (fixture, post_data = {}) => {
256
+ // participant's pending post under it, optionally carrying a fact the
257
+ // author put.
258
+ const setup_pending_post_on = async (fixture, post_fact_content) => {
247
259
  const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
248
260
  const author = await fixture.create_account({
249
261
  username: 'moderate_author',
250
262
  roles: [PARTICIPATION_ROLE]
251
263
  });
264
+ const { post_data, hash } = await author_post_data(fixture, author, post_fact_content);
252
265
  const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: post_data, parent_id: space.cell.id }, author.create_session_headers()), CellCreateOutput);
253
- return { fixture, space, author, post };
266
+ return { fixture, space, author, post, hash };
254
267
  };
255
268
  const setup_pending_post = async () => setup_pending_post_on(await setup_test());
256
269
  // A non-admin's policy root (a parentless non-`space` kind stays open under
257
270
  // the test authorizer, and contributions read the policy off any root) + a
258
- // participant's pending post under it.
259
- const setup_stewarded_post_on = async (fixture, post_data = {}) => {
271
+ // participant's pending post under it, optionally carrying a fact the author
272
+ // put.
273
+ const setup_stewarded_post_on = async (fixture, post_fact_content) => {
260
274
  const root_owner = await fixture.create_account({ username: 'steward_root_owner' });
261
275
  const root = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'forum', data: SPACE_POLICY, visibility: 'public' }, root_owner.create_session_headers()), CellCreateOutput);
262
276
  const author = await fixture.create_account({
263
277
  username: 'steward_author',
264
278
  roles: [PARTICIPATION_ROLE]
265
279
  });
280
+ const { post_data, hash } = await author_post_data(fixture, author, post_fact_content);
266
281
  const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: post_data, parent_id: root.cell.id }, author.create_session_headers()), CellCreateOutput);
267
282
  assert.strictEqual(post.cell.moderation, 'pending', "the root's policy gates the post");
268
- return { fixture, root, root_owner, author, post };
283
+ return { fixture, root, root_owner, author, post, hash };
269
284
  };
270
285
  const setup_stewarded_post = async () => setup_stewarded_post_on(await setup_test());
271
- /** Seed an embedded fact over the daemon channel; returns its `blake3:` hash. */
272
- const put_fact = async (fixture, content) => expect_output(await cross_rpc_call(
273
- // origin-free — the daemon-token middleware discards the credential beside an `Origin`
274
- fixture.fresh_transport({ origin: null }), rpc_path, '_testing_put_fact', { content }, fixture.create_daemon_token_headers()), TestingPutFactOutput).hash;
275
286
  /** The status of a fact GET — `null` headers = anon. */
276
287
  const fact_status = async (fixture, path, headers) => {
277
288
  const transport = headers === null ? fixture.fresh_transport({ origin: null }) : fixture.transport;
@@ -507,9 +518,8 @@ export const describe_cell_moderate_cross_tests = (options) => {
507
518
  assert.deepStrictEqual(await reach(author_h), everywhere, 'author, rejected');
508
519
  });
509
520
  test_if(capabilities.cell_gated_create && capabilities.fact_serving, "a pending post's fact is 404 to anon, even flipped public; live once approved", async () => {
510
- const fixture = await setup_test();
511
- const hash = await put_fact(fixture, 'pending-post-fact');
512
- const { author, post } = await setup_pending_post_on(fixture, { cover: hash });
521
+ const { fixture, author, post, hash } = await setup_pending_post_on(await setup_test(), 'pending-post-fact');
522
+ assert.ok(hash !== null);
513
523
  await set_visibility(fixture, author.create_session_headers(), post.cell.id, 'public');
514
524
  const path = `/api/cells/${post.cell.id}/facts/${hash}`;
515
525
  assert.strictEqual(await fact_status(fixture, path, null), 404, 'anon');
@@ -569,9 +579,8 @@ export const describe_cell_moderate_cross_tests = (options) => {
569
579
  assert.strictEqual(await outcome(fixture, 'cell_get', get, null), 'cell_not_found', 'anon');
570
580
  });
571
581
  test_if(capabilities.cell_gated_create && capabilities.fact_serving, "the root owner fetches a pending post's fact; anon 404", async () => {
572
- const fixture = await setup_test();
573
- const hash = await put_fact(fixture, 'stewarded-post-fact');
574
- const { root_owner, post } = await setup_stewarded_post_on(fixture, { cover: hash });
582
+ const { fixture, root_owner, post, hash } = await setup_stewarded_post_on(await setup_test(), 'stewarded-post-fact');
583
+ assert.ok(hash !== null);
575
584
  const path = `/api/cells/${post.cell.id}/facts/${hash}`;
576
585
  assert.strictEqual(await fact_status(fixture, path, null), 404, 'anon');
577
586
  assert.strictEqual(await fact_status(fixture, path, root_owner.create_session_headers()), 200, 'steward');
@@ -0,0 +1,4 @@
1
+ import '../assert_dev_env.js';
2
+ import type { RpcPathCapabilityGatedCrossSuiteOptions } from './setup.js';
3
+ export declare const describe_cell_ref_admission_cross_tests: (options: RpcPathCapabilityGatedCrossSuiteOptions) => void;
4
+ //# sourceMappingURL=cell_ref_admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_ref_admission.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_ref_admission.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AA6B1E,eAAO,MAAM,uCAAuC,GACnD,SAAS,uCAAuC,KAC9C,IA8eF,CAAC"}