@fuzdev/fuz_app 0.94.0 → 0.96.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 (39) hide show
  1. package/dist/auth/account_queries.d.ts +20 -5
  2. package/dist/auth/account_queries.d.ts.map +1 -1
  3. package/dist/auth/account_queries.js +21 -4
  4. package/dist/auth/request_context.js +2 -2
  5. package/dist/db/CLAUDE.md +6 -3
  6. package/dist/db/cell_queries.js +8 -1
  7. package/dist/db/create_db.d.ts.map +1 -1
  8. package/dist/db/create_db.js +2 -14
  9. package/dist/db/db_pg.d.ts +30 -2
  10. package/dist/db/db_pg.d.ts.map +1 -1
  11. package/dist/db/db_pg.js +33 -2
  12. package/dist/db/fact_queries.d.ts.map +1 -1
  13. package/dist/server/serve_fact_route.d.ts +12 -0
  14. package/dist/server/serve_fact_route.d.ts.map +1 -1
  15. package/dist/server/serve_fact_route.js +40 -0
  16. package/dist/testing/CLAUDE.md +38 -4
  17. package/dist/testing/cross_backend/capabilities.d.ts +25 -4
  18. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  19. package/dist/testing/cross_backend/capabilities.js +9 -4
  20. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  21. package/dist/testing/cross_backend/cell_crud.js +23 -0
  22. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  23. package/dist/testing/cross_backend/default_backend_configs.js +7 -0
  24. package/dist/testing/cross_backend/peer_ping_ws.d.ts +23 -0
  25. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -0
  26. package/dist/testing/cross_backend/peer_ping_ws.js +209 -0
  27. package/dist/testing/db.d.ts.map +1 -1
  28. package/dist/testing/db.js +6 -1
  29. package/dist/testing/middleware.js +4 -4
  30. package/dist/testing/transports/ws_client.d.ts +40 -1
  31. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  32. package/dist/testing/transports/ws_client.js +46 -1
  33. package/dist/testing/transports/ws_transport.d.ts +10 -1
  34. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  35. package/dist/testing/transports/ws_transport.js +6 -10
  36. package/dist/testing/ws_round_trip.d.ts +11 -1
  37. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  38. package/dist/testing/ws_round_trip.js +14 -10
  39. package/package.json +6 -2
@@ -159,14 +159,29 @@ export declare const query_account_has_any: (deps: QueryDeps) => Promise<boolean
159
159
  */
160
160
  export declare const query_create_actor: (deps: QueryDeps, account_id: string, name: string) => Promise<Actor>;
161
161
  /**
162
- * List every actor on an account, ordered by `created_at`.
163
- *
164
- * Used by `resolve_acting_actor` to resolve the acting actor for a
165
- * request: 1 actor picks transparently, multiple require an explicit
166
- * `acting` field on the request payload. For lookups by id, use
162
+ * List every actor on an account, ordered by `created_at`, **including
163
+ * soft-deleted (tombstoned) rows**.
164
+ *
165
+ * Used by the admin/snapshot handlers (`account_delete` / `account_purge` /
166
+ * `account_undelete`) that must enumerate every actor that ever existed on
167
+ * the account to snapshot names into per-actor audit events. For the
168
+ * acting-actor resolution path — which must never resolve a tombstoned
169
+ * actor — use `query_active_actors_by_account`. For lookups by id, use
167
170
  * `query_actor_by_id` instead.
168
171
  */
169
172
  export declare const query_actors_by_account: (deps: QueryDeps, account_id: string) => Promise<Array<Actor>>;
173
+ /**
174
+ * List **active** (non-tombstoned) actors on an account, ordered by
175
+ * `created_at`.
176
+ *
177
+ * Filters `deleted_at IS NULL` so a soft-deleted actor can never be resolved
178
+ * as the acting actor (and carry its `role_grants` into the role gate). Used
179
+ * by `resolve_acting_actor` to resolve the acting actor for a request: 1
180
+ * actor picks transparently, multiple require an explicit `acting` field on
181
+ * the request payload. The admin/snapshot handlers that legitimately need
182
+ * tombstoned rows stay on the unfiltered `query_actors_by_account`.
183
+ */
184
+ export declare const query_active_actors_by_account: (deps: QueryDeps, account_id: string) => Promise<Array<Actor>>;
170
185
  /**
171
186
  * Find an actor by id.
172
187
  */
