@fuzdev/fuz_app 0.85.0 → 0.86.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 (38) hide show
  1. package/dist/actions/CLAUDE.md +4 -2
  2. package/dist/actions/perform_action.d.ts.map +1 -1
  3. package/dist/actions/perform_action.js +8 -4
  4. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  5. package/dist/auth/role_grant_offer_actions.js +4 -3
  6. package/dist/hono_context.d.ts +9 -7
  7. package/dist/hono_context.d.ts.map +1 -1
  8. package/dist/http/CLAUDE.md +14 -3
  9. package/dist/http/pending_effects.d.ts +43 -0
  10. package/dist/http/pending_effects.d.ts.map +1 -1
  11. package/dist/http/pending_effects.js +53 -0
  12. package/dist/http/route_spec.d.ts.map +1 -1
  13. package/dist/http/route_spec.js +14 -9
  14. package/dist/testing/CLAUDE.md +31 -3
  15. package/dist/testing/cross_backend/capabilities.d.ts +20 -7
  16. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  17. package/dist/testing/cross_backend/testing_backdoor.d.ts +6 -0
  18. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -0
  19. package/dist/testing/cross_backend/testing_backdoor.js +126 -0
  20. package/dist/testing/cross_backend/testing_reset_actions.d.ts +12 -3
  21. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  22. package/dist/testing/cross_backend/testing_reset_actions.js +40 -9
  23. package/dist/testing/cross_backend/testing_server_core.d.ts +13 -2
  24. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
  25. package/dist/testing/cross_backend/testing_server_core.js +19 -7
  26. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +5 -2
  27. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  28. package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
  29. package/dist/testing/mock_fs.d.ts +1 -0
  30. package/dist/testing/mock_fs.d.ts.map +1 -1
  31. package/dist/testing/mock_fs.js +1 -6
  32. package/dist/testing/surface_invariants.d.ts +34 -1
  33. package/dist/testing/surface_invariants.d.ts.map +1 -1
  34. package/dist/testing/surface_invariants.js +49 -1
  35. package/dist/testing/ws_round_trip.d.ts +1 -0
  36. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  37. package/dist/testing/ws_round_trip.js +1 -38
  38. package/package.json +1 -1
@@ -506,8 +506,10 @@ Per-message side-effect queues: `pending_effects` (eager) drains via
506
506
  handlers via `emit_after_commit`) drains via `flush_post_commit_effects`.
507
507
  Both flush in the same `try/finally` that releases the request controller,
508
508
  so fire-and-forget audit / notification effects pushed by the handler
509
- complete (or reject visibly) before the next message dispatches. See
510
- `http/CLAUDE.md` §Pending Effects.
509
+ complete (or reject visibly) before the next message dispatches. The
510
+ deferred queue is **discarded on rollback** before it reaches that flush (a
511
+ rolled-back message fires no post-commit effect). See `http/CLAUDE.md`
512
+ §Pending Effects.
511
513
 
512
514
  **Lifecycle hooks.** `on_socket_open({ws, connection_id, identity, notify, signal})`
513
515
  fires after `transport.add_connection` but before the first message;
@@ -1 +1 @@
1
- {"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,KAAK,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,OAAO,EAEN,KAAK,gBAAgB,EAErB,KAAK,kBAAkB,EACvB,MAAM,oBAAoB,CAAC;AAY5B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAA+B,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE7E;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,MAAM,EAAE,SAAS,CAAC;IAClB,mGAAmG;IACnG,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,yDAAyD;IACzD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uEAAuE;IACvE,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,MAAM,EAAE,WAAW,CAAC;IACpB,sFAAsF;IACtF,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,uDAAuD;IACvD,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAC,eAAe,EAAE,cAAc,GAAG,IAAI,CAAA;KAAC,CAAC;CAClD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IACjC,gGAAgG;IAChG,EAAE,EAAE,EAAE,CAAC;IACP;;;OAGG;IACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;OAGG;IACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3C,uEAAuE;IACvE,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC5B;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC;AAE9D;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAC1B,OAAO,kBAAkB,EACzB,MAAM,iBAAiB,KACrB,OAAO,CAAC,mBAAmB,CAwJ7B,CAAC;AAoFF;;;GAGG;AACH,eAAO,MAAM,iCAAiC,GAC7C,IAAI,gBAAgB,EACpB,QAAQ,mBAAmB,KACzB;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,gBAAgB,CAAA;CAAC,GAAG,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAAG;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAC,CAK5F,CAAC"}
1
+ {"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,KAAK,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAGpC,OAAO,EAEN,KAAK,gBAAgB,EAErB,KAAK,kBAAkB,EACvB,MAAM,oBAAoB,CAAC;AAY5B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAA+B,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE7E;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,MAAM,EAAE,SAAS,CAAC;IAClB,mGAAmG;IACnG,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,yDAAyD;IACzD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uEAAuE;IACvE,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,MAAM,EAAE,WAAW,CAAC;IACpB,sFAAsF;IACtF,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,uDAAuD;IACvD,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAC,eAAe,EAAE,cAAc,GAAG,IAAI,CAAA;KAAC,CAAC;CAClD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IACjC,gGAAgG;IAChG,EAAE,EAAE,EAAE,CAAC;IACP;;;OAGG;IACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;OAGG;IACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3C,uEAAuE;IACvE,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC5B;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC;AAE9D;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAC1B,OAAO,kBAAkB,EACzB,MAAM,iBAAiB,KACrB,OAAO,CAAC,mBAAmB,CA6J7B,CAAC;AAoFF;;;GAGG;AACH,eAAO,MAAM,iCAAiC,GAC7C,IAAI,gBAAgB,EACpB,QAAQ,mBAAmB,KACzB;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,gBAAgB,CAAA;CAAC,GAAG,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAAG;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAC,CAK5F,CAAC"}
@@ -41,6 +41,7 @@ import { DEV } from 'esm-env';
41
41
  import { apply_authorization_phase, has_any_scoped_role, } from '../auth/request_context.js';
42
42
  import {} from '../hono_context.js';
43
43
  import { is_void_schema } from '../http/schema_helpers.js';
44
+ import { dispatch_with_post_commit_rollback } from '../http/pending_effects.js';
44
45
  import { JSONRPC_VERSION, } from '../http/jsonrpc.js';
45
46
  import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, http_status_to_jsonrpc_error_code, JSONRPC_ERROR_CODES, } from '../http/jsonrpc_errors.js';
46
47
  import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, } from '../http/error_schemas.js';
@@ -159,11 +160,14 @@ export const perform_action = async (input, deps) => {
159
160
  }
160
161
  return { kind: 'ok', result: output };
161
162
  };
163
+ // Dispatch — transaction for mutations, pool for reads. Wrapped so a thrown
164
+ // handler discards the post-commit effects it queued (`emit_after_commit`):
165
+ // its transaction rolled back, so those effects must not announce state that
166
+ // never committed. The eager `pending_effects` queue survives rollback
167
+ // (attempt audits). See `dispatch_with_post_commit_rollback` (canonical
168
+ // contract) and docs/security.md §"Post-commit WS fan-out".
162
169
  try {
163
- if (use_transaction) {
164
- return await db.transaction((tx) => execute(tx));
165
- }
166
- return await execute(db);
170
+ return await dispatch_with_post_commit_rollback(post_commit_effects, () => use_transaction ? db.transaction((tx) => execute(tx)) : execute(db));
167
171
  }
168
172
  catch (err) {
169
173
  // Duck-type check: Error with numeric `code` signals a JSON-RPC error.
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA0B1B,OAAO,EAA4C,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACpG,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAiC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AA6BD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,EAAE,6BAavC,CAAC;AAIF;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG;IAC/C,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,CAmdjB,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA0B1B,OAAO,EAA4C,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACpG,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAiC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AA6BD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,EAAE,6BAavC,CAAC;AAIF;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG;IAC/C,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,CAodjB,CAAC"}
@@ -248,9 +248,10 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
248
248
  }));
249
249
  // Audit events are written in-transaction by query_accept_offer; wire
250
250
  // them through `audit.notify` post-commit so SSE/WS broadcasts fire.
