@fuzdev/fuz_app 0.145.0 → 0.146.1

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 (60) hide show
  1. package/dist/actions/CLAUDE.md +7 -6
  2. package/dist/auth/CLAUDE.md +5 -2
  3. package/dist/auth/account_action_specs.d.ts.map +1 -1
  4. package/dist/auth/account_action_specs.js +2 -1
  5. package/dist/auth/account_route_schema.d.ts.map +1 -1
  6. package/dist/auth/account_route_schema.js +9 -2
  7. package/dist/auth/account_routes.d.ts +8 -0
  8. package/dist/auth/account_routes.d.ts.map +1 -1
  9. package/dist/auth/account_routes.js +12 -3
  10. package/dist/auth/request_context.d.ts +23 -0
  11. package/dist/auth/request_context.d.ts.map +1 -1
  12. package/dist/auth/request_context.js +32 -1
  13. package/dist/auth/token_scope.d.ts +22 -14
  14. package/dist/auth/token_scope.d.ts.map +1 -1
  15. package/dist/auth/token_scope.js +23 -14
  16. package/dist/http/CLAUDE.md +2 -2
  17. package/dist/http/auth_shape.d.ts.map +1 -1
  18. package/dist/http/auth_shape.js +11 -6
  19. package/dist/http/proxy.d.ts +22 -21
  20. package/dist/http/proxy.d.ts.map +1 -1
  21. package/dist/http/proxy.js +28 -33
  22. package/dist/http/route_spec.d.ts +8 -0
  23. package/dist/http/route_spec.d.ts.map +1 -1
  24. package/dist/server/env.d.ts.map +1 -1
  25. package/dist/server/env.js +1 -1
  26. package/dist/server/fact_security_headers.d.ts +5 -0
  27. package/dist/server/fact_security_headers.d.ts.map +1 -1
  28. package/dist/server/fact_security_headers.js +5 -0
  29. package/dist/server/serve_fact_route.d.ts +12 -7
  30. package/dist/server/serve_fact_route.d.ts.map +1 -1
  31. package/dist/server/serve_fact_route.js +30 -23
  32. package/dist/server/x_accel.d.ts +37 -9
  33. package/dist/server/x_accel.d.ts.map +1 -1
  34. package/dist/server/x_accel.js +136 -19
  35. package/dist/testing/CLAUDE.md +73 -22
  36. package/dist/testing/cross_backend/capabilities.d.ts +0 -18
  37. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  38. package/dist/testing/cross_backend/capabilities.js +1 -4
  39. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  40. package/dist/testing/cross_backend/default_backend_configs.js +0 -7
  41. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  42. package/dist/testing/cross_backend/fact_serving.js +85 -12
  43. package/dist/testing/cross_backend/identity_parity.d.ts +11 -0
  44. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
  45. package/dist/testing/cross_backend/identity_parity.js +124 -9
  46. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  47. package/dist/testing/cross_backend/login_security.js +28 -0
  48. package/dist/testing/cross_backend/revocation_drivers.d.ts +70 -0
  49. package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -0
  50. package/dist/testing/cross_backend/revocation_drivers.js +104 -0
  51. package/dist/testing/cross_backend/sse_round_trip.d.ts +6 -1
  52. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  53. package/dist/testing/cross_backend/sse_round_trip.js +52 -16
  54. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  55. package/dist/testing/cross_backend/token_scope_surface.js +33 -0
  56. package/dist/testing/cross_backend/ws_round_trip.d.ts +13 -1
  57. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  58. package/dist/testing/cross_backend/ws_round_trip.js +155 -39
  59. package/dist/ui/Datatable.svelte +3 -3
  60. package/package.json +1 -1
@@ -33,6 +33,10 @@
33
33
  * - carry, as top-level directives of its own body, an exact `internal;` and
34
34
  * each `fact_security_header_directives` line (token-equal: whitespace and
35
35
  * quoting are free, the words are not);
36
+ * - set those headers with no other `add_header` — one naming a
37
+ * `FACT_SECURITY_HEADERS` header (ASCII case-insensitive) that is not
38
+ * token-equal to its required line is refused, since nginx sends both values
39
+ * and a client may read only the first (`sniff, nosniff`);
36
40
  * - hold only simple directives — any nested block (`location`, `if`,
37
41
  * `limit_except`, …) is refused, and so is `include`, since included files
38
42
  * are not followed.
@@ -53,13 +57,34 @@
53
57
  * own modifier and path words and the bodies of the locations serving the
54
58
  * prefix.
55
59
  *