@@ -1 +1 @@
1
- {"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"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"}
@@ -218,16 +218,33 @@ export const query_create_actor = async (deps, account_id, name) => {
218
218
  return assert_row(row, 'INSERT INTO actor');
219
219
  };
220
220
  /**
221
- * List every actor on an account, ordered by `created_at`.
221
+ * List every actor on an account, ordered by `created_at`, **including
222
+ * soft-deleted (tombstoned) rows**.
222
223
  *
223
- * Used by `resolve_acting_actor` to resolve the acting actor for a
224
- * request: 1 actor picks transparently, multiple require an explicit
225
- * `acting` field on the request payload. For lookups by id, use
224
+ * Used by the admin/snapshot handlers (`account_delete` / `account_purge` /
225
+ * `account_undelete`) that must enumerate every actor that ever existed on
226
+ * the account to snapshot names into per-actor audit events. For the
227
+ * acting-actor resolution path — which must never resolve a tombstoned
228
+ * actor — use `query_active_actors_by_account`. For lookups by id, use
226
229
  * `query_actor_by_id` instead.
227
230
  */
228
231
  export const query_actors_by_account = async (deps, account_id) => {
229
232
  return deps.db.query(`SELECT * FROM actor WHERE account_id = $1 ORDER BY created_at ASC, id ASC`, [account_id]);
230
233
  };
234
+ /**
235
+ * List **active** (non-tombstoned) actors on an account, ordered by
236
+ * `created_at`.
237
+ *
238
+ * Filters `deleted_at IS NULL` so a soft-deleted actor can never be resolved
239
+ * as the acting actor (and carry its `role_grants` into the role gate). Used
240
+ * by `resolve_acting_actor` to resolve the acting actor for a request: 1
241
+ * actor picks transparently, multiple require an explicit `acting` field on
242
+ * the request payload. The admin/snapshot handlers that legitimately need
243
+ * tombstoned rows stay on the unfiltered `query_actors_by_account`.
244
+ */
245
+ export const query_active_actors_by_account = async (deps, account_id) => {
246
+ return deps.db.query(`SELECT * FROM actor WHERE account_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC, id ASC`, [account_id]);
247
+ };
231
248
  /**
232
249
  * Find an actor by id.
233
250
  */
@@ -36,7 +36,7 @@
36
36
  */
37
37
  import { is_role_grant_active } from './account_schema.js';
38
38
  import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
39
- import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from './account_queries.js';
39
+ import { query_account_by_id, query_actor_by_id, query_active_actors_by_account, } from './account_queries.js';
40
40
  import { query_role_grant_find_active_for_actor } from './role_grant_queries.js';
41
41
  import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
42
42
  import { is_public_auth, needs_actor } from '../http/auth_shape.js';
@@ -174,7 +174,7 @@ export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
174
174
  * @param acting_actor_id - the requested acting actor id, or `undefined`
175
175
  */
176
176
  export const resolve_acting_actor = async (deps, account_id, acting_actor_id) => {
177
- const actors = await query_actors_by_account(deps, account_id);
177
+ const actors = await query_active_actors_by_account(deps, account_id);
178
178
  if (actors.length === 0)
179
179
  return { ok: false, reason: 'no_actors' };
180
180
  if (acting_actor_id == null) {
package/dist/db/CLAUDE.md CHANGED
@@ -15,9 +15,12 @@ the code.
15
15
  - `db.ts` — `Db` abstraction over pg + PGlite, `query` / `query_one` /
16
16
  `transaction`, `no_nested_transaction` guard.
17
17
  - `create_db.ts` — URL-based driver auto-detection (`postgres` /
18
- `pglite-file` / `pglite-memory`); registers the int8 parser so
19
- `BIGSERIAL` columns read as JS numbers.
20
- - `db_pg.ts` / `db_pglite.ts` — driver adapters.
18
+ `pglite-file` / `pglite-memory`).
19
+ - `db_pg.ts` / `db_pglite.ts` — driver adapters. `db_pg.ts` also exports
20
+ `register_pg_type_parsers`, the shared int8→number coercion (so
21
+ `BIGSERIAL` columns read as JS numbers, matching PGlite) — called by both
22
+ `create_db` and the test pg factory (`testing/db.ts`) so prod and tests
23
+ read the same wire shape from one registration site.
21
24
  - `migrate.ts` — advisory-lock migration runner (`run_migrations`,
22
25
  `baseline`), `Migration` / `MigrationNamespace` / `MigrationError`.
23
26
  - `query_deps.ts` — `QueryDeps = {db}`, the first param to every `query_*`.
@@ -394,8 +394,15 @@ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_
394
394
  * Returns `null` (rather than `[]`) when no refs are present — the column
395
395
  * is nullable and the `idx_cell_refs` partial index is `WHERE refs IS NOT
396
396
  * NULL`, so an empty array would force every cell into the index.
397
+ *
398
+ * Sorts the deduped refs lexicographically so the stored `text[]` is
399
+ * deterministic and byte-identical to the Rust twin (`fuz_cell`'s
400
+ * `extract_refs`, which sorts the same `blake3:`-prefixed element form).
401
+ * The shared `fact_hash_extract_refs` deliberately preserves traversal
402
+ * order per its contract, so the sort lives here at the storage boundary,
403
+ * not in the util.
397
404
  */
398
405
  const derive_refs = (data) => {
399
406
  const refs = fact_hash_extract_refs(data);
400
- return refs.length > 0 ? refs : null;
407
+ return refs.length > 0 ? refs.sort() : null;
401
408
  };
@@ -1 +1 @@
1
- {"version":3,"file":"create_db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/create_db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAIxC,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP,iFAAiF;IACjF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,cAAc,CA6C5E,CAAC"}
1
+ {"version":3,"file":"create_db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/create_db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAIxC,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP,iFAAiF;IACjF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,cAAc,CAiC5E,CAAC"}
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * @module
14
14
  */
15
- import { create_pg_db } from './db_pg.js';
15
+ import { create_pg_db, register_pg_type_parsers } from './db_pg.js';
16
16
  import { create_pglite_db } from './db_pglite.js';
17
17
  /**
18
18
  * Create a database connection based on a URL.
@@ -32,19 +32,7 @@ import { create_pglite_db } from './db_pglite.js';
32
32
  export const create_db = async (database_url) => {
33
33
  if (database_url.startsWith('postgres://') || database_url.startsWith('postgresql://')) {
34
34
  const { default: pg } = await import('pg');
35
- // Parse int8 (BIGINT) as a JS number. pg defaults to returning int8
36
- // as a string to avoid 2^53 precision loss; our only int8 column
37
- // today (`audit_log.seq`) stays well under that bound, and reading
38
- // as number keeps the wire shape uniform across the SERIAL→BIGSERIAL
39
- // widening.
40
- //
41
- // CAVEAT: pg.types.setTypeParser mutates pg.types globally — every
42
- // pg.Pool in the process inherits the coercion, including pools the
43
- // consumer constructs against unrelated databases. Any future int8
44
- // column that could legitimately exceed 2^53 (file sizes, byte
45
- // offsets) will silently round; if one lands, localize via a
46
- // per-pool `types` override instead of widening this global parser.
47
- pg.types.setTypeParser(20, (val) => Number(val));
35
+ await register_pg_type_parsers();
48
36
  const pool = new pg.Pool({ connectionString: database_url });
49
37
  const { db, close } = create_pg_db(pool);
50
38
  return {
@@ -2,13 +2,41 @@
2
2
  * PostgreSQL driver adapter for `Db`.
3
3
  *
4
4
  * Provides `create_pg_db()` to construct a `Db` backed by a `pg.Pool`.
5
- * Only imports `pg` types — the actual `pg` package is dynamically imported
6
- * by callers (e.g., `create_db`) that construct the `Pool`.
5
+ * Statically imports only `pg` types; the `pg` runtime is dynamically
6
+ * imported where needed — by callers (e.g., `create_db`) that construct the
7
+ * `Pool`, and by `register_pg_type_parsers` to reach `pg.types`. pglite-only
8
+ * consumers never reach either path, so `pg` stays an optional peer dep.
7
9
  *
8
10
  * @module
9
11
  */
10
12
  import type { Pool } from 'pg';
11
13
  import { type DbDriverResult } from './db.js';
14
+ /**
15
+ * Register the shared pg type-parser overrides on the module-global `pg.types`.
16
+ *
17
+ * Dynamically imports the `pg` runtime (so pglite-only consumers, who never
18
+ * call this, don't need `pg` installed) and coerces int8 (`BIGINT`, OID 20)
19
+ * to a JS number. pg defaults to returning
20
+ * int8 as a string to avoid 2^53 precision loss; our int8 columns today
21
+ * (`audit_log.seq`, `cell_history.id`, `fact.size`) stay well under that
22
+ * bound, and reading as a number keeps the wire shape uniform with PGlite —
23
+ * which returns int8 as a number — so `AuditLogEvent.seq` and friends
24
+ * validate identically across both drivers.
25
+ *
26
+ * Both `create_db` (production) and the test pg factory (`testing/db.ts`)
27
+ * register through this single site so test and prod read the same shape; a
28
+ * divergence here is exactly the test/prod write-semantics gap the parser
29
+ * exists to close.
30
+ *
31
+ * CAVEAT: `setTypeParser` mutates `pg.types` globally — every `pg.Pool` in
32
+ * the process inherits the coercion, including pools the consumer constructs
33
+ * against unrelated databases. Any future int8 column that could legitimately
34
+ * exceed 2^53 (byte offsets, counters) will silently round; if one lands,
35
+ * localize via a per-pool `types` override instead of widening this global.
36
+ *
37
+ * @mutates `pg.types` - registers the int8 parser on the global pg type registry
38
+ */
39
+ export declare const register_pg_type_parsers: () => Promise<void>;
12
40
  /**
13
41
  * Create a `Db` backed by a `pg.Pool`.
14
42
  *
@@ -1 +1 @@
1
- {"version":3,"file":"db_pg.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/db_pg.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,IAAI,CAAC;AAE7B,OAAO,EAA4B,KAAK,cAAc,EAAC,MAAM,SAAS,CAAC;AA2BvE;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,IAAI,KAAG,cAGxC,CAAC"}
1
+ {"version":3,"file":"db_pg.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/db_pg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,IAAI,CAAC;AAE7B,OAAO,EAA4B,KAAK,cAAc,EAAC,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,IAAI,CAG7D,CAAC;AA2BF;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,IAAI,KAAG,cAGxC,CAAC"}
package/dist/db/db_pg.js CHANGED
@@ -2,12 +2,43 @@
2
2
  * PostgreSQL driver adapter for `Db`.
3
3
  *
4
4
  * Provides `create_pg_db()` to construct a `Db` backed by a `pg.Pool`.
5
- * Only imports `pg` types — the actual `pg` package is dynamically imported
6
- * by callers (e.g., `create_db`) that construct the `Pool`.
5
+ * Statically imports only `pg` types; the `pg` runtime is dynamically
6
+ * imported where needed — by callers (e.g., `create_db`) that construct the
7
+ * `Pool`, and by `register_pg_type_parsers` to reach `pg.types`. pglite-only
8
+ * consumers never reach either path, so `pg` stays an optional peer dep.
7
9
  *
8
10
  * @module
9
11
  */
10
12
  import { Db, no_nested_transaction } from './db.js';
13
+ /**
14
+ * Register the shared pg type-parser overrides on the module-global `pg.types`.
15
+ *
16
+ * Dynamically imports the `pg` runtime (so pglite-only consumers, who never
17
+ * call this, don't need `pg` installed) and coerces int8 (`BIGINT`, OID 20)
18
+ * to a JS number. pg defaults to returning
19
+ * int8 as a string to avoid 2^53 precision loss; our int8 columns today
20
+ * (`audit_log.seq`, `cell_history.id`, `fact.size`) stay well under that
21
+ * bound, and reading as a number keeps the wire shape uniform with PGlite —
22
+ * which returns int8 as a number — so `AuditLogEvent.seq` and friends
23
+ * validate identically across both drivers.
24
+ *
25
+ * Both `create_db` (production) and the test pg factory (`testing/db.ts`)
26
+ * register through this single site so test and prod read the same shape; a
27
+ * divergence here is exactly the test/prod write-semantics gap the parser
28
+ * exists to close.
29
+ *
30
+ * CAVEAT: `setTypeParser` mutates `pg.types` globally — every `pg.Pool` in
31
+ * the process inherits the coercion, including pools the consumer constructs
32
+ * against unrelated databases. Any future int8 column that could legitimately
33
+ * exceed 2^53 (byte offsets, counters) will silently round; if one lands,
34
+ * localize via a per-pool `types` override instead of widening this global.
35
+ *
36
+ * @mutates `pg.types` - registers the int8 parser on the global pg type registry
37
+ */
38
+ export const register_pg_type_parsers = async () => {
39
+ const { types } = await import('pg');
40
+ types.setTypeParser(20, (val) => Number(val));
41
+ };
11
42
  /**
12
43
  * Create a transaction implementation for a `pg.Pool`.
13
44
  *
@@ -1 +1 @@
1
- {"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE/C,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,+BAA+B,CAAC;AAE5D,0CAA0C;AAC1C,MAAM,WAAW,OAAO;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAC1B,MAAM,SAAS,EACf,OAAO;IACN,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACb,KACC,OAAO,CAAC,OAAO,CASjB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,aAAa,QAAQ,EACrB,eAAe,KAAK,CAAC,QAAQ,CAAC,KAC5B,OAAO,CAAC,IAAI,CAQd,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM,SAAS,EAAE,MAAM,QAAQ,KAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAO5F,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,MAAM,QAAQ,KACZ,OAAO,CAAC,WAAW,GAAG,IAAI,CAO5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM,SAAS,EAAE,MAAM,QAAQ,KAAG,OAAO,CAAC,OAAO,CAMrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,aAAa,QAAQ,KACnB,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAMzB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,SAAS,EACf,MAAM,QAAQ,KACZ,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,GAAG,IAAI,CAQ5D,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,QAAQ,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,IAAI,GAAG,IAAI,EACvB,cAAc,MAAM,KAClB,OAAO,CAAC,qBAAqB,CAwC/B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,IAAI,KACd,OAAO,CAAC,KAAK,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,CAAC,CAiB5E,CAAC"}
1
+ {"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"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"}
@@ -63,6 +63,18 @@
63
63
  * is the doc cell, so serving goes view-doc-cell → doc-cell-refs-include-hash
64
64
  * → serve.
65
65
  *
66
+ * ## Untrusted-content hardening
67
+ *
68
+ * Fact bytes are served with a producer-supplied `content_type` emitted
69
+ * verbatim, inline. To stop a fact stored as (or sniffable to) `text/html`
70
+ * from executing as stored XSS for any reader of a referencing cell, **every**
71
+ * served-fact response carries `X-Content-Type-Options: nosniff` +
72
+ * `Content-Security-Policy: default-src 'none'; sandbox` (the
73
+ * `FACT_SECURITY_HEADERS` set, applied in all three serve branches). The same
74
+ * pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
75
+ * `Content-Disposition: attachment` is deliberately **not** added — it would
76
+ * force-download and break legitimate inline image rendering.
77
+ *
66
78
  * ## Defense-in-depth
67
79
  *
68
80
  * The `external_url` regex is re-validated before issuing
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AAMH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAepD,OAAO,EAAsC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAsC7C,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;AAwGD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SA8CF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SAgCF,CAAC"}
1
+ {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAMH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAepD,OAAO,EAAsC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAkE7C,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;AA2GD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SA8CF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SAgCF,CAAC"}
@@ -63,6 +63,18 @@
63
63
  * is the doc cell, so serving goes view-doc-cell → doc-cell-refs-include-hash
64
64
  * → serve.
65
65
  *
66
+ * ## Untrusted-content hardening
67
+ *
68
+ * Fact bytes are served with a producer-supplied `content_type` emitted
69
+ * verbatim, inline. To stop a fact stored as (or sniffable to) `text/html`
70
+ * from executing as stored XSS for any reader of a referencing cell, **every**
71
+ * served-fact response carries `X-Content-Type-Options: nosniff` +
72
+ * `Content-Security-Policy: default-src 'none'; sandbox` (the
73
+ * `FACT_SECURITY_HEADERS` set, applied in all three serve branches). The same
74
+ * pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
75
+ * `Content-Disposition: attachment` is deliberately **not** added — it would
76
+ * force-download and break legitimate inline image rendering.
77
+ *
66
78
  * ## Defense-in-depth
67
79
  *
68
80
  * The `external_url` regex is re-validated before issuing
@@ -92,6 +104,31 @@ import { can_view_cell } from '../auth/cell_authorize.js';
92
104
  import { parse_file_fact_url } from '../db/file_fact_url.js';
93
105
  /** `Cache-Control` for fact responses — 5 min revocation window. */
94
106
  const CACHE_CONTROL = 'private, max-age=300';
107
+ /**
108
+ * `X-Content-Type-Options` for fact responses — block MIME sniffing so an
109
+ * `application/octet-stream` fact can't be sniffed into executable HTML.
110
+ */
111
+ const X_CONTENT_TYPE_OPTIONS = 'nosniff';
112
+ /**
113
+ * `Content-Security-Policy` for fact responses. Fact bytes carry a
114
+ * producer-supplied `content_type` served verbatim inline, so a fact stored as
115
+ * (or sniffable to) `text/html` would otherwise execute as stored XSS for any
116
+ * reader of a referencing cell. `default-src 'none'; sandbox` neutralizes
117
+ * script execution and sub-resource loads even when a fact is rendered inline;
118
+ * harmless for directly-served images (they load no sub-resources). The same
119
+ * pair, byte-identical, is set by the Rust twin (`fuz_fact_serving::serve`).
120
+ */
121
+ const CONTENT_SECURITY_POLICY = "default-src 'none'; sandbox";
122
+ /**
123
+ * The untrusted-content hardening headers applied to every served-fact
124
+ * response across all three branches (embedded, `X-Accel-Redirect`,
125
+ * disk-stream). `Content-Disposition: attachment` is deliberately omitted — it
126
+ * would force-download and break legitimate inline image rendering.
127
+ */
128
+ const FACT_SECURITY_HEADERS = {
129
+ 'X-Content-Type-Options': X_CONTENT_TYPE_OPTIONS,
130
+ 'Content-Security-Policy': CONTENT_SECURITY_POLICY,
131
+ };
95
132
  /**
96
133
  * Path-param schema for the bare-hash route. Matching the canonical
97
134
  * fact-hash form here pushes malformed-hash 400s through the framework's
@@ -150,6 +187,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
150
187
  'Content-Type': content_type,
151
188
  'Content-Length': size,
152
189
  'Cache-Control': CACHE_CONTROL,
190
+ ...FACT_SECURITY_HEADERS,
153
191
  });
154
192
  }
155
193
  // External — defense-in-depth re-validate the URL before trusting it
@@ -167,6 +205,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
167
205
  'Content-Length': size,
168
206
  'Cache-Control': CACHE_CONTROL,
169
207
  'X-Accel-Redirect': `${x_accel_redirect_prefix}${shard}/${rest}`,
208
+ ...FACT_SECURITY_HEADERS,
170
209
  });
171
210
  }
172
211
  // Dev / tests: stream from disk. `createReadStream` errors land on the
@@ -178,6 +217,7 @@ const serve_fact_bytes = async (c, route, hash, config) => {
178
217
  'Content-Type': content_type,
179
218
  'Content-Length': size,
180
219
  'Cache-Control': CACHE_CONTROL,
220
+ ...FACT_SECURITY_HEADERS,
181
221
  });
182
222
  };
183
223
  /**
@@ -894,7 +894,10 @@ source of truth for wire-shape conformance.
894
894
  - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
895
895
  (the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
896
896
  `account_lifecycle` / `fact_serving` / `ready` / `account_status` /
897
- `oversized_reject_closes_connection` — each has a `test_if` reader),
897
+ `oversized_reject_closes_connection` / `peer_request` — each has a `test_if`
898
+ reader; `peer_request` (server-initiated requests — the `ActionPeer`
899
+ `peer/ping` round-trip) is `true` only for the Rust spine and `false` for the
900
+ TS family, whose server request transport is deferred),
898
901
  `test_if(cond, name, fn)`
899
902
  for capability-gated cases, and `in_process_capabilities` preset. The
900
903
  non-gating wiring facts (`bearer_auth` / `trusted_proxy` / `login_rate_limit`,
@@ -1076,6 +1079,30 @@ where one exists (forge) and be offered the role; harmless on the auth-only
1076
1079
  spine. Gated on `capabilities.ws`; cross-process only. fuz_app's own wiring is
1077
1080
  `src/test/cross_backend/role_grant_offer_notification_ws.cross.test.ts`.
1078
1081
 
1082
+ ### `cross_backend/peer_ping_ws.ts` — `describe_peer_ping_ws_tests`
1083
+
1084
+ The server→client **request/response** sibling of the notification suite above —
1085
+ the machinery proof for `ActionPeer`. `peer/ping` is `initiator: both`; the
1086
+ client→server direction already exists as `heartbeat`, this exercises the new
1087
+ server→client one. The observable trigger is a client invoking the `peer/ping`
1088
+ **action** over its own socket: the handler initiates a `peer/ping` request back
1089
+ to that socket (`ctx.request_client`), awaits the client's echo, validates it
1090
+ against `PingResponse`, and returns the validated shape — so one client RPC
1091
+ drives the whole round-trip and every outcome (success / `Timeout` / wrong-shape
1092
+ / client-error) surfaces as that RPC's wire response. The client attaches an
1093
+ `on_request` responder **at construction** (the `create_ws_transport` seam) so
1094
+ the inbound server-initiated request is answered as soon as it arrives; the
1095
+ security negatives use the raw `WsClient.send` to inject unsolicited /
1096
+ cross-connection frames. Seven cases: the positive round-trip plus
1097
+ unsolicited-response rejection, per-connection id isolation, never-reply
1098
+ `Timeout`, wrong-shape rejection, client-error forwarding, and the HTTP
1099
+ no-transport path. Gated on `capabilities.peer_request` — `true` only on the
1100
+ Rust spine (server-initiated requests are Rust-first canonical), so it `.skip`s
1101
+ on the TS spines whose server request transport is the deferred convergence
1102
+ item; `peer/ping` is a protocol action (manifest-excluded), so parity is
1103
+ behavioral here, not via the manifest gate. Cross-process only; fuz_app's own
1104
+ wiring is `src/test/cross_backend/peer_ping_ws.cross.test.ts`.
1105
+
1079
1106
  ### `cross_backend/sse_round_trip.ts` — `describe_cross_process_sse_tests`
1080
1107
 
1081
1108
  Cross-process counterpart to the in-process `sse_round_trip.ts` harness —
@@ -1177,12 +1204,19 @@ in-process legs (plain `gro test`) are `src/test/auth/cell_crud_parity.db.test.t
1177
1204
  wire-frame types, and
1178
1205
  predicates (`is_notification`, `is_response_for`, ...). Both in-process
1179
1206
  (`ws_round_trip.ts`) and cross-process (`ws_transport.ts`) impls satisfy
1180
- this interface.
1181
- - `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?})`
1207
+ this interface. Also owns `deliver_inbound` — the shared
1208
+ parse→(respond|push)→resolve receive core both impls call (so the responder
1209
+ branch is written once) — plus the `WsRequestResponder` / `WsResponderOutcome`
1210
+ types for the **`on_request`** seam: a responder answers a server-initiated
1211
+ request (`{result}` / `{error}` / `undefined` to swallow), the path
1212
+ `describe_peer_ping_ws_tests` drives. With no `on_request` the seam is inert
1213
+ (a server-initiated request flows to `messages` as before).
1214
+ - `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?, on_request?})`
1182
1215
  builds a real-upgrade WS client using the `ws` npm package (optional
1183
1216
  peerDep; consumers wiring cross-process tests `npm install --save-dev ws`).
1184
1217
  Threads the keeper cookie onto the upgrade so per-action auth succeeds on
1185
- the first message.
1218
+ the first message. `on_request` (attached at construction, before the upgrade
1219
+ completes) answers server-initiated requests via the shared `deliver_inbound`.
1186
1220
  - `testing/transports/sse_frame_reader.ts` — `create_sse_frame_reader(reader, default_timeout_ms?)`,
1187
1221
  the transport-agnostic SSE framing core over a
1188
1222
  `ReadableStreamDefaultReader<Uint8Array>`: `\n\n` framing, per-read timeout,
@@ -139,12 +139,33 @@ export interface BackendCapabilities {
139
139
  * smuggle detector. See `docs/security.md` §"Body Size Limiting".
140
140
  */
141
141
  readonly oversized_reject_closes_connection: boolean;
142
+ /**
143
+ * The backend can **initiate** a JSON-RPC request to a connected client
144
+ * and await its typed reply (the server→client request/response direction
145
+ * `ActionPeer` adds). Gates `describe_peer_ping_ws_tests` — the on-demand
146
+ * `peer/ping` round-trip plus its security negatives (unsolicited-response
147
+ * rejection, per-connection id isolation, never-replying `Timeout`,
148
+ * wrong-shape reply rejection).
149
+ *
150
+ * `true` for the Rust spine (server-initiated requests landed Rust-first
151
+ * canonical); `false` for the TS family — the TS server
152
+ * `BackendWebsocketTransport.send()` request path is the deferred
153
+ * twin-impl convergence item, so a TS backend can't yet drive the
154
+ * round-trip. Like ws/sse/cells, `peer/ping` stays off the standard
155
+ * declared surface (it's a protocol action, manifest-excluded), so this
156
+ * flag is the only opt-in into the peer parity coverage.
157
+ */
158
+ readonly peer_request: boolean;
142
159
  }
143
160
  /**
144
- * Capability declarations for the in-process Hono transport. Every flag
145
- * is `true` because in-process testing exercises the full backend with
146
- * no missing optional behaviors. Cross-process consumers
147
- * declare each flag explicitly per backend.
161
+ * Capability declarations for the in-process Hono transport. Nearly every
162
+ * flag is `true` because in-process testing exercises the full backend
163
+ * with no missing optional behaviors. The one exception is `peer_request`:
164
+ * the in-process driver runs the **TS** server (`register_action_ws`),
165
+ * whose server-initiated request transport is deferred, so it can't drive
166
+ * a `peer/ping` round-trip — and `describe_peer_ping_ws_tests` is
167
+ * cross-process-only regardless. Cross-process consumers declare each flag
168
+ * explicitly per backend.
148
169
  */
149
170
  export declare const in_process_capabilities: BackendCapabilities;
150
171
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwC9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAUpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwC9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAWpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
@@ -36,10 +36,14 @@ import '../assert_dev_env.js';
36
36
  */
37
37
  import { test } from 'vitest';
38
38
  /**
39
- * Capability declarations for the in-process Hono transport. Every flag
40
- * is `true` because in-process testing exercises the full backend with
41
- * no missing optional behaviors. Cross-process consumers
42
- * declare each flag explicitly per backend.
39
+ * Capability declarations for the in-process Hono transport. Nearly every
40
+ * flag is `true` because in-process testing exercises the full backend
41
+ * with no missing optional behaviors. The one exception is `peer_request`:
42
+ * the in-process driver runs the **TS** server (`register_action_ws`),
43
+ * whose server-initiated request transport is deferred, so it can't drive
44
+ * a `peer/ping` round-trip — and `describe_peer_ping_ws_tests` is
45
+ * cross-process-only regardless. Cross-process consumers declare each flag
46
+ * explicitly per backend.
43
47
  */
44
48
  export const in_process_capabilities = Object.freeze({
45
49
  ws: true,
@@ -51,6 +55,7 @@ export const in_process_capabilities = Object.freeze({
51
55
  ready: true,
52
56
  account_status: true,
53
57
  oversized_reject_closes_connection: true,
58
+ peer_request: false,
54
59
  });
55
60
  /**
56
61
  * Shape notes for the in-process Hono transport — every wiring fact present
@@ -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;AAsD9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAGzD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IAwSlF,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;AAsD9B,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,YAAY,CAAC;AAGzD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IA6VlF,CAAC"}
@@ -83,6 +83,29 @@ export const describe_cell_crud_cross_tests = (options) => {
83
83
  assert.ok(!gone.ok, 'deleted cell still readable');
84
84
  assert.strictEqual(error_reason(gone), 'cell_not_found');
85
85
  });
86
+ test_if(capabilities.cell_crud, 'refs are deduped and lexicographically sorted (deterministic, cross-impl-identical)', async () => {
87
+ const fixture = await setup_test();
88
+ const owner = await fixture.create_account({ username: 'cell_refs_owner' });
89
+ const t = fixture.fresh_transport();
90
+ const owner_headers = owner.create_session_headers();
91
+ // Three valid distinct fact hashes that sort a < b < c, embedded in
92
+ // `data` in a deliberately unsorted, nested, duplicated order. Both
93
+ // spines must derive `refs` as the sorted [a, b, c] regardless of
94
+ // traversal order or `HashSet` iteration — the byte-identical
95
+ // cross-backend contract (Rust `extract_refs` ↔ TS `derive_refs`).
96
+ const ref_a = `blake3:${'a'.repeat(64)}`;
97
+ const ref_b = `blake3:${'b'.repeat(64)}`;
98
+ const ref_c = `blake3:${'c'.repeat(64)}`;
99
+ const sorted = [ref_a, ref_b, ref_c];
100
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note', cover: ref_c, nested: { attachments: [ref_b, ref_a, ref_b] } } }, owner_headers), CellCreateOutput);
101
+ assert.deepStrictEqual(created.cell.refs, sorted, 'create did not return sorted refs');
102
+ // Read it back — the stored column round-trips sorted too.
103
+ const got = expect_output(await cross_rpc_call(t, rpc_path, 'cell_get', { id: created.cell.id }, owner_headers), CellGetOutput);
104
+ assert.deepStrictEqual(got.cell.refs, sorted, 'get did not return sorted refs');
105
+ // Update reorders the embedded refs — the new column is still sorted.
106
+ const updated = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id: created.cell.id, data: { refs: [ref_b, ref_c, ref_a] } }, owner_headers), CellUpdateOutput);
107
+ assert.deepStrictEqual(updated.cell.refs, sorted, 'update did not return sorted refs');
108
+ });
86
109
  test_if(capabilities.cell_crud, 'anon sees public cells only; private is 404', async () => {
87
110
  const fixture = await setup_test();
88
111
  const owner = await fixture.create_account({ username: 'cell_anon_owner' });
@@ -1 +1 @@
1
- {"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAiBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAgBtC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBAIrC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
1
+ {"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAqBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAmBtC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBAIrC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
@@ -35,6 +35,10 @@ export const ts_default_capabilities = Object.freeze({
35
35
  // posture). A Bun-served consumer overrides to `false` (see the bun spine
36
36
  // config) — fail-loud rather than silently skipping the smuggle detector.
37
37
  oversized_reject_closes_connection: true,
38
+ // The TS server can't yet initiate server→client requests (the
39
+ // `BackendWebsocketTransport.send()` request path is the deferred
40
+ // twin-impl convergence), so the TS family skips the peer/ping suite.
41
+ peer_request: false,
38
42
  });
39
43
  /**
40
44
  * Capabilities for the Rust family. The non-gating wiring facts live in
@@ -57,6 +61,9 @@ export const rust_default_capabilities = Object.freeze({
57
61
  // hyper sends an RST on the oversized-body reject — the connection closes
58
62
  // and the pipelined request is never reached.
59
63
  oversized_reject_closes_connection: true,
64
+ // Server-initiated requests landed Rust-first canonical — the Rust spine
65
+ // drives the `peer/ping` round-trip, so the family opts into the suite.
66
+ peer_request: true,
60
67
  });
61
68
  /**
62
69
  * Shape notes for TS-family backends — wiring facts, not gating flags.