@fuzdev/fuz_app 0.127.0 → 0.128.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 (44) hide show
  1. package/dist/actions/CLAUDE.md +35 -18
  2. package/dist/actions/connection_closer.d.ts +16 -4
  3. package/dist/actions/connection_closer.d.ts.map +1 -1
  4. package/dist/actions/connection_closer.js +20 -4
  5. package/dist/actions/register_action_ws.d.ts +10 -2
  6. package/dist/actions/register_action_ws.d.ts.map +1 -1
  7. package/dist/actions/register_action_ws.js +6 -3
  8. package/dist/actions/register_ws_endpoint.d.ts +3 -1
  9. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  10. package/dist/actions/register_ws_endpoint.js +3 -1
  11. package/dist/auth/CLAUDE.md +13 -9
  12. package/dist/auth/audit_emitter.d.ts +59 -12
  13. package/dist/auth/audit_emitter.d.ts.map +1 -1
  14. package/dist/auth/audit_emitter.js +53 -15
  15. package/dist/auth/middleware.d.ts +30 -0
  16. package/dist/auth/middleware.d.ts.map +1 -1
  17. package/dist/auth/middleware.js +59 -0
  18. package/dist/realtime/sse_auth_guard.d.ts +3 -1
  19. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  20. package/dist/server/app_server.d.ts +39 -12
  21. package/dist/server/app_server.d.ts.map +1 -1
  22. package/dist/server/app_server.js +92 -39
  23. package/dist/testing/CLAUDE.md +36 -23
  24. package/dist/testing/app_server.d.ts +1 -1
  25. package/dist/testing/app_server.d.ts.map +1 -1
  26. package/dist/testing/attack_surface.d.ts +1 -1
  27. package/dist/testing/attack_surface.d.ts.map +1 -1
  28. package/dist/testing/audit_drift_guard.d.ts +4 -2
  29. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  30. package/dist/testing/audit_drift_guard.js +14 -4
  31. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  32. package/dist/testing/cross_backend/account_lifecycle.js +1 -2
  33. package/dist/testing/cross_backend/capabilities.d.ts +3 -1
  34. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  35. package/dist/testing/cross_backend/testing_reset_actions.d.ts +27 -18
  36. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  37. package/dist/testing/cross_backend/testing_reset_actions.js +30 -19
  38. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  39. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  40. package/dist/testing/cross_backend/ws_round_trip.js +40 -4
  41. package/dist/testing/stubs.d.ts +23 -6
  42. package/dist/testing/stubs.d.ts.map +1 -1
  43. package/dist/testing/stubs.js +39 -14
  44. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,EAQN,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,MAAM,yBAAyB,CAAC;AAejC,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAsDpF,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACtC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,sBAAsB,KAAG,IA2J3E,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uCAAuC,GACnD,UAAU,2BAA2B,GAAG,IAAI,GAAG,SAAS,KACtD,2BAA2B,GAAG,IAWhC,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC5C,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,gHAAgH;IAChH,uBAAuB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,sBAAsB,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;IAC5D;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,4BAA4B,KACnC,IA4EF,CAAC"}