60
+ * The prefix itself must be an absolute path ending in exactly one `/` — the
61
+ * server appends `<shard>/<rest>` straight onto it, so `/_facts` would redirect
62
+ * to `/_factsab/…`, outside the validated location. At least one location
63
+ * serving the prefix must sit exactly at it (`/_facts` or `/_facts/`): one only
64
+ * below it (`location ^~ /_facts/00/`) leaves every other redirect path to
65
+ * whatever location matches next, and nginx compares location paths
66
+ * byte-for-byte, so `/_facts//` sits below it too.
67
+ *
68
+ * Module directives that undo those guarantees are refused too:
69
+ *
70
+ * - `ssi` with any value but `off`, anywhere in the config. SSI `include
71
+ * virtual` is a subrequest, which `internal;` admits, so an SSI-processed
72
+ * response — a fact inheriting `ssi on` from `http` or `server`, or any page
73
+ * carrying user content — could include another fact's bytes.
74
+ * - `more_set_headers` / `more_clear_headers` (headers-more) everywhere except
75
+ * inside a `location` that doesn't serve the prefix (or a block within one):
76
+ * they run after `add_header` and can strip the hardening headers, so they
77
+ * are refused in the facts location and at every level above it, including
78
+ * any block there (`if`, another `server`).
79
+ *
56
80
  * Also refused: an `=` location on the prefix or below it (it can't serve the
57
81
  * nested paths the redirect emits); a regex location (`~` / `~*`) whose pattern
58
82
  * spells out the prefix (after unescaping `\/`, ASCII case-folded for `~*`) —
59
- * dead under the rules above, refused for the clearer message; and a config
60
- * with no location serving the prefix. `include` at server level is allowed,
61
- * but its file is not read — keep facts-related locations out of included
62
- * files.
83
+ * dead under the rules above, refused for the clearer message; a config with no
84
+ * location at the prefix; and blocks nested more than 64 deep (a parse error, so
85
+ * the walks can't exhaust the stack). `include` at server level is allowed, but
86
+ * its file is not read — keep facts-related locations and the directives above
87
+ * out of included files.
63
88
  *
64
89
  * The threat model is operator mistakes, not an adversarial config author: the
65
90
  * prefix check is textual, so a value assembled from pieces (`/_fac$1`) evades
@@ -73,8 +98,10 @@
73
98
  * the server is trusted to point into the prefix; an untrusted one needs
74
99
  * `proxy_ignore_headers X-Accel-Redirect;` (or the `fastcgi_` / `uwsgi_` /
75
100
  * `scgi_` equivalent).
76
- * - Module code can redirect internally too (ssi `include virtual`, njs
77
- * `internalRedirect`, perl, echo), so run trusted modules and content only.
101
+ * - Other module code can redirect internally too (njs `internalRedirect`, perl,
102
+ * echo), or strip the hardening headers as headers-more can (njs
103
+ * `js_header_filter`, lua-nginx-module `header_filter_by_lua*`), and the
104
+ * check sees none of it, so run trusted modules and content only.
78
105
  *
79
106
  * The Rust twin is `fuz_fact_serving`'s `nginx.rs`, held to the same rules and
80
107
  * the same fixture corpus.
@@ -129,6 +156,13 @@ const collect_errors = (config, facts_location) => {
129
156
  `(e.g. \`/_facts/\`).`
130
157
  ];
131
158
  }
159
+ if (facts_location.length !== prefix.length + 1) {
160
+ return [
161
+ `X-Accel redirect prefix \`${facts_location}\` must end in exactly one \`/\` (e.g. ` +
162
+ `\`/_facts/\`) — the server appends \`<shard>/<rest>\` straight onto it, so any other ` +
163
+ `ending redirects outside the validated location.`
164
+ ];
165
+ }
132
166
  const directives = parse_directives(tokenize(config));
133
167
  const locations = [];
134
168
  collect_locations(directives, null, locations);
@@ -140,7 +174,7 @@ const collect_errors = (config, facts_location) => {
140
174
  ]);
141
175
  const rendered = fact_security_header_directives();
142
176
  const errors = [];
