@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.
- package/dist/auth/CLAUDE.md +8 -0
- package/dist/auth/cell_action_specs.d.ts +13 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +13 -0
- package/dist/auth/cell_actions.d.ts +7 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +30 -0
- package/dist/auth/cell_ref_admission.d.ts +51 -0
- package/dist/auth/cell_ref_admission.d.ts.map +1 -0
- package/dist/auth/cell_ref_admission.js +69 -0
- package/dist/auth/deps.d.ts +6 -3
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/db/CLAUDE.md +34 -21
- package/dist/db/cell_ddl.d.ts +3 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +3 -2
- package/dist/db/cell_queries.d.ts +31 -6
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +45 -18
- package/dist/db/fact_ddl.d.ts +29 -4
- package/dist/db/fact_ddl.d.ts.map +1 -1
- package/dist/db/fact_ddl.js +43 -4
- package/dist/db/fact_holder_queries.d.ts +66 -0
- package/dist/db/fact_holder_queries.d.ts.map +1 -0
- package/dist/db/fact_holder_queries.js +75 -0
- package/dist/db/fact_queries.d.ts +9 -7
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_store.d.ts +65 -9
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +49 -20
- package/dist/server/serve_fact_route.d.ts +24 -4
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +24 -4
- package/dist/testing/CLAUDE.md +25 -1
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +6 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +25 -16
- package/dist/testing/cross_backend/cell_ref_admission.d.ts +4 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_ref_admission.js +268 -0
- package/dist/testing/cross_backend/expected_schema.json +66 -10
- package/dist/testing/cross_backend/fact_serving.d.ts +19 -1
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +31 -9
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +6 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +13 -3
- package/package.json +1 -1
package/dist/db/fact_store.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PG-backed `
|
|
2
|
+
* PG-backed `HeldFactStore` implementation.
|
|
3
3
|
*
|
|
4
4
|
* Wraps the raw queries in `db/fact_queries.ts` with the lifecycle the
|
|
5
|
-
* `
|
|
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
|
|
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
|
-
|
|
102
|
+
await this.#insert_row({
|
|
86
103
|
hash,
|
|
87
104
|
bytes: row_bytes,
|
|
88
105
|
external_url: row_external_url,
|
|
89
|
-
content_type: options
|
|
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
|
-
|
|
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
|
|
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
|
|
251
|
+
if (options.refs !== undefined)
|
|
223
252
|
return options.refs;
|
|
224
|
-
if (options
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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
|
*
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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 `
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"}
|