1
+ {"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,EAQN,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,MAAM,yBAAyB,CAAC;AAejC,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAsDpF,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACtC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,sBAAsB,KAAG,IA2J3E,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uCAAuC,GACnD,UAAU,2BAA2B,GAAG,IAAI,GAAG,SAAS,KACtD,2BAA2B,GAAG,IAWhC,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC5C,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,iIAAiI;IACjI,uBAAuB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,sBAAsB,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;IAC5D;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,4BAA4B,KACnC,IA4EF,CAAC"}
@@ -46,8 +46,10 @@ export interface RecordingAuditEmitter {
46
46
  * forward from the input envelope. Tests asserting on role-grant-shape
47
47
  * emissions read out of the same homogeneous `calls` array.
48
48
  * `notify` is a no-op; `add_listener` records into a local array that
49
- * `listener_count` reports (registered listeners never fire — this emitter
50
- * captures `emit` shapes, not fan-out).
49
+ * `listener_count` reports, and returns a remover with the production
50
+ * semantics (per registration, idempotent) — registered listeners never fire,
51
+ * since this emitter captures `emit` shapes, not fan-out. `drain_inflight` resolves at once:
52
+ * a recorded call writes nothing, so nothing is ever in flight.
51
53
  *
52
54
  * `emit` AND `emit_pool` both append to `calls`, so a test reads either
53
55
  * entry point's writes off the same array.
@@ -1 +1 @@
1
- {"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAiB,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEhF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAiB5C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,KAAK,CAAC,aAAa,CAAC,KAC9B,qBA8BF,CAAC"}
1
+ {"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAiB,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEhF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAiB5C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,KAAK,CAAC,aAAa,CAAC,KAC9B,qBAqCF,CAAC"}
@@ -44,8 +44,10 @@ export const install_audit_drift_guard = () => {
44
44
  * forward from the input envelope. Tests asserting on role-grant-shape
45
45
  * emissions read out of the same homogeneous `calls` array.
46
46
  * `notify` is a no-op; `add_listener` records into a local array that
47
- * `listener_count` reports (registered listeners never fire — this emitter
48
- * captures `emit` shapes, not fan-out).
47
+ * `listener_count` reports, and returns a remover with the production
48
+ * semantics (per registration, idempotent) — registered listeners never fire,
49
+ * since this emitter captures `emit` shapes, not fan-out. `drain_inflight` resolves at once:
50
+ * a recorded call writes nothing, so nothing is ever in flight.
49
51
  *
50
52
  * `emit` AND `emit_pool` both append to `calls`, so a test reads either
51
53
  * entry point's writes off the same array.
@@ -65,6 +67,7 @@ export const install_audit_drift_guard = () => {
65
67
  */
66
68
  export const create_recording_audit_emitter = (calls_ref) => {
67
69
  const calls = calls_ref ?? [];
70
+ // one entry per registration, as `create_audit_emitter` keeps them
68
71
  const listeners = [];
69
72
  const emitter = {
70
73
  emit: (_ctx, input) => {
@@ -88,9 +91,16 @@ export const create_recording_audit_emitter = (calls_ref) => {
88
91
  },
89
92
  notify: () => undefined,
90
93
  add_listener: (listener) => {
91
- listeners.push(listener);
94
+ const entry = { listener };
95
+ listeners.push(entry);
96
+ return () => {
97
+ const index = listeners.indexOf(entry);
98
+ if (index !== -1)
99
+ listeners.splice(index, 1);
100
+ };
92
101
  },
93
- listener_count: () => listeners.length
102
+ listener_count: () => listeners.length,
103
+ drain_inflight: () => Promise.resolve()
94
104
  };
95
105
  return { emitter, calls };
96
106
  };
@@ -1 +1 @@
1
- {"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAqUF,CAAC"}
1
+ {"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAoUF,CAAC"}
@@ -88,8 +88,7 @@ export const describe_account_lifecycle_cross_tests = (options) => {
88
88
  const del = await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: fixture.account.id }, fixture.create_session_headers());
89
89
  assert.strictEqual(error_reason(del), ERROR_CANNOT_DELETE_KEEPER);
90
90
  // Deterministic barrier before reading: await in-flight
91
- // fire-and-forget audit writes (the real await on the Rust spine;
92
- // satisfied-by-construction on the TS spine via await_pending_effects).
91
+ // fire-and-forget audit writes on either spine.
93
92
  const td = fixture.fresh_transport({ origin: null });
94
93
  const drained = await cross_rpc_call(td, rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
95
94
  assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
@@ -183,7 +183,9 @@ export interface BackendCapabilities {
183
183
  * (`account_session_revoke_all` among them), so a socket can revoke the
184
184
  * session it is running on. Gates the self-revocation case in
185
185
  * `describe_cross_process_ws_tests`: the socket reads the reply to its
186
- * request, then is closed with `WS_CLOSE_SESSION_REVOKED`.
186
+ * request, then is closed with `WS_CLOSE_SESSION_REVOKED`. Also gates that
187
+ * suite's WS-mutation audit case (with `rpc_path`; it calls
188
+ * `_testing_drain_effects`).
187
189
  *
188
190
  * `true` on fuz_app's own spine presets — the TS spine binary mounts its
189
191
  * full action set on `/api/ws`, and the Rust `testing_spine_stub` serves
@@ -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;AA4C9B;;;;;;;;;;;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;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;OAYG;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;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAsBpC,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;AA4C9B;;;;;;;;;;;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;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;OAeG;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;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAsBpC,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"}
@@ -71,6 +71,7 @@ import '../assert_dev_env.js';
71
71
  import { z } from 'zod';
72
72
  import { type RpcAction } from '../../actions/action_rpc.js';
73
73
  import type { AppDeps } from '../../auth/deps.js';
74
+ import type { AuditEmitter } from '../../auth/audit_emitter.js';
74
75
  import type { SessionOptions } from '../../auth/session_cookie.js';
75
76
  import type { DaemonTokenState } from '../../auth/daemon_token.js';
76
77
  import type { Db } from '../../db/db.js';
@@ -162,16 +163,21 @@ export declare const testing_reset_action_spec: {
162
163
  * the cross-backend conformance suite uses in place of a poll/sleep before
163
164
  * asserting on audit rows.
164
165
  *
165
- * On the TS spine the barrier is **satisfied by construction**: the test
166
- * binary runs `await_pending_effects: true`, so every **HTTP** mutation's
167
- * fire-and-forget audit emits are awaited before its response returns — by
168
- * the time a later drain call runs, prior emits are already durable. A
169
- * WS-dispatched mutation replies before its effects flush, so on TS this
170
- * action is not a barrier for WS-originated emits; issue audit-asserting
171
- * mutations over HTTP. The action still
172
- * exists so the cross-backend test body calls the same method on every
173
- * backend; the Rust spine (whose audit writes are detached tokio tasks)
174
- * does the real await in `AuditEmitter::drain_inflight`.
166
+ * The handler awaits `AuditEmitter.drain_inflight`, which waits until no
167
+ * tracked `emit` write is outstanding — so the binary's emitter must be built
168
+ * with `track_inflight: true`, or the drain resolves at once and is no barrier.
169
+ * Tracking is what makes it a barrier whatever the transport: an **HTTP**
170
+ * mutation on a binary running `await_pending_effects: true` has its writes
171
+ * settled by the time it responds, but a **WS** mutation is answered before
172
+ * its effects flush (reply-then-close for a self-revocation), so its audit
173
+ * write can still be in flight when the caller reads the reply. The Rust
174
+ * spine does the same await in `AuditEmitter::drain_inflight`, over an
175
+ * emitter built with `new_with_inflight_tracking`.
176
+ *
177
+ * Covered: every `emit` that ran before the drain was dispatched, and any
178
+ * started while it waits. A write that starts after the drain resolves is
179
+ * not — nor are eager writes outside `emit` (session touch, token usage),
180
+ * which only the dispatching transport's own flush awaits.
175
181
  *
176
182
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
177
183
  */
@@ -243,13 +249,15 @@ export declare const testing_mint_session_action_spec: {
243
249
  readonly description: string;
244
250
  };
245
251
  /**
246
- * Build the standalone `_testing_drain_effects` action. No deps — on TS the
247
- * barrier is satisfied by `await_pending_effects` (see the spec doc), so the
248
- * handler just returns `{ok: true}`. Mount it on any test endpoint whose
249
- * suite asserts on audit rows (the spine binary bundles it via
252
+ * Build the standalone `_testing_drain_effects` action over `audit` — the
253
+ * emitter the backend's handlers emit through (`deps.audit`), built with
254
+ * `track_inflight: true` (see the spec doc). Mount it on any test endpoint
255
+ * whose suite asserts on audit rows (the spine binary bundles it via
250
256
  * `create_testing_actions`; in-process suites mount it directly).
257
+ *
258
+ * @param audit - the backend's emitter, whose in-flight writes the action awaits
251
259
  */
252
- export declare const create_testing_drain_effects_action: () => RpcAction;
260
+ export declare const create_testing_drain_effects_action: (audit: AuditEmitter) => RpcAction;
253
261
  /**
254
262
  * `_testing_put_fact` — seed an **embedded** fact (`fact.bytes`) for the
255
263
  * cross-process fact-serving suite, which drives over real HTTP and has no
@@ -475,9 +483,10 @@ export interface CreateTestingActionsOptions {
475
483
  /**
476
484
  * Build the testing RPC actions for a test binary's registry.
477
485
  *
478
- * Returns `_testing_reset` — the single privileged action test binaries
479
- * register. The test binary calls this at server-assembly time and
480
- * registers the result on its dispatcher.
486
+ * Returns the bundled `_testing_*` actions (module doc). The test binary
487
+ * calls this at server-assembly time and registers the result on its
488
+ * dispatcher. `_testing_drain_effects` drains `deps.audit`, so the binary's
489
+ * `audit_factory` builds the emitter with `track_inflight: true`.
481
490
  *
482
491
  * The reset action's table-wipe list mirrors
483
492
  * `auth_integration_truncate_tables` from `testing/db.ts` — the
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAKzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAqCzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACmB,CAAC;AAE3E;;;;;;;;;;;;;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;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC;;;;;;;;;;;;;;;;;;;;CAYJ,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,uCAAuC,QAAO,SAGzD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAaF,CAAC;AAyB/C,eAAO,MAAM,qCAAqC,GACjD,SAAS,aAAa,CAAC,SAAS,CAAC,KAC/B,SAMF,CAAC;AAEF,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,CAsIjB,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAKzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAqCzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;;;GAQG;AACH,eAAO,MAAM,mCAAmC,GAAI,OAAO,YAAY,KAAG,SAIvE,CAAC;AAEJ;;;;;;;;;;;;;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;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC;;;;;;;;;;;;;;;;;;;;CAYJ,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,uCAAuC,QAAO,SAGzD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAaF,CAAC;AAyB/C,eAAO,MAAM,qCAAqC,GACjD,SAAS,aAAa,CAAC,SAAS,CAAC,KAC/B,SAMF,CAAC;AAEF,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;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAsIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
@@ -168,16 +168,21 @@ export const testing_reset_action_spec = {
168
168
  * the cross-backend conformance suite uses in place of a poll/sleep before
169
169
  * asserting on audit rows.
170
170
  *
171
- * On the TS spine the barrier is **satisfied by construction**: the test
172
- * binary runs `await_pending_effects: true`, so every **HTTP** mutation's
173
- * fire-and-forget audit emits are awaited before its response returns — by
174
- * the time a later drain call runs, prior emits are already durable. A
175
- * WS-dispatched mutation replies before its effects flush, so on TS this
176
- * action is not a barrier for WS-originated emits; issue audit-asserting
177
- * mutations over HTTP. The action still
178
- * exists so the cross-backend test body calls the same method on every
179
- * backend; the Rust spine (whose audit writes are detached tokio tasks)
180
- * does the real await in `AuditEmitter::drain_inflight`.
171
+ * The handler awaits `AuditEmitter.drain_inflight`, which waits until no
172
+ * tracked `emit` write is outstanding — so the binary's emitter must be built
173
+ * with `track_inflight: true`, or the drain resolves at once and is no barrier.
174
+ * Tracking is what makes it a barrier whatever the transport: an **HTTP**
175
+ * mutation on a binary running `await_pending_effects: true` has its writes
176
+ * settled by the time it responds, but a **WS** mutation is answered before
177
+ * its effects flush (reply-then-close for a self-revocation), so its audit
178
+ * write can still be in flight when the caller reads the reply. The Rust
179
+ * spine does the same await in `AuditEmitter::drain_inflight`, over an
180
+ * emitter built with `new_with_inflight_tracking`.
181
+ *
182
+ * Covered: every `emit` that ran before the drain was dispatched, and any
183
+ * started while it waits. A write that starts after the drain resolves is
184
+ * not — nor are eager writes outside `emit` (session touch, token usage),
185
+ * which only the dispatching transport's own flush awaits.
181
186
  *
182
187
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
183
188
  */
@@ -246,13 +251,18 @@ export const testing_mint_session_action_spec = {
246
251
  'signed cookie value (exercises the authoritative server-side DB-row expiry gate).'
247
252
  };
248
253
  /**
249
- * Build the standalone `_testing_drain_effects` action. No deps — on TS the
250
- * barrier is satisfied by `await_pending_effects` (see the spec doc), so the
251
- * handler just returns `{ok: true}`. Mount it on any test endpoint whose
252
- * suite asserts on audit rows (the spine binary bundles it via
254
+ * Build the standalone `_testing_drain_effects` action over `audit` — the
255
+ * emitter the backend's handlers emit through (`deps.audit`), built with
256
+ * `track_inflight: true` (see the spec doc). Mount it on any test endpoint
257
+ * whose suite asserts on audit rows (the spine binary bundles it via
253
258
  * `create_testing_actions`; in-process suites mount it directly).
259
+ *
260
+ * @param audit - the backend's emitter, whose in-flight writes the action awaits
254
261
  */
255
- export const create_testing_drain_effects_action = () => rpc_action(testing_drain_effects_action_spec, async () => ({ ok: true }));
262
+ export const create_testing_drain_effects_action = (audit) => rpc_action(testing_drain_effects_action_spec, async () => {
263
+ await audit.drain_inflight();
264
+ return { ok: true };
265
+ });
256
266
  /**
257
267
  * `_testing_put_fact` — seed an **embedded** fact (`fact.bytes`) for the
258
268
  * cross-process fact-serving suite, which drives over real HTTP and has no
@@ -405,9 +415,10 @@ export const create_testing_action_manifest_action = (mounted) => {
405
415
  /**
406
416
  * Build the testing RPC actions for a test binary's registry.
407
417
  *
408
- * Returns `_testing_reset` — the single privileged action test binaries
409
- * register. The test binary calls this at server-assembly time and
410
- * registers the result on its dispatcher.
418
+ * Returns the bundled `_testing_*` actions (module doc). The test binary
419
+ * calls this at server-assembly time and registers the result on its
420
+ * dispatcher. `_testing_drain_effects` drains `deps.audit`, so the binary's
421
+ * `audit_factory` builds the emitter with `track_inflight: true`.
411
422
  *
412
423
  * The reset action's table-wipe list mirrors
413
424
  * `auth_integration_truncate_tables` from `testing/db.ts` — the
@@ -533,7 +544,7 @@ export const create_testing_actions = (deps, options) => {
533
544
  });
534
545
  return { hash };
535
546
  }),
536
- create_testing_drain_effects_action(),
547
+ create_testing_drain_effects_action(deps.audit),
537
548
  create_testing_schema_snapshot_action(),
538
549
  create_testing_migration_tracker_action()
539
550
  ];
@@ -38,7 +38,8 @@ export interface CrossProcessWsTestOptions {
38
38
  * oversized message closing with `WS_CLOSE_MESSAGE_TOO_BIG`, anonymous-upgrade
39
39
  * refusal, disallowed-origin refusal, and — when `rpc_path` is supplied —
40
40
  * session revocation and account deletion closing the live socket, plus a
41
- * self-revoking socket reading its reply before its close.
41
+ * self-revoking socket reading its reply before its close, and a WS mutation's
42
+ * audit row read back after `_testing_drain_effects`.
42
43
  */
43
44
  export declare const describe_cross_process_ws_tests: (options: CrossProcessWsTestOptions) => void;
44
45
  //# sourceMappingURL=ws_round_trip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6F9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,iEAAiE;AACjE,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IA+RpF,CAAC"}
1
+ {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwG9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,iEAAiE;AACjE,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IAkVpF,CAAC"}
@@ -55,7 +55,11 @@ import '../assert_dev_env.js';
55
55
  * `capabilities.ws_self_revocation_reply` — reads the reply to its request
56
56
  * first. The TS spine queues the close behind the commit and sends the reply
57
57
  * ahead of it, so the caller is never left without an answer; the Rust spine
58
- * does not answer yet, which that flag records.
58
+ * does not answer yet, which that flag records. Under the same gate (and
59
+ * `rpc_path`), a socket mints an API token over WS and the token's
60
+ * `token_create` audit row is read back after `_testing_drain_effects` — the
61
+ * barrier a WS mutation needs, since a WS reply can precede its audit write.
62
+ * The binary's `create_testing_actions` mount provides the drain.
59
63
  *
60
64
  * Gated on `capabilities.ws` — backends without an end-to-end WS transport
61
65
  * skip (the cases still surface as `.skip` in the report). Cross-process
@@ -68,14 +72,15 @@ import { assert, describe } from 'vitest';
68
72
  import { assert_rejects } from '@fuzdev/fuz_util/testing.ts';
69
73
  import { heartbeat_action_spec } from '../../actions/heartbeat.js';
70
74
  import { DEFAULT_WS_MAX_MESSAGE_BYTES, WS_CLOSE_MESSAGE_TOO_BIG, WS_CLOSE_SESSION_REVOKED } from '../../actions/transports.js';
71
- import { account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
72
- import { account_delete_action_spec } from '../../auth/admin_action_specs.js';
75
+ import { account_session_revoke_all_action_spec, account_token_create_action_spec } from '../../auth/account_action_specs.js';
76
+ import { account_delete_action_spec, audit_log_list_action_spec, AuditLogListOutput } from '../../auth/admin_action_specs.js';
73
77
  import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
74
78
  import { is_response_for } from '../transports/ws_client.js';
75
79
  import { create_ws_transport } from '../transports/ws_transport.js';
76
80
  import { connect_raw_ws, is_raw_ws_close, RAW_WS_OPCODE_TEXT } from '../transports/ws_raw_client.js';
77
81
  import { create_rpc_post_init } from '../rpc_helpers.js';
78
82
  import { test_if } from './capabilities.js';
83
+ import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
79
84
  /** Origin guaranteed to fail the `http://localhost:*` allowlist the test backends run with. */
80
85
  const DISALLOWED_ORIGIN = 'http://disallowed.example';
81
86
  /**
@@ -84,7 +89,8 @@ const DISALLOWED_ORIGIN = 'http://disallowed.example';
84
89
  * oversized message closing with `WS_CLOSE_MESSAGE_TOO_BIG`, anonymous-upgrade
85
90
  * refusal, disallowed-origin refusal, and — when `rpc_path` is supplied —
86
91
  * session revocation and account deletion closing the live socket, plus a
87
- * self-revoking socket reading its reply before its close.
92
+ * self-revoking socket reading its reply before its close, and a WS mutation's
93
+ * audit row read back after `_testing_drain_effects`.
88
94
  */
89
95
  export const describe_cross_process_ws_tests = (options) => {
90
96
  const { setup_test, capabilities, base_url, ws_path, origin, rpc_path, max_message_bytes = DEFAULT_WS_MAX_MESSAGE_BYTES } = options;
@@ -308,5 +314,35 @@ export const describe_cross_process_ws_tests = (options) => {
308
314
  await client.close();
309
315
  }
310
316
  });
317
+ // A WS mutation is answered before its effects flush, so its audit row
318
+ // can land after the reply. `_testing_drain_effects` is the barrier: once
319
+ // it returns, the row is visible on either spine.
320
+ test_if(capabilities.ws && capabilities.ws_account_actions && rpc_path !== undefined, "a WS mutation's audit row is visible after _testing_drain_effects", async () => {
321
+ const fixture = await setup_test();
322
+ const client = await create_ws_transport({
323
+ base_url,
324
+ ws_path,
325
+ cookies: fixture.transport.cookies(),
326
+ origin
327
+ });
328
+ try {
329
+ await client.request(1, account_token_create_action_spec.method, {
330
+ name: 'ws drain probe',
331
+ scope: { kind: 'full' },
332
+ lifetime: { kind: 'eternal' }
333
+ });
334
+ }
335
+ finally {
336
+ await client.close();
337
+ }
338
+ const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
339
+ assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
340
+ // `_testing_reset` wiped audit_log at setup, so the mint is the only
341
+ // token_create event
342
+ const listed = expect_output(await cross_rpc_call(fixture.transport, rpc_path, audit_log_list_action_spec.method, { event_type: 'token_create' }, fixture.create_session_headers()), AuditLogListOutput);
343
+ assert.strictEqual(listed.events.length, 1, JSON.stringify(listed.events));
344
+ assert.strictEqual(listed.events[0].outcome, 'success');
345
+ assert.strictEqual(listed.events[0].account_id, fixture.account.id);
346
+ });
311
347
  });
312
348
  };
@@ -62,7 +62,8 @@ export declare const stub_mw: (_c: any, next: any) => Promise<void>;
62
62
  * immediately; `notify` is a no-op; `add_listener` throws, so a test that
63
63
  * wires a listener fails loudly instead of silently never firing
64
64
  * (`create_recording_audit_emitter` accepts listeners); `listener_count`
65
- * returns 0. Tests asserting on real audit-row persistence (or on listener
65
+ * returns 0; `drain_inflight` resolves immediately, since nothing is ever
66
+ * written. Tests asserting on real audit-row persistence (or on listener
66
67
  * fan-out) build a real emitter via `create_audit_emitter` against a stub or
67
68
  * real DB — `create_test_app` already does this on the test backend.
68
69
  */
@@ -84,7 +85,11 @@ export declare const stub_app_deps: AppDeps;
84
85
  * Create no-op `AppDeps` for auth surface testing.
85
86
  */
86
87
  export declare const create_stub_app_deps: () => AppDeps;
87
- /** Create the API middleware stub array matching `create_auth_middleware_specs` output. */
88
+ /**
89
+ * Create the API middleware stub array matching `create_auth_middleware_specs`
90
+ * output. Like the real factory's, each spec gets its own pass-through handler,
91
+ * so `assert_middleware_stack_mounted` can tell the layers apart by identity.
92
+ */
88
93
  export declare const create_stub_api_middleware: (options?: {
89
94
  /** Include the daemon_token middleware layer. */
90
95
  include_daemon_token?: boolean;
@@ -136,7 +141,15 @@ export interface CreateTestAppSurfaceSpecOptions {
136
141
  * the exact patterns the upgrade gate matches.
137
142
  */
138
143
  allowed_origins?: ReadonlyArray<RegExp>;
139
- /** Transform middleware array (e.g., zap's `extend_middleware_for_zap_binary`). */
144
+ /**
145
+ * Transform middleware array — `AppServerOptions.transform_middleware`.
146
+ * Receives shallow copies of a stub `trusted_proxy` spec (at `'*'`)
147
+ * followed by the stub auth stack, in a fresh array, as
148
+ * `create_app_server` passes the real ones. Throws, like
149
+ * `create_app_server`, if the result doesn't keep each of those specs
150
+ * mounted once at its original path in its original order
151
+ * (`assert_middleware_stack_mounted`).
152
+ */
140
153
  transform_middleware?: (specs: Array<MiddlewareSpec>) => Array<MiddlewareSpec>;
141
154
  /**
142
155
  * Bootstrap config — symmetric with `AppServerOptions.bootstrap`. Discriminated
@@ -162,15 +175,19 @@ export interface CreateTestAppSurfaceSpecOptions {
162
175
  * Mirrors `create_app_server`'s route assembly: consumer routes +
163
176
  * factory-managed bootstrap routes + surface generation — including its
164
177
  * refusal of an `rpc_endpoints` or `ws_endpoints` path outside the auth
165
- * middleware's scope (`assert_endpoint_in_auth_scope`). If
178
+ * middleware's scope (`assert_endpoint_in_auth_scope`) and of a
179
+ * `transform_middleware` that doesn't keep the proxy and auth stack mounted
180
+ * (`assert_middleware_stack_mounted`). The middleware list mirrors the real
181
+ * one: a stub `trusted_proxy` spec at `'*'`, then the stub auth stack. If
166
182
  * `create_app_server` changes how it wires routes, update this helper
167
183
  * to stay in sync (single source of truth for all consumers).
168
184
  *
169
185
  * @param options - surface spec options
170
186
  * @returns the surface spec for the standard suites
171
- * @throws Error when an endpoint path is outside `AUTH_MIDDLEWARE_PATH`, or a
187
+ * @throws Error when an endpoint path is outside `AUTH_MIDDLEWARE_PATH`, a
172
188
  * `ws_endpoints` spec declares no `allowed_origins` and no `allowed_origins`
173
- * option is given
189
+ * option is given, or `transform_middleware` moves, drops, duplicates,
190
+ * reorders, wraps, or replaces the stub proxy spec or a stub auth spec
174
191
  */
175
192
  export declare const create_test_app_surface_spec: (options: CreateTestAppSurfaceSpecOptions) => AppSurfaceSpec;
176
193
  //# sourceMappingURL=stubs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
@@ -19,7 +19,7 @@ import { resolve_ws_endpoints } from '../actions/ws_endpoint_spec.js';
19
19
  import { AUDIT_LOG_SSE_MAX_PER_SCOPE } from '../realtime/sse_auth_guard.js';
20
20
  import { SubscriberRegistry } from '../realtime/subscriber_registry.js';
21
21
  import { BaseServerEnv } from '../server/env.js';
22
- import { AUTH_MIDDLEWARE_PATH, assert_endpoint_in_auth_scope } from '../auth/middleware.js';
22
+ import { AUTH_MIDDLEWARE_PATH, assert_endpoint_in_auth_scope, assert_middleware_stack_mounted } from '../auth/middleware.js';
23
23
  /**
24
24
  * Create a Proxy that throws descriptive errors on any property access or method call.
25
25
  *
@@ -99,7 +99,8 @@ const stub_db = create_noop_stub('stub_db');
99
99
  * immediately; `notify` is a no-op; `add_listener` throws, so a test that
100
100
  * wires a listener fails loudly instead of silently never firing
101
101
  * (`create_recording_audit_emitter` accepts listeners); `listener_count`
102
- * returns 0. Tests asserting on real audit-row persistence (or on listener
102
+ * returns 0; `drain_inflight` resolves immediately, since nothing is ever
103
+ * written. Tests asserting on real audit-row persistence (or on listener
103
104
  * fan-out) build a real emitter via `create_audit_emitter` against a stub or
104
105
  * real DB — `create_test_app` already does this on the test backend.
105
106
  */
@@ -111,7 +112,8 @@ export const create_test_audit_emitter = () => ({
111
112
  add_listener: () => {
112
113
  throw new Error('create_test_audit_emitter accepts no listeners — use create_recording_audit_emitter');
113
114
  },
114
- listener_count: () => 0
115
+ listener_count: () => 0,
116
+ drain_inflight: async () => { }
115
117
  });
116
118
  /**
117
119
  * Build a no-op `AuditLogSse` for tests that wire `audit_sse` into the
@@ -159,16 +161,27 @@ export const create_stub_app_deps = () => ({
159
161
  audit: create_test_audit_emitter(),
160
162
  connection_closer: create_realtime_closer()
161
163
  });
162
- /** Create the API middleware stub array matching `create_auth_middleware_specs` output. */
164
+ /** A fresh pass-through middleware — a distinct handler per call, unlike the shared `stub_mw`. */
165
+ const create_stub_mw = () => async (_c, next) => next();
166
+ /**
167
+ * Create the API middleware stub array matching `create_auth_middleware_specs`
168
+ * output. Like the real factory's, each spec gets its own pass-through handler,
169
+ * so `assert_middleware_stack_mounted` can tell the layers apart by identity.
170
+ */
163
171
  export const create_stub_api_middleware = (options) => {
164
172
  const specs = [
165
- { name: 'origin', path: AUTH_MIDDLEWARE_PATH, handler: stub_mw, errors: { 403: ApiError } },
166
- { name: 'session', path: AUTH_MIDDLEWARE_PATH, handler: stub_mw },
167
- { name: 'request_context', path: AUTH_MIDDLEWARE_PATH, handler: stub_mw },
173
+ {
174
+ name: 'origin',
175
+ path: AUTH_MIDDLEWARE_PATH,
176
+ handler: create_stub_mw(),
177
+ errors: { 403: ApiError }
178
+ },
179
+ { name: 'session', path: AUTH_MIDDLEWARE_PATH, handler: create_stub_mw() },
180
+ { name: 'request_context', path: AUTH_MIDDLEWARE_PATH, handler: create_stub_mw() },
168
181
  {
169
182
  name: 'bearer_auth',
170
183
  path: AUTH_MIDDLEWARE_PATH,
171
- handler: stub_mw,
184
+ handler: create_stub_mw(),
172
185
  errors: { 401: ApiError, 403: ApiError, 429: RateLimitError }
173
186
  }
174
187
  ];
@@ -176,7 +189,7 @@ export const create_stub_api_middleware = (options) => {
176
189
  specs.push({
177
190
  name: 'daemon_token',
178
191
  path: AUTH_MIDDLEWARE_PATH,
179
- handler: stub_mw,
192
+ handler: create_stub_mw(),
180
193
  errors: { 401: ApiError, 500: ApiError, 503: ApiError }
181
194
  });
182
195
  }
@@ -241,15 +254,19 @@ const to_self_resolved_ws_endpoint = (endpoint) => {
241
254
  * Mirrors `create_app_server`'s route assembly: consumer routes +
242
255
  * factory-managed bootstrap routes + surface generation — including its
243
256
  * refusal of an `rpc_endpoints` or `ws_endpoints` path outside the auth
244
- * middleware's scope (`assert_endpoint_in_auth_scope`). If
257
+ * middleware's scope (`assert_endpoint_in_auth_scope`) and of a
258
+ * `transform_middleware` that doesn't keep the proxy and auth stack mounted
259
+ * (`assert_middleware_stack_mounted`). The middleware list mirrors the real
260
+ * one: a stub `trusted_proxy` spec at `'*'`, then the stub auth stack. If
245
261
  * `create_app_server` changes how it wires routes, update this helper
246
262
  * to stay in sync (single source of truth for all consumers).
247
263
  *
248
264
  * @param options - surface spec options
249
265
  * @returns the surface spec for the standard suites
250
- * @throws Error when an endpoint path is outside `AUTH_MIDDLEWARE_PATH`, or a
266
+ * @throws Error when an endpoint path is outside `AUTH_MIDDLEWARE_PATH`, a
251
267
  * `ws_endpoints` spec declares no `allowed_origins` and no `allowed_origins`
252
- * option is given
268
+ * option is given, or `transform_middleware` moves, drops, duplicates,
269
+ * reorders, wraps, or replaces the stub proxy spec or a stub auth spec
253
270
  */
254
271
  export const create_test_app_surface_spec = (options) => {
255
272
  const ctx = create_stub_app_server_context(options.session_options);
@@ -292,9 +309,17 @@ export const create_test_app_surface_spec = (options) => {
292
309
  ])
293
310
  : [];
294
311
  const route_specs = [...consumer_routes, ...rpc_route_specs, ...bootstrap_route_specs];
295
- let middleware_specs = create_stub_api_middleware();
312
+ // mirrors `create_app_server`'s `[proxy_spec, ...auth_middleware]`
313
+ const stack_specs = [
314
+ { name: 'trusted_proxy', path: '*', handler: create_stub_mw() },
315
+ ...create_stub_api_middleware()
316
+ ];
317
+ let middleware_specs = stack_specs;
296
318
  if (options.transform_middleware) {
297
- middleware_specs = options.transform_middleware(middleware_specs);
319
+ // the transform gets copies in a fresh array, so mutating what it was
320
+ // handed can't rewrite the originals the check matches against
321
+ middleware_specs = options.transform_middleware(stack_specs.map((spec) => ({ ...spec })));
322
+ assert_middleware_stack_mounted(stack_specs, middleware_specs);
298
323
  }
299
324
  return create_app_surface_spec({
300
325
  middleware_specs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.127.0",
3
+ "version": "0.128.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",