143
- let matched = false;
177
+ let at_prefix = false;
144
178
  for (const location of locations) {
145
179
  const label = location_label(location);
146
180
  switch (location.modifier) {
@@ -150,7 +184,10 @@ const collect_errors = (config, facts_location) => {
150
184
  case 'prefix_no_regex': {
151
185
  if (!path_is_under(location.path, prefix))
152
186
  break;
153
- matched = true;
187
+ // nginx compares location paths byte-for-byte, so `/_facts//` never matches
188
+ // `/_facts/ab/…` — only `/_facts` and `/_facts/` sit at the prefix
189
+ if (location.path === prefix || location.path === prefix + '/')
190
+ at_prefix = true;
154
191
  if (location.modifier !== 'prefix_no_regex') {
155
192
  errors.push(`${label} must use \`^~\` (\`location ^~ ${location.path}\`) — on a plain prefix ` +
156
193
  `location nginx goes on to check regex locations, any of which can serve fact ` +
@@ -174,6 +211,16 @@ const collect_errors = (config, facts_location) => {
174
211
  }
175
212
  for (const directive of location.body) {
176
213
  const name = directive.args[0] ?? '';
214
+ const header = directive.args[1];
215
+ if (directive.block === null && name === 'add_header' && header !== undefined) {
216
+ const index = required.findIndex((args) => to_ascii_lowercase(args[1]) === to_ascii_lowercase(header));
217
+ if (index !== -1 && !args_equal(directive.args, required[index])) {
218
+ errors.push(`${label} has another \`add_header ${header}\` (line ${directive.line}) — nginx ` +
219
+ `sends every \`add_header\` for a header and a client may read only the ` +
220
+ `first, so the facts location may set \`${required[index][1]}\` only with ` +
221
+ `\`${rendered[index]}\`.`);
222
+ }
223
+ }
177
224
  if (directive.block !== null) {
178
225
  errors.push(`${label} contains a nested \`${name}\` block (line ${directive.line}) — the facts ` +
179
226
  `location must hold only simple directives, since a nested block can ` +
@@ -209,11 +256,13 @@ const collect_errors = (config, facts_location) => {
209
256
  }
210
257
  }
211
258
  collect_prefix_mention_errors(directives, prefix, errors);
212
- if (!matched) {
259
+ collect_module_errors(directives, prefix, true, errors);
260
+ if (!at_prefix) {
213
261
  errors.unshift(`No nginx \`location ^~ ${facts_location}\` block found — the X-Accel facts ` +
214
- `location must exist and be marked \`internal;\` so only the authz'd handler's ` +
215
- `redirect can reach it. A public facts location bypasses every cell-visibility ` +
216
- `check.`);
262
+ `location must exist exactly at the prefix (one only below it leaves the other ` +
263
+ `redirect paths uncovered) and be marked \`internal;\` so only the authz'd ` +
264
+ `handler's redirect can reach it. A public facts location bypasses every ` +
265
+ `cell-visibility check.`);
217
266
  }
218
267
  return errors;
219
268
  };
@@ -255,12 +304,13 @@ export class XAccelConfigError extends Error {
255
304
  * The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
256
305
  * into a location proven `internal;` and header-carrying at boot.
257
306
  *
258
- * @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
307
+ * @param redirect_prefix - the X-Accel redirect prefix, ending in `/` (e.g. `/_facts/`)
259
308
  * @param nginx_config - the nginx config string to validate against
260
309
  * @returns the validated `XAccelConfig` carrying `redirect_prefix`
261
310
  * @throws `XAccelConfigError` carrying every violation
262
- * `validate_facts_internal_location` finds — a missing, public, header-less,
263
- * exact-match, or regex-shadowed facts location.
311
+ * `validate_facts_internal_location` finds — a prefix not ending in `/`, a
312
+ * missing, public, header-less, re-headered, exact-match, or regex-shadowed
313
+ * facts location, or `ssi` / headers-more reaching it.
264
314
  */
265
315
  export const create_x_accel_config = (redirect_prefix, nginx_config) => {
266
316
  const validation = validate_facts_internal_location(nginx_config, redirect_prefix);
@@ -282,7 +332,9 @@ const path_is_under = (path, prefix) => {
282
332
  * `true` when `body` has a top-level simple directive whose words are exactly
283
333
  * `args` (a block directive with those words doesn't count).
284
334
  */
285
- const has_directive = (body, args) => body.some((d) => d.block === null && d.args.length === args.length && d.args.every((a, i) => a === args[i]));
335
+ const has_directive = (body, args) => body.some((d) => d.block === null && args_equal(d.args, args));
336
+ /** `true` when `a` and `b` hold the same words in the same order. */
337
+ const args_equal = (a, b) => a.length === b.length && a.every((word, i) => word === b[i]);
286
338
  const MODIFIER_TEXT = {
287
339
  prefix: '',
288
340
  named: '',
@@ -361,6 +413,59 @@ const collect_prefix_mention_errors = (directives, prefix, errors) => {
361
413
  collect_prefix_mention_errors(directive.block, prefix, errors);
362
414
  }
363
415
  };
416
+ /**
417
+ * The headers-more module's response-header directives, which run after
418
+ * `add_header` and so can strip or rewrite the fact hardening headers.
419
+ */
420
+ const HEADERS_MORE_DIRECTIVES = new Set([
421
+ 'more_set_headers',
422
+ 'more_clear_headers'
423
+ ]);
424
+ /**
425
+ * Refuse the module directives that undo the facts location's guarantees:
426
+ *
427
+ * - `ssi` with any value but `off`, in any block at any depth. SSI `include
428
+ * virtual` is a subrequest, which `internal;` admits, so an SSI-processed
429
+ * response — a fact served from the facts location (inheriting `ssi on` from
430
+ * `http` or `server`), or any page carrying user content — could include
431
+ * another fact's bytes by its path. A sibling location's `ssi on` is as
432
+ * dangerous as an inherited one, so the rule is config-wide.
433
+ * - a headers-more directive anywhere except inside a `location` that doesn't
434
+ * serve the prefix (or a block within one): in the facts location's body, or
435
+ * at any level above it (`http`, `server`, top level, and any block there such
436
+ * as `if` or another `server`).
437
+ *
438
+ * `reaches_facts` is `true` while the walk is in a scope the facts location
439
+ * inherits from or is.
440
+ */
441
+ const collect_module_errors = (directives, prefix, reaches_facts, errors) => {
442
+ for (const directive of directives) {
443
+ const name = directive.args[0] ?? '';
444
+ if (directive.block === null) {
445
+ if (name === 'ssi' &&
446
+ !(directive.args.length === 2 && to_ascii_lowercase(directive.args[1]) === 'off')) {
447
+ errors.push(`\`ssi\` (line ${directive.line}) is not \`off\` — SSI \`include virtual\` subrequests ` +
448
+ `reach the \`internal;\` facts location, so an SSI-processed response could ` +
449
+ `include any fact's bytes by its path. Remove it or set \`ssi off;\`.`);
450
+ }
451
+ else if (reaches_facts && HEADERS_MORE_DIRECTIVES.has(name)) {
452
+ errors.push(`\`${name}\` (line ${directive.line}) applies to the facts location — a ` +
453
+ `headers-more directive in or above it can strip or rewrite the fact hardening ` +
454
+ `headers after \`add_header\` sets them. Move it into the locations that need it.`);
455
+ }
456
+ continue;
457
+ }
458
+ let reaches = reaches_facts;
459
+ if (name === 'location') {
460
+ const parsed = parse_location_args(directive.args.slice(1));
461
+ reaches =
462
+ parsed !== null &&
463
+ (parsed.modifier === 'prefix' || parsed.modifier === 'prefix_no_regex') &&
464
+ path_is_under(parsed.path, prefix);
465
+ }
466
+ collect_module_errors(directive.block, prefix, reaches, errors);
467
+ }
468
+ };
364
469
  /**
365
470
  * Gather every `location` block at any depth, in document order, each with the
366
471
  * name of the block directly enclosing it.
@@ -543,11 +648,20 @@ const tokenize = (config) => {
543
648
  }
544
649
  return tokens;
545
650
  };
651
+ /**
652
+ * The deepest block nesting the parser accepts — far beyond any real config, and
653
+ * low enough that the recursive walks can't exhaust the stack.
654
+ */
655
+ const MAX_NESTING_DEPTH = 64;
546
656
  const parse_directives = (tokens) => {
547
657
  const cursor = { pos: 0 };
548
- return parse_block(tokens, cursor, null);
658
+ return parse_block(tokens, cursor, null, 0);
549
659
  };
550
- const parse_block = (tokens, cursor, open_line) => {
660
+ /**
661
+ * Parse one block's directives; `depth` is the number of blocks enclosing them
662
+ * (`0` at the top level).
663
+ */
664
+ const parse_block = (tokens, cursor, open_line, depth) => {
551
665
  const out = [];
552
666
  for (;;) {
553
667
  const token = tokens[cursor.pos];
@@ -580,8 +694,11 @@ const parse_block = (tokens, cursor, open_line) => {
580
694
  break;
581
695
  }
582
696
  else if (next?.kind === '{') {
697
+ if (depth >= MAX_NESTING_DEPTH) {
698
+ throw new NginxParseError(`blocks nested deeper than ${MAX_NESTING_DEPTH} levels (line ${next.line})`);
699
+ }
583
700
  cursor.pos++;
584
- const block = parse_block(tokens, cursor, next.line);
701
+ const block = parse_block(tokens, cursor, next.line, depth + 1);
585
702
  out.push({ args, line, block });
586
703
  break;
587
704
  }
@@ -932,10 +932,10 @@ source of truth for wire-shape conformance.
932
932
  (the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
933
933
  `cell_gated_create` / `account_lifecycle` / `fact_serving` / `ready` /
934
934
  `account_status` / `peer_request` /
935
- `ws_handshake_pipelining` / `ws_account_actions` / `ws_self_revocation_reply` /
935
+ `ws_handshake_pipelining` / `ws_account_actions` /
936
936
  `ws_role_gated` / `ws_refusal_body` — each has a `test_if`
937
- reader (`ws_self_revocation_reply` forks one case's assertion instead, and
938
- `ws_refusal_body` how the front-door table reads a WS refusal); `peer_request` (server-initiated requests — the ActionPeer
937
+ reader (`ws_refusal_body` picks how the front-door table reads a WS
938
+ refusal instead); `peer_request` (server-initiated requests — the ActionPeer
939
939
  `peer/ping` round-trip) is `true` for both the Rust spine and the TS spine
940
940
  (`BackendWebsocketTransport.request_connection`); `ts_default_capabilities`
941
941
  keeps it `false` like sse/ready until a backend wires the `peer/ping` HTTP +
@@ -966,7 +966,8 @@ source of truth for wire-shape conformance.
966
966
  generic round-trip can't drive (it can't delete the subject). `true` on every
967
967
  spine. `fact_serving`
968
968
  gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference +
969
- admin-only bare-hash fact-serving parity suite); like cells it stays off the
969
+ admin-only bare-hash fact-serving parity suite, incl. the served fact's
970
+ hardening headers and bodyless masked 404s); like cells it stays off the
970
971
  declared surface and is `true` on every spine that mounts the serve routes +
971
972
  the `_testing_put_fact` seeder (whose optional `holder` records a
972
973
  `fact_holder` row, so a non-admin can reference what it seeds) — it also
@@ -991,11 +992,9 @@ source of truth for wire-shape conformance.
991
992
  write raw bytes to).
992
993
  `ws_account_actions` (the WS endpoint mounts the account actions — `true` on
993
994
  fuz_app's own spine presets, `false` in the family defaults) gates the WS
994
- round-trip suite's self-revocation case and, with `rpc_path`, its
995
- WS-mutation audit case (which calls `_testing_drain_effects`);
996
- `ws_self_revocation_reply` says what the self-revocation case requires: the reply ahead of the close (`true`, the TS
997
- family) or the close alone (`false`, the Rust family, whose socket loop
998
- drops the response once the connection is closed — a recorded divergence).
995
+ round-trip suite's self-revocation case (which requires the reply ahead of
996
+ the close on every backend) and, with `rpc_path`, its WS-mutation audit case
997
+ (which calls `_testing_drain_effects`).
999
998
 
1000
999
  ### `cross_backend/standard.ts` — `describe_standard_cross_process_tests`
1001
1000
 
@@ -1117,7 +1116,8 @@ call (not folded into `describe_standard_cross_process_tests`) because it
1117
1116
  needs raw `base_url` / `ws_path` the standard bundle doesn't carry, mirroring
1118
1117
  how `describe_ws_round_trip_tests` sits beside `describe_standard_tests`
1119
1118
  in-process. `describe_cross_process_ws_tests({setup_test, capabilities,
1120
- base_url, ws_path, origin?, rpc_path?, max_message_bytes?})` opens a live
1119
+ base_url, ws_path, origin?, rpc_path?, max_message_bytes?, logout_path?,
1120
+ password_path?, max_tokens?})` opens a live
1121
1121
  `WebSocket` via `create_ws_transport` (the `ws` npm package) and asserts
1122
1122
  cases against the upgrade stack `register_ws_endpoint` wires (origin →
1123
1123
  `require_auth` → dispatch): authed upgrade round-trips `heartbeat`,
@@ -1135,8 +1135,19 @@ anonymous upgrade refused, disallowed-origin upgrade refused, and — gated on
1135
1135
  mid-connection (`account_session_revoke_all` over the keeper session channel;
1136
1136
  asserted via `WsClient.wait_for_close`) and when its account is deleted
1137
1137
  (`account_delete` by the keeper, also gated on
1138
- `capabilities.account_lifecycle`; the close code is
1139
- `WS_CLOSE_SESSION_REVOKED`). Per-connection auth is enforced **at upgrade
1138
+ `capabilities.account_lifecycle`), each with `WS_CLOSE_SESSION_REVOKED` and
1139
+ `WS_CLOSE_SESSION_REVOKED_REASON`. The rest of the revocation table closes a second
1140
+ account's socket with `WS_CLOSE_SESSION_REVOKED` and
1141
+ `WS_CLOSE_SESSION_REVOKED_REASON`: `account_token_revoke` its revoked token's
1142
+ bearer socket (a socket on its other token stays open);
1143
+ `admin_token_revoke_all`, `POST /logout` (`logout_path`), `POST /password`
1144
+ (`password_path`) and `account_purge` (also gated on
1145
+ `capabilities.account_lifecycle`) its bearer and session sockets; and
1146
+ `max_tokens` further mints (default `DEFAULT_MAX_TOKENS`; `null` skips) the
1147
+ bearer socket of the token the cap evicts. The account-wide rows come from
1148
+ `cross_backend/revocation_drivers.ts` (`create_account_revocations`, shared
1149
+ with the SSE suite), and a case that opens several sockets closes whichever
1150
+ opened when a later open fails. Per-connection auth is enforced **at upgrade
1140
1151
  time**, so the negative upgrade cases assert the upgrade itself rejects, not
1141
1152
  a per-message error; the close cases prove a revocation closes an
1142
1153
  already-open socket, since per-message dispatch never re-checks credential
@@ -1144,8 +1155,7 @@ validity. Omit `rpc_path` to skip the close cases (consumers without the
1144
1155
  standard account and admin actions on their RPC endpoint). Two cases are gated
1145
1156
  on `capabilities.ws_account_actions` (the WS endpoint mounts the account
1146
1157
  actions): a socket that revokes its own sessions over that socket is closed
1147
- with `WS_CLOSE_SESSION_REVOKED`, and — where the backend declares
1148
- `capabilities.ws_self_revocation_reply` — reads the reply first; and (with
1158
+ with `WS_CLOSE_SESSION_REVOKED` and its reason after reading the reply to its request; and (with
1149
1159
  `rpc_path`) a socket mints an API token over WS, and its `token_create` audit
1150
1160
  row is read back after `_testing_drain_effects` — a WS reply can precede its
1151
1161
  audit write, so the drain is the barrier (the binary mounts it via
@@ -1349,7 +1359,11 @@ account-wide close) and then just the one (`account_session_revoke`, the
1349
1359
  session-scoped close), each dropping the live stream (asserted via
1350
1360
  `SseTransport.wait_for_close`); the keeper soft-deletes a second admin whose
1351
1361
  stream is open (`account_delete`, also gated on
1352
- `capabilities.account_lifecycle`), dropping it; and one stream past the per-session cap
1362
+ `capabilities.account_lifecycle`), dropping it; `admin_token_revoke_all`,
1363
+ `POST /logout` (`logout_path`), `POST /password` (`password_path`) and
1364
+ `account_purge` (also gated on `capabilities.account_lifecycle`) of a second
1365
+ admin each drop its stream — the rows `create_account_revocations`
1366
+ (`cross_backend/revocation_drivers.ts`) shares with the WS suite; and one stream past the per-session cap
1353
1367
  (`max_per_scope`, default `AUDIT_LOG_SSE_MAX_PER_SCOPE`; `null` skips) on one
1354
1368
  session closes the oldest and leaves the rest open. The data-frame + close
1355
1369
  cases gate on `rpc_path` (they drive the standard account/admin actions); all
@@ -1359,6 +1373,20 @@ cases gate on `capabilities.sse`. Cross-process only — wire from a
1359
1373
  TS spines wire `audit_log_sse` and the Rust `spine_stub` serves the same
1360
1374
  stream.
1361
1375
 
1376
+ ### `cross_backend/revocation_drivers.ts` — shared revocation drivers
1377
+
1378
+ The account-wide revocations both transport suites close a live connection on,
1379
+ as one table: `create_account_revocations({capabilities, rpc_path,
1380
+ logout_path, password_path})` → `AccountRevocation` rows (`{label, slug,
1381
+ gate, revoke(fixture, target)}`) for `admin_token_revoke_all`, `POST /logout`,
1382
+ `POST /password`, and `account_purge` (`gate` reads
1383
+ `capabilities.account_lifecycle`). `revoke` asserts the revocation succeeded;
1384
+ the target must hold `DEFAULT_TEST_PASSWORD`. A revocation added here is
1385
+ covered on WS and SSE at once. The drivers stand alone too: `cross_post_as`
1386
+ (a JSON `POST` on an account's session, asserting 200),
1387
+ `cross_admin_token_revoke_all`, and `cross_account_purge` (over the daemon
1388
+ token, no `Origin`).
1389
+
1362
1390
  ### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
1363
1391
 
1364
1392
  The cell-layer parity coverage is split across two sibling suites. Cells
@@ -1669,7 +1697,8 @@ cases are `src/test/auth/cell_ref_admission{,.fact_less}.db.test.ts`.
1669
1697
  `describe_token_scope_surface_cross_tests({setup_test, capabilities, rpc_path?,
1670
1698
  sse_path?})` — the `surface:<name>` twin of the conformance slate's
1671
1699
  `token_scope_cases`, which cover only the `rpc:<method>` arm. Token scoping's
1672
- RPC-only rule (_a narrowed api token reaches no non-RPC surface_) is the
1700
+ RPC-only rule (_a narrowed api token reaches no non-RPC surface that reads
1701
+ account data or carries authority_) is the
1673
1702
  load-bearing half of the design and had **no** cross-impl gate: the schema gate
1674
1703
  can't see it (no column), the action-manifest gate can't see it (no method), and
1675
1704
  the spec-derived suites can't see it (REST routes, off the declared RPC surface).
@@ -1682,7 +1711,12 @@ fact read (`capabilities.fact_serving`) — asserting the exact flat
1682
1711
  contract on (`token_scope_denied_body` ↔
1683
1712
  `fuz_auth::token_scope_surface_denied_response`).
1684
1713
 
1685
- The fifth case is the one with no prior pin anywhere: **scope outranks role**. A
1714
+ The account status read (`capabilities.account_status`) answers anonymous
1715
+ callers, so rule 3 there is the anonymous answer: a narrowed token on
1716
+ `GET /api/account/status` gets the anonymous `401`, status and body byte for
1717
+ byte, while the same account's full-scope bearer reads the account.
1718
+
1719
+ One case has no prior pin anywhere: **scope outranks role**. A
1686
1720
  narrowed token on an account that _also_ lacks the gating role must hear
1687
1721
  `token_scope_required`, not `insufficient_permissions` — the observable the
1688
1722
  TS↔Rust ordering divergence produced before both spines converged on running the
@@ -1911,16 +1945,18 @@ asserting each spec declares `credential_types: ['daemon_token']`) + the
1911
1945
 
1912
1946
  ### Identity-primitive parity — `cross_backend/identity_parity.ts`
1913
1947
 
1914
- `describe_identity_parity_cross_tests({setup_test, login_path?, signup_path?})` —
1948
+ `describe_identity_parity_cross_tests({setup_test, login_path?, signup_path?, bootstrap_path?, password_path?})` —
1915
1949
  the imperative suite pinning the `primitive_schemas` twins (`Username`,
1916
1950
  `UsernameProvided`, `Email`) + login/signup input handling end-to-end over real
1917
1951
  HTTP (flat REST `/login` + `/signup`, so imperative, not a `conformance_table`
1918
- row). Six facets, each a TS↔Rust reimplementation pair that the spec-derived
1952
+ row). The facets, each a TS↔Rust reimplementation pair that the spec-derived
1919
1953
  round-trip / conformance suites never vary:
1920
1954
 
1921
1955
  - **Login canonicalization** — case-insensitive + whitespace-trim lookup
1922
- (`UsernameProvided` `.trim().toLowerCase()` + `LOWER(username)`), plus a
1923
- Turkish-`İ` homograph **no-Unicode-fold-collision** negative.
1956
+ (`UsernameProvided` `.trim().toLowerCase()` + `LOWER(username)`), the trimmed
1957
+ set being JS `String.prototype.trim`'s on every spine (U+FEFF padding trimmed,
1958
+ U+0085 not), plus a Turkish-`İ` homograph **no-Unicode-fold-collision**
1959
+ negative.
1924
1960
  - **Username-or-email login** — an account created with an email logs in via
1925
1961
  that email, case-insensitively both directions; username login still works;
1926
1962
  non-existent email → 401. This is the corner that surfaced and closed the Rust
@@ -1933,7 +1969,11 @@ round-trip / conformance suites never vary:
1933
1969
  `account_login` (closed a cluster where three of these fell through to a
1934
1970
  lookup-miss 401 on Rust and a whitespace-only identifier 401'd on TS). Asserted
1935
1971
  by error reason, not just status.
1936
- - **ASCII-only creation invariant** — non-ASCII username → 400 at signup.
1972
+ - **ASCII-only creation invariant** — non-ASCII username → 400 at signup, and a
1973
+ padded creation name (space, tab, U+0085) → 400, never trimmed into a legal
1974
+ one — at signup, and at bootstrap (`bootstrap_path`, default
1975
+ `/api/account/bootstrap`, `null` to skip), where the body is validated ahead
1976
+ of the window check, so the 400 holds after bootstrap completed.
1937
1977
  - **Length + format regex parity** — both `[USERNAME_LENGTH_MIN,
1938
1978
  USERNAME_LENGTH_MAX]` = `[3, 39]` boundary edges (just-outside → 400,
1939
1979
  just-inside → 403 no-matching-invite) and the creation regex shape (leading
@@ -1948,6 +1988,13 @@ USERNAME_LENGTH_MAX]` = `[3, 39]` boundary edges (just-outside → 400,
1948
1988
  Zod's `z.email()` rejects — the deliberately-looser shared rule. **Closed a
1949
1989
  real divergence**: the Rust spine previously length-checked the email only,
1950
1990
  accepting any non-empty string TS rejected.
1991
+ - **Malformed JSON body** — an unparseable (`{`) or non-object (`[]`) body typed
1992
+ `application/json` → 400 `{error: "invalid_json_body"}` and nothing else, on
1993
+ login, signup, bootstrap (ahead of its window check), and the password change
1994
+ (`password_path`, default `/api/account/password`, `null` skips; parsed after
1995
+ its credential and session-only gates, so the case sends a session and an
1996
+ anonymous one still answers 401). A Rust backend needs the spine's
1997
+ `parse_json_body`.
1951
1998
 
1952
1999
  Ungated (login + signup are on every spine). Runs both legs via the shared
1953
2000
  `{setup_test}` protocol: in-process (`identity_parity.db.test.ts`, plain
@@ -1970,6 +2017,10 @@ over real HTTP on both spines:
1970
2017
  - **XFF keying** — exhaust one forwarded IP to 429, then a _distinct_ forwarded IP
1971
2018
  → 401 (not 429), proving the limiter keys on the resolved `X-Forwarded-For` IP,
1972
2019
  not the loopback TCP peer (a backend ignoring XFF would 429 the fresh IP too).
2020
+ - **a malformed hop stops the walk** — after exhausting one forwarded IP, a
2021
+ chain with that IP left of an `ip:port` entry → 401, not 429: the walk stops at
2022
+ the malformed entry and keys on the connection IP (its audit row says so),
2023
+ never on the client-written entry past it.
1973
2024
  - **a success does not refund the per-IP budget** — the distributed-spray
1974
2025
  backstop. Failed guesses against distinct usernames from one forwarded IP,
1975
2026
  interleaved with the attacker's own successful logins, must still 429 within a
@@ -176,24 +176,6 @@ export interface BackendCapabilities {
176
176
  * once the account actions are there.
177
177
  */
178
178
  readonly ws_account_actions: boolean;
179
- /**
180
- * A socket that revokes the credential it is running on reads the reply to
181
- * that request **before** the revocation's close. Read by the
182
- * self-revocation case in `describe_cross_process_ws_tests` (which
183
- * `ws_account_actions` gates): with it `true` the case requires the reply
184
- * frame ahead of the `WS_CLOSE_SESSION_REVOKED` close; with it `false` the
185
- * case requires the close alone.
186
- *
187
- * `true` for the TS family — `register_action_ws` sends the response and
188
- * only then flushes the request's post-commit queue, where the close is.
189
- * `false` for the Rust family: its dispatch runs the post-commit queue
190
- * before handing the response to the socket loop, and the loop, finding the
191
- * connection already closed, writes the close frame and drops the response.
192
- * The caller's revocation still took effect; it is not told so. The flag
193
- * records that divergence so the suite stays green on both spines without
194
- * hiding it — flip it once the Rust spine answers first.
195
- */
196
- readonly ws_self_revocation_reply: boolean;
197
179
  /**
198
180
  * A test `CellCreateAuthorize` policy is live-mounted on the backend's cell
199
181
  * layer — creating a `kind: 'gated'` cell requires the `participant` role
@@ -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;AA2C9B;;;;;;;;;;;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;;;;;;OAMG;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;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBA2BpC,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;AA2C9B;;;;;;;;;;;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;;;;;;OAMG;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;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAwBpC,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"}
@@ -28,7 +28,7 @@ import '../assert_dev_env.js';
28
28
  *
29
29
  * **Gating flags vs shape notes.** `BackendCapabilities` holds only flags a
30
30
  * suite actually gates on (each has a `test_if(capabilities.X, ...)` reader,
31
- * or — `ws_self_revocation_reply` — forks one case's assertion).
31
+ * or — `ws_refusal_body` — picks how a case reads a refusal).
32
32
  * Wiring facts that gate nothing — `bearer_auth` / `trusted_proxy` /
33
33
  * `login_rate_limit` — live in the parallel `BackendShapeNotes` record
34
34
  * (`in_process_shape_notes` here, `ts_default_shape_notes` /
@@ -66,9 +66,6 @@ export const in_process_capabilities = Object.freeze({
66
66
  ws_handshake_pipelining: false,
67
67
  // Cross-process-only: the case reads a real socket's frames in wire order.
68
68
  ws_account_actions: false,
69
- // Read only by the case `ws_account_actions` gates; the in-process
70
- // dispatcher does answer first.
71
- ws_self_revocation_reply: true,
72
69
  // Cross-process-only: the test `CellCreateAuthorize` policy is mounted on the
73
70
  // spine binaries' full mount + the Rust stub, not the in-process default app.
74
71
  cell_gated_create: false,
@@ -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":"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"}
1
+ {"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"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"}
@@ -47,9 +47,6 @@ export const ts_default_capabilities = Object.freeze({
47
47
  // fuz_app's spine configs opt in (the spine binary mounts the account
48
48
  // actions on `/api/ws`); a consumer does once its endpoint carries them.
49
49
  ws_account_actions: false,
50
- // `register_action_ws` sends a request's response before it flushes the
51
- // post-commit queue a self-revocation's close is on.
52
- ws_self_revocation_reply: true,
53
50
  // Off by default — the `cell_gated_create` test policy is a fuz_app test
54
51
  // fixture, mounted only on the spine binaries (`ts_spine_*`), not generic
55
52
  // consumers.
@@ -88,10 +85,6 @@ export const rust_default_capabilities = Object.freeze({
88
85
  // Off by default, as on the TS family: a consumer opts in once its WS
89
86
  // endpoint mounts the account actions. The `testing_spine_stub` preset does.
90
87
  ws_account_actions: false,
91
- // The Rust dispatch runs the post-commit queue — the self-revocation's
92
- // close — before the socket loop writes the response, and the loop then
93
- // drops it. A divergence from the TS spine; see the flag's doc.
94
- ws_self_revocation_reply: false,
95
88
  // Off by default — the `testing_spine_stub` preset opts in (it mounts the
96
89
  // `TestCellGatedCreateAuthorize` policy); a generic Rust consumer doesn't.
97
90
  cell_gated_create: false,
@@ -1 +1 @@
1
- {"version":3,"file":"fact_serving.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/fact_serving.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiD9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElG;;;;;GAKG;AACH,MAAM,WAAW,2BAA4B,SAAQ,uCAAuC;IAC3F,QAAQ,CAAC,sBAAsB,CAAC,EAAE,SAAS,CAAC;CAC5C;AAgBD,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAC1B,SAAS,IAAI,CAAC,WAAW,EAAE,iBAAiB,GAAG,6BAA6B,CAAC,EAC7E,UAAU,MAAM,EAChB,OAAO,iBAAiB,KACtB,OAAO,CAAC,MAAM,CAaV,CAAC;AAgBR,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAgOxF,CAAC"}
1
+ {"version":3,"file":"fact_serving.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/fact_serving.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElG;;;;;GAKG;AACH,MAAM,WAAW,2BAA4B,SAAQ,uCAAuC;IAC3F,QAAQ,CAAC,sBAAsB,CAAC,EAAE,SAAS,CAAC;CAC5C;AAgBD,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAC1B,SAAS,IAAI,CAAC,WAAW,EAAE,iBAAiB,GAAG,6BAA6B,CAAC,EAC7E,UAAU,MAAM,EAChB,OAAO,iBAAiB,KACtB,OAAO,CAAC,MAAM,CAaV,CAAC;AAgDR,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAkRxF,CAAC"}