@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
package/dist/realtime/sse.js
CHANGED
|
@@ -4,12 +4,30 @@
|
|
|
4
4
|
* Provides generic helpers for creating SSE response streams
|
|
5
5
|
* and a notification type aligned with JSON-RPC 2.0.
|
|
6
6
|
*
|
|
7
|
+
* ## Outbound bound
|
|
8
|
+
*
|
|
9
|
+
* `SseStream.send` never waits on its client: each frame is written to the
|
|
10
|
+
* response stream, which holds it until the client reads. What a stream holds
|
|
11
|
+
* unread is kept within `SSE_OUTBOUND_QUEUE_BYTES` — each frame charged its
|
|
12
|
+
* bytes as written (`data: ` prefix and terminator included) plus
|
|
13
|
+
* `OUTBOUND_FRAME_OVERHEAD_BYTES`, and a frame always accepted when nothing is
|
|
14
|
+
* unread, so a single frame larger than the budget is still deliverable. A
|
|
15
|
+
* frame that does not fit **ends the stream**: it is not written, what was
|
|
16
|
+
* unread is dropped, the body ends with no final frame, and the stream's
|
|
17
|
+
* `on_close` listeners run. One `warn` line names the stream's account (when
|
|
18
|
+
* the caller passed it) and what was unread; no audit row is written, since each would itself be fanned to every
|
|
19
|
+
* stream. A client that is reading reconnects and misses the frames in
|
|
20
|
+
* between. The twin of the Rust spine's `fuz_realtime` SSE bound — the same
|
|
21
|
+
* budget and charge.
|
|
22
|
+
*
|
|
7
23
|
* @module
|
|
8
24
|
*/
|
|
9
25
|
import { streamSSE } from 'hono/streaming';
|
|
10
26
|
import { z } from 'zod';
|
|
11
27
|
import { DEV } from 'esm-env';
|
|
12
|
-
import {
|
|
28
|
+
import { utf8_length } from '../actions/transports.js';
|
|
29
|
+
import { OutboundBudget } from './outbound_budget.js';
|
|
30
|
+
import { SSE_CONNECTED_COMMENT, SSE_OUTBOUND_QUEUE_BYTES } from './sse_constants.js';
|
|
13
31
|
const is_node_connection_stream = (value) => {
|
|
14
32
|
if (typeof value !== 'object' || value === null)
|
|
15
33
|
return false;
|
|
@@ -85,10 +103,15 @@ const watch_client_disconnect = (c, on_gone) => {
|
|
|
85
103
|
* Uses `hono_stream.write()` directly (not `writeSSE`) to avoid
|
|
86
104
|
* Hono's HTML callback resolution — keeps the same `data: JSON\n\n` format.
|
|
87
105
|
*
|
|
88
|
-
*
|
|
106
|
+
* What the stream holds unread is bounded by `SSE_OUTBOUND_QUEUE_BYTES`; a
|
|
107
|
+
* frame past it ends the stream (module doc, "Outbound bound").
|
|
108
|
+
*
|
|
109
|
+
* @param log - logger for serialization and `on_close` listener errors, and
|
|
110
|
+
* for the warning when the stream ends on its outbound budget
|
|
111
|
+
* @param options - what the overflow warning names
|
|
89
112
|
* @returns object with the streaming `Response` and an `SseStream` controller
|
|
90
113
|
*/
|
|
91
|
-
export const create_sse_response = (c, log) => {
|
|
114
|
+
export const create_sse_response = (c, log, options) => {
|
|
92
115
|
const { promise, resolve } = Promise.withResolvers();
|
|
93
116
|
const close_listeners = [];
|
|
94
117
|
let resolved = false;
|
|
@@ -107,24 +130,53 @@ export const create_sse_response = (c, log) => {
|
|
|
107
130
|
}
|
|
108
131
|
};
|
|
109
132
|
let sse_stream;
|
|
133
|
+
// charges the frames written to the stream and not yet read from it
|
|
134
|
+
const budget = new OutboundBudget(SSE_OUTBOUND_QUEUE_BYTES);
|
|
110
135
|
const response = streamSSE(c, async (hono_stream) => {
|
|
136
|
+
// Write one frame within the budget, or end the stream. The write
|
|
137
|
+
// settles once the response body has taken the frame, which frees its
|
|
138
|
+
// charge.
|
|
139
|
+
const write = (frame) => {
|
|
140
|
+
const bytes = utf8_length(frame);
|
|
141
|
+
if (!budget.admit(bytes)) {
|
|
142
|
+
// same line as the Rust spine's registry, so one grep finds either
|
|
143
|
+
log.warn('sse: outbound queue overflow, ending the stream', {
|
|
144
|
+
account_id: options?.account_id ?? null,
|
|
145
|
+
...budget.to_overflow(bytes)
|
|
146
|
+
});
|
|
147
|
+
budget.clear();
|
|
148
|
+
// drops what the stream still holds and ends the body; `do_close`
|
|
149
|
+
// runs as its abort listener
|
|
150
|
+
hono_stream.abort();
|
|
151
|
+
do_close();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
void hono_stream.write(frame).then(() => {
|
|
155
|
+
// a stream that ended has had its charges cleared
|
|
156
|
+
if (!resolved && !hono_stream.aborted)
|
|
157
|
+
budget.release(bytes);
|
|
158
|
+
});
|
|
159
|
+
};
|
|
111
160
|
sse_stream = {
|
|
112
161
|
send(data) {
|
|
113
162
|
if (resolved || hono_stream.aborted)
|
|
114
163
|
return;
|
|
164
|
+
let frame;
|
|
115
165
|
try {
|
|
116
166
|
// JSON.stringify (no space arg) never produces literal newlines,
|
|
117
167
|
// so single-line `data:` framing is safe per the SSE spec.
|
|
118
|
-
|
|
168
|
+
frame = `data: ${JSON.stringify(data)}\n\n`;
|
|
119
169
|
}
|
|
120
170
|
catch (e) {
|
|
121
171
|
log.error('send failed to serialize data:', e);
|
|
172
|
+
return;
|
|
122
173
|
}
|
|
174
|
+
write(frame);
|
|
123
175
|
},
|
|
124
176
|
comment(text) {
|
|
125
177
|
if (resolved || hono_stream.aborted)
|
|
126
178
|
return;
|
|
127
|
-
|
|
179
|
+
write(`: ${text}\n`);
|
|
128
180
|
},
|
|
129
181
|
close: do_close,
|
|
130
182
|
on_close(fn) {
|
|
@@ -143,7 +195,7 @@ export const create_sse_response = (c, log) => {
|
|
|
143
195
|
hono_stream.onAbort(do_close);
|
|
144
196
|
// flush an SSE comment to push headers through proxies (Vite, nginx),
|
|
145
197
|
// ensuring EventSource fires onopen without waiting for the first data event
|
|
146
|
-
|
|
198
|
+
write(SSE_CONNECTED_COMMENT);
|
|
147
199
|
await promise;
|
|
148
200
|
});
|
|
149
201
|
// after the callback above has built `sse_stream`, which runs synchronously
|
|
@@ -14,4 +14,16 @@
|
|
|
14
14
|
* SSE tests assert the stream emits this on connect.
|
|
15
15
|
*/
|
|
16
16
|
export declare const SSE_CONNECTED_COMMENT = ": connected\n\n";
|
|
17
|
+
/**
|
|
18
|
+
* The byte budget of what one SSE stream holds for its client to read
|
|
19
|
+
* (`realtime/sse.ts` module doc, "Outbound bound").
|
|
20
|
+
*
|
|
21
|
+
* 1 MiB — equal to the default cap on one request body
|
|
22
|
+
* (`DEFAULT_MAX_BODY_SIZE`). An audit row is bounded by the request that
|
|
23
|
+
* produced it, so on a server that keeps that default the budget holds a burst
|
|
24
|
+
* of ordinary rows or one at the largest. The value is fixed and does not
|
|
25
|
+
* follow the server's body limit. Same value as the Rust spine's
|
|
26
|
+
* `SSE_OUTBOUND_QUEUE_BYTES`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const SSE_OUTBOUND_QUEUE_BYTES: number;
|
|
17
29
|
//# sourceMappingURL=sse_constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse_constants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"sse_constants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,QAAc,CAAC"}
|
|
@@ -14,3 +14,15 @@
|
|
|
14
14
|
* SSE tests assert the stream emits this on connect.
|
|
15
15
|
*/
|
|
16
16
|
export const SSE_CONNECTED_COMMENT = `: connected\n\n`;
|
|
17
|
+
/**
|
|
18
|
+
* The byte budget of what one SSE stream holds for its client to read
|
|
19
|
+
* (`realtime/sse.ts` module doc, "Outbound bound").
|
|
20
|
+
*
|
|
21
|
+
* 1 MiB — equal to the default cap on one request body
|
|
22
|
+
* (`DEFAULT_MAX_BODY_SIZE`). An audit row is bounded by the request that
|
|
23
|
+
* produced it, so on a server that keeps that default the budget holds a burst
|
|
24
|
+
* of ordinary rows or one at the largest. The value is fixed and does not
|
|
25
|
+
* follow the server's body limit. Same value as the Rust spine's
|
|
26
|
+
* `SSE_OUTBOUND_QUEUE_BYTES`.
|
|
27
|
+
*/
|
|
28
|
+
export const SSE_OUTBOUND_QUEUE_BYTES = 1024 * 1024;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_server.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Hono } from 'hono';
|
|
11
11
|
import { logger } from 'hono/logger';
|
|
12
|
-
import { bodyLimit } from 'hono/body-limit';
|
|
13
12
|
import { z } from 'zod';
|
|
14
13
|
import { session_cookie_options } from '../auth/session_cookie.js';
|
|
15
14
|
import { create_audit_log_sse, create_sse_connection_closer, audit_log_event_specs } from '../realtime/sse_auth_guard.js';
|
|
@@ -29,7 +28,7 @@ import { flush_pending_effects, flush_post_commit_effects } from '../http/pendin
|
|
|
29
28
|
import { assert_endpoint_in_auth_scope, assert_middleware_stack_mounted, create_auth_middleware_specs } from '../auth/middleware.js';
|
|
30
29
|
import { fuz_auth_guard_resolver } from '../auth/auth_guard_resolver.js';
|
|
31
30
|
import { create_fuz_authorization_handler } from '../auth/request_context.js';
|
|
32
|
-
import {
|
|
31
|
+
import { create_body_limit_middleware } from '../http/body_limit.js';
|
|
33
32
|
import { create_rpc_endpoint } from '../actions/action_rpc.js';
|
|
34
33
|
import { register_ws_endpoint } from '../actions/register_ws_endpoint.js';
|
|
35
34
|
import { resolve_ws_endpoints } from '../actions/ws_endpoint_spec.js';
|
|
@@ -483,10 +482,7 @@ const assemble_app_server = async (options, releases) => {
|
|
|
483
482
|
// Default 1 MiB; pass null to disable.
|
|
484
483
|
if (options.max_body_size !== null) {
|
|
485
484
|
const max_size = options.max_body_size ?? DEFAULT_MAX_BODY_SIZE;
|
|
486
|
-
app.use(
|
|
487
|
-
maxSize: max_size,
|
|
488
|
-
onError: (c) => c.json({ error: ERROR_PAYLOAD_TOO_LARGE }, 413)
|
|
489
|
-
}));
|
|
485
|
+
app.use(create_body_limit_middleware(max_size));
|
|
490
486
|
}
|
|
491
487
|
apply_middleware_specs(app, middleware_specs);
|
|
492
488
|
const authorize = create_fuz_authorization_handler({ db: deps.db });
|
|
@@ -524,6 +520,7 @@ const assemble_app_server = async (options, releases) => {
|
|
|
524
520
|
// added below instead, so the remover is the server's
|
|
525
521
|
connection_closer: null,
|
|
526
522
|
max_connections_per_account: endpoint.max_connections_per_account,
|
|
523
|
+
outbound_queue_bytes: endpoint.outbound_queue_bytes,
|
|
527
524
|
heartbeat: endpoint.heartbeat,
|
|
528
525
|
artificial_delay: endpoint.artificial_delay,
|
|
529
526
|
max_message_bytes: endpoint.max_message_bytes,
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -223,7 +223,7 @@ test-only by construction.
|
|
|
223
223
|
|
|
224
224
|
### `schema_introspect.ts` — `query_schema_snapshot`
|
|
225
225
|
|
|
226
|
-
- `query_schema_snapshot(db, options?)` — introspects a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and enum types (`pg_enum` labels in declared `enumsortorder`, so a `cell_visibility` label-set/order drift is gated). The `schema_version` migration tracker is always excluded — it's framework bookkeeping, not domain schema
|
|
226
|
+
- `query_schema_snapshot(db, options?)` — introspects a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and enum types (`pg_enum` labels in declared `enumsortorder`, so a `cell_visibility` label-set/order drift is gated). The `schema_version` migration tracker is always excluded — it's framework bookkeeping, not domain schema; twin impls keep it identical (same namespaces, names and partitioning), which `query_migration_tracker` + `assert_migration_trackers_equal` gate separately. Twinned by `fuz_db::query_schema_snapshot` (Rust); the `_testing_schema_snapshot` RPC's wire validator is the shared `SchemaSnapshot`, so the enum field must serialize on both sides.
|
|
227
227
|
- `SchemaSnapshot` — the Zod schema is canonical (co-located in `schema_introspect.ts`; the cross-impl `_testing_schema_snapshot` RPC action reuses it as its wire validator, and the type is `z.infer`'d from it). Fully JSON-serializable; every collection deterministically sorted on capture so structural equality is stable across runs.
|
|
228
228
|
- `query_migration_tracker(db)` / `MigrationTracker` — the migration-_identity_ twin of the snapshot: reads the `schema_version` tracker rows (`[{namespace, name, sequence}]`) the snapshot deliberately _excludes_. `MigrationTracker` is the `_testing_migration_tracker` RPC's wire validator; pairs with `assert_migration_trackers_equal` to gate that the two spines record byte-identical migration identity (the swap-freely invariant), catching a migration-name or namespace-partitioning drift the provenance-agnostic schema snapshot can't see.
|
|
229
229
|
|
|
@@ -235,7 +235,7 @@ test-only by construction.
|
|
|
235
235
|
- `SchemaDiff` — tagged-union per drift kind: `table_only_in`, `column_only_in`, `column_field_differs`, `index_only_in`, `index_definition_differs`, `constraint_only_in`, `constraint_differs`, `sequence_only_in`, `sequence_data_type_differs`, `enum_only_in`, `enum_labels_differ` (enum labels compared positionally — declared order is significant).
|
|
236
236
|
- `diff_migration_trackers(a, b)` / `format_migration_tracker_diffs(diffs, labels?)` / `assert_migration_trackers_equal(a, b, labels?)` — the migration-identity twins of the snapshot diff/format/assert, over two `MigrationTracker`s. `MigrationTrackerDiff` is keyed on `(namespace, name)` (the `schema_version` PK): `tracker_row_only_in` (a migration rename or namespace re-partition) and `tracker_sequence_differs` (a re-order).
|
|
237
237
|
|
|
238
|
-
fuz_app's own spine gates this cross-process via the `cross_backend_parity` project (`schema_parity.cross.test.ts` + the dual-spawn `global_setup_schema_parity.ts`), diffing the TS spine ↔ `testing_spine_stub` full schema — `npm run test:cross:parity`. The forge has its own deno↔rust parity gate.
|
|
238
|
+
fuz_app's own spine gates this cross-process via the `cross_backend_parity` project (`schema_parity.cross.test.ts` + the dual-spawn `global_setup_schema_parity.ts`), diffing the TS spine ↔ `testing_spine_stub` full schema — `npm run test:cross:parity`. Both legs run on PostgreSQL there: index definitions are compared as the engine renders them, and pglet (what `memory://` gives a TS backend) leaves sort direction, `NULLS` placement, operator classes and `NULLS NOT DISTINCT` out of `indexdef`, so a snapshot taken on it differs from a PostgreSQL one with identical DDL. The forge has its own deno↔rust parity gate.
|
|
239
239
|
|
|
240
240
|
## Cross-impl RPC-manifest parity
|
|
241
241
|
|
|
@@ -552,7 +552,7 @@ cover real sockets.
|
|
|
552
552
|
|
|
553
553
|
Three layers:
|
|
554
554
|
|
|
555
|
-
1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`, `create_stub_upgrade()`, `MinimalActionEnvironment`, `dispatch_ws_message(on_message, event, ws)`. The fake context sets the keys the production auth middleware sets — `AUTH_SESSION_TOKEN_HASH_KEY` included, as `hash_session_token(auth_session_id)` — because the dispatcher reads the credential through `get_resolved_auth(c)`; a hand-built upgrade context must do the same. `create_stub_upgrade()` stands in for a runtime adapter's `upgradeWebSocket`: it passes a request without `Upgrade: websocket` to `next()`, and records an upgrade request — its real `Context` and the endpoint's events factory — in `upgrades`, answering `200`. So a test can send `app.request('/api/ws', {headers: {upgrade: 'websocket', cookie | authorization, origin, 'x-forwarded-for'}})` through an assembled app's whole middleware chain and build the socket's events from the `Context` that chain produced (`src/test/server/create_app_server_ws_endpoints.dispatch.db.test.ts`); `get_create_events()` still returns the last mounted factory, for a hand-built context.
|
|
555
|
+
1. **Primitives** — `create_fake_ws(options?)` (a real `WSContext` over capture arrays `sends` / `closes`, plus `socket` — the fake runtime socket behind `ws.raw`, shaped like Node's `ws`: a `bufferedAmount` the transport's outbound writer reads for backpressure, `stalled` (start it with `{stalled: true}`) to model a client that stopped reading, and `terminate` / `terminated` for the frameless cut; `readyState` reads open until closed or terminated), `create_fake_hono_context(opts)`, `create_stub_upgrade()`, `MinimalActionEnvironment`, `dispatch_ws_message(on_message, event, ws)`. The fake context sets the keys the production auth middleware sets — `AUTH_SESSION_TOKEN_HASH_KEY` included, as `hash_session_token(auth_session_id)` — because the dispatcher reads the credential through `get_resolved_auth(c)`; a hand-built upgrade context must do the same. `create_stub_upgrade()` stands in for a runtime adapter's `upgradeWebSocket`: it passes a request without `Upgrade: websocket` to `next()`, and records an upgrade request — its real `Context` and the endpoint's events factory — in `upgrades`, answering `200`. So a test can send `app.request('/api/ws', {headers: {upgrade: 'websocket', cookie | authorization, origin, 'x-forwarded-for'}})` through an assembled app's whole middleware chain and build the socket's events from the `Context` that chain produced (`src/test/server/create_app_server_ws_endpoints.dispatch.db.test.ts`); `get_create_events()` still returns the last mounted factory, for a hand-built context.
|
|
556
556
|
2. **Harness** — `create_ws_test_harness({actions, transport?, heartbeat?, log?, on_socket_open?, on_socket_close?})` → `WsTestHarness`. `connect(identity?)` is async and resolves after the connection is admitted and `on_socket_open` completes, so broadcasts sent immediately after `await harness.connect()` reach the client. The harness pre-bakes its `RequestContext` (`TEST_CONTEXT_PRESET_KEY`), which also skips the admission credential re-read — there are no session or token rows behind a harness identity — so tests of the re-read itself drive `register_action_ws` with a real database and no preset (`src/test/actions/register_action_ws.admission.db.test.ts`). The harness threads its own `create_stub_db()` into the dispatcher's `db` slot so handlers declaring `side_effects: true` execute under the same transaction wrap they would in production (the stub's `transaction(fn)` synchronously calls `fn(stub_db)`); domain deps reach handlers via factory closures, the same way HTTP RPC factories already wire them. Audit fan-out runs through whatever `audit` emitter the consumer supplied to its action factory closure (typically `create_test_audit_emitter()` for unit harnesses).
|
|
557
557
|
3. **Round-trip helpers** — predicates + wire-frame types live in `transports/ws_client.ts` (shared with the cross-process `ws_transport.ts` impl): `is_notification(method)`, `is_notification_with<P>(method, match)` (type-guard combinator — narrows `wait_for` return type), `is_response_for(id)`, `JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` / `JsonrpcErrorResponseFrame<D>` (typed wire-frame shapes distinct from the runtime Zod schemas in `http/jsonrpc.ts` — generic over `params` / `result` / `data` so tests narrow without casts).
|
|
558
558
|
|
|
@@ -971,10 +971,13 @@ source of truth for wire-shape conformance.
|
|
|
971
971
|
case — `GET /api/account/status` is bundled into `create_account_route_specs`,
|
|
972
972
|
so every spine serves it (`true`); the gate fails loud if a declaring backend
|
|
973
973
|
doesn't mount it (replacing the old runtime 404-sniff-skip).
|
|
974
|
-
`oversized_reject_closes_connection`
|
|
975
|
-
smuggling probe (`true` Node/Deno/Rust —
|
|
976
|
-
|
|
977
|
-
|
|
974
|
+
`oversized_reject_closes_connection` picks the arm of the body-size
|
|
975
|
+
smuggling probe (`true` Node/Deno/Rust — the connection closes after an
|
|
976
|
+
oversized-body 413, at once on Node and once the declared body has arrived
|
|
977
|
+
on Deno, so the pipelined request goes unanswered; `false` Bun —
|
|
978
|
+
`Bun.serve` keeps the connection and serves the next request, correctly
|
|
979
|
+
framed, so the suite requires no desync there instead: exactly the 413 and
|
|
980
|
+
the reply to the pipelined request, which a backend that closes fails).
|
|
978
981
|
`ws_handshake_pipelining` gates the WS round-trip suite's
|
|
979
982
|
frames-with-the-handshake case (`true` Node/Deno/Rust — frames written in the
|
|
980
983
|
same TCP write as the upgrade request reach the socket; `false` Bun, whose
|
|
@@ -1218,6 +1221,36 @@ ws_path})`. The wire literals are independent copies of the producers'
|
|
|
1218
1221
|
constants, so a value changed on one spine fails here. Cross-process only;
|
|
1219
1222
|
fuz_app's own wiring is `src/test/cross_backend/ws_inbound_dispatch.cross.test.ts`.
|
|
1220
1223
|
|
|
1224
|
+
### `cross_backend/ws_outbound_overflow.ts` — `describe_ws_outbound_overflow_cross_tests`
|
|
1225
|
+
|
|
1226
|
+
Real-upgrade parity for the **per-connection outbound bound** — the byte
|
|
1227
|
+
budget both spines hold a WebSocket's waiting frames to
|
|
1228
|
+
(`BackendWebsocketTransport`'s `outbound_queue_bytes`, the Rust `fuz_realtime`
|
|
1229
|
+
outbound queue; same default, same per-frame charge). The close code and its
|
|
1230
|
+
reason are wire contract — a client reads `WS_CLOSE_OUTBOUND_OVERFLOW` as "you
|
|
1231
|
+
fell behind, reconnect" — so this is the pin that runs on both.
|
|
1232
|
+
`describe_ws_outbound_overflow_cross_tests({setup_test, capabilities, base_url,
|
|
1233
|
+
ws_path, origin?, rpc_path?})` drives `_testing_flood_notifications`, which
|
|
1234
|
+
queues a burst onto the caller's own connection in one synchronous run, so the
|
|
1235
|
+
burst waits at once whether or not the client reads and the overflow is
|
|
1236
|
+
deterministic. Cases: a burst within the budget arrives whole and in order
|
|
1237
|
+
ahead of its response, the socket left open — a small one, and one of a few
|
|
1238
|
+
mebibytes in megabyte frames, most of the default budget, which waits on the backend's write pump where megabyte frames outrun the socket's buffer (Deno; Node and Bun write the burst through); a burst far past it closes the
|
|
1239
|
+
socket with the code and the exact reason, no response for the flooding request
|
|
1240
|
+
arrives, and any flood frames that did arrive are a contiguous prefix of the
|
|
1241
|
+
burst — while a second connection on the same account keeps dispatching; a
|
|
1242
|
+
burst past the action's own size limit is refused `invalid_params` with the
|
|
1243
|
+
socket left open; and, with `rpc_path`, the action over HTTP refuses
|
|
1244
|
+
`invalid_params` with reason `no_connection`. Not asserted, because each
|
|
1245
|
+
implementation decides them: how many frames accepted before the overflow
|
|
1246
|
+
reach the client, and the boundary a burst of tiny frames overflows at. The
|
|
1247
|
+
flood action is daemon-token-gated, so the flooding socket upgrades with the
|
|
1248
|
+
daemon token and no `Origin` (`create_ws_transport`'s `origin: null`). The wire
|
|
1249
|
+
literals are independent copies of the producers' constants. Gated on
|
|
1250
|
+
`capabilities.ws`; cross-process only, and only for a backend that mounts the
|
|
1251
|
+
flood action on its WS endpoint. fuz_app's own wiring is
|
|
1252
|
+
`src/test/cross_backend/ws_outbound_overflow.cross.test.ts`.
|
|
1253
|
+
|
|
1221
1254
|
### `cross_backend/ws_connection_cap.ts` — `describe_ws_connection_cap_cross_tests`
|
|
1222
1255
|
|
|
1223
1256
|
Real-upgrade parity for the **per-account WebSocket connection cap** — the
|
|
@@ -1408,7 +1441,7 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1408
1441
|
request (`{result}` / `{error}` / `undefined` to swallow), the path
|
|
1409
1442
|
`describe_peer_ping_ws_tests` drives. With no `on_request` the seam is inert
|
|
1410
1443
|
(a server-initiated request flows to `messages` as before).
|
|
1411
|
-
- `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?, on_request?, headers?})`
|
|
1444
|
+
- `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?, on_request?, headers?})` (`origin: null` sends no `Origin`, which a daemon-token upgrade needs)
|
|
1412
1445
|
builds a real-upgrade WS client using the `ws` npm package (optional
|
|
1413
1446
|
peerDep; consumers wiring cross-process tests `npm install --save-dev ws`).
|
|
1414
1447
|
Threads the keeper cookie onto the upgrade so per-action auth succeeds on
|
|
@@ -1496,6 +1529,17 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1496
1529
|
emitter, so the shared suite body can call the barrier on every backend
|
|
1497
1530
|
uniformly.
|
|
1498
1531
|
|
|
1532
|
+
Also bundled: `_testing_flood_notifications`
|
|
1533
|
+
(`create_testing_flood_notifications_action()`) — queues `count`
|
|
1534
|
+
notifications of `size` payload bytes each (method `_testing_flood`, params
|
|
1535
|
+
`{index, payload}`) onto the caller's own WebSocket connection in one
|
|
1536
|
+
synchronous run and answers `{queued: count}`, so a suite can push a
|
|
1537
|
+
connection's outbound queue past its byte budget from outside. Refused with
|
|
1538
|
+
`invalid_params` past `TESTING_FLOOD_MAX_COUNT`, `TESTING_FLOOD_MAX_SIZE`, or
|
|
1539
|
+
`TESTING_FLOOD_MAX_TOTAL_BYTES` (`count` × `size`), and over HTTP (reason
|
|
1540
|
+
`no_connection` — there is no connection to queue onto). The Rust mirror is
|
|
1541
|
+
`fuz_testing::create_testing_flood_notifications_action_spec`.
|
|
1542
|
+
|
|
1499
1543
|
Also bundled: `_testing_mint_session` — mints a backdated-expiry
|
|
1500
1544
|
`auth_session` row for an account (via `mint_test_session` in `app_server.ts`)
|
|
1501
1545
|
and returns its signed cookie value (future-dated payload). Backs the
|
|
@@ -1601,6 +1645,22 @@ module's own tests (`account_status.test.ts`, `action_rpc.test.ts`,
|
|
|
1601
1645
|
`serve_fact_route.db.test.ts`); fuz_app's own wiring is
|
|
1602
1646
|
`cross_backend/query_shape.cross.test.ts`.
|
|
1603
1647
|
|
|
1648
|
+
### Grant-verb role grammar parity — `cross_backend/role_grant_role_grammar.ts`
|
|
1649
|
+
|
|
1650
|
+
`describe_role_grant_role_grammar_cross_tests({setup_test, rpc_path?})` — an
|
|
1651
|
+
off-grammar `role` (uppercase, hyphen, digit, leading or trailing underscore,
|
|
1652
|
+
space, empty, or all lowercase and one character past the length cap) on `role_grant_assign` and `role_grant_offer_create` answers
|
|
1653
|
+
`invalid_params` with no `data.reason`, and after the refusals no
|
|
1654
|
+
`role_grant_create` / `role_grant_offer_create` audit row carries one of those
|
|
1655
|
+
strings. The control — an on-grammar role no registry holds, refused past
|
|
1656
|
+
validation with `role_not_web_grantable` — keeps the audit read from being
|
|
1657
|
+
vacuous, and the same refusal for a role exactly at the length cap pins that
|
|
1658
|
+
boundary. Exists because the action-manifest parity gate captures no param
|
|
1659
|
+
schemas while the Rust spine parses params by hand, and the grant verbs write
|
|
1660
|
+
the role they refuse into their failure rows. Ungated; cross-process only (it
|
|
1661
|
+
reads the audit log behind `_testing_drain_effects`). fuz_app's own wiring is
|
|
1662
|
+
`src/test/cross_backend/role_grant_role_grammar.cross.test.ts`.
|
|
1663
|
+
|
|
1604
1664
|
### API-token lifetime parity — `cross_backend/token_lifetime.ts`
|
|
1605
1665
|
|
|
1606
1666
|
`describe_token_lifetime_cross_tests({setup_test, rpc_path?})` — the
|
|
@@ -1645,8 +1705,8 @@ drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
|
|
|
1645
1705
|
|
|
1646
1706
|
`describe_testing_backdoor_cross_tests({setup_test, rpc_path?, ws?})` —
|
|
1647
1707
|
the negative-credential parity suite for the `_testing_*` backdoor actions.
|
|
1648
|
-
For each `_testing_*` action except `_testing_drain_effects` (the
|
|
1649
|
-
|
|
1708
|
+
For each `_testing_*` action except `_testing_drain_effects` (the privileged
|
|
1709
|
+
writes, the connection flood, and the introspection reads) it fires three principals over real HTTP: **anonymous** →
|
|
1650
1710
|
401 `unauthenticated`, **session** → 403 `forbidden` `credential_type_required`,
|
|
1651
1711
|
**bearer** → 403 `forbidden` `credential_type_required` — and, when `ws`
|
|
1652
1712
|
(`{base_url, ws_path, origin?}`) is passed, sends each on a socket opened with
|
|
@@ -5,7 +5,7 @@ import './assert_dev_env.js';
|
|
|
5
5
|
* `describe_standard_admin_integration_tests` creates a composable test suite
|
|
6
6
|
* that exercises admin account listing, role_grant grant/revoke (via the RPC
|
|
7
7
|
* surface — see `role_grant_offer_create` / `role_grant_revoke`), session/token
|
|
8
|
-
* management, and audit log routes against a real
|
|
8
|
+
* management, and audit log routes against a real database.
|
|
9
9
|
*
|
|
10
10
|
* Consumers call it with their route factory, session config, role schema,
|
|
11
11
|
* and RPC endpoint specs — all admin route tests come for free.
|
|
@@ -34,7 +34,7 @@ export interface AuditCompletenessTestOptions {
|
|
|
34
34
|
* Composable audit log completeness test suite.
|
|
35
35
|
*
|
|
36
36
|
* Verifies that every auth mutation route produces the correct audit log
|
|
37
|
-
* event type. Exercises routes via HTTP requests against a real
|
|
37
|
+
* event type. Exercises routes via HTTP requests against a real
|
|
38
38
|
* database, then reads events back through the `audit_log_list` RPC
|
|
39
39
|
* (the production observation path the admin UI consumes).
|
|
40
40
|
*
|
|
@@ -96,7 +96,7 @@ const json_session_headers = (fixture, extra) => fixture.create_session_headers(
|
|
|
96
96
|
* Composable audit log completeness test suite.
|
|
97
97
|
*
|
|
98
98
|
* Verifies that every auth mutation route produces the correct audit log
|
|
99
|
-
* event type. Exercises routes via HTTP requests against a real
|
|
99
|
+
* event type. Exercises routes via HTTP requests against a real
|
|
100
100
|
* database, then reads events back through the `audit_log_list` RPC
|
|
101
101
|
* (the production observation path the admin UI consumes).
|
|
102
102
|
*
|
|
@@ -36,7 +36,7 @@ export interface RecordingAuditEmitter {
|
|
|
36
36
|
* Build a no-op `AuditEmitter` that records every `emit`, `emit_pool`, and
|
|
37
37
|
* `emit_role_grant_target` call into `calls` as an `AuditLogInput`. Use to
|
|
38
38
|
* capture audit metadata shapes in unit tests (e.g. password change failure
|
|
39
|
-
* outcome, role-grant create denial) without standing up
|
|
39
|
+
* outcome, role-grant create denial) without standing up a database and the
|
|
40
40
|
* `query_audit_log` pipeline.
|
|
41
41
|
*
|
|
42
42
|
* **Capture scope — all four production fan-out shapes.**
|
|
@@ -34,7 +34,7 @@ export const install_audit_drift_guard = () => {
|
|
|
34
34
|
* Build a no-op `AuditEmitter` that records every `emit`, `emit_pool`, and
|
|
35
35
|
* `emit_role_grant_target` call into `calls` as an `AuditLogInput`. Use to
|
|
36
36
|
* capture audit metadata shapes in unit tests (e.g. password change failure
|
|
37
|
-
* outcome, role-grant create denial) without standing up
|
|
37
|
+
* outcome, role-grant create denial) without standing up a database and the
|
|
38
38
|
* `query_audit_log` pipeline.
|
|
39
39
|
*
|
|
40
40
|
* **Capture scope — all four production fan-out shapes.**
|
|
@@ -6,15 +6,15 @@ export interface BodySizeSmugglingCrossTestOptions {
|
|
|
6
6
|
/** RPC endpoint path to target. Default `/api/rpc`. */
|
|
7
7
|
readonly rpc_path?: string;
|
|
8
8
|
/**
|
|
9
|
-
* Whether the backend closes the connection
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
* and
|
|
16
|
-
* consumer that
|
|
17
|
-
*
|
|
9
|
+
* Whether the backend closes the connection after an oversized-body `413`
|
|
10
|
+
* (`capabilities.oversized_reject_closes_connection`). `true` (default)
|
|
11
|
+
* requires that the pipelined GET goes unanswered — at most one response.
|
|
12
|
+
* `false` (Bun) requires no desync instead: exactly two well-formed
|
|
13
|
+
* responses, the 413 and one matching what the backend answers that GET
|
|
14
|
+
* sent alone — so `rpc_path` must answer a bare `GET` with a stable status
|
|
15
|
+
* and body, and a backend that closes after the 413 fails. Default `true`
|
|
16
|
+
* so a consumer that leaves the flag out fails loud on a backend that
|
|
17
|
+
* keeps the connection.
|
|
18
18
|
*/
|
|
19
19
|
readonly closes_connection?: boolean;
|
|
20
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgE9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACrC;AAkGD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IA0GF,CAAC"}
|