@fuzdev/fuz_app 0.64.0 → 0.66.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 +510 -946
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/action_event_data.d.ts +1 -1
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/cancel.d.ts +2 -2
- package/dist/actions/cancel.js +3 -3
- package/dist/actions/connection_closer.d.ts +1 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +1 -4
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -2
- package/dist/auth/CLAUDE.md +570 -1871
- package/dist/auth/account_schema.d.ts +1 -1
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +1 -1
- package/dist/auth/audit_log_ddl.d.ts +1 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +1 -1
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +1 -5
- package/dist/auth/bootstrap_routes.d.ts +7 -1
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -11
- package/dist/auth/daemon_token_middleware.d.ts +15 -5
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +24 -15
- package/dist/auth/invite_queries.d.ts +17 -7
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +19 -8
- package/dist/auth/keyring.d.ts +6 -6
- package/dist/auth/keyring.js +8 -8
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +4 -2
- package/dist/auth/signup_routes.d.ts +47 -1
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +103 -52
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/create_db.js +13 -0
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.js +3 -3
- package/dist/env/resolve.d.ts +44 -7
- package/dist/env/resolve.d.ts.map +1 -1
- package/dist/env/resolve.js +94 -27
- package/dist/http/CLAUDE.md +243 -522
- package/dist/http/error_schemas.d.ts +0 -4
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +0 -4
- package/dist/http/ip_canonical.d.ts +5 -4
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +8 -4
- package/dist/http/jsonrpc.d.ts +23 -7
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +19 -3
- package/dist/http/origin.d.ts +1 -1
- package/dist/http/origin.js +1 -1
- package/dist/http/surface.d.ts +9 -2
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +2 -2
- package/dist/server/app_server.d.ts +41 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +10 -4
- package/dist/server/env.d.ts +7 -7
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +14 -14
- package/dist/server/static.d.ts +4 -4
- package/dist/server/static.js +7 -7
- package/dist/testing/CLAUDE.md +740 -418
- package/dist/testing/admin_integration.d.ts +18 -23
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +230 -216
- package/dist/testing/app_server.d.ts +141 -39
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +157 -44
- package/dist/testing/audit_completeness.d.ts +25 -22
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +198 -159
- package/dist/testing/bootstrap_success.d.ts +28 -0
- package/dist/testing/bootstrap_success.d.ts.map +1 -0
- package/dist/testing/bootstrap_success.js +144 -0
- package/dist/testing/cross_backend/backend_config.d.ts +113 -0
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/backend_config.js +1 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/bench_report.js +83 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/scenario.js +28 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
- package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
- package/dist/testing/cross_backend/capabilities.d.ts +65 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
- package/dist/testing/cross_backend/capabilities.js +47 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_backend_configs.js +111 -0
- package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
- package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_secrets.js +39 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_spine_surface.js +121 -0
- package/dist/testing/cross_backend/setup.d.ts +451 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/setup.js +581 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/spawn_backend.js +229 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/sse_round_trip.js +137 -0
- package/dist/testing/cross_backend/standard.d.ts +96 -0
- package/dist/testing/cross_backend/standard.d.ts.map +1 -0
- package/dist/testing/cross_backend/standard.js +49 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_bun.js +59 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_core.js +68 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_deno.js +37 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_node.js +50 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_round_trip.js +113 -0
- package/dist/testing/data_exposure.d.ts +11 -14
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +123 -146
- package/dist/testing/db_entities.d.ts +22 -1
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +24 -1
- package/dist/testing/integration.d.ts +56 -21
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +294 -319
- package/dist/testing/integration_helpers.d.ts +16 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/mock_fs.d.ts.map +1 -1
- package/dist/testing/mock_fs.js +0 -2
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +9 -0
- package/dist/testing/role_grant_helpers.d.ts +31 -0
- package/dist/testing/role_grant_helpers.d.ts.map +1 -0
- package/dist/testing/role_grant_helpers.js +46 -0
- package/dist/testing/round_trip.d.ts +20 -16
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +61 -86
- package/dist/testing/rpc_helpers.d.ts +10 -4
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +1 -1
- package/dist/testing/rpc_round_trip.d.ts +24 -21
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +87 -104
- package/dist/testing/schema_introspect.d.ts +106 -0
- package/dist/testing/schema_introspect.d.ts.map +1 -0
- package/dist/testing/schema_introspect.js +123 -0
- package/dist/testing/schema_parity.d.ts +144 -0
- package/dist/testing/schema_parity.d.ts.map +1 -0
- package/dist/testing/schema_parity.js +233 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -68
- package/dist/testing/standard.d.ts +56 -25
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +62 -5
- package/dist/testing/stubs.d.ts +21 -6
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +33 -23
- package/dist/testing/testing_rate_limiter.d.ts +59 -0
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
- package/dist/testing/testing_rate_limiter.js +74 -0
- package/dist/testing/transports/bootstrap.d.ts +52 -0
- package/dist/testing/transports/bootstrap.d.ts.map +1 -0
- package/dist/testing/transports/bootstrap.js +70 -0
- package/dist/testing/transports/fetch_transport.d.ts +81 -0
- package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
- package/dist/testing/transports/fetch_transport.js +74 -0
- package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
- package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
- package/dist/testing/transports/sse_frame_reader.js +84 -0
- package/dist/testing/transports/sse_transport.d.ts +54 -0
- package/dist/testing/transports/sse_transport.d.ts.map +1 -0
- package/dist/testing/transports/sse_transport.js +51 -0
- package/dist/testing/transports/ws_client.d.ts +108 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_client.js +56 -0
- package/dist/testing/transports/ws_transport.d.ts +43 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -0
- package/dist/testing/transports/ws_transport.js +169 -0
- package/dist/testing/ws_round_trip.d.ts +21 -103
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +42 -40
- package/dist/ui/CLAUDE.md +5 -3
- package/dist/ui/MenuLink.svelte +16 -16
- package/dist/ui/MenuLink.svelte.d.ts +13 -4
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
- package/package.json +10 -4
package/dist/testing/stubs.js
CHANGED
|
@@ -17,7 +17,6 @@ import { create_app_surface_spec, } from '../http/surface.js';
|
|
|
17
17
|
import { AUDIT_LOG_SSE_MAX_PER_SCOPE } from '../realtime/sse_auth_guard.js';
|
|
18
18
|
import { SubscriberRegistry } from '../realtime/subscriber_registry.js';
|
|
19
19
|
import { BaseServerEnv } from '../server/env.js';
|
|
20
|
-
/* eslint-disable @typescript-eslint/require-await */
|
|
21
20
|
/**
|
|
22
21
|
* Create a Proxy that throws descriptive errors on any property access or method call.
|
|
23
22
|
*
|
|
@@ -102,10 +101,10 @@ const stub_db = create_noop_stub('stub_db');
|
|
|
102
101
|
* `create_test_app` already does this on the test backend.
|
|
103
102
|
*/
|
|
104
103
|
export const create_test_audit_emitter = () => ({
|
|
105
|
-
emit: () => { },
|
|
106
|
-
emit_role_grant_target: () => { },
|
|
107
|
-
emit_pool: async () => { },
|
|
108
|
-
notify: () => { },
|
|
104
|
+
emit: () => { },
|
|
105
|
+
emit_role_grant_target: () => { },
|
|
106
|
+
emit_pool: async () => { },
|
|
107
|
+
notify: () => { },
|
|
109
108
|
on_event_chain: Object.freeze([]),
|
|
110
109
|
});
|
|
111
110
|
/**
|
|
@@ -123,9 +122,9 @@ export const create_stub_audit_sse = () => {
|
|
|
123
122
|
max_per_scope: AUDIT_LOG_SSE_MAX_PER_SCOPE,
|
|
124
123
|
});
|
|
125
124
|
return {
|
|
126
|
-
subscribe: () => () => { },
|
|
125
|
+
subscribe: () => () => { },
|
|
127
126
|
log: new Logger('test:audit_sse', { level: 'off' }),
|
|
128
|
-
on_audit_event: () => { },
|
|
127
|
+
on_audit_event: () => { },
|
|
129
128
|
registry,
|
|
130
129
|
};
|
|
131
130
|
};
|
|
@@ -146,7 +145,7 @@ export const stub_app_deps = {
|
|
|
146
145
|
export const create_stub_app_deps = () => ({
|
|
147
146
|
stat: async () => null,
|
|
148
147
|
read_text_file: async () => '',
|
|
149
|
-
delete_file: async (_path) => { },
|
|
148
|
+
delete_file: async (_path) => { },
|
|
150
149
|
keyring: create_noop_stub('keyring'),
|
|
151
150
|
password: create_noop_stub('password'),
|
|
152
151
|
db: stub_db,
|
|
@@ -193,7 +192,7 @@ export const create_stub_app_server_context = (session_options) => {
|
|
|
193
192
|
db_type: 'pglite-memory',
|
|
194
193
|
db_name: 'test',
|
|
195
194
|
migration_results: [],
|
|
196
|
-
close: async () => { },
|
|
195
|
+
close: async () => { },
|
|
197
196
|
},
|
|
198
197
|
bootstrap_status: { available: false, token_path: null },
|
|
199
198
|
session_options,
|
|
@@ -207,7 +206,14 @@ export const create_stub_app_server_context = (session_options) => {
|
|
|
207
206
|
};
|
|
208
207
|
};
|
|
209
208
|
/**
|
|
210
|
-
* Create an `AppSurfaceSpec` for
|
|
209
|
+
* Create an `AppSurfaceSpec` for the standard testing suites.
|
|
210
|
+
*
|
|
211
|
+
* Used by both in-process and cross-process tests as the schema source —
|
|
212
|
+
* the cross-process-ness lives in the transport + per-test fixture, not
|
|
213
|
+
* here. The on-disk `*_attack_surface.json` snapshot is observability
|
|
214
|
+
* (gen-time drift detection via `assert_surface_matches_snapshot`); the
|
|
215
|
+
* suites consume the spec object this function returns, not the JSON
|
|
216
|
+
* file.
|
|
211
217
|
*
|
|
212
218
|
* Mirrors `create_app_server`'s route assembly: consumer routes +
|
|
213
219
|
* factory-managed bootstrap routes + surface generation. If
|
|
@@ -215,18 +221,11 @@ export const create_stub_app_server_context = (session_options) => {
|
|
|
215
221
|
* to stay in sync (single source of truth for all consumers).
|
|
216
222
|
*
|
|
217
223
|
* @param options - surface spec options
|
|
218
|
-
* @returns the surface spec for
|
|
224
|
+
* @returns the surface spec for the standard suites
|
|
219
225
|
*/
|
|
220
226
|
export const create_test_app_surface_spec = (options) => {
|
|
221
227
|
const ctx = create_stub_app_server_context(options.session_options);
|
|
222
228
|
const consumer_routes = options.create_route_specs(ctx);
|
|
223
|
-
// Mirror create_app_server's factory-managed route assembly
|
|
224
|
-
const bootstrap_routes = create_bootstrap_route_specs(ctx.deps, {
|
|
225
|
-
session_options: options.session_options,
|
|
226
|
-
bootstrap_status: { available: false, token_path: null },
|
|
227
|
-
ip_rate_limiter: null,
|
|
228
|
-
});
|
|
229
|
-
const prefix = options.bootstrap_route_prefix ?? '/api/account';
|
|
230
229
|
// Auto-mount rpc endpoints (mirrors create_app_server) so consumer
|
|
231
230
|
// `create_route_specs` does not need to call `create_rpc_endpoint`.
|
|
232
231
|
const resolved_rpc_endpoints = typeof options.rpc_endpoints === 'function'
|
|
@@ -240,11 +239,22 @@ export const create_test_app_surface_spec = (options) => {
|
|
|
240
239
|
// Resolve ws endpoints (mirrors create_app_server). Surface-only —
|
|
241
240
|
// no `register_ws_endpoint` call here, so no `upgradeWebSocket` needed.
|
|
242
241
|
const resolved_ws_endpoints = typeof options.ws_endpoints === 'function' ? options.ws_endpoints(ctx) : options.ws_endpoints;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
// Bootstrap routes mirror `create_app_server`: mounted for `surface_only`
|
|
243
|
+
// and `live` modes; omitted for `disabled` / undefined. Surface generation
|
|
244
|
+
// uses an `available: false` placeholder regardless of mode — the handler
|
|
245
|
+
// short-circuits to 403 ALREADY_BOOTSTRAPPED, which is what surface tests
|
|
246
|
+
// assert on. Live token_path is passed through for shape symmetry only.
|
|
247
|
+
const bootstrap_route_specs = options.bootstrap && options.bootstrap.mode !== 'disabled'
|
|
248
|
+
? prefix_route_specs(options.bootstrap.route_prefix ?? '/api/account', create_bootstrap_route_specs(ctx.deps, {
|
|
249
|
+
session_options: options.session_options,
|
|
250
|
+
bootstrap_status: {
|
|
251
|
+
available: false,
|
|
252
|
+
token_path: options.bootstrap.mode === 'live' ? options.bootstrap.token_path : null,
|
|
253
|
+
},
|
|
254
|
+
ip_rate_limiter: null,
|
|
255
|
+
}))
|
|
256
|
+
: [];
|
|
257
|
+
const route_specs = [...consumer_routes, ...rpc_route_specs, ...bootstrap_route_specs];
|
|
248
258
|
let middleware_specs = create_stub_api_middleware();
|
|
249
259
|
if (options.transform_middleware) {
|
|
250
260
|
middleware_specs = options.transform_middleware(middleware_specs);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Test-only `RateLimiter` subclass with bucket tracking + reset_all.
|
|
4
|
+
*
|
|
5
|
+
* Production code never instantiates this; test binaries swap it in for
|
|
6
|
+
* production `RateLimiter` so `_testing_reset` can clear every bucket
|
|
7
|
+
* between cases. Mirrors the `TestingArgon2idHasher` pattern from the
|
|
8
|
+
* Rust spine — same call surface as the production class, plus test-only
|
|
9
|
+
* knobs (`reset_all`, `tracked_keys`).
|
|
10
|
+
*
|
|
11
|
+
* Constructor + every overridden method preserve production semantics
|
|
12
|
+
* by delegating to `super.*` after tracking; `reset_all` walks the
|
|
13
|
+
* tracked-keys set and calls `super.reset` per entry. Tests that depend
|
|
14
|
+
* on burst behavior get identical results between production and test
|
|
15
|
+
* instantiations.
|
|
16
|
+
*
|
|
17
|
+
* Usage in a test binary:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import {TestingRateLimiter} from '@fuzdev/fuz_app/testing/testing_rate_limiter.js';
|
|
21
|
+
*
|
|
22
|
+
* const limiter = new TestingRateLimiter(default_login_ip_rate_limit);
|
|
23
|
+
* await create_app_server({backend, ip_rate_limiter: limiter, ...});
|
|
24
|
+
*
|
|
25
|
+
* // Inside the `_testing_reset` handler's `reset_state`:
|
|
26
|
+
* limiter.reset_all();
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { RateLimiter, type RateLimitResult } from '../rate_limiter.js';
|
|
32
|
+
/**
|
|
33
|
+
* `RateLimiter` plus bucket tracking. Every `check`/`record` call adds
|
|
34
|
+
* its key to `#seen_keys`; `reset` removes it; `reset_all` clears every
|
|
35
|
+
* tracked bucket. Drop-in replacement anywhere a `RateLimiter` is
|
|
36
|
+
* expected — the type is nominally compatible via subclassing.
|
|
37
|
+
*/
|
|
38
|
+
export declare class TestingRateLimiter extends RateLimiter {
|
|
39
|
+
#private;
|
|
40
|
+
check(key: string, now?: number): RateLimitResult;
|
|
41
|
+
record(key: string, now?: number): RateLimitResult;
|
|
42
|
+
reset(key: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Clear every bucket this limiter has been asked about. Idempotent;
|
|
45
|
+
* safe to call before any check/record activity. Designed to be invoked
|
|
46
|
+
* from a `_testing_reset` handler's `reset_state` callback so the test
|
|
47
|
+
* binary's rate-limit buckets don't leak across test cases.
|
|
48
|
+
*/
|
|
49
|
+
reset_all(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Snapshot of every bucket key this limiter has observed via
|
|
52
|
+
* `check`/`record`. Doesn't reflect post-cleanup pruning — keys that
|
|
53
|
+
* `cleanup()` removed remain in `tracked_keys` until `reset`/`reset_all`
|
|
54
|
+
* runs (or the limiter is disposed). Useful for assertions like
|
|
55
|
+
* "limiter saw exactly N IPs" in tests.
|
|
56
|
+
*/
|
|
57
|
+
get tracked_keys(): ReadonlySet<string>;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=testing_rate_limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing_rate_limiter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/testing_rate_limiter.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAC,WAAW,EAAE,KAAK,eAAe,EAAC,MAAM,oBAAoB,CAAC;AAErE;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,WAAW;;IAGzC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,eAAe;IAKjD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,eAAe;IAKlD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKjC;;;;;OAKG;IACH,SAAS,IAAI,IAAI;IAOjB;;;;;;OAMG;IACH,IAAI,YAAY,IAAI,WAAW,CAAC,MAAM,CAAC,CAEtC;CACD"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Test-only `RateLimiter` subclass with bucket tracking + reset_all.
|
|
4
|
+
*
|
|
5
|
+
* Production code never instantiates this; test binaries swap it in for
|
|
6
|
+
* production `RateLimiter` so `_testing_reset` can clear every bucket
|
|
7
|
+
* between cases. Mirrors the `TestingArgon2idHasher` pattern from the
|
|
8
|
+
* Rust spine — same call surface as the production class, plus test-only
|
|
9
|
+
* knobs (`reset_all`, `tracked_keys`).
|
|
10
|
+
*
|
|
11
|
+
* Constructor + every overridden method preserve production semantics
|
|
12
|
+
* by delegating to `super.*` after tracking; `reset_all` walks the
|
|
13
|
+
* tracked-keys set and calls `super.reset` per entry. Tests that depend
|
|
14
|
+
* on burst behavior get identical results between production and test
|
|
15
|
+
* instantiations.
|
|
16
|
+
*
|
|
17
|
+
* Usage in a test binary:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import {TestingRateLimiter} from '@fuzdev/fuz_app/testing/testing_rate_limiter.js';
|
|
21
|
+
*
|
|
22
|
+
* const limiter = new TestingRateLimiter(default_login_ip_rate_limit);
|
|
23
|
+
* await create_app_server({backend, ip_rate_limiter: limiter, ...});
|
|
24
|
+
*
|
|
25
|
+
* // Inside the `_testing_reset` handler's `reset_state`:
|
|
26
|
+
* limiter.reset_all();
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { RateLimiter } from '../rate_limiter.js';
|
|
32
|
+
/**
|
|
33
|
+
* `RateLimiter` plus bucket tracking. Every `check`/`record` call adds
|
|
34
|
+
* its key to `#seen_keys`; `reset` removes it; `reset_all` clears every
|
|
35
|
+
* tracked bucket. Drop-in replacement anywhere a `RateLimiter` is
|
|
36
|
+
* expected — the type is nominally compatible via subclassing.
|
|
37
|
+
*/
|
|
38
|
+
export class TestingRateLimiter extends RateLimiter {
|
|
39
|
+
#seen_keys = new Set();
|
|
40
|
+
check(key, now) {
|
|
41
|
+
this.#seen_keys.add(key);
|
|
42
|
+
return super.check(key, now);
|
|
43
|
+
}
|
|
44
|
+
record(key, now) {
|
|
45
|
+
this.#seen_keys.add(key);
|
|
46
|
+
return super.record(key, now);
|
|
47
|
+
}
|
|
48
|
+
reset(key) {
|
|
49
|
+
this.#seen_keys.delete(key);
|
|
50
|
+
super.reset(key);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clear every bucket this limiter has been asked about. Idempotent;
|
|
54
|
+
* safe to call before any check/record activity. Designed to be invoked
|
|
55
|
+
* from a `_testing_reset` handler's `reset_state` callback so the test
|
|
56
|
+
* binary's rate-limit buckets don't leak across test cases.
|
|
57
|
+
*/
|
|
58
|
+
reset_all() {
|
|
59
|
+
for (const key of this.#seen_keys) {
|
|
60
|
+
super.reset(key);
|
|
61
|
+
}
|
|
62
|
+
this.#seen_keys.clear();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Snapshot of every bucket key this limiter has observed via
|
|
66
|
+
* `check`/`record`. Doesn't reflect post-cleanup pruning — keys that
|
|
67
|
+
* `cleanup()` removed remain in `tracked_keys` until `reset`/`reset_all`
|
|
68
|
+
* runs (or the limiter is disposed). Useful for assertions like
|
|
69
|
+
* "limiter saw exactly N IPs" in tests.
|
|
70
|
+
*/
|
|
71
|
+
get tracked_keys() {
|
|
72
|
+
return this.#seen_keys;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
3
|
+
import type { BackendConfig } from '../cross_backend/backend_config.js';
|
|
4
|
+
import type { FetchTransport } from './fetch_transport.js';
|
|
5
|
+
/** Input for `bootstrap()`. */
|
|
6
|
+
export interface BootstrapOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The cookie-threading HTTP transport pointed at the binary. After
|
|
9
|
+
* `bootstrap()` resolves, the transport carries the keeper session
|
|
10
|
+
* cookie — every later call against it is authenticated as keeper.
|
|
11
|
+
*/
|
|
12
|
+
readonly transport: FetchTransport;
|
|
13
|
+
/**
|
|
14
|
+
* Backend config — used for `bootstrap_path` plus the
|
|
15
|
+
* `bootstrap.username` / `bootstrap.password` / `bootstrap.token`
|
|
16
|
+
* credentials. The runner already wrote `bootstrap.token` to
|
|
17
|
+
* `bootstrap.token_path` before spawning, so the binary picks the
|
|
18
|
+
* token up at startup.
|
|
19
|
+
*/
|
|
20
|
+
readonly config: BackendConfig;
|
|
21
|
+
}
|
|
22
|
+
/** The keeper credentials captured from `POST /api/account/bootstrap`. */
|
|
23
|
+
export interface BootstrapResult {
|
|
24
|
+
/**
|
|
25
|
+
* Same transport that came in, now carrying the keeper session
|
|
26
|
+
* cookie in its jar. Returned for call-site clarity (callers don't
|
|
27
|
+
* have to remember the mutation happens in place).
|
|
28
|
+
*/
|
|
29
|
+
readonly transport: FetchTransport;
|
|
30
|
+
/** Account JSON returned by `POST /bootstrap`. */
|
|
31
|
+
readonly account: {
|
|
32
|
+
readonly id: Uuid;
|
|
33
|
+
readonly username: string;
|
|
34
|
+
};
|
|
35
|
+
/** Actor JSON returned by `POST /bootstrap`. */
|
|
36
|
+
readonly actor: {
|
|
37
|
+
readonly id: Uuid;
|
|
38
|
+
};
|
|
39
|
+
/** Raw `Set-Cookie` values for threading into a WS transport. */
|
|
40
|
+
readonly cookies: ReadonlyArray<string>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fire `POST {config.bootstrap_path}` and capture the keeper session.
|
|
44
|
+
*
|
|
45
|
+
* @throws Error when the binary refuses bootstrap (non-2xx response) or
|
|
46
|
+
* the body fails to parse as the expected `{account, actor}` envelope.
|
|
47
|
+
* The error carries the status + raw body so a mistyped token /
|
|
48
|
+
* username collision / boot-time DB drift surfaces with enough
|
|
49
|
+
* context to debug.
|
|
50
|
+
*/
|
|
51
|
+
export declare const bootstrap: (options: BootstrapOptions) => Promise<BootstrapResult>;
|
|
52
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAE5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,oCAAoC,CAAC;AACtE,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAezD,+BAA+B;AAC/B,MAAM,WAAW,gBAAgB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAC/B;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE;QAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACjE,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;KAAC,CAAC;IACpC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACxC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GAAU,SAAS,gBAAgB,KAAG,OAAO,CAAC,eAAe,CA4BlF,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stateless cross-process bootstrap.
|
|
4
|
+
*
|
|
5
|
+
* POSTs `{bootstrap_path}` against the running test binary with the
|
|
6
|
+
* preconfigured token + username + password, parses the
|
|
7
|
+
* `BootstrapOutput` envelope, captures the session `Set-Cookie` onto
|
|
8
|
+
* the supplied `FetchTransport` (which carries it on every subsequent
|
|
9
|
+
* call), and returns the keeper credentials.
|
|
10
|
+
*
|
|
11
|
+
* Fires exactly once per backend lifetime — `spawn_backend` calls it
|
|
12
|
+
* inside vitest's `globalSetup`. Per-test fixtures re-use the captured
|
|
13
|
+
* keeper credentials; fresh per-test accounts come from
|
|
14
|
+
* `fixture.create_account()` (signup+login through production RPC),
|
|
15
|
+
* not re-bootstrap. The hybrid reset model in `default_cross_process_setup`
|
|
16
|
+
* depends on this — re-bootstrap would race the bootstrap lock and
|
|
17
|
+
* in-memory caches.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
23
|
+
/**
|
|
24
|
+
* The `BootstrapOutput` envelope shape the cross-process bootstrap call
|
|
25
|
+
* cares about. Looser than the full `BootstrapOutput` Zod schema in
|
|
26
|
+
* `auth/bootstrap_account.ts` — that schema is the canonical wire shape
|
|
27
|
+
* on the server side, and this one is a structural subset the runner
|
|
28
|
+
* uses to extract the keeper identity. Kept local so cross-process
|
|
29
|
+
* testing doesn't pull the full auth-domain schema into its dep graph.
|
|
30
|
+
*/
|
|
31
|
+
const BootstrapResponse = z.object({
|
|
32
|
+
account: z.object({ id: Uuid, username: z.string() }),
|
|
33
|
+
actor: z.object({ id: Uuid }),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Fire `POST {config.bootstrap_path}` and capture the keeper session.
|
|
37
|
+
*
|
|
38
|
+
* @throws Error when the binary refuses bootstrap (non-2xx response) or
|
|
39
|
+
* the body fails to parse as the expected `{account, actor}` envelope.
|
|
40
|
+
* The error carries the status + raw body so a mistyped token /
|
|
41
|
+
* username collision / boot-time DB drift surfaces with enough
|
|
42
|
+
* context to debug.
|
|
43
|
+
*/
|
|
44
|
+
export const bootstrap = async (options) => {
|
|
45
|
+
const { transport, config } = options;
|
|
46
|
+
const response = await transport(config.bootstrap_path, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
token: config.bootstrap.token,
|
|
51
|
+
username: config.bootstrap.username,
|
|
52
|
+
password: config.bootstrap.password,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const body = await response.text().catch(() => '<unreadable>');
|
|
57
|
+
throw new Error(`bootstrap(${config.name}) failed: status=${response.status} body=${body}`);
|
|
58
|
+
}
|
|
59
|
+
const raw = await response.json();
|
|
60
|
+
const parsed = BootstrapResponse.safeParse(raw);
|
|
61
|
+
if (!parsed.success) {
|
|
62
|
+
throw new Error(`bootstrap(${config.name}) returned unexpected body: ${JSON.stringify(raw)} (${parsed.error.message})`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
transport,
|
|
66
|
+
account: parsed.data.account,
|
|
67
|
+
actor: parsed.data.actor,
|
|
68
|
+
cookies: transport.cookies(),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cookie-threading HTTP transport for cross-process tests.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the global `fetch` against a base URL, carries cookies across
|
|
6
|
+
* requests in a `Map`-backed jar so the session cookie set on bootstrap
|
|
7
|
+
* is re-sent on every subsequent call. Satisfies the `RpcTestTransport`
|
|
8
|
+
* shape `Hono.request` already does — so every suite body that takes
|
|
9
|
+
* `transport: RpcTestTransport` works against a cross-process binary
|
|
10
|
+
* unchanged.
|
|
11
|
+
*
|
|
12
|
+
* The `Origin` header is threaded onto every request because the
|
|
13
|
+
* backend's allowlist (`{ZZZ,ZAP,FUZ}_ALLOWED_ORIGINS`) rejects mutations
|
|
14
|
+
* without an `Origin`. Cross-process tests run with
|
|
15
|
+
* `ALLOWED_ORIGINS=http://localhost:*`, so defaulting `origin` to the
|
|
16
|
+
* configured `base_url` is safe.
|
|
17
|
+
*
|
|
18
|
+
* The cookie jar is intentionally simple — it does not honour `Domain`,
|
|
19
|
+
* `Path`, `Expires`, or `SameSite` attributes. Cross-process tests
|
|
20
|
+
* always hit a single host:port, so name-keyed last-write-wins matches
|
|
21
|
+
* the behaviour real browsers exhibit against the same surface.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import type { RpcTestTransport } from '../rpc_helpers.js';
|
|
26
|
+
/** Construction options for `create_fetch_transport`. */
|
|
27
|
+
export interface FetchTransportOptions {
|
|
28
|
+
/** Base URL the binary is reachable at — e.g. `http://localhost:8788`. */
|
|
29
|
+
readonly base_url: string;
|
|
30
|
+
/**
|
|
31
|
+
* Initial cookie values to seed the jar. Pass the `Set-Cookie` values
|
|
32
|
+
* captured from a prior `bootstrap()` call to keep the keeper session
|
|
33
|
+
* across a transport-recreation boundary. Each entry is a full
|
|
34
|
+
* `Set-Cookie` value (the same string `Headers.getSetCookie()` returns).
|
|
35
|
+
*/
|
|
36
|
+
readonly initial_cookies?: ReadonlyArray<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Origin header threaded onto every request when the caller hasn't set
|
|
39
|
+
* one. Defaults to `base_url`; backends running with
|
|
40
|
+
* `ALLOWED_ORIGINS=http://localhost:*` accept `http://localhost:<port>`
|
|
41
|
+
* matching the spawned binary.
|
|
42
|
+
*
|
|
43
|
+
* Pass `null` to disable the default — useful for bearer-only probes
|
|
44
|
+
* that must not look like browser-context requests (the auth middleware
|
|
45
|
+
* silently discards bearer credentials when `Origin`/`Referer` is
|
|
46
|
+
* present). Callers can still set `Origin` per-call via `init.headers`.
|
|
47
|
+
*/
|
|
48
|
+
readonly origin?: string | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The transport shape: callable as `RpcTestTransport` plus a `cookies()`
|
|
52
|
+
* accessor that returns the current jar state. The accessor exists so
|
|
53
|
+
* `ws_transport` can thread the session cookie onto the WS upgrade
|
|
54
|
+
* without an HTTP round trip.
|
|
55
|
+
*/
|
|
56
|
+
export interface FetchTransport extends RpcTestTransport {
|
|
57
|
+
/**
|
|
58
|
+
* Snapshot of every cookie currently in the jar, formatted as full
|
|
59
|
+
* `Set-Cookie` values (`name=value`). Used by `ws_transport` to
|
|
60
|
+
* compose the `Cookie` header on the upgrade request.
|
|
61
|
+
*/
|
|
62
|
+
readonly cookies: () => ReadonlyArray<string>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build a cookie-threading transport pinned to `options.base_url`. The
|
|
66
|
+
* returned function carries a private `Map<name, cookie-head>` jar that
|
|
67
|
+
* updates on every response's `Set-Cookie` and re-sends on every
|
|
68
|
+
* subsequent request.
|
|
69
|
+
*
|
|
70
|
+
* Request rewriting:
|
|
71
|
+
*
|
|
72
|
+
* - Absolute URLs (`http://other.example/...`) pass through verbatim —
|
|
73
|
+
* handy for cross-origin negative tests that target a deliberately
|
|
74
|
+
* different host.
|
|
75
|
+
* - Relative URLs are resolved against `base_url`.
|
|
76
|
+
* - `Origin` is set to `options.origin ?? base_url` unless the caller
|
|
77
|
+
* already provided one.
|
|
78
|
+
* - `Cookie` is set from the jar unless the caller already provided one.
|
|
79
|
+
*/
|
|
80
|
+
export declare const create_fetch_transport: (options: FetchTransportOptions) => FetchTransport;
|
|
81
|
+
//# sourceMappingURL=fetch_transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/fetch_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AAExD,yDAAyD;AACzD,MAAM,WAAW,qBAAqB;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACvD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;CAC9C;AAkBD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,qBAAqB,KAAG,cAyCvE,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse the `name=value` head of a `Set-Cookie` value. Returns `null`
|
|
4
|
+
* for malformed inputs (missing `=`, empty name). Drops every attribute
|
|
5
|
+
* after the first `;` — the jar is name-keyed and the lifetime
|
|
6
|
+
* attributes (`Expires`, `Max-Age`, `Path`, `Domain`, `SameSite`)
|
|
7
|
+
* don't affect cross-process test plumbing.
|
|
8
|
+
*/
|
|
9
|
+
const parse_set_cookie = (value) => {
|
|
10
|
+
const head = value.split(';', 1)[0].trim();
|
|
11
|
+
const eq = head.indexOf('=');
|
|
12
|
+
if (eq <= 0)
|
|
13
|
+
return null;
|
|
14
|
+
const name = head.slice(0, eq).trim();
|
|
15
|
+
if (!name)
|
|
16
|
+
return null;
|
|
17
|
+
return { name, cookie: head };
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Build a cookie-threading transport pinned to `options.base_url`. The
|
|
21
|
+
* returned function carries a private `Map<name, cookie-head>` jar that
|
|
22
|
+
* updates on every response's `Set-Cookie` and re-sends on every
|
|
23
|
+
* subsequent request.
|
|
24
|
+
*
|
|
25
|
+
* Request rewriting:
|
|
26
|
+
*
|
|
27
|
+
* - Absolute URLs (`http://other.example/...`) pass through verbatim —
|
|
28
|
+
* handy for cross-origin negative tests that target a deliberately
|
|
29
|
+
* different host.
|
|
30
|
+
* - Relative URLs are resolved against `base_url`.
|
|
31
|
+
* - `Origin` is set to `options.origin ?? base_url` unless the caller
|
|
32
|
+
* already provided one.
|
|
33
|
+
* - `Cookie` is set from the jar unless the caller already provided one.
|
|
34
|
+
*/
|
|
35
|
+
export const create_fetch_transport = (options) => {
|
|
36
|
+
const { base_url, initial_cookies, origin } = options;
|
|
37
|
+
const jar = new Map();
|
|
38
|
+
if (initial_cookies) {
|
|
39
|
+
for (const raw of initial_cookies) {
|
|
40
|
+
const parsed = parse_set_cookie(raw);
|
|
41
|
+
if (parsed)
|
|
42
|
+
jar.set(parsed.name, parsed.cookie);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const default_origin = origin === null ? null : (origin ?? base_url);
|
|
46
|
+
const cookies = () => Array.from(jar.values());
|
|
47
|
+
const transport = (async (url, init) => {
|
|
48
|
+
const target = /^https?:\/\//i.test(url) ? url : `${base_url}${url}`;
|
|
49
|
+
const headers = new Headers(init.headers);
|
|
50
|
+
if (default_origin !== null && !headers.has('Origin')) {
|
|
51
|
+
headers.set('Origin', default_origin);
|
|
52
|
+
}
|
|
53
|
+
if (!headers.has('Cookie') && jar.size > 0) {
|
|
54
|
+
headers.set('Cookie', Array.from(jar.values()).join('; '));
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(target, { ...init, headers });
|
|
57
|
+
// `Headers.getSetCookie()` returns each `Set-Cookie` value as a
|
|
58
|
+
// separate string — the only way to read multiple cookies set in
|
|
59
|
+
// one response (Headers.get() collapses to a single comma-joined
|
|
60
|
+
// string). Node 19.7+ ships it; vitest's runtime supports it.
|
|
61
|
+
const set_cookies = response.headers.getSetCookie();
|
|
62
|
+
for (const raw of set_cookies) {
|
|
63
|
+
const parsed = parse_set_cookie(raw);
|
|
64
|
+
if (parsed)
|
|
65
|
+
jar.set(parsed.name, parsed.cookie);
|
|
66
|
+
}
|
|
67
|
+
return response;
|
|
68
|
+
});
|
|
69
|
+
// Attach the cookies() accessor onto the callable. Using a property
|
|
70
|
+
// definition keeps it non-enumerable-ish and avoids polluting `.bind`
|
|
71
|
+
// / `.call` reflection on the function.
|
|
72
|
+
Object.defineProperty(transport, 'cookies', { value: cookies, enumerable: false });
|
|
73
|
+
return transport;
|
|
74
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* SSE frame reader over a `ReadableStreamDefaultReader<Uint8Array>`.
|
|
4
|
+
*
|
|
5
|
+
* Transport-agnostic core shared by the in-process SSE route suite
|
|
6
|
+
* (`testing/sse_round_trip.ts`, reading a Hono `Response.body`) and the
|
|
7
|
+
* cross-process `transports/sse_transport.ts` (reading a streaming `fetch`
|
|
8
|
+
* body): `\n\n`-delimited framing, a per-read timeout (so vitest can't hang
|
|
9
|
+
* on a stalled stream), and `wait_for_close` for server-initiated close
|
|
10
|
+
* detection (the auth-guard revocation seam).
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** Default per-read / wait-for-close timeout. */
|
|
15
|
+
export declare const SSE_FRAME_READ_TIMEOUT_MS = 2000;
|
|
16
|
+
/** Frame-level reader returned by `create_sse_frame_reader`. */
|
|
17
|
+
export interface SseFrameReader {
|
|
18
|
+
/**
|
|
19
|
+
* Read one complete SSE frame (up to the next `\n\n`), without the
|
|
20
|
+
* trailing terminator. Throws if the per-read timeout elapses or the
|
|
21
|
+
* stream ends before a frame arrives.
|
|
22
|
+
*/
|
|
23
|
+
read_frame: (timeout_ms?: number) => Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Drain until the server closes the stream. Resolves `true` if the
|
|
26
|
+
* stream closes within `timeout_ms`, `false` on timeout.
|
|
27
|
+
*/
|
|
28
|
+
wait_for_close: (timeout_ms?: number) => Promise<boolean>;
|
|
29
|
+
/** Cancel the underlying reader. Safe to call when already closed. */
|
|
30
|
+
cancel: () => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Wrap a byte-stream reader in `\n\n`-delimited SSE frame parsing.
|
|
34
|
+
*
|
|
35
|
+
* Preserves bytes past a frame terminator in an internal buffer for the next
|
|
36
|
+
* `read_frame`. `read_frame` and `wait_for_close` both race each underlying
|
|
37
|
+
* read against `timeout_ms` so a misbehaving stream surfaces as a failure
|
|
38
|
+
* rather than a vitest hang.
|
|
39
|
+
*/
|
|
40
|
+
export declare const create_sse_frame_reader: (reader: ReadableStreamDefaultReader<Uint8Array>, default_timeout_ms?: number) => SseFrameReader;
|
|
41
|
+
//# sourceMappingURL=sse_frame_reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse_frame_reader.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/sse_frame_reader.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;GAWG;AAEH,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,gEAAgE;AAChE,MAAM,WAAW,cAAc;IAC9B;;;;OAIG;IACH,UAAU,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD;;;OAGG;IACH,cAAc,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,sEAAsE;IACtE,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GACnC,QAAQ,2BAA2B,CAAC,UAAU,CAAC,EAC/C,2BAA8C,KAC5C,cA8DF,CAAC"}
|