@fuzdev/fuz_app 0.125.0 → 0.127.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 (94) hide show
  1. package/dist/actions/CLAUDE.md +56 -20
  2. package/dist/actions/action_rpc.d.ts +14 -11
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +5 -4
  5. package/dist/actions/connection_closer.d.ts +13 -3
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +7 -5
  8. package/dist/actions/register_action_ws.d.ts +18 -13
  9. package/dist/actions/register_action_ws.d.ts.map +1 -1
  10. package/dist/actions/register_action_ws.js +8 -6
  11. package/dist/actions/register_ws_endpoint.d.ts +6 -3
  12. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  13. package/dist/actions/register_ws_endpoint.js +6 -3
  14. package/dist/actions/transports.d.ts +9 -0
  15. package/dist/actions/transports.d.ts.map +1 -1
  16. package/dist/actions/transports.js +9 -0
  17. package/dist/actions/transports_ws_backend.d.ts +15 -0
  18. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  19. package/dist/actions/transports_ws_backend.js +25 -5
  20. package/dist/actions/ws_endpoint_spec.d.ts +24 -4
  21. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  22. package/dist/actions/ws_endpoint_spec.js +15 -1
  23. package/dist/auth/middleware.d.ts +29 -1
  24. package/dist/auth/middleware.d.ts.map +1 -1
  25. package/dist/auth/middleware.js +35 -1
  26. package/dist/http/CLAUDE.md +3 -3
  27. package/dist/http/surface.d.ts +12 -9
  28. package/dist/http/surface.d.ts.map +1 -1
  29. package/dist/rate_limiter.d.ts +43 -3
  30. package/dist/rate_limiter.d.ts.map +1 -1
  31. package/dist/rate_limiter.js +27 -3
  32. package/dist/realtime/sse_auth_guard.d.ts +2 -1
  33. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  34. package/dist/realtime/sse_auth_guard.js +4 -2
  35. package/dist/realtime/subscriber_registry.d.ts +12 -0
  36. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  37. package/dist/realtime/subscriber_registry.js +18 -2
  38. package/dist/server/app_server.d.ts +128 -55
  39. package/dist/server/app_server.d.ts.map +1 -1
  40. package/dist/server/app_server.js +110 -61
  41. package/dist/server/app_server_context.d.ts +17 -9
  42. package/dist/server/app_server_context.d.ts.map +1 -1
  43. package/dist/testing/CLAUDE.md +83 -25
  44. package/dist/testing/app_server.d.ts +6 -3
  45. package/dist/testing/app_server.d.ts.map +1 -1
  46. package/dist/testing/app_server.js +20 -20
  47. package/dist/testing/connection_closer_helpers.d.ts +10 -4
  48. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  49. package/dist/testing/connection_closer_helpers.js +4 -0
  50. package/dist/testing/cross_backend/capabilities.d.ts +3 -3
  51. package/dist/testing/cross_backend/default_backend_configs.d.ts +12 -12
  52. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  53. package/dist/testing/cross_backend/default_backend_configs.js +11 -10
  54. package/dist/testing/cross_backend/full_spine_mount.d.ts +35 -11
  55. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  56. package/dist/testing/cross_backend/full_spine_mount.js +26 -9
  57. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +16 -1
  58. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  59. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +5 -4
  60. package/dist/testing/cross_backend/spine_surface_constants.d.ts +43 -3
  61. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/spine_surface_constants.js +43 -3
  63. package/dist/testing/cross_backend/testing_backdoor.d.ts +15 -1
  64. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/testing_backdoor.js +51 -6
  66. package/dist/testing/cross_backend/testing_reset_actions.d.ts +6 -3
  67. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/testing_reset_actions.js +6 -3
  69. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
  70. package/dist/testing/cross_backend/testing_server_bun.js +5 -1
  71. package/dist/testing/cross_backend/testing_server_core.d.ts +72 -41
  72. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
  73. package/dist/testing/cross_backend/testing_server_core.js +22 -14
  74. package/dist/testing/cross_backend/testing_server_deno.js +1 -1
  75. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -1
  76. package/dist/testing/cross_backend/testing_server_node.js +20 -7
  77. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +15 -2
  78. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  79. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -8
  80. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +26 -0
  81. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -0
  82. package/dist/testing/cross_backend/ws_action_rate_limit.js +261 -0
  83. package/dist/testing/middleware.d.ts.map +1 -1
  84. package/dist/testing/middleware.js +4 -3
  85. package/dist/testing/stubs.d.ts +17 -4
  86. package/dist/testing/stubs.d.ts.map +1 -1
  87. package/dist/testing/stubs.js +46 -13
  88. package/dist/testing/transports/ws_transport.d.ts +7 -0
  89. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  90. package/dist/testing/transports/ws_transport.js +1 -1
  91. package/dist/testing/ws_round_trip.d.ts +43 -37
  92. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  93. package/dist/testing/ws_round_trip.js +28 -46
  94. package/package.json +2 -2
