@fuzdev/fuz_app 0.134.0 → 0.135.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.
- package/dist/actions/CLAUDE.md +91 -9
- package/dist/actions/register_action_ws.d.ts +23 -3
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +57 -37
- package/dist/actions/transports.d.ts +23 -0
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +23 -0
- package/dist/actions/transports_ws_backend.d.ts +132 -9
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +236 -33
- package/dist/actions/ws_endpoint_spec.d.ts +9 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/actions/ws_outbound_writer.d.ts +206 -0
- package/dist/actions/ws_outbound_writer.d.ts.map +1 -0
- package/dist/actions/ws_outbound_writer.js +431 -0
- package/dist/auth/CLAUDE.md +2 -2
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +3 -1
- package/dist/auth/cell_actions.js +1 -1
- package/dist/auth/role_schema.d.ts +14 -3
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +14 -2
- package/dist/db/migrate.d.ts +1 -1
- package/dist/db/migrate.js +2 -2
- package/dist/http/CLAUDE.md +2 -1
- package/dist/http/body_limit.d.ts +61 -0
- package/dist/http/body_limit.d.ts.map +1 -0
- package/dist/http/body_limit.js +87 -0
- package/dist/http/db_routes.js +2 -2
- package/dist/http/error_schemas.d.ts +2 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +2 -0
- package/dist/realtime/outbound_budget.d.ts +91 -0
- package/dist/realtime/outbound_budget.d.ts.map +1 -0
- package/dist/realtime/outbound_budget.js +108 -0
- package/dist/realtime/sse.d.ts +36 -3
- package/dist/realtime/sse.d.ts.map +1 -1
- package/dist/realtime/sse.js +58 -6
- package/dist/realtime/sse_constants.d.ts +12 -0
- package/dist/realtime/sse_constants.d.ts.map +1 -1
- package/dist/realtime/sse_constants.js +12 -0
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +3 -6
- package/dist/testing/CLAUDE.md +70 -10
- package/dist/testing/admin_integration.js +1 -1
- package/dist/testing/audit_completeness.d.ts +1 -1
- package/dist/testing/audit_completeness.js +1 -1
- package/dist/testing/audit_drift_guard.d.ts +1 -1
- package/dist/testing/audit_drift_guard.js +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +9 -9
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +110 -60
- package/dist/testing/cross_backend/capabilities.d.ts +18 -15
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +1 -0
- package/dist/testing/cross_backend/default_backend_configs.js +5 -5
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts +6 -0
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_role_grammar.js +117 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +2 -3
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +2 -3
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +8 -4
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +82 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +101 -2
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +9 -7
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +29 -0
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_outbound_overflow.js +211 -0
- package/dist/testing/integration.js +1 -1
- package/dist/testing/schema_introspect.d.ts +10 -2
- package/dist/testing/schema_introspect.d.ts.map +1 -1
- package/dist/testing/schema_introspect.js +15 -7
- package/dist/testing/transports/ws_transport.d.ts +4 -2
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +2 -1
- package/dist/testing/ws_round_trip.d.ts +32 -2
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +26 -4
- package/package.json +1 -1
|
@@ -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
|
|
1
|
+
{"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,10 +8,12 @@ import '../assert_dev_env.js';
|
|
|
8
8
|
* expired-by-construction server-side session for the expiry conformance
|
|
9
9
|
* cases), **`_testing_put_fact`** (seed an embedded fact for the fact-serving
|
|
10
10
|
* suite), **`_testing_schema_snapshot`** (introspect the live schema for
|
|
11
|
-
* cross-impl parity diffing against a Rust backend's `fuz_db` snapshot),
|
|
11
|
+
* cross-impl parity diffing against a Rust backend's `fuz_db` snapshot),
|
|
12
12
|
* **`_testing_migration_tracker`** (dump the `schema_version` tracker rows for
|
|
13
13
|
* cross-impl migration-identity parity — the provenance half the snapshot gate
|
|
14
|
-
* excludes by design)
|
|
14
|
+
* excludes by design), and **`_testing_flood_notifications`** (queue a burst
|
|
15
|
+
* of notifications onto the caller's own WebSocket connection, to push its
|
|
16
|
+
* outbound queue past its byte budget).
|
|
15
17
|
*
|
|
16
18
|
* A further daemon-token action, **`_testing_action_manifest`** (dump the live
|
|
17
19
|
* RPC registry for cross-impl method-set + auth-shape parity), lives here too
|
|
@@ -72,6 +74,7 @@ import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
|
72
74
|
import { fact_hash_bytes } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
73
75
|
import { FactHashSchema } from '@fuzdev/fuz_util/hash_schemas.ts';
|
|
74
76
|
import { rpc_action } from '../../actions/action_rpc.js';
|
|
77
|
+
import { jsonrpc_errors } from '../../http/jsonrpc_errors.js';
|
|
75
78
|
import { emit_after_commit } from '../../http/pending_effects.js';
|
|
76
79
|
import { protocol_action_specs } from '../../actions/protocol.js';
|
|
77
80
|
import { query_put_fact } from '../../db/fact_queries.js';
|
|
@@ -291,6 +294,101 @@ export const testing_put_fact_action_spec = {
|
|
|
291
294
|
description: 'Test-binary only — seed an embedded fact (blake3 of the UTF-8 content) and return its hash, ' +
|
|
292
295
|
'so the cross-process fact-serving suite can store bytes without a DB handle.'
|
|
293
296
|
};
|
|
297
|
+
/** The wire method of each notification `_testing_flood_notifications` queues. */
|
|
298
|
+
export const TESTING_FLOOD_NOTIFICATION_METHOD = '_testing_flood';
|
|
299
|
+
/**
|
|
300
|
+
* The most notifications one `_testing_flood_notifications` call queues.
|
|
301
|
+
* Empty ones at this count are charged more than
|
|
302
|
+
* `DEFAULT_WS_OUTBOUND_QUEUE_BYTES` between them — each frame's envelope and
|
|
303
|
+
* its overhead — so a burst of tiny frames can overflow a connection on a
|
|
304
|
+
* default transport too. Same value as the Rust spine's.
|
|
305
|
+
*/
|
|
306
|
+
export const TESTING_FLOOD_MAX_COUNT = 65_536;
|
|
307
|
+
/** The largest `payload` one `_testing_flood_notifications` call builds, in bytes. */
|
|
308
|
+
export const TESTING_FLOOD_MAX_SIZE = 16 * 1024 * 1024;
|
|
309
|
+
/**
|
|
310
|
+
* The most payload bytes one `_testing_flood_notifications` call builds across
|
|
311
|
+
* all its notifications — the bound on `count` × `size`. The handler
|
|
312
|
+
* serializes each notification without yielding, so the product is how long
|
|
313
|
+
* one call holds the event loop.
|
|
314
|
+
*/
|
|
315
|
+
export const TESTING_FLOOD_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
316
|
+
/** The `data.reason` of the refusal `_testing_flood_notifications` gives a call over HTTP. */
|
|
317
|
+
export const ERROR_TESTING_FLOOD_NO_CONNECTION = 'no_connection';
|
|
318
|
+
/**
|
|
319
|
+
* `_testing_flood_notifications` — queue a burst of notifications onto the
|
|
320
|
+
* **caller's own WebSocket connection**, all in one synchronous run, so a suite
|
|
321
|
+
* driving the server from outside can push a connection's outbound queue past
|
|
322
|
+
* its byte budget deterministically — without a second producer, and whether
|
|
323
|
+
* or not its client is reading: the connection's write pump gets no turn while
|
|
324
|
+
* the handler runs, so the burst waits at once. Past the budget the server
|
|
325
|
+
* closes the socket with `WS_CLOSE_OUTBOUND_OVERFLOW`.
|
|
326
|
+
*
|
|
327
|
+
* Each notification is
|
|
328
|
+
* `{"jsonrpc":"2.0","method":"_testing_flood","params":{"index":<i>,"payload":"<size × 'x'>"}}`
|
|
329
|
+
* with `index` counting from 0, so every frame is a little longer than `size`
|
|
330
|
+
* bytes and a client can tell which frames reached it.
|
|
331
|
+
*
|
|
332
|
+
* The result is `{queued: count}` — the number asked for, not the number that
|
|
333
|
+
* fit: when the burst overflows the queue the connection is closed and this
|
|
334
|
+
* response is never written. A burst that fits arrives in order, ahead of the
|
|
335
|
+
* response.
|
|
336
|
+
*
|
|
337
|
+
* A call past `TESTING_FLOOD_MAX_COUNT`, `TESTING_FLOOD_MAX_SIZE`, or
|
|
338
|
+
* `TESTING_FLOOD_MAX_TOTAL_BYTES` (`count` × `size`) is refused with
|
|
339
|
+
* `invalid_params` and queues nothing. Over HTTP there is no connection to
|
|
340
|
+
* queue onto, so the call is refused with `invalid_params`, reason
|
|
341
|
+
* `ERROR_TESTING_FLOOD_NO_CONNECTION`, rather than answering as though it had
|
|
342
|
+
* flooded something.
|
|
343
|
+
*
|
|
344
|
+
* `auth` gates on the daemon-token credential, matching `_testing_reset`. The
|
|
345
|
+
* Rust mirror is `fuz_testing::create_testing_flood_notifications_action_spec`.
|
|
346
|
+
*/
|
|
347
|
+
export const testing_flood_notifications_action_spec = {
|
|
348
|
+
method: '_testing_flood_notifications',
|
|
349
|
+
kind: 'request_response',
|
|
350
|
+
initiator: 'frontend',
|
|
351
|
+
auth: TESTING_ACTION_AUTH,
|
|
352
|
+
side_effects: false,
|
|
353
|
+
input: z.strictObject({
|
|
354
|
+
count: z.number().int().min(0).max(TESTING_FLOOD_MAX_COUNT).meta({
|
|
355
|
+
description: 'How many notifications to queue.'
|
|
356
|
+
}),
|
|
357
|
+
size: z.number().int().min(0).max(TESTING_FLOOD_MAX_SIZE).meta({
|
|
358
|
+
description: "The byte length of each notification's `payload` string."
|
|
359
|
+
})
|
|
360
|
+
}),
|
|
361
|
+
output: z.strictObject({
|
|
362
|
+
queued: z.number().int().meta({
|
|
363
|
+
description: 'The number of notifications asked for — not the number that fit.'
|
|
364
|
+
})
|
|
365
|
+
}),
|
|
366
|
+
async: true,
|
|
367
|
+
error_reasons: [ERROR_TESTING_FLOOD_NO_CONNECTION],
|
|
368
|
+
description: "Test-binary only — queue a burst of notifications onto the caller's own WebSocket " +
|
|
369
|
+
'connection in one synchronous run, to push its outbound queue past its byte budget.'
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
372
|
+
* Build the standalone `_testing_flood_notifications` action (see the spec
|
|
373
|
+
* doc). The spine binary bundles it via `create_testing_actions`; mount it
|
|
374
|
+
* directly on an in-process WS endpoint whose suite drives an outbound
|
|
375
|
+
* overflow.
|
|
376
|
+
*/
|
|
377
|
+
export const create_testing_flood_notifications_action = () =>
|
|
378
|
+
// not `async`: the burst must be queued in one synchronous run
|
|
379
|
+
rpc_action(testing_flood_notifications_action_spec, ({ count, size }, ctx) => {
|
|
380
|
+
if (count * size > TESTING_FLOOD_MAX_TOTAL_BYTES) {
|
|
381
|
+
throw jsonrpc_errors.invalid_params(`count * size must be at most ${TESTING_FLOOD_MAX_TOTAL_BYTES}`);
|
|
382
|
+
}
|
|
383
|
+
if (ctx.connection_id === undefined) {
|
|
384
|
+
throw jsonrpc_errors.invalid_params('_testing_flood_notifications needs a WebSocket connection', { reason: ERROR_TESTING_FLOOD_NO_CONNECTION });
|
|
385
|
+
}
|
|
386
|
+
const payload = 'x'.repeat(size);
|
|
387
|
+
for (let index = 0; index < count; index++) {
|
|
388
|
+
ctx.notify(TESTING_FLOOD_NOTIFICATION_METHOD, { index, payload });
|
|
389
|
+
}
|
|
390
|
+
return { queued: count };
|
|
391
|
+
});
|
|
294
392
|
/**
|
|
295
393
|
* `_testing_schema_snapshot` — introspect the live database into a normalized
|
|
296
394
|
* `SchemaSnapshot` for cross-impl parity diffing. The cross-backend harness
|
|
@@ -555,6 +653,7 @@ export const create_testing_actions = (deps, options) => {
|
|
|
555
653
|
return { hash };
|
|
556
654
|
}),
|
|
557
655
|
create_testing_drain_effects_action(deps.audit),
|
|
656
|
+
create_testing_flood_notifications_action(),
|
|
558
657
|
create_testing_schema_snapshot_action(),
|
|
559
658
|
create_testing_migration_tracker_action()
|
|
560
659
|
];
|
|
@@ -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;
|
|
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;AAwDzD,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,6DAA6D;IAC7D,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"}
|
|
@@ -23,13 +23,15 @@ const ts_spine_capabilities = Object.freeze({
|
|
|
23
23
|
/**
|
|
24
24
|
* Capabilities for the **Bun** spine binary — `ts_spine_capabilities` with
|
|
25
25
|
* `oversized_reject_closes_connection: false` and
|
|
26
|
-
* `ws_handshake_pipelining: false`.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* response carries `Connection: close`,
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* `ws_handshake_pipelining: false`.
|
|
27
|
+
*
|
|
28
|
+
* `Bun.serve` does not close the connection after an oversized-body `413`:
|
|
29
|
+
* the response carries `Connection: close`, and Bun keeps the socket open,
|
|
30
|
+
* skips the declared body, and serves the request that follows it (observed
|
|
31
|
+
* on Bun 1.3.11). The next request is framed on `Content-Length`, so nothing
|
|
32
|
+
* is desynchronized, but the smuggling suite's "the pipelined request is never
|
|
33
|
+
* served" assertion does not hold; the flag puts Bun on the suite's no-desync
|
|
34
|
+
* arm, which requires exactly the 413 and the reply to the pipelined request.
|
|
33
35
|
* See `docs/security.md` §"Body Size Limiting".
|
|
34
36
|
*
|
|
35
37
|
* `Bun.serve` also answers `400` to an upgrade request that carries WebSocket
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { type BackendCapabilities } from './capabilities.js';
|
|
3
|
+
import type { SetupTest } from './setup.js';
|
|
4
|
+
/** Options for the outbound-overflow parity suite. */
|
|
5
|
+
export interface WsOutboundOverflowCrossTestOptions {
|
|
6
|
+
/** Per-test fixture producer (`default_cross_process_setup(handle)`). */
|
|
7
|
+
readonly setup_test: SetupTest;
|
|
8
|
+
/** Backend capability flags; every WebSocket case gates on `capabilities.ws`. */
|
|
9
|
+
readonly capabilities: BackendCapabilities;
|
|
10
|
+
/** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
|
|
11
|
+
readonly base_url: string;
|
|
12
|
+
/** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
|
|
13
|
+
readonly ws_path: string;
|
|
14
|
+
/** Origin for the session socket's upgrade. Defaults to `base_url`. */
|
|
15
|
+
readonly origin?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The backend's RPC endpoint path, for the over-HTTP refusal. Omitted, that
|
|
18
|
+
* case is skipped.
|
|
19
|
+
*/
|
|
20
|
+
readonly rpc_path?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Register the outbound-overflow parity suite over a real upgrade: a burst
|
|
24
|
+
* within the budget arrives whole, one past it closes the socket with
|
|
25
|
+
* `WS_CLOSE_OUTBOUND_OVERFLOW` and no response, and the account's other
|
|
26
|
+
* connection is unaffected.
|
|
27
|
+
*/
|
|
28
|
+
export declare const describe_ws_outbound_overflow_cross_tests: (options: WsOutboundOverflowCrossTestOptions) => void;
|
|
29
|
+
//# sourceMappingURL=ws_outbound_overflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws_outbound_overflow.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_outbound_overflow.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAiCzD,sDAAsD;AACtD,MAAM,WAAW,kCAAkC;IAClD,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,iFAAiF;IACjF,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,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,kCAAkC,KACzC,IA0LF,CAAC"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for the **per-connection outbound bound** over a
|
|
4
|
+
* real upgrade.
|
|
5
|
+
*
|
|
6
|
+
* Both spines hold what waits to be written to one WebSocket to a byte budget
|
|
7
|
+
* (`BackendWebsocketTransport`'s `outbound_queue_bytes` on the TS side, the
|
|
8
|
+
* Rust `fuz_realtime` outbound queue on the other; both default to 4 MiB, each
|
|
9
|
+
* waiting frame charged its UTF-8 length plus 64 bytes). A frame that does not
|
|
10
|
+
* fit closes the socket with `WS_CLOSE_OUTBOUND_OVERFLOW` and the reason
|
|
11
|
+
* `outbound queue overflow`; the credential stays good, so a client reconnects
|
|
12
|
+
* under its ordinary backoff. The code and the reason are therefore wire
|
|
13
|
+
* contract, and this suite is the pin that runs on both.
|
|
14
|
+
*
|
|
15
|
+
* It drives the backend's `_testing_flood_notifications` action, which queues
|
|
16
|
+
* a burst of notifications onto the caller's own connection in one synchronous
|
|
17
|
+
* run — so the burst waits at once whether or not the client is reading, and
|
|
18
|
+
* the overflow is deterministic. The cases:
|
|
19
|
+
*
|
|
20
|
+
* - **within the budget** — a small burst arrives whole and in order, ahead of
|
|
21
|
+
* its response, and the socket stays open.
|
|
22
|
+
* - **large, within the budget** — a burst of a few mebibytes, most of the
|
|
23
|
+
* default budget, arrives the same way: every frame whole, in order, the
|
|
24
|
+
* response last. Megabyte frames can outrun a socket's buffer, and where
|
|
25
|
+
* they do — the Deno TS spine, as observed; Node and Bun write the burst
|
|
26
|
+
* through — the frames behind the first wait on the backend's write pump.
|
|
27
|
+
* - **overflow** — a burst far past the budget closes the socket with the code
|
|
28
|
+
* and the exact reason; the response of the flooding request never arrives;
|
|
29
|
+
* any flood frames that did arrive form a contiguous prefix of the burst.
|
|
30
|
+
* A second connection on the same account keeps dispatching throughout.
|
|
31
|
+
* - **refusal** — a burst past the action's own size limit is refused with
|
|
32
|
+
* `invalid_params`, and the socket stays open.
|
|
33
|
+
* - **no connection** — over HTTP RPC the action refuses with `invalid_params`
|
|
34
|
+
* and reason `no_connection`, rather than answering as though it flooded.
|
|
35
|
+
*
|
|
36
|
+
* What the suite does **not** assert, because it is each implementation's own:
|
|
37
|
+
* how many frames accepted before the overflow reach the client (the Rust
|
|
38
|
+
* spine discards them all; the TS spine may deliver the first), and the exact
|
|
39
|
+
* boundary a burst of tiny frames overflows at.
|
|
40
|
+
*
|
|
41
|
+
* The wire literals are deliberate independent copies of the producers'
|
|
42
|
+
* constants — parity by test, so a value changed on one side fails here
|
|
43
|
+
* instead of moving both at once.
|
|
44
|
+
*
|
|
45
|
+
* The flood action is gated on the daemon token, so the flooding socket
|
|
46
|
+
* upgrades with it and no `Origin` (both spines discard a daemon token
|
|
47
|
+
* presented in a browser context). Gated on `capabilities.ws`; cross-process
|
|
48
|
+
* only (`create_ws_transport` needs a real bound socket), and only for a
|
|
49
|
+
* backend that mounts `_testing_flood_notifications` on its WS endpoint.
|
|
50
|
+
*
|
|
51
|
+
* @module
|
|
52
|
+
*/
|
|
53
|
+
import { assert, describe } from 'vitest';
|
|
54
|
+
import { create_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
|
|
55
|
+
import { create_admitted_ws_transport } from '../transports/ws_transport.js';
|
|
56
|
+
import { is_notification, is_response_for } from '../transports/ws_client.js';
|
|
57
|
+
import { test_if } from './capabilities.js';
|
|
58
|
+
import { cross_rpc_call } from './cell_cross_helpers.js';
|
|
59
|
+
/** The close code both spines send on an outbound overflow. */
|
|
60
|
+
const WS_CLOSE_OUTBOUND_OVERFLOW = 4005;
|
|
61
|
+
/** The close reason both spines send with it. */
|
|
62
|
+
const OUTBOUND_OVERFLOW_REASON = 'outbound queue overflow';
|
|
63
|
+
const FLOOD_METHOD = '_testing_flood_notifications';
|
|
64
|
+
const FLOOD_NOTIFICATION_METHOD = '_testing_flood';
|
|
65
|
+
const HEARTBEAT_METHOD = 'heartbeat';
|
|
66
|
+
const INVALID_PARAMS = -32602;
|
|
67
|
+
const REASON_NO_CONNECTION = 'no_connection';
|
|
68
|
+
const MIB = 1024 * 1024;
|
|
69
|
+
/** A burst within any budget. */
|
|
70
|
+
const FITS = { count: 3, size: 8 };
|
|
71
|
+
/**
|
|
72
|
+
* A burst of 3 MiB in megabyte frames: charged a little over 3 MiB with its
|
|
73
|
+
* envelopes and overhead, inside the 4 MiB default budget.
|
|
74
|
+
*/
|
|
75
|
+
const LARGE_FITS = { count: 3, size: MIB };
|
|
76
|
+
/** A burst of 32 MiB — eight times the default budget. */
|
|
77
|
+
const OVERFLOWS = { count: 32, size: MIB };
|
|
78
|
+
/** One mebibyte past the action's `count` × `size` limit. */
|
|
79
|
+
const REFUSED = { count: 65, size: MIB };
|
|
80
|
+
/** How long the test waits for the megabyte burst and its response. */
|
|
81
|
+
const LARGE_BURST_TIMEOUT_MS = 15_000;
|
|
82
|
+
/** How long the test waits for the overflow close. */
|
|
83
|
+
const CLOSE_TIMEOUT_MS = 15_000;
|
|
84
|
+
/**
|
|
85
|
+
* Register the outbound-overflow parity suite over a real upgrade: a burst
|
|
86
|
+
* within the budget arrives whole, one past it closes the socket with
|
|
87
|
+
* `WS_CLOSE_OUTBOUND_OVERFLOW` and no response, and the account's other
|
|
88
|
+
* connection is unaffected.
|
|
89
|
+
*/
|
|
90
|
+
export const describe_ws_outbound_overflow_cross_tests = (options) => {
|
|
91
|
+
const { setup_test, capabilities, base_url, ws_path, origin, rpc_path } = options;
|
|
92
|
+
/** A daemon-token socket the backend has admitted — the one that floods itself. */
|
|
93
|
+
const open_flooder = (fixture) => create_admitted_ws_transport({
|
|
94
|
+
base_url,
|
|
95
|
+
ws_path,
|
|
96
|
+
cookies: [],
|
|
97
|
+
origin: null,
|
|
98
|
+
headers: fixture.create_daemon_token_headers()
|
|
99
|
+
});
|
|
100
|
+
/** The `index` of every flood notification `client` received, in arrival order. */
|
|
101
|
+
const flood_indices = (client) => client.messages
|
|
102
|
+
.filter(is_notification(FLOOD_NOTIFICATION_METHOD))
|
|
103
|
+
.map((msg) => msg.params.index);
|
|
104
|
+
const assert_open_and_answering = async (client, id, message) => {
|
|
105
|
+
assert.deepStrictEqual(await client.request(id, HEARTBEAT_METHOD, {}), {}, message);
|
|
106
|
+
assert.strictEqual(client.close_code, null, message);
|
|
107
|
+
};
|
|
108
|
+
describe('websocket outbound overflow parity', { timeout: 30_000 }, () => {
|
|
109
|
+
test_if(capabilities.ws, 'a burst within the budget arrives in order, ahead of its response', async () => {
|
|
110
|
+
const fixture = await setup_test();
|
|
111
|
+
const client = await open_flooder(fixture);
|
|
112
|
+
try {
|
|
113
|
+
const result = await client.request('fits', FLOOD_METHOD, FITS);
|
|
114
|
+
assert.deepStrictEqual(result, { queued: FITS.count });
|
|
115
|
+
assert.deepStrictEqual(flood_indices(client), [0, 1, 2]);
|
|
116
|
+
const frames = client.messages.filter(is_notification(FLOOD_NOTIFICATION_METHOD));
|
|
117
|
+
for (const frame of frames) {
|
|
118
|
+
assert.strictEqual(frame.params.payload, 'x'.repeat(FITS.size));
|
|
119
|
+
}
|
|
120
|
+
// the whole burst is ahead of the response
|
|
121
|
+
const response_at = client.messages.findIndex(is_response_for('fits'));
|
|
122
|
+
const last_flood_at = client.messages.lastIndexOf(frames.at(-1));
|
|
123
|
+
assert.ok(last_flood_at < response_at, 'the burst precedes its response');
|
|
124
|
+
await assert_open_and_answering(client, 'after_fits', 'the socket stays open');
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await client.close().catch(() => { });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test_if(capabilities.ws, 'a multi-megabyte burst within the budget arrives whole and in order, the response last', async () => {
|
|
131
|
+
const fixture = await setup_test();
|
|
132
|
+
const client = await open_flooder(fixture);
|
|
133
|
+
try {
|
|
134
|
+
const result = await client.request('large_fits', FLOOD_METHOD, LARGE_FITS, LARGE_BURST_TIMEOUT_MS);
|
|
135
|
+
assert.deepStrictEqual(result, { queued: LARGE_FITS.count });
|
|
136
|
+
assert.deepStrictEqual(flood_indices(client), [0, 1, 2]);
|
|
137
|
+
const frames = client.messages.filter(is_notification(FLOOD_NOTIFICATION_METHOD));
|
|
138
|
+
const payload = 'x'.repeat(LARGE_FITS.size);
|
|
139
|
+
for (const frame of frames) {
|
|
140
|
+
// compared by length first: a mismatch would otherwise print a mebibyte
|
|
141
|
+
assert.strictEqual(frame.params.payload.length, LARGE_FITS.size);
|
|
142
|
+
assert.ok(frame.params.payload === payload, 'the payload arrived intact');
|
|
143
|
+
}
|
|
144
|
+
// nothing follows the response: the whole burst is ahead of it
|
|
145
|
+
const response_at = client.messages.findIndex(is_response_for('large_fits'));
|
|
146
|
+
const last_flood_at = client.messages.lastIndexOf(frames.at(-1));
|
|
147
|
+
assert.ok(last_flood_at < response_at, 'the burst precedes its response');
|
|
148
|
+
assert.strictEqual(client.close_code, null, 'a burst within the budget closes nothing');
|
|
149
|
+
await assert_open_and_answering(client, 'after_large_fits', 'the socket stays open');
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await client.close().catch(() => { });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
test_if(capabilities.ws, 'a burst past the budget closes the socket with WS_CLOSE_OUTBOUND_OVERFLOW and no response', async () => {
|
|
156
|
+
const fixture = await setup_test();
|
|
157
|
+
// a second connection on the same account (the keeper's session)
|
|
158
|
+
const bystander = await create_admitted_ws_transport({
|
|
159
|
+
base_url,
|
|
160
|
+
ws_path,
|
|
161
|
+
cookies: fixture.transport.cookies(),
|
|
162
|
+
origin
|
|
163
|
+
});
|
|
164
|
+
const client = await open_flooder(fixture);
|
|
165
|
+
try {
|
|
166
|
+
await client.send(create_jsonrpc_request(FLOOD_METHOD, OVERFLOWS, 'overflows'));
|
|
167
|
+
const closed = await client.wait_for_close(CLOSE_TIMEOUT_MS);
|
|
168
|
+
assert.ok(closed, 'the flooded socket did not close');
|
|
169
|
+
assert.strictEqual(client.close_code, WS_CLOSE_OUTBOUND_OVERFLOW);
|
|
170
|
+
assert.strictEqual(client.close_reason, OUTBOUND_OVERFLOW_REASON);
|
|
171
|
+
// the response of the request whose handler overflowed is not sent
|
|
172
|
+
assert.strictEqual(client.messages.some(is_response_for('overflows')), false, 'no response may follow an overflow');
|
|
173
|
+
// frames accepted before the overflow may or may not arrive; any
|
|
174
|
+
// that do are a contiguous prefix of the burst, and never all of it
|
|
175
|
+
const indices = flood_indices(client);
|
|
176
|
+
assert.ok(indices.length < OVERFLOWS.count, `${indices.length} flood frames arrived`);
|
|
177
|
+
assert.deepStrictEqual(indices, indices.map((_, i) => i), 'the flood frames that arrived are a contiguous prefix from index 0');
|
|
178
|
+
// the account's other connection kept working through it
|
|
179
|
+
await assert_open_and_answering(bystander, 'bystander', "another connection on the account is untouched by the first one's overflow");
|
|
180
|
+
// not a revocation: the daemon token still upgrades
|
|
181
|
+
const again = await open_flooder(fixture);
|
|
182
|
+
await again.close();
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await Promise.all([client.close().catch(() => { }), bystander.close().catch(() => { })]);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
test_if(capabilities.ws, 'a burst past the flood action size limit is refused, and the socket stays open', async () => {
|
|
189
|
+
const fixture = await setup_test();
|
|
190
|
+
const client = await open_flooder(fixture);
|
|
191
|
+
try {
|
|
192
|
+
await client.send(create_jsonrpc_request(FLOOD_METHOD, REFUSED, 'refused'));
|
|
193
|
+
const frame = await client.wait_for(is_response_for('refused'));
|
|
194
|
+
assert.ok(frame.error, 'expected invalid_params but the call succeeded');
|
|
195
|
+
assert.strictEqual(frame.error.code, INVALID_PARAMS);
|
|
196
|
+
assert.deepStrictEqual(flood_indices(client), [], 'a refused burst queues nothing');
|
|
197
|
+
await assert_open_and_answering(client, 'after_refused', 'the socket stays open');
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await client.close().catch(() => { });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
test_if(rpc_path !== undefined, 'over HTTP the flood action refuses with invalid_params, reason no_connection', async () => {
|
|
204
|
+
const fixture = await setup_test();
|
|
205
|
+
const res = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, FLOOD_METHOD, FITS, fixture.create_daemon_token_headers());
|
|
206
|
+
assert.strictEqual(res.ok, false, 'expected a refusal over HTTP');
|
|
207
|
+
assert.strictEqual(res.error?.code, INVALID_PARAMS);
|
|
208
|
+
assert.strictEqual(res.error?.data?.reason, REASON_NO_CONNECTION);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
};
|
|
@@ -4,7 +4,7 @@ import './assert_dev_env.js';
|
|
|
4
4
|
*
|
|
5
5
|
* `describe_standard_integration_tests` creates a composable test suite that
|
|
6
6
|
* exercises the full middleware stack (origin, session, bearer_auth, request_context)
|
|
7
|
-
* against a real
|
|
7
|
+
* against a real database. Consumers call it with their route factory and
|
|
8
8
|
* session config — all auth route tests come for free.
|
|
9
9
|
*
|
|
10
10
|
* Tests use `stub_password_deps` (deterministic hashing, no Argon2 overhead).
|
|
@@ -14,10 +14,18 @@ import './assert_dev_env.js';
|
|
|
14
14
|
* so a label set / ordering drift (e.g. `cell_visibility`) is a gated fact,
|
|
15
15
|
* not invisible
|
|
16
16
|
*
|
|
17
|
-
* Designed for `pg_catalog` introspection —
|
|
18
|
-
*
|
|
17
|
+
* Designed for `pg_catalog` introspection — it runs on PostgreSQL, PGlite and
|
|
18
|
+
* pglet. The snapshot is fully deterministic: every collection sorts by
|
|
19
19
|
* a stable key and excludes time-varying fields like `applied_at`.
|
|
20
20
|
*
|
|
21
|
+
* Index and constraint definitions are compared as the engine renders them,
|
|
22
|
+
* so both sides of a parity comparison need engines that render alike. pglet
|
|
23
|
+
* leaves an index's modifiers out of `pg_indexes.indexdef` — per-key sort
|
|
24
|
+
* direction, `NULLS` placement, operator classes, `NULLS NOT DISTINCT` — so a
|
|
25
|
+
* pglet snapshot diffed against a PostgreSQL one reports every index that
|
|
26
|
+
* uses one, with identical DDL on both sides. Take both snapshots on
|
|
27
|
+
* PostgreSQL.
|
|
28
|
+
*
|
|
21
29
|
* Paired with `testing/schema_parity.ts` for comparison + assertion helpers.
|
|
22
30
|
*
|
|
23
31
|
* @module
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema_introspect.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_introspect.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B
|
|
1
|
+
{"version":3,"file":"schema_introspect.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_introspect.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;iBAWzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,qCAAqC;AACrC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;iBAOxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,qFAAqF;AACrF,eAAO,MAAM,gBAAgB;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;iBAG3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;iBAOzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;iBAOhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,gBAAgB,CAa9E,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AA8DD;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GACjC,IAAI,EAAE,EACN,UAAS,0BAA+B,KACtC,OAAO,CAAC,cAAc,CAyHxB,CAAC"}
|
|
@@ -14,10 +14,18 @@ import './assert_dev_env.js';
|
|
|
14
14
|
* so a label set / ordering drift (e.g. `cell_visibility`) is a gated fact,
|
|
15
15
|
* not invisible
|
|
16
16
|
*
|
|
17
|
-
* Designed for `pg_catalog` introspection —
|
|
18
|
-
*
|
|
17
|
+
* Designed for `pg_catalog` introspection — it runs on PostgreSQL, PGlite and
|
|
18
|
+
* pglet. The snapshot is fully deterministic: every collection sorts by
|
|
19
19
|
* a stable key and excludes time-varying fields like `applied_at`.
|
|
20
20
|
*
|
|
21
|
+
* Index and constraint definitions are compared as the engine renders them,
|
|
22
|
+
* so both sides of a parity comparison need engines that render alike. pglet
|
|
23
|
+
* leaves an index's modifiers out of `pg_indexes.indexdef` — per-key sort
|
|
24
|
+
* direction, `NULLS` placement, operator classes, `NULLS NOT DISTINCT` — so a
|
|
25
|
+
* pglet snapshot diffed against a PostgreSQL one reports every index that
|
|
26
|
+
* uses one, with identical DDL on both sides. Take both snapshots on
|
|
27
|
+
* PostgreSQL.
|
|
28
|
+
*
|
|
21
29
|
* Paired with `testing/schema_parity.ts` for comparison + assertion helpers.
|
|
22
30
|
*
|
|
23
31
|
* @module
|
|
@@ -187,11 +195,11 @@ export const query_schema_snapshot = async (db, options = {}) => {
|
|
|
187
195
|
WHERE schemaname = $1
|
|
188
196
|
ORDER BY tablename ASC, indexname ASC`, [schema]);
|
|
189
197
|
// Constraints — pg_get_constraintdef produces a canonical text rendering.
|
|
190
|
-
// Skip NOT NULL constraints (`contype = 'n'`):
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
198
|
+
// Skip NOT NULL constraints (`contype = 'n'`): a named `pg_constraint` row
|
|
199
|
+
// per NOT NULL column is something an engine may or may not catalog
|
|
200
|
+
// (PostgreSQL 18, PGlite 0.5 and pglet 0.1 each do), and nullability is
|
|
201
|
+
// already captured per-column by `is_nullable` — including the rows would
|
|
202
|
+
// report an engine-cataloging difference as drift between two backends.
|
|
195
203
|
const constraint_rows = await db.query(`SELECT c.conrelid::regclass::text AS table_name,
|
|
196
204
|
c.conname,
|
|
197
205
|
c.contype::text,
|
|
@@ -17,9 +17,11 @@ export interface WsTransportOptions {
|
|
|
17
17
|
* Origin header for the upgrade. Backends running with
|
|
18
18
|
* `ALLOWED_ORIGINS=http://localhost:*` accept `http://localhost:<port>`.
|
|
19
19
|
* Defaults to `base_url` — acceptable because cross-process tests
|
|
20
|
-
* always run against `localhost`.
|
|
20
|
+
* always run against `localhost`. `null` sends no `Origin` at all, which a
|
|
21
|
+
* daemon-token upgrade needs: both spines discard a daemon token (and a
|
|
22
|
+
* bearer token) presented in a browser context.
|
|
21
23
|
*/
|
|
22
|
-
readonly origin?: string;
|
|
24
|
+
readonly origin?: string | null;
|
|
23
25
|
/**
|
|
24
26
|
* Optional per-call default for `wait_for` timeouts. Falls back to
|
|
25
27
|
* `WS_CLIENT_DEFAULT_TIMEOUT_MS` if omitted.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;AAIxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC
|
|
1
|
+
{"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;AAIxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IACzC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,QAAQ,CAyJvF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,kBAAkB,KACzB,OAAO,CAAC,QAAQ,CASlB,CAAC"}
|
|
@@ -50,7 +50,8 @@ export const create_ws_transport = async (options) => {
|
|
|
50
50
|
headers.Cookie = cookies.join('; ');
|
|
51
51
|
const socket = new WebSocket(ws_url, {
|
|
52
52
|
headers,
|
|
53
|
-
|
|
53
|
+
// `ws` sends no `Origin` header when the option is undefined
|
|
54
|
+
origin: origin === null ? undefined : (origin ?? base_url)
|
|
54
55
|
});
|
|
55
56
|
const received = [];
|
|
56
57
|
const waiters = [];
|
|
@@ -48,9 +48,29 @@ import { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
|
|
|
48
48
|
import { type RequestContext } from '../auth/request_context.js';
|
|
49
49
|
import { type CredentialType } from '../hono_context.js';
|
|
50
50
|
import { type WsClient, type WsRequestResponder } from './transports/ws_client.js';
|
|
51
|
+
/**
|
|
52
|
+
* The runtime socket behind a `FakeWs` — what `WSContext.raw` exposes, shaped
|
|
53
|
+
* like Node's `ws`: a `bufferedAmount` the outbound writer reads for
|
|
54
|
+
* backpressure, and a `terminate` it cuts a stalled socket with.
|
|
55
|
+
*/
|
|
56
|
+
export interface FakeWsSocket {
|
|
57
|
+
/**
|
|
58
|
+
* Bytes "handed to the socket and not yet written". Grows by each sent
|
|
59
|
+
* frame's UTF-8 length while `stalled`; set it (e.g. to `0`) to model the
|
|
60
|
+
* client reading.
|
|
61
|
+
*/
|
|
62
|
+
bufferedAmount: number;
|
|
63
|
+
/** While `true`, sent frames stay in `bufferedAmount` — a client that is not reading. */
|
|
64
|
+
stalled: boolean;
|
|
65
|
+
/** Whether `terminate` was called — the socket was cut with no close frame. */
|
|
66
|
+
terminated: boolean;
|
|
67
|
+
/** Cut the socket. Records `terminated` and empties `bufferedAmount`. */
|
|
68
|
+
terminate: () => void;
|
|
69
|
+
}
|
|
51
70
|
/**
|
|
52
71
|
* A `WSContext` paired with capture arrays. Use `sends` to assert on
|
|
53
|
-
* outgoing frames; use `closes` to assert on revocation / close
|
|
72
|
+
* outgoing frames; use `closes` to assert on revocation / close; use
|
|
73
|
+
* `socket` to model a client that stopped reading and to assert on a cut.
|
|
54
74
|
*/
|
|
55
75
|
export interface FakeWs {
|
|
56
76
|
ws: WSContext;
|
|
@@ -59,13 +79,23 @@ export interface FakeWs {
|
|
|
59
79
|
code?: number;
|
|
60
80
|
reason?: string;
|
|
61
81
|
}>;
|
|
82
|
+
/** The fake runtime socket (`ws.raw`). */
|
|
83
|
+
socket: FakeWsSocket;
|
|
84
|
+
}
|
|
85
|
+
/** Options for `create_fake_ws`. */
|
|
86
|
+
export interface CreateFakeWsOptions {
|
|
87
|
+
/** Start with the client not reading (`FakeWsSocket.stalled`). */
|
|
88
|
+
stalled?: boolean;
|
|
62
89
|
}
|
|
63
90
|
/**
|
|
64
91
|
* Build a real `WSContext` backed by in-memory `send`/`close` capture.
|
|
65
92
|
* Parsing of outgoing frames is left to the caller — `sends` holds the
|
|
66
93
|
* raw strings as the dispatcher wrote them.
|
|
94
|
+
*
|
|
95
|
+
* `readyState` reads `1` (open) until the socket is closed (`2`) or
|
|
96
|
+
* terminated (`3`).
|
|
67
97
|
*/
|
|
68
|
-
export declare const create_fake_ws: () => FakeWs;
|
|
98
|
+
export declare const create_fake_ws: (options?: CreateFakeWsOptions) => FakeWs;
|
|
69
99
|
/** Options for `create_fake_hono_context`. */
|
|
70
100
|
export interface FakeHonoContextOptions {
|
|
71
101
|
credential_type: CredentialType;
|
|
@@ -1 +1 @@
|
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAQ,MAAM,MAAM,CAAC;AAC1C,OAAO,EACN,SAAS,EAET,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAEpG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,2BAA2B,CAAC;AAMnC
|
|
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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"}
|