251
- // WS notifications piggyback on the same post-commit microtask so the
252
- // grantor sees "accepted" and each superseded grantor sees
253
- // "supersede" only once the accept has durably committed.
251
+ // WS notifications ride the same deferred post-commit thunk so the
252
+ // grantor sees "accepted" and each superseded grantor sees "supersede"
253
+ // only once the accept has durably committed — and never if it rolls
254
+ // back (the dispatch site discards `post_commit_effects` on rollback).
254
255
  emit_after_commit(ctx, () => {
255
256
  fan_out_audit_events(result.audit_events, audit);
256
257
  if (notification_sender && grantor_account_id) {
@@ -99,13 +99,15 @@ declare module 'hono' {
99
99
  */
100
100
  pending_effects: Array<Promise<void>>;
101
101
  /**
102
- * Post-commit thunks pushed via `emit_after_commit(ctx, fn)`. The
103
- * flush middleware invokes each thunk after the handler returns —
104
- * never inline — so notifications (WS sends, etc.) cannot fire
105
- * mid-transaction. Producers do not push raw thunks directly. The
106
- * flush owns per-thunk `try/catch` + `log.error` so a directly-pushed
107
- * thunk (tests included) cannot escape the safety net.
108
- * Initialized by `create_app_server`. In test mode
102
+ * Post-commit thunks pushed via `emit_after_commit(ctx, fn)`. The flush
103
+ * middleware invokes each thunk after the handler returns — never inline
104
+ * — so notifications (WS sends, etc.) cannot fire mid-transaction; the
105
+ * queue is also discarded when the handler's transaction rolls back, so a
106
+ * thunk never fires for state that never committed. Full contract (the
107
+ * eager-`pending_effects` contrast, the discard mechanism, the flush
108
+ * safety net) lives on `dispatch_with_post_commit_rollback` /
109
+ * `emit_after_commit` in `http/pending_effects.ts`. Producers do not push
110
+ * raw thunks directly. Initialized by `create_app_server`; in test mode
109
111
  * (`await_pending_effects: true`), every thunk completes before the
110
112
  * response returns.
111
113
  */
@@ -1 +1 @@
1
- {"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAO9D;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,mDAInB,CAAC;AAEX,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAE7D,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;;WAKG;QACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B;;;;;WAKG;QACH,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;QACvC;;;;;;WAMG;QACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC;;;;;;;WAOG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC;;;;;;;;;;WAUG;QACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD;;;;WAIG;QACH,mBAAmB,EAAE,OAAO,CAAC;KAC7B;CACD"}
1
+ {"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAO9D;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,mDAInB,CAAC;AAEX,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAE7D,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;;WAKG;QACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B;;;;;WAKG;QACH,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;QACvC;;;;;;WAMG;QACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC;;;;;;;WAOG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC;;;;;;;;;;;;WAYG;QACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD;;;;WAIG;QACH,mBAAmB,EAAE,OAAO,CAAC;KAC7B;CACD"}
@@ -29,7 +29,7 @@ effects, see ../../../docs/architecture.md.
29
29
  - `http/jsonrpc_helpers.ts` — message builders, type guards, input/result normalizers.
30
30
  - `http/common_routes.ts` — health check + readiness probe (`/ready` schema-drift deploy gate) + authenticated server-status + surface route specs.
31
31
  - `http/db_routes.ts` — generic keeper-only table browser route specs (public schema).
32
- - `http/pending_effects.ts` — `emit_after_commit` + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext`.
32
+ - `http/pending_effects.ts` — `emit_after_commit` + `dispatch_with_post_commit_rollback` (the shared rollback-discard wrapper both dispatch sites use) + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext`.
33
33
 
34
34
  ## Route Spec System
35
35
 
@@ -427,7 +427,7 @@ this work that's already started"; thunk pushers say "run this after the
427
427
  handler returns" — and burying both behind one field made
428
428
  `c.var.pending_effects.push(x)` ambiguous at the call site. Splitting turns the field name into the contract.
429
429
 
430
- ### Why `emit_after_commit` defers
430
+ ### Why `emit_after_commit` defers — and discards on rollback
431
431
 
432
432
  The thunk shape is **load-bearing for correctness**. Pushing
433
433
  `Promise.resolve().then(fn)` onto an eager queue — what `emit_after_commit`
@@ -437,6 +437,17 @@ would leak a notification for state that never landed. The thunk defers
437
437
  the work to flush time; the `try/finally` in the flush middleware runs
438
438
  after the handler (and any wrapping transaction) returns.
439
439
 
440
+ Deferral alone isn't enough: the flush runs whether the handler returned or
441
+ threw, so a thrown handler (rolling back its transaction) would still fire the
442
+ thunk it queued. So the contract is **two-sided**: both dispatch sites
443
+ (`http/route_spec.ts`, `actions/perform_action.ts`) wrap their handler in the
444
+ shared `dispatch_with_post_commit_rollback` helper, which discards
445
+ `post_commit_effects` on a handler throw — `emit_after_commit` thus reads as
446
+ **"run iff the wrapping transaction commits."** The eager `pending_effects`
447
+ queue is the deliberate opposite (survives rollback — attempt audits). The full
448
+ two-sided contract + Rust-twin parity note live on the helper's TSDoc in
449
+ `http/pending_effects.ts`.
450
+
440
451
  ```typescript
441
452
  emit_after_commit(ctx, () => notification_sender.send_to_account(account_id, msg));
442
453
  ```
@@ -449,7 +460,7 @@ and any side effect that must run only after the transaction commits.
449
460
 
450
461
  - **The flush owns the safety net.** `flush_post_commit_effects` wraps every thunk in `try/catch` and routes errors through `ctx.log.error`, so one failing send cannot starve sibling effects in the same batch nor corrupt the already-committed response. Per-thunk `try/catch` inside `emit_after_commit` would skip directly-pushed thunks (e.g. tests); centralizing the wrap in the flush closes that gap.
451
462
  - **Test mode (`await_pending_effects: true`) flushes both queues.** Eager: `await flush_pending_effects(pending_effects, log)`. Deferred: `await flush_post_commit_effects(post_commit_effects, log)`. Both complete before the response returns. Production mode wraps the same helpers in `void ...` and threads `on_effect_error` into `flush_pending_effects`'s `on_rejection` callback for fan-out.
452
- - **Same drain location for both.** The outer flush middleware (`server/app_server.ts`) and the per-message WS flush handle the two queues adjacent to each other. The deferred queue does not drain inside the route-spec wrapper / `perform_action` that would tighten the "post-commit" timing further but would force three drain sites (REST wrapper, RPC dispatcher, WS dispatcher) to gain timing no current consumer needs.
463
+ - **Drain at the outer flush; discard at the dispatch site.** The successful-path _drain_ of both queues stays at the outer flush middleware (`server/app_server.ts`) + the per-message WS flush adjacent, one location, no per-wrapper drain timing. What the dispatch sites (route-spec wrapper / `perform_action`) own is the _rollback discard_, via the shared `dispatch_with_post_commit_rollback` helper: on a handler throw they truncate `post_commit_effects` before the outer flush ever sees it, so the flush only drains effects from a committed handler. The eager `pending_effects` queue is never truncated — it drains regardless (attempt audits survive rollback).
453
464
  - Structurally satisfied by both `RouteContext` (HTTP) and `ActionContext` (RPC + WS) — they share the `{log, post_commit_effects}` shape, which is why this helper lives in `http/` rather than `actions/` or `auth/`.
454
465
 
455
466
  WS sends are **not** wrapped by `create_validated_broadcaster` (that only
@@ -17,6 +17,15 @@
17
17
  * returns. Used for WS sends and any work that must observe a committed
18
18
  * transaction.
19
19
  *
20
+ * **Discard on rollback.** A `post_commit_effects` thunk is discarded if the
21
+ * handler's transaction rolls back (a thrown handler) — it must not announce
22
+ * state that never committed. Both dispatch sites enforce this by wrapping
23
+ * their handler in `dispatch_with_post_commit_rollback` (see its TSDoc below
24
+ * for the full two-sided contract and the Rust-twin parity note). The eager
25
+ * `pending_effects` queue is the deliberate opposite: its pool writes run
26
+ * outside the transaction and intentionally **survive** rollback (attempt
27
+ * audits).
28
+ *
20
29
  * The split exists because the two shapes encode different contracts:
21
30
  * eager pushers are saying "wait for this work that's already started";
22
31
  * thunk pushers are saying "run this after the handler returns." Burying
@@ -55,11 +64,45 @@ export interface EmitAfterCommitContext {
55
64
  * The flush owns the per-thunk `try/catch` + `log.error` so any
56
65
  * directly-pushed thunk (tests included) cannot escape the safety net.
57
66
  *
67
+ * Deferral is only half the contract: a queued thunk is also **discarded if
68
+ * the handler's transaction rolls back** — via `dispatch_with_post_commit_rollback`
69
+ * (see its TSDoc). So `fn` runs iff the wrapping transaction commits, never for
70
+ * state that rolled back. Rollback-resilient writes (attempt audits that must
71
+ * land even when the handler fails) belong on the eager `pending_effects` queue
72
+ * instead.
73
+ *
58
74
  * @param ctx - context carrying `log` and the `post_commit_effects` queue
59
75
  * @param fn - side effect to run after commit; may return `void` or `Promise<void>`
60
76
  * @mutates `ctx.post_commit_effects` - appends `fn` verbatim
61
77
  */
62
78
  export declare const emit_after_commit: (ctx: EmitAfterCommitContext, fn: () => void | Promise<void>) => void;
79
+ /**
80
+ * Run a handler dispatch (the handler plus its wrapping `db.transaction`),
81
+ * discarding any `post_commit_effects` it queued via `emit_after_commit` if it
82
+ * throws. A thrown handler rolls back its transaction, so firing those deferred
83
+ * effects would announce state that never committed. On any throw the queue is
84
+ * truncated back to its pre-dispatch depth — **not** cleared, so entries a
85
+ * surrounding scope pre-seeded survive — and the error re-thrown unchanged.
86
+ *
87
+ * The eager `pending_effects` queue is deliberately never touched here: its
88
+ * pool writes run outside the transaction and intentionally survive rollback
89
+ * (attempt audits). `emit_after_commit` thus reads as "run iff the wrapping
90
+ * transaction commits."
91
+ *
92
+ * Both dispatch sites — the REST route wrapper (`http/route_spec.ts`) and the
93
+ * action dispatcher (`actions/perform_action.ts`, the RPC + WS path) — wrap
94
+ * their handler call in this so the discard contract lives in one place. The
95
+ * Rust `fuz_actions` spine pins the same contract.
96
+ *
97
+ * `post_commit_effects` is `undefined` when a handler runs without the
98
+ * app-server pending-effects middleware (bare route / dispatch harnesses):
99
+ * absent ⇒ nothing queued ⇒ nothing to discard.
100
+ *
101
+ * @param post_commit_effects - the deferred queue to truncate on throw, or `undefined`
102
+ * @param dispatch - invokes the handler (and any wrapping transaction)
103
+ * @mutates `post_commit_effects` - truncated to its pre-dispatch depth on throw
104
+ */
105
+ export declare const dispatch_with_post_commit_rollback: <T>(post_commit_effects: Array<() => void | Promise<void>> | undefined, dispatch: () => T | Promise<T>) => Promise<Awaited<T>>;
63
106
  /**
64
107
  * Drain an eager `pending_effects` queue: `Promise.allSettled` the
65
108
  * in-flight handles, route every rejection through `log.error`, and
@@ -1 +1 @@
1
- {"version":3,"file":"pending_effects.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/pending_effects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,KAAK,sBAAsB,EAC3B,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAC5B,IAEF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EACrC,KAAK,MAAM,EACX,eAAe,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,KACtC,OAAO,CAAC,IAAI,CASd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,aAAa,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAClD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAiBd,CAAC"}
1
+ {"version":3,"file":"pending_effects.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/pending_effects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,KAAK,sBAAsB,EAC3B,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAC5B,IAEF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,kCAAkC,GAAU,CAAC,EACzD,qBAAqB,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,EAClE,UAAU,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,KAC5B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAQpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EACrC,KAAK,MAAM,EACX,eAAe,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,KACtC,OAAO,CAAC,IAAI,CASd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,aAAa,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAClD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAiBd,CAAC"}
@@ -17,6 +17,15 @@
17
17
  * returns. Used for WS sends and any work that must observe a committed
18
18
  * transaction.
19
19
  *
20
+ * **Discard on rollback.** A `post_commit_effects` thunk is discarded if the
21
+ * handler's transaction rolls back (a thrown handler) — it must not announce
22
+ * state that never committed. Both dispatch sites enforce this by wrapping
23
+ * their handler in `dispatch_with_post_commit_rollback` (see its TSDoc below
24
+ * for the full two-sided contract and the Rust-twin parity note). The eager
25
+ * `pending_effects` queue is the deliberate opposite: its pool writes run
26
+ * outside the transaction and intentionally **survive** rollback (attempt
27
+ * audits).
28
+ *
20
29
  * The split exists because the two shapes encode different contracts:
21
30
  * eager pushers are saying "wait for this work that's already started";
22
31
  * thunk pushers are saying "run this after the handler returns." Burying
@@ -45,6 +54,13 @@
45
54
  * The flush owns the per-thunk `try/catch` + `log.error` so any
46
55
  * directly-pushed thunk (tests included) cannot escape the safety net.
47
56
  *
57
+ * Deferral is only half the contract: a queued thunk is also **discarded if
58
+ * the handler's transaction rolls back** — via `dispatch_with_post_commit_rollback`
59
+ * (see its TSDoc). So `fn` runs iff the wrapping transaction commits, never for
60
+ * state that rolled back. Rollback-resilient writes (attempt audits that must
61
+ * land even when the handler fails) belong on the eager `pending_effects` queue
62
+ * instead.
63
+ *
48
64
  * @param ctx - context carrying `log` and the `post_commit_effects` queue
49
65
  * @param fn - side effect to run after commit; may return `void` or `Promise<void>`
50
66
  * @mutates `ctx.post_commit_effects` - appends `fn` verbatim
@@ -52,6 +68,43 @@
52
68
  export const emit_after_commit = (ctx, fn) => {
53
69
  ctx.post_commit_effects.push(fn);
54
70
  };
71
+ /**
72
+ * Run a handler dispatch (the handler plus its wrapping `db.transaction`),
73
+ * discarding any `post_commit_effects` it queued via `emit_after_commit` if it
74
+ * throws. A thrown handler rolls back its transaction, so firing those deferred
75
+ * effects would announce state that never committed. On any throw the queue is
76
+ * truncated back to its pre-dispatch depth — **not** cleared, so entries a
77
+ * surrounding scope pre-seeded survive — and the error re-thrown unchanged.
78
+ *
79
+ * The eager `pending_effects` queue is deliberately never touched here: its
80
+ * pool writes run outside the transaction and intentionally survive rollback
81
+ * (attempt audits). `emit_after_commit` thus reads as "run iff the wrapping
82
+ * transaction commits."
83
+ *
84
+ * Both dispatch sites — the REST route wrapper (`http/route_spec.ts`) and the
85
+ * action dispatcher (`actions/perform_action.ts`, the RPC + WS path) — wrap
86
+ * their handler call in this so the discard contract lives in one place. The
87
+ * Rust `fuz_actions` spine pins the same contract.
88
+ *
89
+ * `post_commit_effects` is `undefined` when a handler runs without the
90
+ * app-server pending-effects middleware (bare route / dispatch harnesses):
91
+ * absent ⇒ nothing queued ⇒ nothing to discard.
92
+ *
93
+ * @param post_commit_effects - the deferred queue to truncate on throw, or `undefined`
94
+ * @param dispatch - invokes the handler (and any wrapping transaction)
95
+ * @mutates `post_commit_effects` - truncated to its pre-dispatch depth on throw
96
+ */
97
+ export const dispatch_with_post_commit_rollback = async (post_commit_effects, dispatch) => {
98
+ const depth = post_commit_effects?.length ?? 0;
99
+ try {
100
+ return await dispatch();
101
+ }
102
+ catch (err) {
103
+ if (post_commit_effects)
104
+ post_commit_effects.length = depth;
105
+ throw err;
106
+ }
107
+ };
55
108
  /**
56
109
  * Drain an eager `pending_effects` queue: `Promise.allSettled` the
57
110
  * in-flight handles, route every rejection through `log.error`, and
@@ -1 +1 @@
1
- {"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAW,IAAI,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AACpE,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAE3B,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAKjB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAyC,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAEvF;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IAC1B,cAAc,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACzC,kBAAkB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,UAAU,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAE7F,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,EAAE,EAAE,EAAE,CAAC;IACP;;;;;OAKG;IACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;;;;;OAOG;IACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE7F;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,mEAAmE;IACnE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC;IAClB;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACxF,wBAAgB,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC;AAK5D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACzF,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC;AAK7D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACxF,wBAAgB,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC;AAoJ5D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,KAAK,IAAI,EAAE,OAAO,KAAK,CAAC,cAAc,CAAC,KAAG,IAIhF,CAAC;AAkFF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,iBAAiB,GAC7B,KAAK,IAAI,EACT,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,qBAAqB,iBAAiB,EACtC,KAAK,MAAM,EACX,IAAI,EAAE,EACN,YAAY,oBAAoB,KAC9B,IAkEF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,EAAE,OAAO,KAAK,CAAC,SAAS,CAAC,KAAG,KAAK,CAAC,SAAS,CAK3F,CAAC"}
@@ -16,6 +16,7 @@ import { DEV } from 'esm-env';
16
16
  import { ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_ROUTE_PARAMS, ERROR_INVALID_QUERY_PARAMS, } from './error_schemas.js';
17
17
  import { ThrownJsonrpcError, jsonrpc_error_code_to_http_status, jsonrpc_error_code_to_name, } from './jsonrpc_errors.js';
18
18
  import { is_null_schema, merge_error_schemas } from './schema_helpers.js';
19
+ import { dispatch_with_post_commit_rollback } from './pending_effects.js';
19
20
  import { assert_route_auth_acting_biconditional } from './auth_shape.js';
20
21
  export function get_route_input(c, _schema) {
21
22
  return c.get('validated_input');
@@ -321,17 +322,21 @@ export const apply_route_specs = (app, specs, resolve_auth_guards, log, db, auth
321
322
  // Step 1: adapt RouteHandler → Handler (construct RouteContext, call spec.handler)
322
323
  const use_transaction = spec.transaction ?? spec.method !== 'GET';
323
324
  const inner = spec.handler;
324
- let handler = use_transaction
325
- ? (c) => db.transaction(async (tx) => inner(c, {
326
- db: tx,
325
+ // Discard post-commit effects a handler queued (`emit_after_commit`) if it
326
+ // throws a thrown handler rolls back its wrapping transaction, so firing
327
+ // those effects would announce state that never committed. The eager
328
+ // `pending_effects` queue (attempt audits, run outside the transaction) is
329
+ // untouched. Shared with the action dispatcher (actions/perform_action.ts)
330
+ // via `dispatch_with_post_commit_rollback`.
331
+ let handler = (c) => {
332
+ const route_context = {
327
333
  pending_effects: c.var.pending_effects,
328
334
  post_commit_effects: c.var.post_commit_effects,
329
- }))
330
- : (c) => inner(c, {
331
- db,
332
- pending_effects: c.var.pending_effects,
333
- post_commit_effects: c.var.post_commit_effects,
334
- });
335
+ };
336
+ return dispatch_with_post_commit_rollback(c.var.post_commit_effects, () => use_transaction
337
+ ? db.transaction(async (tx) => inner(c, { db: tx, ...route_context }))
338
+ : inner(c, { db, ...route_context }));
339
+ };
335
340
  // Step 2: output validation
336
341
  handler = wrap_output_validation(handler, spec.output, merged_errors, log);
337
342
  // Step 3: error catch layer
@@ -13,9 +13,13 @@ testing-patterns. This file is a reference index for the helpers themselves.
13
13
 
14
14
  ## Production guard — always the first import
15
15
 
16
- Every module here starts with `import './assert_dev_env.js';` — reads `DEV`
17
- from `esm-env` and throws if false, preventing production-bundle inclusion.
18
- Enforced by grep, not a linter; make this the first line in new modules.
16
+ Every runtime-reachable module here starts with `import './assert_dev_env.js';`
17
+ — reads `DEV` from `esm-env` and throws if false, preventing production-bundle
18
+ inclusion. Make this the first line in new modules. Enforced by
19
+ `src/test/testing/assert_dev_env_coverage.test.ts`, which fails if any module
20
+ omits the guard. The sole exemption is `cross_backend/make_cross_backend_project.ts`
21
+ — a vitest-project factory consumed by consumers' `vite.config.ts` at config
22
+ time (never runtime), where a throwing guard would break `vite build`.
19
23
 
20
24
  ## Stubs, factories, mocks
21
25
 
@@ -274,6 +278,7 @@ RPC / WS structural invariants (options-free, apply over `surface.rpc_endpoints`
274
278
  * `assert_ws_method_descriptions_present` — every WS method on every endpoint has a non-empty `description`.
275
279
  * `assert_ws_endpoints_include_protocol_actions` — every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`).
276
280
  * `assert_ws_notifications_have_null_auth` — WS method `kind === 'remote_notification' ⟺ auth === null`; guards against drift between spec union and surface emitter.
281
+ * `assert_no_testing_methods` — no `_testing_*` backdoor action (`TESTING_METHOD_PREFIX`) appears as an RPC or WS method on the declared surface. The test-binary actions are live-mounted only; this guards against a future wiring change folding `create_testing_actions(...)` into the surface-generating registry.
277
282
 
278
283
  Per-endpoint duplicate method names and the auth-shape biconditional are
279
284
  already enforced at startup by `compile_action_registry` (see
@@ -1208,6 +1213,29 @@ spine bootstrap (auth + cell + cell_history + fact) and is regenerated +
1208
1213
  drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
1209
1214
  (`UPDATE_SCHEMA_READY=1`, then `gro format`).
1210
1215
 
1216
+ ### Testing-backdoor credential gate — `cross_backend/testing_backdoor.ts`
1217
+
1218
+ `describe_testing_backdoor_cross_tests({setup_test, capabilities, rpc_path?})` —
1219
+ the negative-credential parity suite for the `_testing_*` backdoor actions.
1220
+ For each of `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
1221
+ `_testing_schema_snapshot` (the three privileged writes plus the schema-dump
1222
+ read) it fires three principals over real HTTP: **anonymous** → 401, **session** →
1223
+ 403 `credential_type_required`, **bearer** → 403 `credential_type_required` —
1224
+ proving the daemon-token gate that fences each backdoor action holds end-to-end
1225
+ on the real dispatcher (the spec-derived `describe_rpc_attack_surface_tests`
1226
+ never enumerates them because they're off the declared surface). Each method is
1227
+ sent with **valid** params so the session/bearer cases clear the 400
1228
+ input-validation phase and reach the 403 credential gate (order is
1229
+ 401 → 400 → 403); the handler never runs. Cited property: `docs/security.md`
1230
+ §Test Backdoor Actions. Cross-process only (the `_testing_*` actions are
1231
+ mounted on the spawned binary, not the in-process app — like the ws/sse
1232
+ suites); ungated, since every cross backend that uses
1233
+ `default_cross_process_setup` mounts them. fuz_app's own wiring is
1234
+ `src/test/cross_backend/testing_backdoor.cross.test.ts`. The complement is the
1235
+ in-process spec-level gate test (`src/test/testing/testing_actions_auth.test.ts`,
1236
+ asserting each spec declares `credential_types: ['daemon_token']`) + the
1237
+ `assert_no_testing_methods` surface invariant.
1238
+
1211
1239
  ### Building a TS test-server binary — `testing_server_core.ts` + adapters
1212
1240
 
1213
1241
  The reusable shape for standing up a **spawnable TS** cross-process test
@@ -7,19 +7,32 @@ import '../assert_dev_env.js';
7
7
  export interface BackendCapabilities {
8
8
  /**
9
9
  * Bearer token auth (`Authorization: Bearer <token>`) is wired through
10
- * the backend's middleware stack. Gates the bearer-token cases in
11
- * `describe_standard_integration_tests` and `describe_rate_limiting_tests`.
10
+ * the backend's middleware stack.
11
+ *
12
+ * **Declared for backend-shape documentation, not gating.** No suite reads
13
+ * this flag — the bearer-token cases in `describe_standard_integration_tests`
14
+ * / `describe_rate_limiting_tests` run unconditionally (every spine wires
15
+ * bearer auth). Fold into a typed capability taxonomy if these gain real
16
+ * gating readers.
12
17
  */
13
18
  readonly bearer_auth: boolean;
14
19
  /**
15
- * Trusted-proxy XFF parsing is wired (`X-Forwarded-For` etc.). Gates
16
- * the proxy-resolution cases in `describe_standard_integration_tests`
17
- * and the future cross-process proxy integration suite.
20
+ * Trusted-proxy XFF parsing is wired (`X-Forwarded-For` etc.).
21
+ *
22
+ * **Declared for backend-shape documentation, not gating.** No suite reads
23
+ * this flag (there is no cross-process proxy-resolution suite); it records
24
+ * the proxy-default difference between the TS family (`false`) and the Rust
25
+ * family (`true`). Fold into a typed capability taxonomy if it gains real
26
+ * gating readers.
18
27
  */
19
28
  readonly trusted_proxy: boolean;
20
29
  /**
21
- * Per-account login rate limiting is wired. Gates the per-account
22
- * rate-limit cases in `describe_rate_limiting_tests`.
30
+ * Per-account login rate limiting is wired.
31
+ *
32
+ * **Declared for backend-shape documentation, not gating.** No suite reads
33
+ * this flag; the `describe_rate_limiting_tests` per-account cases are
34
+ * in-process-only and don't cross a process boundary. Fold into a typed
35
+ * capability taxonomy if it gains real gating readers.
23
36
  */
24
37
  readonly login_rate_limit: boolean;
25
38
  /**
@@ -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;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;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;;;;;;;;OAQG;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;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAWpC,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;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;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;;;;;;;;OAQG;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;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAWpC,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"}
@@ -0,0 +1,6 @@
1
+ import '../assert_dev_env.js';
2
+ import type { CellCrossTestOptions } from './cell_cross_helpers.js';
3
+ /** Options for the testing-backdoor negative-credential suite. */
4
+ export type TestingBackdoorCrossTestOptions = CellCrossTestOptions;
5
+ export declare const describe_testing_backdoor_cross_tests: (options: TestingBackdoorCrossTestOptions) => void;
6
+ //# sourceMappingURL=testing_backdoor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyD9B,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,yBAAyB,CAAC;AAgElE,kEAAkE;AAClE,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,CAAC;AAEnE,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAiCF,CAAC"}
@@ -0,0 +1,126 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend negative-credential suite for the `_testing_*` backdoor
4
+ * actions.
5
+ *
6
+ * `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
7
+ * `_testing_schema_snapshot` are privileged test-binary actions the
8
+ * production wire never exposes — three direct DB writes (full auth wipe,
9
+ * forged session row, raw fact insert) plus a full-schema introspection read
10
+ * (the highest info-leak of the set were the gate to break). Their only
11
+ * structural fence is the **daemon-token** credential gate on
12
+ * each spec's `auth` axis. A test binary live-mounts them on its RPC
13
+ * endpoint but keeps them off the declared surface — so the spec-derived
14
+ * `describe_rpc_attack_surface_tests` never enumerates them, and nothing
15
+ * else fires them with a non-daemon credential to prove the gate holds
16
+ * end-to-end. This suite does, against each impl's real auth resolution.
17
+ *
18
+ * For every backdoor method, three principals:
19
+ *
20
+ * - **anonymous** (no credential) → `401` (pre-validation auth refuses an
21
+ * account-less caller before anything else).
22
+ * - **session** (the keeper's browser-context cookie) → `403`
23
+ * `credential_type_required` — a session cookie, even one carrying the
24
+ * keeper role, tops out below the daemon-token channel.
25
+ * - **bearer** (the keeper's api-token, non-browser context) → `403`
26
+ * `credential_type_required` — same ceiling; an api token cannot reach
27
+ * keeper operations.
28
+ *
29
+ * Each method is sent with **valid** params so the session/bearer cases
30
+ * clear the dispatcher's input-validation (400) phase and actually reach the
31
+ * post-authorization credential gate (the order is 401 → 400 → 403); the
32
+ * handler never runs (the gate refuses first), so the writes never execute.
33
+ *
34
+ * Complements the spec-level gate check (which pins that each spec *declares*
35
+ * `credential_types: ['daemon_token']`) and the surface-absence invariant
36
+ * (`assert_no_testing_methods`) — this one pins the runtime 401/403 behavior
37
+ * on both impls. Cited property: `security.md` §Test Backdoor Actions
38
+ * (daemon-token-gated, off-surface, DEV-excluded).
39
+ *
40
+ * Cross-process only — the `_testing_*` actions are mounted on the spawned
41
+ * binary, not the in-process app — like the ws/sse suites. Wire from a
42
+ * `*.cross.test.ts`. Requires the standard `_testing_*` actions mounted (the
43
+ * same precondition `default_cross_process_setup` already imposes for its
44
+ * per-test `_testing_reset`); ungated, since every cross backend mounts them.
45
+ *
46
+ * `$lib`-free by contract (relative specifiers only), like the sibling
47
+ * cross-backend suites.
48
+ *
49
+ * @module
50
+ */
51
+ import { describe, test, assert } from 'vitest';
52
+ import { ERROR_CREDENTIAL_TYPE_REQUIRED } from '../../http/error_schemas.js';
53
+ import { rpc_call } from '../rpc_helpers.js';
54
+ import { SPINE_RPC_PATH } from './default_spine_surface.js';
55
+ /** A well-formed UUID that never names a real row. */
56
+ const NIL_UUID = '00000000-0000-0000-0000-000000000000';
57
+ /**
58
+ * The backdoor methods + a **valid** params payload each (so the
59
+ * session/bearer cases reach the 403 credential gate rather than a 400 on
60
+ * input validation). The handlers never run — the gate refuses first.
61
+ */
62
+ const backdoor_methods = [
63
+ { method: '_testing_reset', params: {} },
64
+ { method: '_testing_mint_session', params: { account_id: NIL_UUID, expires_in_seconds: -60 } },
65
+ { method: '_testing_put_fact', params: { content: 'backdoor-probe' } },
66
+ // The schema-dump read — `exclude_tables` is optional, so `{}` is valid
67
+ // and clears the 400 phase like the writes above.
68
+ { method: '_testing_schema_snapshot', params: {} },
69
+ ];
70
+ const principals = [
71
+ {
72
+ name: 'anonymous',
73
+ status: 401,
74
+ // Fresh jar so the keeper cookie (cross-process) can't leak in.
75
+ resolve: (f) => ({ transport: f.fresh_transport(), headers: {} }),
76
+ },
77
+ {
78
+ name: 'session',
79
+ status: 403,
80
+ reason: ERROR_CREDENTIAL_TYPE_REQUIRED,
81
+ resolve: (f) => ({ transport: f.transport, headers: f.create_session_headers() }),
82
+ },
83
+ {
84
+ name: 'bearer',
85
+ status: 403,
86
+ reason: ERROR_CREDENTIAL_TYPE_REQUIRED,
87
+ // Bearer is discarded in a browser context, so suppress Origin (empty
88
+ // jar + no Origin) — the credential must actually resolve so the refusal
89
+ // lands on the credential-type gate, not on bearer-discard (→ 401).
90
+ resolve: (f) => ({
91
+ transport: f.fresh_transport({ origin: null }),
92
+ headers: f.create_bearer_headers(),
93
+ suppress_default_origin: true,
94
+ }),
95
+ },
96
+ ];
97
+ export const describe_testing_backdoor_cross_tests = (options) => {
98
+ const { setup_test } = options;
99
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
100
+ describe('testing backdoor credential gate parity', () => {
101
+ for (const { method, params } of backdoor_methods) {
102
+ for (const principal of principals) {
103
+ test(`${method} rejects ${principal.name} → ${principal.status}`, async () => {
104
+ const fixture = await setup_test();
105
+ const { transport, headers, suppress_default_origin } = principal.resolve(fixture);
106
+ const res = await rpc_call({
107
+ app: transport,
108
+ path: rpc_path,
109
+ method,
110
+ params,
111
+ headers,
112
+ ...(suppress_default_origin && { suppress_default_origin: true }),
113
+ });
114
+ const label = `${method} ${principal.name}`;
115
+ assert.ok(!res.ok, `${label}: expected denial (${principal.status}) but the call succeeded`);
116
+ assert.strictEqual(res.status, principal.status, `${label}: status`);
117
+ // `!res.ok` narrows `res` to the error variant for `res.error`.
118
+ if (principal.reason !== undefined && !res.ok) {
119
+ const reason = res.error.data?.reason;
120
+ assert.strictEqual(reason, principal.reason, `${label}: error.data.reason`);
121
+ }
122
+ });
123
+ }
124
+ }
125
+ });
126
+ };
@@ -183,9 +183,18 @@ export declare const testing_drain_effects_action_spec: {
183
183
  * `_testing_mint_session` — mint an expired-by-construction server-side
184
184
  * session for an existing account and return its signed cookie value.
185
185
  *
186
- * The minted `auth_session` row's `expires_at` is backdated (negative
187
- * `expires_in_seconds`) while the returned cookie's own signed payload
188
- * stays valid (future). Cross-process auth resolution therefore passes the
186
+ * `expires_in_seconds` is **constrained negative** (`z.number().int().negative()`)
187
+ * so the action is structurally incapable of minting a *usable* session: it
188
+ * can only produce an already-backdated, already-dead `auth_session` row. The
189
+ * daemon-token gate + loopback binding already fence the backdoor, but the
190
+ * negative constraint is the make-impossible-states floor — even a misuse
191
+ * can't forge a valid session for an arbitrary `account_id`. The Rust mirror
192
+ * (`fuz_testing::create_testing_mint_session_action_spec`) enforces the same
193
+ * floor.
194
+ *
195
+ * The minted `auth_session` row's `expires_at` is backdated while the
196
+ * returned cookie's own signed payload stays valid (future). Cross-process
197
+ * auth resolution therefore passes the
189
198
  * cookie-payload gate (`parse_session`) and is refused by the authoritative
190
199
  * DB-row gate (`query_session_get_valid` — `WHERE expires_at > NOW()`) —
191
200
  * the gate the in-process payload-expiry tests never reach and the one that
@@ -1 +1 @@
1
- {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAGvE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,gBAAgB,CAAC;AAmBvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAeC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACiB,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAqIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
1
+ {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAIvE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,gBAAgB,CAAC;AAkCvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACiB,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAqIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
@@ -68,6 +68,20 @@ import { auth_integration_truncate_tables } from '../db.js';
68
68
  import { query_schema_snapshot, SchemaSnapshot } from '../schema_introspect.js';
69
69
  import { query_create_actor } from '../../auth/account_queries.js';
70
70
  import { create_test_account_with_credentials, mint_test_session, DEFAULT_TEST_PASSWORD, } from '../app_server.js';
71
+ /**
72
+ * Shared `auth` axis for every `_testing_*` action: keeper-only via the
73
+ * daemon-token credential, no acting actor. This is the entire structural
74
+ * fence on the backdoor surface (these actions run direct DB writes the
75
+ * production wire never exposes), so all five specs reference this one const
76
+ * rather than re-declaring it — a single source of truth the gate test
77
+ * (`testing_actions_auth.test.ts`) pins. Mirrors the Rust `DAEMON_TOKEN_ONLY`
78
+ * / shared `AuthSpec` in `fuz_testing`.
79
+ */
80
+ const TESTING_ACTION_AUTH = {
81
+ account: 'required',
82
+ actor: 'none',
83
+ credential_types: ['daemon_token'],
84
+ };
71
85
  /** Output shape for an individual seeded account (keeper or extra). */
72
86
  const SeededAccountShape = z.strictObject({
73
87
  account: z.strictObject({ id: Uuid, username: z.string() }),
@@ -104,7 +118,7 @@ export const testing_reset_action_spec = {
104
118
  method: '_testing_reset',
105
119
  kind: 'request_response',
106
120
  initiator: 'frontend',
107
- auth: { account: 'required', actor: 'none', credential_types: ['daemon_token'] },
121
+ auth: TESTING_ACTION_AUTH,
108
122
  side_effects: true,
109
123
  input: z.strictObject({
110
124
  extra_keeper_roles: z.array(z.string()).optional(),
@@ -154,7 +168,7 @@ export const testing_drain_effects_action_spec = {
154
168
  method: '_testing_drain_effects',
155
169
  kind: 'request_response',
156
170
  initiator: 'frontend',
157
- auth: { account: 'required', actor: 'none', credential_types: ['daemon_token'] },
171
+ auth: TESTING_ACTION_AUTH,
158
172
  side_effects: false,
159
173
  input: z.void(),
160
174
  output: z.strictObject({ ok: z.boolean() }),
@@ -165,9 +179,18 @@ export const testing_drain_effects_action_spec = {
165
179
  * `_testing_mint_session` — mint an expired-by-construction server-side
166
180
  * session for an existing account and return its signed cookie value.
167
181
  *
168
- * The minted `auth_session` row's `expires_at` is backdated (negative
169
- * `expires_in_seconds`) while the returned cookie's own signed payload
170
- * stays valid (future). Cross-process auth resolution therefore passes the
182
+ * `expires_in_seconds` is **constrained negative** (`z.number().int().negative()`)
183
+ * so the action is structurally incapable of minting a *usable* session: it
184
+ * can only produce an already-backdated, already-dead `auth_session` row. The
185
+ * daemon-token gate + loopback binding already fence the backdoor, but the
186
+ * negative constraint is the make-impossible-states floor — even a misuse
187
+ * can't forge a valid session for an arbitrary `account_id`. The Rust mirror
188
+ * (`fuz_testing::create_testing_mint_session_action_spec`) enforces the same
189
+ * floor.
190
+ *
191
+ * The minted `auth_session` row's `expires_at` is backdated while the
192
+ * returned cookie's own signed payload stays valid (future). Cross-process
193
+ * auth resolution therefore passes the
171
194
  * cookie-payload gate (`parse_session`) and is refused by the authoritative
172
195
  * DB-row gate (`query_session_get_valid` — `WHERE expires_at > NOW()`) —
173
196
  * the gate the in-process payload-expiry tests never reach and the one that
@@ -186,11 +209,19 @@ export const testing_mint_session_action_spec = {
186
209
  method: '_testing_mint_session',
187
210
  kind: 'request_response',
188
211
  initiator: 'frontend',
189
- auth: { account: 'required', actor: 'none', credential_types: ['daemon_token'] },
212
+ auth: TESTING_ACTION_AUTH,
190
213
  side_effects: true,
191
214
  input: z.strictObject({
192
215
  account_id: Uuid,
193
- expires_in_seconds: z.number().int(),
216
+ expires_in_seconds: z
217
+ .number()
218
+ .int()
219
+ .negative()
220
+ .meta({
221
+ description: 'Seconds to offset the session row from NOW(). Constrained negative so this ' +
222
+ 'backdoor can ONLY mint an already-expired (backdated) row — never a usable ' +
223
+ 'session for an arbitrary account. Its sole use is the expired-session gate.',
224
+ }),
194
225
  }),
195
226
  output: z.strictObject({ session_cookie: z.string() }),
196
227
  async: true,
@@ -223,7 +254,7 @@ export const testing_put_fact_action_spec = {
223
254
  method: '_testing_put_fact',
224
255
  kind: 'request_response',
225
256
  initiator: 'frontend',
226
- auth: { account: 'required', actor: 'none', credential_types: ['daemon_token'] },
257
+ auth: TESTING_ACTION_AUTH,
227
258
  side_effects: true,
228
259
  input: z.strictObject({
229
260
  content: z.string(),
@@ -248,7 +279,7 @@ export const testing_schema_snapshot_action_spec = {
248
279
  method: '_testing_schema_snapshot',
249
280
  kind: 'request_response',
250
281
  initiator: 'frontend',
251
- auth: { account: 'required', actor: 'none', credential_types: ['daemon_token'] },
282
+ auth: TESTING_ACTION_AUTH,
252
283
  side_effects: false,
253
284
  input: z.strictObject({ exclude_tables: z.array(z.string()).optional() }),
254
285
  output: SchemaSnapshot,
@@ -127,14 +127,25 @@ export interface StartTestingServerOptions {
127
127
  /** Optional logger; defaults to a `[daemon_name]`-namespaced `Logger`. */
128
128
  log?: LoggerType;
129
129
  }
130
+ /**
131
+ * Loopback bind hosts — the only ones the test binary may serve on. It ships
132
+ * deterministic dev secrets (fixed cookie keys + bootstrap token in
133
+ * `default_secrets.ts`), so binding any network-reachable interface would let
134
+ * anyone who knows those fixed keys forge cookies against it. An allowlist
135
+ * (not an `0.0.0.0`/`::` blocklist) closes the gap a concrete LAN/public
136
+ * interface IP — e.g. `--host 192.168.1.50` — would otherwise slip through.
137
+ * Covers `localhost`, the IPv4 loopback `127.0.0.0/8`, and IPv6 `::1`.
138
+ */
139
+ export declare const is_loopback_host: (host: string) => boolean;
130
140
  /**
131
141
  * Boot a test-mode server using the supplied runtime adapter.
132
142
  *
133
143
  * Mirrors a production `start_server` at the surface level — stale-daemon
134
144
  * check, daemon-info write, bind, graceful drain — but the app is the
135
145
  * caller's no-domain (or domain) {@link StartTestingServerOptions.build_app}
136
- * and the runtime boundary is the {@link TestingServerAdapter}. Refuses to
137
- * bind an open host (the test binary must stay on loopback).
146
+ * and the runtime boundary is the {@link TestingServerAdapter}. Refuses any
147
+ * non-loopback bind host (the test binary must stay on loopback — see
148
+ * `is_loopback_host`).
138
149
  */
139
150
  export declare const start_testing_server: (options: StartTestingServerOptions) => Promise<void>;
140
151
  //# sourceMappingURL=testing_server_core.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing_server_core.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_core.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAG1E,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,iBAAiB,EAAE,gBAAgB,CAAC;IACpC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACpC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,OAAO,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,mFAAmF;IACnF,iBAAiB,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,iBAAiB,CAAC;IACpD,8EAA8E;IAC9E,KAAK,EAAE,CAAC,OAAO,EAAE;QAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,KAAK,WAAW,CAAC;IACxF,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,yBAAyB,EAAE,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAClE,oEAAoE;IACpE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,GAAG,EAAE,IAAI,CAAC;IACV,qEAAqE;IACrE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,uEAAuE;IACvE,eAAe,CAAC,EAAE,CAAC,iBAAiB,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChE;AAED,gDAAgD;AAChD,MAAM,WAAW,yBAAyB;IACzC,+CAA+C;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,GAAG,CAAC,EAAE,UAAU,CAAC;CACjB;AAKD;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAU,SAAS,yBAAyB,KAAG,OAAO,CAAC,IAAI,CA4D3F,CAAC"}
1
+ {"version":3,"file":"testing_server_core.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_core.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAG1E,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,iBAAiB,EAAE,gBAAgB,CAAC;IACpC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACpC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,OAAO,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,mFAAmF;IACnF,iBAAiB,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,iBAAiB,CAAC;IACpD,8EAA8E;IAC9E,KAAK,EAAE,CAAC,OAAO,EAAE;QAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,KAAK,WAAW,CAAC;IACxF,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,yBAAyB,EAAE,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAClE,oEAAoE;IACpE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,GAAG,EAAE,IAAI,CAAC;IACV,qEAAqE;IACrE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,uEAAuE;IACvE,eAAe,CAAC,EAAE,CAAC,iBAAiB,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChE;AAED,gDAAgD;AAChD,MAAM,WAAW,yBAAyB;IACzC,+CAA+C;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,GAAG,CAAC,EAAE,UAAU,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,OAG/C,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAAU,SAAS,yBAAyB,KAAG,OAAO,CAAC,IAAI,CA4D3F,CAAC"}
@@ -1,24 +1,36 @@
1
1
  import '../assert_dev_env.js';
2
2
  import { Logger } from '@fuzdev/fuz_util/log.js';
3
3
  import { write_daemon_info, read_daemon_info, is_daemon_running } from '../../cli/daemon.js';
4
- /** Hosts that expose the test binary beyond loopback — refused at startup. */
5
- const OPEN_HOSTS = new Set(['0.0.0.0', '::', '[::]']);
4
+ /**
5
+ * Loopback bind hosts — the only ones the test binary may serve on. It ships
6
+ * deterministic dev secrets (fixed cookie keys + bootstrap token in
7
+ * `default_secrets.ts`), so binding any network-reachable interface would let
8
+ * anyone who knows those fixed keys forge cookies against it. An allowlist
9
+ * (not an `0.0.0.0`/`::` blocklist) closes the gap a concrete LAN/public
10
+ * interface IP — e.g. `--host 192.168.1.50` — would otherwise slip through.
11
+ * Covers `localhost`, the IPv4 loopback `127.0.0.0/8`, and IPv6 `::1`.
12
+ */
13
+ export const is_loopback_host = (host) => {
14
+ const h = host.replace(/^\[(.*)\]$/, '$1'); // unwrap an `[::1]`-style IPv6 literal
15
+ return h === 'localhost' || h === '::1' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
16
+ };
6
17
  /**
7
18
  * Boot a test-mode server using the supplied runtime adapter.
8
19
  *
9
20
  * Mirrors a production `start_server` at the surface level — stale-daemon
10
21
  * check, daemon-info write, bind, graceful drain — but the app is the
11
22
  * caller's no-domain (or domain) {@link StartTestingServerOptions.build_app}
12
- * and the runtime boundary is the {@link TestingServerAdapter}. Refuses to
13
- * bind an open host (the test binary must stay on loopback).
23
+ * and the runtime boundary is the {@link TestingServerAdapter}. Refuses any
24
+ * non-loopback bind host (the test binary must stay on loopback — see
25
+ * `is_loopback_host`).
14
26
  */
15
27
  export const start_testing_server = async (options) => {
16
28
  const { adapter, daemon_name, host, port, app_version, build_app } = options;
17
29
  const log = options.log ?? new Logger(`[${daemon_name}]`);
18
30
  const { runtime } = adapter;
19
- if (OPEN_HOSTS.has(host)) {
20
- log.error(`FATAL: binding to '${host}' exposes the test binary to your entire network. ` +
21
- `Use --host localhost (default) or 127.0.0.1 instead.`);
31
+ if (!is_loopback_host(host)) {
32
+ log.error(`FATAL: binding to '${host}' exposes the test binary (which ships deterministic ` +
33
+ `dev secrets) beyond loopback. Use --host localhost (default), 127.0.0.1, or ::1 instead.`);
22
34
  adapter.exit(1);
23
35
  }
24
36
  const stale = await read_daemon_info(runtime, daemon_name);
@@ -23,8 +23,11 @@ export declare const TS_SPINE_DIR_ENV = "FUZ_TESTING_TS_SPINE_DIR";
23
23
  * Audit-log SSE stream path the TS spine binary serves (it wires
24
24
  * `audit_log_sse`). Matches `SPINE_SSE_PATH` in `testing/cross_backend/default_spine_surface.ts`
25
25
  * and the cross-process SSE suite's default. Scoped to the TS configs
26
- * (which advertise `capabilities.sse: true`) — the Rust spine doesn't serve
27
- * the stream, so the shared `ts_default_capabilities` stays `sse: false`.
26
+ * (which advertise `capabilities.sse: true`) — the shared
27
+ * `ts_default_capabilities` stays `sse: false` because not every consumer
28
+ * wires `audit_log_sse`, so the default stays honest for backends that don't
29
+ * serve the stream. (The Rust `testing_spine_stub` does serve it and opts in
30
+ * via `rust_spine_stub_backend_config`.)
28
31
  */
29
32
  export declare const TS_SPINE_SSE_PATH = "/api/admin/audit/stream";
30
33
  /** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
@@ -1 +1 @@
1
- {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAOvD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAS3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aA6BF,CAAC"}
1
+ {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAOvD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAS3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aA6BF,CAAC"}
@@ -7,8 +7,11 @@ export const TS_SPINE_DIR_ENV = 'FUZ_TESTING_TS_SPINE_DIR';
7
7
  * Audit-log SSE stream path the TS spine binary serves (it wires
8
8
  * `audit_log_sse`). Matches `SPINE_SSE_PATH` in `testing/cross_backend/default_spine_surface.ts`
9
9
  * and the cross-process SSE suite's default. Scoped to the TS configs
10
- * (which advertise `capabilities.sse: true`) — the Rust spine doesn't serve
11
- * the stream, so the shared `ts_default_capabilities` stays `sse: false`.
10
+ * (which advertise `capabilities.sse: true`) — the shared
11
+ * `ts_default_capabilities` stays `sse: false` because not every consumer
12
+ * wires `audit_log_sse`, so the default stays honest for backends that don't
13
+ * serve the stream. (The Rust `testing_spine_stub` does serve it and opts in
14
+ * via `rust_spine_stub_backend_config`.)
12
15
  */
13
16
  export const TS_SPINE_SSE_PATH = '/api/admin/audit/stream';
14
17
  /**
@@ -1,3 +1,4 @@
1
+ import './assert_dev_env.js';
1
2
  /**
2
3
  * In-memory file system mock for tests that use dependency-injected
3
4
  * `read_file` and `write_file` callbacks. Avoids module-level mocking.
@@ -1 +1 @@
1
- {"version":3,"file":"mock_fs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/mock_fs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,MAAM;IACtB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,gBAAe,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,KAAG,MAsB3E,CAAC"}
1
+ {"version":3,"file":"mock_fs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/mock_fs.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;GAKG;AAEH,MAAM,WAAW,MAAM;IACtB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,gBAAe,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,KAAG,MAsB3E,CAAC"}
@@ -1,9 +1,4 @@
1
- /**
2
- * In-memory file system mock for tests that use dependency-injected
3
- * `read_file` and `write_file` callbacks. Avoids module-level mocking.
4
- *
5
- * @module
6
- */
1
+ import './assert_dev_env.js';
7
2
  /**
8
3
  * Creates an in-memory file system for tests.
9
4
  *
@@ -142,6 +142,38 @@ export declare const assert_ws_endpoints_include_protocol_actions: (surface: App
142
142
  * surface mocks the shape incorrectly.
143
143
  */
144
144
  export declare const assert_ws_notifications_have_null_auth: (surface: AppSurface) => void;
145
+ /**
146
+ * Reserved method-name prefix for the daemon-token-gated test-backdoor
147
+ * actions (`_testing_reset`, `_testing_mint_session`, `_testing_put_fact`,
148
+ * `_testing_drain_effects`, `_testing_schema_snapshot`). Test binaries
149
+ * live-mount these on their RPC endpoint but they must never appear on a
150
+ * **declared** surface.
151
+ */
152
+ export declare const TESTING_METHOD_PREFIX = "_testing_";
153
+ /**
154
+ * No `_testing_*` backdoor action ever appears as a method on the declared
155
+ * surface (RPC or WS).
156
+ *
157
+ * The test-backdoor actions (`_testing_reset` et al.) are daemon-token-gated
158
+ * privileged actions a consumer's test binary appends to its live RPC
159
+ * endpoint at assembly time — but they are deliberately excluded from
160
+ * surface generation (`spine_rpc_endpoints` and every consumer's
161
+ * `create_*_app_surface_spec` omit them) so the published attack surface,
162
+ * the committed `*_attack_surface.json` snapshot, and codegen never carry
163
+ * a backdoor. This invariant is the structural guard against a future
164
+ * wiring change that folds `create_testing_actions(...)` into the *declared*
165
+ * registry instead of the live-only append — the surface stays the
166
+ * authoritative "what the server exposes" map only if a backdoor can never
167
+ * hide in it.
168
+ *
169
+ * Pairs with the wire-level negative-credential check
170
+ * (`describe_testing_backdoor_cross_tests`, cross-process) and the
171
+ * spec-level gate check: this one pins absence-from-surface, those pin
172
+ * the daemon-token gate and the 401/403 behavior.
173
+ *
174
+ * @throws AssertionError naming the offending endpoint + method.
175
+ */
176
+ export declare const assert_no_testing_methods: (surface: AppSurface) => void;
145
177
  /**
146
178
  * Configuration for security policy invariants.
147
179
  *
@@ -282,7 +314,8 @@ export declare const assert_surface_invariants: (surface: AppSurface) => void;
282
314
  * `actions/CLAUDE.md` §Registry compile) — these assertions cover only
283
315
  * the contract-surface concerns that a runtime registration check
284
316
  * cannot: empty descriptions, missing protocol-action spread on WS
285
- * endpoints, and kind ⇔ auth drift on WS methods.
317
+ * endpoints, kind ⇔ auth drift on WS methods, and a `_testing_*`
318
+ * backdoor action leaking onto the declared surface.
286
319
  *
287
320
  * @throws AssertionError on the first invariant violation; the message
288
321
  * names the offending endpoint, method, and field.
@@ -1 +1 @@
1
- {"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
@@ -452,6 +452,52 @@ export const assert_ws_notifications_have_null_auth = (surface) => {
452
452
  }
453
453
  }
454
454
  };
455
+ /**
456
+ * Reserved method-name prefix for the daemon-token-gated test-backdoor
457
+ * actions (`_testing_reset`, `_testing_mint_session`, `_testing_put_fact`,
458
+ * `_testing_drain_effects`, `_testing_schema_snapshot`). Test binaries
459
+ * live-mount these on their RPC endpoint but they must never appear on a
460
+ * **declared** surface.
461
+ */
462
+ export const TESTING_METHOD_PREFIX = '_testing_';
463
+ /**
464
+ * No `_testing_*` backdoor action ever appears as a method on the declared
465
+ * surface (RPC or WS).
466
+ *
467
+ * The test-backdoor actions (`_testing_reset` et al.) are daemon-token-gated
468
+ * privileged actions a consumer's test binary appends to its live RPC
469
+ * endpoint at assembly time — but they are deliberately excluded from
470
+ * surface generation (`spine_rpc_endpoints` and every consumer's
471
+ * `create_*_app_surface_spec` omit them) so the published attack surface,
472
+ * the committed `*_attack_surface.json` snapshot, and codegen never carry
473
+ * a backdoor. This invariant is the structural guard against a future
474
+ * wiring change that folds `create_testing_actions(...)` into the *declared*
475
+ * registry instead of the live-only append — the surface stays the
476
+ * authoritative "what the server exposes" map only if a backdoor can never
477
+ * hide in it.
478
+ *
479
+ * Pairs with the wire-level negative-credential check
480
+ * (`describe_testing_backdoor_cross_tests`, cross-process) and the
481
+ * spec-level gate check: this one pins absence-from-surface, those pin
482
+ * the daemon-token gate and the 401/403 behavior.
483
+ *
484
+ * @throws AssertionError naming the offending endpoint + method.
485
+ */
486
+ export const assert_no_testing_methods = (surface) => {
487
+ for (const ep of surface.rpc_endpoints) {
488
+ for (const method of ep.methods) {
489
+ assert.ok(!method.name.startsWith(TESTING_METHOD_PREFIX), `RPC endpoint '${ep.path}' exposes test-backdoor method '${method.name}' on the ` +
490
+ `declared surface — '${TESTING_METHOD_PREFIX}*' actions must be live-mounted only, ` +
491
+ `never folded into the surface-generating registry`);
492
+ }
493
+ }
494
+ for (const ep of surface.ws_endpoints) {
495
+ for (const method of ep.methods) {
496
+ assert.ok(!method.name.startsWith(TESTING_METHOD_PREFIX), `WS endpoint '${ep.path}' exposes test-backdoor method '${method.name}' on the ` +
497
+ `declared surface — '${TESTING_METHOD_PREFIX}*' actions must be live-mounted only`);
498
+ }
499
+ }
500
+ };
455
501
  /** Default patterns for sensitive REST routes that should be rate-limited. */
456
502
  const DEFAULT_SENSITIVE_PATTERNS = [
457
503
  /\/login$/,
@@ -637,7 +683,8 @@ export const assert_surface_invariants = (surface) => {
637
683
  * `actions/CLAUDE.md` §Registry compile) — these assertions cover only
638
684
  * the contract-surface concerns that a runtime registration check
639
685
  * cannot: empty descriptions, missing protocol-action spread on WS
640
- * endpoints, and kind ⇔ auth drift on WS methods.
686
+ * endpoints, kind ⇔ auth drift on WS methods, and a `_testing_*`
687
+ * backdoor action leaking onto the declared surface.
641
688
  *
642
689
  * @throws AssertionError on the first invariant violation; the message
643
690
  * names the offending endpoint, method, and field.
@@ -647,6 +694,7 @@ export const assert_rpc_ws_surface_invariants = (surface) => {
647
694
  assert_ws_method_descriptions_present(surface);
648
695
  assert_ws_endpoints_include_protocol_actions(surface);
649
696
  assert_ws_notifications_have_null_auth(surface);
697
+ assert_no_testing_methods(surface);
650
698
  };
651
699
  /**
652
700
  * Run security policy invariants. Configurable with sensible defaults.
@@ -1,3 +1,4 @@
1
+ import './assert_dev_env.js';
1
2
  /**
2
3
  * In-process test helpers for WebSocket JSON-RPC round-trips.
3
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAO,MAAM,MAAM,CAAC;AACxC,OAAO,EACN,SAAS,EAET,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAC/C,OAAO,EAAc,KAAK,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,4BAA4B,CAAC;AAEvD,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,kCAAkC,CAAC;AAE7E,OAAO,EAAqB,KAAK,uBAAuB,EAAC,MAAM,kCAAkC,CAAC;AAElG,OAAO,EAAC,yBAAyB,EAAC,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAsB,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAEpF,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAKN,KAAK,QAAQ,EACb,MAAM,2BAA2B,CAAC;AAMnC;;;GAGG;AACH,MAAM,WAAW,MAAM;IACtB,EAAE,EAAE,SAAS,CAAC;IACd,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;CAChD;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAO,MAajC,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACtC,eAAe,EAAE,cAAc,CAAC;IAChC,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,eAAe,CAAC,EAAE,cAAc,CAAC;CACjC;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,sBAAsB,KAAG,OAavE,CAAC;AAEF,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,iBAAiB,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACtE;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAO,WAatC,CAAC;AAEF;;;;GAIG;AACH,qBAAa,wBAAyB,YAAW,sBAAsB;;IACtE,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAa;gBAEjC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC;IAGjD,qBAAqB,IAAI,SAAS;IAGlC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;CAG/D;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,YAAY,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,EAC9C,OAAO,YAAY,EACnB,IAAI,SAAS,KACX,OAAO,CAAC,IAAI,CAId,CAAC;AAMF,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IACjC,wEAAwE;IACxE,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,yFAAyF;IACzF,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sFAAsF;IACtF,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,4CAA4C;AAC5C,MAAM,WAAW,0BAA0B;IAC1C;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B,kEAAkE;IAClE,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;OAIG;IACH,SAAS,CAAC,EAAE,uBAAuB,CAAC,WAAW,CAAC,CAAC;IACjD,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,cAAc,CAAC,EAAE,uBAAuB,CAAC,gBAAgB,CAAC,CAAC;IAC3D,yDAAyD;IACzD,eAAe,CAAC,EAAE,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;CAC7D;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,yBAAyB,CAAC;IACrC;;;;;;;;;;OAUG;IACH,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC7D;AAiED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,0BAA0B,KAAG,aA0M5E,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,eAAe,QAAO,iBAGjC,CAAC;AAYH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,GAAI,IAAI,SAAS,MAAM,EAAE,SAAS;IACjE,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACtC,KAAG,IAIH,CAAC"}
@@ -1,41 +1,4 @@
1
- /**
2
- * In-process test helpers for WebSocket JSON-RPC round-trips.
3
- *
4
- * Drives `register_action_ws` without an HTTP server. Consumers supply
5
- * specs + handlers; the harness constructs real `WSContext` instances
6
- * backed by test-owned `send`/`close` pairs, fakes the authenticated
7
- * Hono context (`request_context`, credential type, session id, api
8
- * token id), and exposes a `connect()` factory returning a `WsClient`
9
- * per connection.
10
- *
11
- * Three layers are exported:
12
- *
13
- * - **Primitives** (`create_fake_ws`, `create_fake_hono_context`,
14
- * `create_stub_upgrade`, `MinimalActionEnvironment`,
15
- * `dispatch_ws_message`) — used by fuz_app's own dispatcher tests
16
- * and by consumers wiring tight one-off tests.
17
- * - **Harness** (`create_ws_test_harness`, `keeper_identity`) — the
18
- * high-level driver. Give it specs + handlers, get back
19
- * `{transport, connect()}`. `connect()` is async and resolves after
20
- * `on_socket_open` completes, so broadcasts sent immediately after
21
- * `await harness.connect()` reach the client. Returns a `WsClient`
22
- * (shared interface — see `transports/ws_client.ts`); the same
23
- * interface is implemented by `transports/ws_transport.ts` for
24
- * cross-process tests.
25
- * - **Broadcast wiring** — `build_broadcast_api` for wiring a typed
26
- * broadcast API against the harness's transport. Wire-frame types
27
- * + predicates (`is_notification`, `is_response_for`,
28
- * `JsonrpcNotificationFrame`, ...) live in `transports/ws_client.ts`
29
- * so both in-process and cross-process drivers reference one source.
30
- *
31
- * Hono's wire upgrade is skipped — the Node test runtime has no
32
- * `@hono/node-ws` adapter — but the full dispatch path is exercised
33
- * (per-action auth, input validation, `ctx.notify` back to the
34
- * originating socket, broadcast via `BackendWebsocketTransport`, and
35
- * close-on-revoke).
36
- *
37
- * @module
38
- */
1
+ import './assert_dev_env.js';
39
2
  import { WSContext, createWSMessageEvent, } from 'hono/ws';
40
3
  import { Logger } from '@fuzdev/fuz_util/log.js';
41
4
  import { create_uuid } from '@fuzdev/fuz_util/id.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",