@@ -7,15 +7,20 @@ import '../assert_dev_env.js';
7
7
  * the `cross_backend/*` suites (and the cross-impl bench) can drive it the
8
8
  * same way they drive the Rust spine. This module owns the runtime-neutral
9
9
  * orchestration — stale-daemon check, daemon-info write, serve, post-serve
10
- * WS attach, graceful drain shutdown — and delegates the runtime-boundary
10
+ * WS attach, graceful shutdown — and delegates the runtime-boundary
11
11
  * primitives (HTTP serve, WS upgrade construction, signals, pid, exit) to a
12
- * {@link TestingServerAdapter}. The two shipped adapters are
13
- * `testing/cross_backend/testing_server_node.ts` (`@hono/node-server` + `@hono/node-ws`) and
14
- * `testing/cross_backend/testing_server_deno.ts` (`Deno.serve` + `@hono/deno`).
12
+ * `TestingServerAdapter`. The shipped adapters are
13
+ * `testing/cross_backend/testing_server_node.ts` (`@hono/node-server` + `@hono/node-ws`),
14
+ * `testing/cross_backend/testing_server_deno.ts` (`Deno.serve` + `@hono/deno`), and
15
+ * `testing/cross_backend/testing_server_bun.ts` (`Bun.serve` + `@hono/bun`).
15
16
  *
16
- * The app itself — routes, RPC, DB, `_testing_reset`, optional WS mount —
17
- * is the caller's {@link StartTestingServerOptions.build_app} seam, so this
18
- * core stays domain-free. fuz_app's own `testing_spine_server` passes a
17
+ * The app itself — routes, RPC, DB, `_testing_reset`, WS endpoints — is the
18
+ * caller's `StartTestingServerOptions.build_app` seam, so this core
19
+ * stays domain-free. WebSocket endpoints mount through `create_app_server`'s
20
+ * `ws_endpoints`: `build_app` asks its context to prepare the runtime's
21
+ * upgrade helper and passes the resulting factory as
22
+ * `create_upgrade_websocket`; the core attaches it to the server once
23
+ * `serve` returns (Node's `injectWebSocket`). fuz_app's own `testing_spine_server` passes a
19
24
  * no-domain build; consumers (zzz, fuz_forge) pass their domain build.
20
25
  *
21
26
  * **NEVER ships in a release.** This module lives under `cross_backend/` and
@@ -33,32 +38,52 @@ import type { RuntimeDeps } from '../../runtime/deps.js';
33
38
  /**
34
39
  * Adapter-built handle to a bound HTTP server.
35
40
  *
36
- * `shutdown` stops accepting new connections and drains in-flight ones.
37
- * `native` is an adapter-specific server reference — used by Node's
38
- * `@hono/node-ws` `injectWebSocket(server)` post-serve hook; Deno leaves it
39
- * unset.
41
+ * `shutdown` stops accepting new connections at once and settles when the
42
+ * in-flight ones have drained — the core starts it, closes the app (which
43
+ * ends live WebSockets), then awaits it. `native` is an adapter-specific
44
+ * server reference — used by Node's `@hono/node-ws` `injectWebSocket(server)`
45
+ * post-serve hook.
40
46
  */
41
47
  export interface ServeHandle {
42
48
  shutdown: () => Promise<void>;
43
49
  /** Adapter-specific server ref for post-serve hooks. Type-erased at the seam. */
44
50
  native?: unknown;
45
51
  }
52
+ /** Options for `TestingServerAdapter.prepare_websocket`. */
53
+ export interface PrepareWebsocketOptions {
54
+ /**
55
+ * Largest inbound WebSocket message, in bytes — pass the largest
56
+ * `WsEndpointSpec.max_message_bytes` of the mounted endpoints; a smaller
57
+ * value refuses messages the endpoint would accept. Node's `ws` server takes
58
+ * it as its frame cap (`maxPayload`), so an oversized message is refused
59
+ * before it is buffered whole; Deno and Bun ignore it, leaving the
60
+ * per-message check `register_ws_endpoint` makes (both close with
61
+ * `WS_CLOSE_MESSAGE_TOO_BIG`). The default sits far below `ws`'s own
62
+ * 100 MiB, so a build raising an endpoint's cap must pass it here too.
63
+ *
64
+ * @default DEFAULT_WS_MAX_MESSAGE_BYTES
65
+ */
66
+ max_message_bytes?: number;
67
+ }
46
68
  /**
47
69
  * Result of an adapter's WS preparation step.
48
70
  *
49
- * `upgrade_websocket` is the Hono `UpgradeWebSocket` closure the caller's
50
- * WS mount uses to register the endpoint. `attach_to_server` runs after
51
- * `serve()` returns a {@link ServeHandle} — Node uses it for
52
- * `injectWebSocket(server)`; Deno leaves it undefined.
71
+ * `create_upgrade_websocket` is the factory a build passes to
72
+ * `create_app_server` as `AppServerOptions.create_upgrade_websocket`.
73
+ * `attach_to_server` runs after `serve()` returns a `ServeHandle` —
74
+ * Node uses it for `injectWebSocket(server)`, and it does nothing when the
75
+ * factory was never called (no `ws_endpoints`); Deno and Bun leave it
76
+ * undefined.
53
77
  */
54
78
  export interface PreparedWebsocket {
55
- upgrade_websocket: UpgradeWebSocket;
79
+ create_upgrade_websocket: (app: Hono) => UpgradeWebSocket;
56
80
  attach_to_server?: (handle: ServeHandle) => void;
57
81
  }
58
82
  /**
59
83
  * Runtime adapter contract for the test-binary entry. Each adapter
60
- * (`testing/cross_backend/testing_server_node.ts`, `testing/cross_backend/testing_server_deno.ts`) implements this and
61
- * hands the shape to {@link start_testing_server}.
84
+ * (`testing/cross_backend/testing_server_node.ts`, `testing/cross_backend/testing_server_deno.ts`,
85
+ * `testing/cross_backend/testing_server_bun.ts`) implements this and hands the shape to
86
+ * `start_testing_server`.
62
87
  */
63
88
  export interface TestingServerAdapter {
64
89
  /** Human-readable runtime label for log output (e.g. `"Node"`, `"Deno"`). */
@@ -67,9 +92,9 @@ export interface TestingServerAdapter {
67
92
  runtime: RuntimeDeps;
68
93
  /** Extract the raw TCP connection IP from a Hono context. */
69
94
  get_connection_ip: (c: Context) => string | undefined;
70
- /** Build the WS upgrade closure after the caller's `build_app` returns the app. */
71
- prepare_websocket: (app: Hono) => PreparedWebsocket;
72
- /** Bind `app.fetch` to `port` on `hostname`; return a {@link ServeHandle}. */
95
+ /** Prepare the runtime's WS upgrade helper and its post-serve attach step. */
96
+ prepare_websocket: (options?: PrepareWebsocketOptions) => PreparedWebsocket;
97
+ /** Bind `app.fetch` to `port` on `hostname`; return a `ServeHandle`. */
73
98
  serve: (options: {
74
99
  fetch: Hono['fetch'];
75
100
  port: number;
@@ -82,24 +107,31 @@ export interface TestingServerAdapter {
82
107
  /** Forceful exit on graceful-shutdown completion or fatal error. */
83
108
  exit: (code: number) => never;
84
109
  }
85
- /**
86
- * The assembled app a {@link StartTestingServerOptions.build_app} seam
87
- * returns.
88
- *
89
- * `mount_websocket` is invoked by the core after the app exists and the
90
- * adapter prepared the WS upgrade closure — the closure mounts the WS
91
- * endpoint(s) (e.g. via `register_ws_endpoint`) and wires any
92
- * audit-revocation guards. Omit it for an HTTP-only binary.
93
- */
110
+ /** What the core hands a `StartTestingServerOptions.build_app` seam. */
111
+ export interface BuildTestingAppContext {
112
+ /**
113
+ * Prepare the runtime adapter's WS upgrade helper and return the factory
114
+ * to pass to `create_app_server` as `create_upgrade_websocket`. The core
115
+ * runs the adapter's attach step once the server is bound, so a build
116
+ * that mounts `ws_endpoints` needs nothing else. An HTTP-only build
117
+ * doesn't call it. Call it at most once per build: the Node adapter
118
+ * attaches one upgrade listener per preparation, and two would race on
119
+ * the shared socket.
120
+ */
121
+ prepare_websocket: (options?: PrepareWebsocketOptions) => (app: Hono) => UpgradeWebSocket;
122
+ }
123
+ /** The assembled app a `StartTestingServerOptions.build_app` seam returns. */
94
124
  export interface BuiltTestingApp {
95
- /** The assembled Hono app (HTTP routes + RPC already mounted). */
125
+ /** The assembled Hono app (HTTP routes, RPC, and WS endpoints already mounted). */
96
126
  app: Hono;
97
- /** Tear down backend(s) + DB + any rotation on graceful shutdown. */
127
+ /**
128
+ * Tear down backend(s) + DB + any rotation on graceful shutdown. Called
129
+ * after the listener stops accepting connections and before its drain is
130
+ * awaited, so it must end live WebSockets (`AppServer.close` does).
131
+ */
98
132
  close: () => Promise<void>;
99
- /** Mount WS endpoint(s) given the runtime-prepared upgrade closure. */
100
- mount_websocket?: (upgrade_websocket: UpgradeWebSocket) => void;
101
133
  }
102
- /** Options for {@link start_testing_server}. */
134
+ /** Options for `start_testing_server`. */
103
135
  export interface StartTestingServerOptions {
104
136
  /** Runtime-boundary adapter (Node or Deno). */
105
137
  adapter: TestingServerAdapter;
@@ -120,10 +152,9 @@ export interface StartTestingServerOptions {
120
152
  /**
121
153
  * Build the app. Closes over the entry's runtime + connection-IP getter
122
154
  * + password deps + resolved config — so this core never touches the
123
- * domain. Returns the assembled app, a `close` teardown, and an optional
124
- * `mount_websocket` hook.
155
+ * domain. Returns the assembled app and a `close` teardown.
125
156
  */
126
- build_app: () => Promise<BuiltTestingApp>;
157
+ build_app: (context: BuildTestingAppContext) => Promise<BuiltTestingApp>;
127
158
  /** Optional logger; defaults to a `[daemon_name]`-namespaced `Logger`. */
128
159
  log?: LoggerType;
129
160
  }
@@ -141,9 +172,9 @@ export declare const is_loopback_host: (host: string) => boolean;
141
172
  * Boot a test-mode server using the supplied runtime adapter.
142
173
  *
143
174
  * Mirrors a production `start_server` at the surface level — stale-daemon
144
- * check, daemon-info write, bind, graceful drain — but the app is the
145
- * caller's no-domain (or domain) {@link StartTestingServerOptions.build_app}
146
- * and the runtime boundary is the {@link TestingServerAdapter}. Refuses any
175
+ * check, daemon-info write, bind, graceful shutdown — but the app is the
176
+ * caller's no-domain (or domain) `StartTestingServerOptions.build_app`
177
+ * and the runtime boundary is the `TestingServerAdapter`. Refuses any
147
178
  * non-loopback bind host (the test binary must stay on loopback — see
148
179
  * `is_loopback_host`).
149
180
  */
@@ -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,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAU,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAG5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD;;;;;;;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;QAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,WAAW,CAAC;IAC1F,+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
+ {"version":3,"file":"testing_server_core.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_core.ts"],"names":[],"mappings":"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"}
@@ -18,9 +18,9 @@ export const is_loopback_host = (host) => {
18
18
  * Boot a test-mode server using the supplied runtime adapter.
19
19
  *
20
20
  * Mirrors a production `start_server` at the surface level — stale-daemon
21
- * check, daemon-info write, bind, graceful drain — but the app is the
22
- * caller's no-domain (or domain) {@link StartTestingServerOptions.build_app}
23
- * and the runtime boundary is the {@link TestingServerAdapter}. Refuses any
21
+ * check, daemon-info write, bind, graceful shutdown — but the app is the
22
+ * caller's no-domain (or domain) `StartTestingServerOptions.build_app`
23
+ * and the runtime boundary is the `TestingServerAdapter`. Refuses any
24
24
  * non-loopback bind host (the test binary must stay on loopback — see
25
25
  * `is_loopback_host`).
26
26
  */
@@ -42,12 +42,14 @@ export const start_testing_server = async (options) => {
42
42
  log.warn(`stale daemon.json (pid ${stale.pid} not running), replacing`);
43
43
  }
44
44
  }
45
- const built = await build_app();
46
- let ws;
47
- if (built.mount_websocket) {
48
- ws = adapter.prepare_websocket(built.app);
49
- built.mount_websocket(ws.upgrade_websocket);
50
- }
45
+ const prepared_websockets = [];
46
+ const built = await build_app({
47
+ prepare_websocket: (ws_options) => {
48
+ const prepared = adapter.prepare_websocket(ws_options);
49
+ prepared_websockets.push(prepared);
50
+ return prepared.create_upgrade_websocket;
51
+ }
52
+ });
51
53
  await write_daemon_info(runtime, daemon_name, {
52
54
  version: 1,
53
55
  pid: adapter.pid,
@@ -57,7 +59,8 @@ export const start_testing_server = async (options) => {
57
59
  });
58
60
  log.info(`Listening on http://${host}:${port} (${adapter.runtime_label}, test mode)`);
59
61
  const server = adapter.serve({ fetch: built.app.fetch, port, hostname: host });
60
- ws?.attach_to_server?.(server);
62
+ for (const prepared of prepared_websockets)
63
+ prepared.attach_to_server?.(server);
61
64
  let shutting_down = false;
62
65
  const shutdown = async () => {
63
66
  if (shutting_down)
@@ -65,11 +68,16 @@ export const start_testing_server = async (options) => {
65
68
  shutting_down = true;
66
69
  log.info('shutting down...');
67
70
  try {
68
- // Drain HTTP first (stop accepting + let in-flight requests finish)
69
- // before tearing down the backend, so a request still draining
70
- // never hits a closed DB.
71
- await server.shutdown();
71
+ // Stop accepting first so no new request reaches a closed DB, but
72
+ // don't await the drain yet: live WebSockets hold it open until
73
+ // `built.close()` ends them (`AppServer.close`), so awaiting it
74
+ // first would wait on sockets nothing is closing. A request already
75
+ // in flight when the signal lands may still see the backend close
76
+ // under it — acceptable for a test binary, where shutdown follows
77
+ // the suite.
78
+ const drained = server.shutdown();
72
79
  await built.close();
80
+ await drained;
73
81
  }
74
82
  catch (error) {
75
83
  log.error('shutdown error:', error);
@@ -23,7 +23,7 @@ export const create_deno_testing_adapter = () => ({
23
23
  runtime: create_deno_runtime([]),
24
24
  get_connection_ip: (c) => c.env?.remoteAddr?.hostname,
25
25
  // Deno's WS upgrade is module-level and stateless — no post-serve attach.
26
- prepare_websocket: () => ({ upgrade_websocket: upgradeWebSocket }),
26
+ prepare_websocket: () => ({ create_upgrade_websocket: () => upgradeWebSocket }),
27
27
  serve: ({ fetch, port, hostname }) => {
28
28
  const server = Deno.serve({ port, hostname }, fetch);
29
29
  const handle = { shutdown: () => server.shutdown(), native: server };
@@ -1 +1 @@
1
- {"version":3,"file":"testing_server_node.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_node.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,KAAK,EAAe,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAUlF,mDAAmD;AACnD,eAAO,MAAM,2BAA2B,QAAO,oBAsB7C,CAAC"}
1
+ {"version":3,"file":"testing_server_node.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_node.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2B9B,OAAO,KAAK,EAAe,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAUlF,mDAAmD;AACnD,eAAO,MAAM,2BAA2B,QAAO,oBA+B7C,CAAC"}
@@ -3,9 +3,12 @@ import '../assert_dev_env.js';
3
3
  * Node runtime adapter for spawnable cross-process test server binaries.
4
4
  *
5
5
  * Binds `@hono/node-server`'s `serve()` and `@hono/node-ws`'s two-phase
6
- * `createNodeWebSocket({app})` / `injectWebSocket(server)`. The shared
7
- * `testing/cross_backend/testing_server_core.ts` owns the rest. A test binary builds this adapter
8
- * and hands it to `start_testing_server` alongside its `build_app` seam.
6
+ * `createNodeWebSocket({app})` / `injectWebSocket(server)`: the factory
7
+ * `prepare_websocket` returns builds the first half from the app
8
+ * `create_app_server` assembles, and the core runs the second once `serve()`
9
+ * returns. The shared `testing/cross_backend/testing_server_core.ts` owns
10
+ * the rest. A test binary builds this adapter and hands it to
11
+ * `start_testing_server` alongside its `build_app` seam.
9
12
  *
10
13
  * `@hono/node-server` + `@hono/node-ws` are **optional** peer deps (same
11
14
  * posture as `ws`) — only test binaries import them; production bundles
@@ -17,6 +20,7 @@ import process from 'node:process';
17
20
  import { serve } from '@hono/node-server';
18
21
  import { getConnInfo } from '@hono/node-server/conninfo';
19
22
  import { createNodeWebSocket } from '@hono/node-ws';
23
+ import { DEFAULT_WS_MAX_MESSAGE_BYTES } from '../../actions/transports.js';
20
24
  import { create_node_runtime } from '../../runtime/node.js';
21
25
  const node_serve_handle = (server) => ({
22
26
  shutdown: () => new Promise((resolve, reject) => {
@@ -29,14 +33,23 @@ export const create_node_testing_adapter = () => ({
29
33
  runtime_label: 'Node',
30
34
  runtime: create_node_runtime(),
31
35
  get_connection_ip: (c) => getConnInfo(c).remote.address,
32
- prepare_websocket: (app) => {
33
- const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });
36
+ prepare_websocket: (options) => {
37
+ const max_payload = options?.max_message_bytes ?? DEFAULT_WS_MAX_MESSAGE_BYTES;
38
+ let node_ws;
34
39
  return {
35
- upgrade_websocket: upgradeWebSocket,
40
+ create_upgrade_websocket: (app) => {
41
+ node_ws = createNodeWebSocket({ app });
42
+ // `@hono/node-ws` builds its `ws` server with no options and takes
43
+ // none, so its frame cap would stay `ws`'s 100 MiB default. `ws` reads
44
+ // `options.maxPayload` per upgrade, so setting it here — before any
45
+ // connection — caps every socket at the endpoint's message limit.
46
+ node_ws.wss.options.maxPayload = max_payload;
47
+ return node_ws.upgradeWebSocket;
48
+ },
36
49
  attach_to_server: (handle) => {
37
50
  // `handle.native` is the `ServerType` from `serve()` —
38
51
  // type-erased at the {@link ServeHandle} seam, so it downcasts here.
39
- injectWebSocket(handle.native);
52
+ node_ws?.injectWebSocket(handle.native);
40
53
  }
41
54
  };
42
55
  },
@@ -17,8 +17,6 @@ import '../assert_dev_env.js';
17
17
  * @module
18
18
  */
19
19
  import type { BackendConfig } from './backend_config.js';
20
- /** Env var naming the backend root dir; `{dir}/run/daemon_token` must match `bootstrap.daemon_token_path`. */
21
- export declare const TS_SPINE_DIR_ENV = "FUZ_TESTING_TS_SPINE_DIR";
22
20
  /** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
23
21
  export declare const TS_SPINE_NODE_DEFAULT_PORT = 1178;
24
22
  /** Default port for the Deno TS spine binary. */
@@ -47,6 +45,21 @@ export interface TsSpineBackendConfigOptions {
47
45
  * client IP. Mirrors `SpineStubBackendConfigOptions.enable_login_rate_limit`.
48
46
  */
49
47
  readonly enable_login_rate_limit?: boolean;
48
+ /**
49
+ * Enable the action rate limiters (`FUZ_ACTION_RATE_LIMIT_ENABLED=true`) —
50
+ * one per-IP and one per-account limiter shared by the RPC and WS
51
+ * endpoints. Off by default, like `enable_login_rate_limit`; set only for
52
+ * the dedicated security cross project, which drives them through
53
+ * `ws_action_rate_limit.ts`. Mirrors `SpineStubBackendConfigOptions.enable_action_rate_limit`.
54
+ */
55
+ readonly enable_action_rate_limit?: boolean;
56
+ /**
57
+ * Replace both action limiters' `max_attempts`
58
+ * (`FUZ_ACTION_RATE_LIMIT_MAX_ATTEMPTS`) so a suite reaches the cap in a few
59
+ * calls. Requires `enable_action_rate_limit` — the binary refuses to boot
60
+ * otherwise. Mirrors `SpineStubBackendConfigOptions.action_rate_limit_max_attempts`.
61
+ */
62
+ readonly action_rate_limit_max_attempts?: number;
50
63
  }
51
64
  /**
52
65
  * `BackendConfig` for the Node TS spine binary — spawned via `gro run`
@@ -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,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AA0C3D,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;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAC3C;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgCF,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,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAsDzD,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;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAiCF,CAAC"}
@@ -1,15 +1,14 @@
1
1
  import '../assert_dev_env.js';
2
2
  import { build_test_backend_paths } from './build_test_backend_paths.js';
3
- import { LOGIN_RATE_LIMIT_ENABLED_ENV, make_default_ts_backend_config, ts_default_capabilities } from './default_backend_configs.js';
4
- import { SPINE_SSE_PATH } from './spine_surface_constants.js';
5
- /** Env var naming the backend root dir; `{dir}/run/daemon_token` must match `bootstrap.daemon_token_path`. */
6
- export const TS_SPINE_DIR_ENV = 'FUZ_TESTING_TS_SPINE_DIR';
3
+ import { action_rate_limit_env, make_default_ts_backend_config, ts_default_capabilities } from './default_backend_configs.js';
4
+ import { LOGIN_RATE_LIMIT_ENABLED_ENV, SPINE_SSE_PATH, TS_SPINE_DIR_ENV } from './spine_surface_constants.js';
7
5
  /**
8
6
  * Capabilities for the TS spine binary — `ts_default_capabilities` plus `sse`
9
7
  * (the binary wires `audit_log_sse`), `ready` (the binary live-mounts the
10
8
  * `/ready` deploy gate in `build_spine_app`), `peer_request` (the backend
11
9
  * WS transport's `request_connection` path drives server→client `peer/ping`),
12
- * `ws_account_actions` (the binary mounts the account actions on `/api/ws`),
10
+ * `ws_account_actions` (the binary mounts its full action set on `/api/ws`,
11
+ * the account actions among them),
13
12
  * and `cell_gated_create` (the binary's `full_spine_mount` mounts the
14
13
  * `test_cell_gated_create_authorize` policy).
15
14
  */
@@ -78,7 +77,8 @@ export const ts_spine_node_backend_config = (options = {}) => {
78
77
  paths,
79
78
  extra_env: {
80
79
  [TS_SPINE_DIR_ENV]: paths.root,
81
- ...login_rate_limit_env(enable_login_rate_limit)
80
+ ...login_rate_limit_env(enable_login_rate_limit),
81
+ ...action_rate_limit_env(options)
82
82
  },
83
83
  capabilities: ts_spine_capabilities
84
84
  }),
@@ -105,7 +105,8 @@ export const ts_spine_bun_backend_config = (options = {}) => {
105
105
  paths,
106
106
  extra_env: {
107
107
  [TS_SPINE_DIR_ENV]: paths.root,
108
- ...login_rate_limit_env(enable_login_rate_limit)
108
+ ...login_rate_limit_env(enable_login_rate_limit),
109
+ ...action_rate_limit_env(options)
109
110
  },
110
111
  capabilities: ts_spine_bun_capabilities
111
112
  }),
@@ -149,7 +150,8 @@ export const ts_spine_deno_backend_config = (options = {}) => {
149
150
  paths,
150
151
  extra_env: {
151
152
  [TS_SPINE_DIR_ENV]: paths.root,
152
- ...login_rate_limit_env(enable_login_rate_limit)
153
+ ...login_rate_limit_env(enable_login_rate_limit),
154
+ ...action_rate_limit_env(options)
153
155
  },
154
156
  capabilities: ts_spine_capabilities
155
157
  }),
@@ -0,0 +1,26 @@
1
+ import '../assert_dev_env.js';
2
+ import type { SetupTest } from './setup.js';
3
+ /** Options for `describe_ws_action_rate_limit_cross_tests`. */
4
+ export interface WsActionRateLimitCrossTestOptions {
5
+ /** Per-test fixture producer (cross-process only — see the module doc). */
6
+ readonly setup_test: SetupTest;
7
+ /** Base URL the backend is reachable at (e.g. `http://localhost:1177`). */
8
+ readonly base_url: string;
9
+ /** WebSocket endpoint path (e.g. `/api/ws`). */
10
+ readonly ws_path: string;
11
+ /** JSON-RPC endpoint path. Default `/api/rpc` (the spine convention). */
12
+ readonly rpc_path?: string;
13
+ /**
14
+ * The `max_attempts` both action limiters were started with
15
+ * (`action_rate_limit_max_attempts` on the backend config). At least 2 —
16
+ * the shared-bucket case splits the budget across the two transports.
17
+ */
18
+ readonly max_attempts: number;
19
+ }
20
+ /**
21
+ * Register the action rate-limit suite: the account axis over WS, one account
22
+ * bucket across HTTP RPC and WS, the per-IP axis keyed by `X-Forwarded-For`
23
+ * across both transports, and the account axis at the RPC and WS dispatchers.
24
+ */
25
+ export declare const describe_ws_action_rate_limit_cross_tests: (options: WsActionRateLimitCrossTestOptions) => void;
26
+ //# sourceMappingURL=ws_action_rate_limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws_action_rate_limit.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_action_rate_limit.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmE9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAyC5C,+DAA+D;AAC/D,MAAM,WAAW,iCAAiC;IACjD,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAkBD;;;;GAIG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,iCAAiC,KACxC,IAqRF,CAAC"}