@fuzdev/fuz_app 0.92.0 → 0.94.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/README.md +2 -2
- package/dist/actions/action_codegen.d.ts +18 -18
- package/dist/actions/action_codegen.js +30 -30
- package/dist/actions/action_event.js +1 -1
- package/dist/actions/action_event_helpers.d.ts +1 -1
- package/dist/actions/action_event_types.d.ts +1 -1
- package/dist/actions/action_rpc.d.ts +2 -2
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/perform_action.d.ts +2 -2
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.js +1 -1
- package/dist/actions/request_tracker.svelte.d.ts +1 -1
- package/dist/actions/request_tracker.svelte.js +1 -1
- package/dist/actions/rpc_client.d.ts +1 -1
- package/dist/actions/socket.svelte.d.ts +2 -2
- package/dist/actions/transports_ws.js +1 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
- package/dist/actions/transports_ws_backend.d.ts +1 -1
- package/dist/actions/transports_ws_backend.js +2 -2
- package/dist/auth/account_action_specs.js +1 -1
- package/dist/auth/account_schema.d.ts +1 -1
- package/dist/auth/account_schema.js +1 -1
- package/dist/auth/actor_lookup_action_specs.js +1 -1
- package/dist/auth/actor_lookup_queries.d.ts +1 -1
- package/dist/auth/actor_search_action_specs.js +1 -1
- package/dist/auth/actor_search_queries.d.ts +1 -1
- package/dist/auth/admin_action_specs.js +1 -1
- package/dist/auth/api_token.js +1 -1
- package/dist/auth/api_token_queries.d.ts +1 -1
- package/dist/auth/app_settings_schema.d.ts +1 -1
- package/dist/auth/app_settings_schema.js +1 -1
- package/dist/auth/audit_emitter.d.ts +2 -2
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_schema.d.ts +1 -1
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/bearer_auth.d.ts +1 -1
- package/dist/auth/bootstrap_account.d.ts +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -1
- package/dist/auth/bootstrap_routes.d.ts +1 -1
- package/dist/auth/bootstrap_routes.js +1 -1
- package/dist/auth/cell_action_specs.js +2 -2
- package/dist/auth/cell_audit_action_specs.js +1 -1
- package/dist/auth/cell_audit_metadata.js +1 -1
- package/dist/auth/cell_field_action_specs.js +1 -1
- package/dist/auth/cell_field_audit_metadata.js +1 -1
- package/dist/auth/cell_grant_action_specs.js +1 -1
- package/dist/auth/cell_grant_audit_metadata.js +1 -1
- package/dist/auth/cell_item_action_specs.d.ts +1 -1
- package/dist/auth/cell_item_action_specs.js +3 -3
- package/dist/auth/cell_item_audit_metadata.js +1 -1
- package/dist/auth/cell_relation_visibility.d.ts +1 -1
- package/dist/auth/cleanup.d.ts +1 -1
- package/dist/auth/daemon_token_middleware.d.ts +1 -1
- package/dist/auth/deps.d.ts +2 -2
- package/dist/auth/invite_schema.d.ts +1 -1
- package/dist/auth/invite_schema.js +1 -1
- package/dist/auth/request_context.d.ts +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +1 -1
- package/dist/auth/role_grant_offer_notifications.d.ts +1 -1
- package/dist/auth/role_grant_offer_notifications.js +1 -1
- package/dist/auth/role_grant_offer_queries.d.ts +1 -1
- package/dist/auth/role_grant_offer_schema.d.ts +1 -1
- package/dist/auth/role_grant_offer_schema.js +1 -1
- package/dist/auth/role_grant_queries.d.ts +1 -1
- package/dist/auth/session_queries.d.ts +1 -1
- package/dist/auth/session_queries.js +1 -1
- package/dist/auth/signup_route_schema.js +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/logger.d.ts +1 -1
- package/dist/cli/logger.js +1 -1
- package/dist/db/CLAUDE.md +1 -1
- package/dist/db/cell_audit_queries.d.ts +1 -1
- package/dist/db/cell_field_queries.d.ts +1 -1
- package/dist/db/cell_grant_queries.d.ts +1 -1
- package/dist/db/cell_item_queries.d.ts +1 -1
- package/dist/db/cell_queries.d.ts +3 -3
- package/dist/db/cell_queries.js +1 -1
- package/dist/db/fact_disk_storage.d.ts +2 -2
- package/dist/db/fact_disk_storage.js +4 -4
- package/dist/db/fact_queries.d.ts +1 -1
- package/dist/db/fact_store.d.ts +3 -3
- package/dist/db/fact_store.js +2 -2
- package/dist/db/file_fact_url.d.ts +1 -1
- package/dist/db/file_fact_url.js +1 -1
- package/dist/db/migrate.js +1 -1
- package/dist/db/status.js +1 -1
- package/dist/http/auth_shape.js +2 -2
- package/dist/http/common_routes.d.ts +1 -1
- package/dist/http/db_routes.d.ts +1 -1
- package/dist/http/origin.js +1 -1
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/proxy.d.ts +1 -1
- package/dist/http/route_spec.d.ts +1 -1
- package/dist/rate_limiter.js +1 -1
- package/dist/realtime/sse.d.ts +1 -1
- package/dist/realtime/sse_auth_guard.d.ts +1 -1
- package/dist/runtime/deno.js +1 -1
- package/dist/server/app_backend.d.ts +1 -1
- package/dist/server/app_backend.js +1 -1
- package/dist/server/env.js +1 -1
- package/dist/server/fact_write.d.ts +2 -2
- package/dist/server/fact_write.js +1 -1
- package/dist/server/serve_fact_route.d.ts +1 -1
- package/dist/server/serve_fact_route.js +2 -2
- package/dist/server/startup.d.ts +1 -1
- package/dist/testing/CLAUDE.md +50 -5
- package/dist/testing/adversarial_input.js +1 -1
- package/dist/testing/app_server.d.ts +1 -1
- package/dist/testing/app_server.js +1 -1
- package/dist/testing/auth_apps.js +1 -1
- package/dist/testing/cross_backend/bench/bench_report.d.ts +1 -1
- package/dist/testing/cross_backend/bench/bench_report.js +2 -2
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +1 -1
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +1 -1
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +1 -1
- package/dist/testing/cross_backend/cell_relations.js +1 -1
- package/dist/testing/cross_backend/conformance_case.d.ts +4 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +26 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +21 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +109 -7
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +1 -1
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +64 -0
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/{create_schema_parity_global_setup.js → create_dual_spawn_global_setup.js} +1 -1
- package/dist/testing/cross_backend/default_backend_configs.d.ts +12 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +12 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +1 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +9 -2
- package/dist/testing/cross_backend/fact_serving.js +1 -1
- package/dist/testing/cross_backend/login_security.d.ts +11 -0
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -0
- package/dist/testing/cross_backend/login_security.js +130 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +17 -0
- 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 +7 -2
- package/dist/testing/cross_backend/setup.d.ts +1 -1
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +2 -2
- package/dist/testing/cross_backend/testing_server_core.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_core.js +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +11 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +23 -7
- package/dist/testing/cross_backend/ws_round_trip.js +1 -1
- package/dist/testing/db.js +4 -4
- package/dist/testing/middleware.js +1 -1
- package/dist/testing/role_grant_helpers.d.ts +1 -1
- package/dist/testing/rpc_helpers.d.ts +18 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +24 -2
- package/dist/testing/schema_generators.d.ts +1 -1
- package/dist/testing/schema_generators.js +1 -1
- package/dist/testing/stubs.js +1 -1
- package/dist/testing/testing_rate_limiter.d.ts +1 -1
- package/dist/testing/testing_rate_limiter.js +1 -1
- package/dist/testing/transports/bootstrap.d.ts +1 -1
- package/dist/testing/transports/bootstrap.js +1 -1
- package/dist/testing/ws_round_trip.d.ts +2 -2
- package/dist/testing/ws_round_trip.js +2 -2
- package/dist/ui/AdminSurface.svelte +1 -1
- package/dist/ui/AppShell.svelte +1 -1
- package/dist/ui/BootstrapForm.svelte +1 -1
- package/dist/ui/CLAUDE.md +1 -1
- package/dist/ui/ConfirmButton.svelte +2 -2
- package/dist/ui/ConfirmButton.svelte.d.ts +1 -1
- package/dist/ui/LoginForm.svelte +1 -1
- package/dist/ui/MenuLink.svelte +1 -1
- package/dist/ui/MenuLink.svelte.d.ts +1 -1
- package/dist/ui/PopoverButton.svelte +1 -1
- package/dist/ui/PopoverButton.svelte.d.ts +1 -1
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/account_sessions_state.svelte.js +1 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +1 -1
- package/dist/ui/admin_invites_state.svelte.d.ts +1 -1
- package/dist/ui/admin_invites_state.svelte.js +1 -1
- package/dist/ui/admin_sessions_state.svelte.d.ts +1 -1
- package/dist/ui/app_settings_state.svelte.js +1 -1
- package/dist/ui/async_slot.svelte.d.ts +2 -2
- package/dist/ui/async_slot.svelte.js +2 -2
- package/dist/ui/audit_log_state.svelte.js +1 -1
- package/dist/ui/auth_state.svelte.d.ts +1 -1
- package/dist/ui/auth_state.svelte.js +3 -3
- package/dist/ui/format_scope.js +1 -1
- package/dist/ui/popover.svelte.d.ts +1 -1
- package/dist/ui/popover.svelte.js +2 -2
- package/dist/ui/position_helpers.js +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +1 -1
- package/dist/ui/sidebar_state.svelte.js +1 -1
- package/dist/ui/table_state.svelte.d.ts +1 -1
- package/dist/ui/table_state.svelte.js +2 -2
- package/package.json +3 -2
- package/dist/testing/cross_backend/create_schema_parity_global_setup.d.ts +0 -59
- package/dist/testing/cross_backend/create_schema_parity_global_setup.d.ts.map +0 -1
|
@@ -16,11 +16,17 @@ import '../assert_dev_env.js';
|
|
|
16
16
|
* row runs `as` resolves to a `TestFixture` accessor via
|
|
17
17
|
* `resolve_principal` — no inline credential minting.
|
|
18
18
|
*
|
|
19
|
+
* Every response also passes an always-on **no-fingerprint** invariant
|
|
20
|
+
* (`assert_no_fingerprint_headers` over `FINGERPRINT_HEADERS` — `Server` /
|
|
21
|
+
* `X-Powered-By` / `WWW-Authenticate` must stay absent on both spines), and
|
|
22
|
+
* a row may pin further header expectations via `expect.headers`. Headers are
|
|
23
|
+
* deliberately kept out of the equivalence-group `{status, body}` comparison.
|
|
24
|
+
*
|
|
19
25
|
* @module
|
|
20
26
|
*/
|
|
21
27
|
import { assert, describe, test } from 'vitest';
|
|
22
28
|
import { find_auth_route, rest_auth_route_suffixes, } from '../integration_helpers.js';
|
|
23
|
-
import { find_rpc_action, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
|
|
29
|
+
import { find_rpc_action, headers_to_record, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
|
|
24
30
|
import {} from './conformance_case.js';
|
|
25
31
|
import { xfail_until } from './xfail.js';
|
|
26
32
|
/**
|
|
@@ -91,19 +97,70 @@ const assert_fields = (actual, fields, label) => {
|
|
|
91
97
|
assert.deepEqual(record[key], expected, `${label}: field '${key}'`);
|
|
92
98
|
}
|
|
93
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Response headers that fingerprint the backend implementation or framework.
|
|
102
|
+
* Neither spine emits these; the runner asserts they stay absent on EVERY
|
|
103
|
+
* conformance response so a framework upgrade or consumer middleware that adds
|
|
104
|
+
* (say) `Server:` to one spine can't silently become a backend-identifying
|
|
105
|
+
* oracle. Lowercased — matched against the lowercased-key snapshot
|
|
106
|
+
* `headers_to_record` produces.
|
|
107
|
+
*/
|
|
108
|
+
export const FINGERPRINT_HEADERS = [
|
|
109
|
+
'server',
|
|
110
|
+
'x-powered-by',
|
|
111
|
+
'www-authenticate',
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Assert a response carries none of the `FINGERPRINT_HEADERS`. Run on every
|
|
115
|
+
* case unconditionally — the always-on no-fingerprint floor.
|
|
116
|
+
*/
|
|
117
|
+
export const assert_no_fingerprint_headers = (headers, label) => {
|
|
118
|
+
for (const name of FINGERPRINT_HEADERS) {
|
|
119
|
+
assert.ok(!(name in headers), `${label}: response leaked backend-fingerprinting header '${name}: ${headers[name]}' — ` +
|
|
120
|
+
`Server / X-Powered-By / WWW-Authenticate must stay absent on both spines.`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Assert each declared header expectation: a string value must be present and
|
|
125
|
+
* equal (header name matched case-insensitively), `null` must be absent. The
|
|
126
|
+
* negative-space twin for headers — `expect.headers` pins a header beyond the
|
|
127
|
+
* always-on no-fingerprint floor.
|
|
128
|
+
*/
|
|
129
|
+
export const assert_expected_headers = (headers, expected, label) => {
|
|
130
|
+
for (const [name, value] of Object.entries(expected)) {
|
|
131
|
+
const key = name.toLowerCase();
|
|
132
|
+
if (value === null) {
|
|
133
|
+
assert.ok(!(key in headers), `${label}: header '${name}' expected absent but present as '${headers[key]}'`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
assert.strictEqual(headers[key], value, `${label}: header '${name}'`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Run a case's header invariants: the always-on no-fingerprint floor on every
|
|
142
|
+
* response plus any declared `expect.headers`. Header assertions are kept off
|
|
143
|
+
* the equivalence-group `{status, body}` normalized response — `Set-Cookie` /
|
|
144
|
+
* `Date` legitimately vary, so headers are never compared for byte-identity.
|
|
145
|
+
*/
|
|
146
|
+
const assert_response_headers = (headers, c) => {
|
|
147
|
+
assert_no_fingerprint_headers(headers, c.name);
|
|
148
|
+
if (c.expect.headers)
|
|
149
|
+
assert_expected_headers(headers, c.expect.headers, c.name);
|
|
150
|
+
};
|
|
94
151
|
const is_success_status = (status) => status >= 200 && status < 300;
|
|
95
152
|
/**
|
|
96
153
|
* Run one conformance case end-to-end: resolve the principal, dispatch the
|
|
97
|
-
* request, and assert the expected status / reason / fields.
|
|
154
|
+
* request, and assert the expected status / reason / fields. Returns the
|
|
155
|
+
* normalized response so the caller can feed an equivalence group.
|
|
98
156
|
*/
|
|
99
157
|
const run_case = async (c, options, resolved_rpc_endpoints) => {
|
|
100
158
|
const fixture = await options.setup_test();
|
|
101
159
|
const { transport, headers, suppress_default_origin } = await resolve_principal(fixture, c.request.as, options.principals);
|
|
102
160
|
if (c.request.method.startsWith('/')) {
|
|
103
|
-
|
|
104
|
-
return;
|
|
161
|
+
return run_rest_case(c, options, transport, headers);
|
|
105
162
|
}
|
|
106
|
-
|
|
163
|
+
return run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
|
|
107
164
|
};
|
|
108
165
|
/** Dispatch + assert a case targeting an RPC method. */
|
|
109
166
|
const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
|
|
@@ -121,6 +178,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
121
178
|
...(c.request.verb && { verb: c.request.verb }),
|
|
122
179
|
...(suppress_default_origin && { suppress_default_origin: true }),
|
|
123
180
|
});
|
|
181
|
+
assert_response_headers(res.headers, c);
|
|
124
182
|
if (is_success_status(c.expect.status)) {
|
|
125
183
|
assert.ok(res.ok, `${c.name}: expected success (${c.expect.status}) but got error ${JSON.stringify(res.ok ? undefined : res.error)}`);
|
|
126
184
|
assert.strictEqual(res.status, c.expect.status, `${c.name}: status`);
|
|
@@ -128,7 +186,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
128
186
|
assert.ok(parsed.success, `${c.name}: result does not match spec.output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
|
|
129
187
|
if (c.expect.fields)
|
|
130
188
|
assert_fields(res.result, c.expect.fields, c.name);
|
|
131
|
-
return;
|
|
189
|
+
return { status: res.status, body: res.result };
|
|
132
190
|
}
|
|
133
191
|
assert.ok(!res.ok, `${c.name}: expected error status ${c.expect.status} but got success`);
|
|
134
192
|
assert.strictEqual(res.status, c.expect.status, `${c.name}: error status`);
|
|
@@ -145,6 +203,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
145
203
|
}
|
|
146
204
|
if (c.expect.fields)
|
|
147
205
|
assert_fields(res.error.data, c.expect.fields, c.name);
|
|
206
|
+
return { status: res.status, body: res.error };
|
|
148
207
|
};
|
|
149
208
|
/** Dispatch + assert a case targeting one of the 6 REST auth routes. */
|
|
150
209
|
const run_rest_case = async (c, options, transport, headers) => {
|
|
@@ -165,6 +224,7 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
165
224
|
c.request.params !== undefined && { body: JSON.stringify(c.request.params) }),
|
|
166
225
|
};
|
|
167
226
|
const response = await transport(route.path, init);
|
|
227
|
+
assert_response_headers(headers_to_record(response.headers), c);
|
|
168
228
|
assert.strictEqual(response.status, c.expect.status, `${c.name}: status`);
|
|
169
229
|
const body = await response.json().catch(() => undefined);
|
|
170
230
|
if (is_success_status(c.expect.status)) {
|
|
@@ -177,6 +237,7 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
177
237
|
}
|
|
178
238
|
if (c.expect.fields)
|
|
179
239
|
assert_fields(body, c.expect.fields, c.name);
|
|
240
|
+
return { status: response.status, body };
|
|
180
241
|
};
|
|
181
242
|
/**
|
|
182
243
|
* Register a `describe` block running every `ConformanceCase` as a
|
|
@@ -186,10 +247,28 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
186
247
|
*/
|
|
187
248
|
export const describe_conformance_table_tests = (options) => {
|
|
188
249
|
const resolved_rpc_endpoints = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
250
|
+
// Per-group normalized responses, populated as the member case tests run
|
|
251
|
+
// and consulted by the per-group byte-identity assertions registered after
|
|
252
|
+
// the case loop — vitest runs a describe's tests in registration order, so
|
|
253
|
+
// the group assertions execute last, after every member has recorded.
|
|
254
|
+
const equivalence_groups = new Map();
|
|
255
|
+
const group_names = new Set();
|
|
256
|
+
for (const c of options.cases) {
|
|
257
|
+
if (c.expect.equivalence_group)
|
|
258
|
+
group_names.add(c.expect.equivalence_group);
|
|
259
|
+
}
|
|
189
260
|
describe(options.suite_name ?? 'conformance table', () => {
|
|
190
261
|
for (const c of options.cases) {
|
|
191
262
|
const label = c.note ? `${c.name} — ${c.note}` : c.name;
|
|
192
|
-
const
|
|
263
|
+
const group = c.expect.equivalence_group;
|
|
264
|
+
const body = async () => {
|
|
265
|
+
const normalized = await run_case(c, options, resolved_rpc_endpoints);
|
|
266
|
+
if (group) {
|
|
267
|
+
const members = equivalence_groups.get(group) ?? [];
|
|
268
|
+
members.push({ name: c.name, normalized });
|
|
269
|
+
equivalence_groups.set(group, members);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
193
272
|
if (c.xfail) {
|
|
194
273
|
xfail_until(c.xfail.tracking_id, c.xfail.reason, label, body);
|
|
195
274
|
}
|
|
@@ -197,5 +276,28 @@ export const describe_conformance_table_tests = (options) => {
|
|
|
197
276
|
test(label, body);
|
|
198
277
|
}
|
|
199
278
|
}
|
|
279
|
+
// Negative-space gate: every member of an equivalence group must have
|
|
280
|
+
// produced a byte-identical `{status, body}`. This holds the impl under
|
|
281
|
+
// test to "a prober cannot distinguish these masked paths" — the
|
|
282
|
+
// promotion of a masked pair (wrong-password ≡ account-not-found,
|
|
283
|
+
// found-but-unauthorized ≡ not-found) from "same status + reason" to
|
|
284
|
+
// "wire-indistinguishable". Runs per impl, so each spine is held to it.
|
|
285
|
+
for (const group of group_names) {
|
|
286
|
+
test(`equivalence group '${group}' — members are wire-indistinguishable`, () => {
|
|
287
|
+
const members = equivalence_groups.get(group) ?? [];
|
|
288
|
+
assert.ok(members.length >= 2, `equivalence group '${group}': expected >= 2 members to have recorded, got ` +
|
|
289
|
+
`${members.length} — a group pins indistinguishability between paths, so it ` +
|
|
290
|
+
`needs at least a pair (check the member cases ran and weren't all xfail).`);
|
|
291
|
+
const [first, ...rest] = members;
|
|
292
|
+
// `members.length >= 2` guarantees `first` is defined; the explicit
|
|
293
|
+
// assert narrows it for the typechecker (noUncheckedIndexedAccess).
|
|
294
|
+
assert.ok(first);
|
|
295
|
+
for (const member of rest) {
|
|
296
|
+
assert.deepStrictEqual(member.normalized, first.normalized, `equivalence group '${group}': '${member.name}' is distinguishable from ` +
|
|
297
|
+
`'${first.name}' — a prober could tell these masked paths apart. The ` +
|
|
298
|
+
`normalized {status, body} must be byte-identical across all members.`);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
200
302
|
});
|
|
201
303
|
};
|
|
@@ -13,7 +13,7 @@ import '../assert_dev_env.js';
|
|
|
13
13
|
*
|
|
14
14
|
* ```ts
|
|
15
15
|
* import {create_cross_backend_global_setup} from
|
|
16
|
-
* '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.
|
|
16
|
+
* '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.ts';
|
|
17
17
|
* import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
|
|
18
18
|
* import './cross_test_types.js'; // augments inject('backend_handle')
|
|
19
19
|
*
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generic **dual-spawn** vitest `globalSetup` factory — spawns + bootstraps
|
|
4
|
+
* *two* backends at once and `provide`s both serialized handles, for any
|
|
5
|
+
* cross-impl gate that needs both alive together. (The per-backend
|
|
6
|
+
* `create_cross_backend_global_setup` derives one backend from the project
|
|
7
|
+
* name; this brings up a pair.)
|
|
8
|
+
*
|
|
9
|
+
* Its primary use is the **schema- / action-manifest-parity** gates (capture
|
|
10
|
+
* each backend over a `_testing_*` introspection RPC and diff with
|
|
11
|
+
* `assert_schema_snapshots_equal` / `assert_action_manifests_equal`) — which is
|
|
12
|
+
* why the default `provide_keys` are `parity_handle_*`. The login-security gate
|
|
13
|
+
* (`global_setup_login_security.ts`) reuses it with its own keys; any future
|
|
14
|
+
* two-backend gate can too.
|
|
15
|
+
*
|
|
16
|
+
* A consumer's dual-spawn `global_setup.ts` collapses to:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import {create_dual_spawn_global_setup} from
|
|
20
|
+
* '@fuzdev/fuz_app/testing/cross_backend/create_dual_spawn_global_setup.ts';
|
|
21
|
+
* import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
|
|
22
|
+
* import './cross_test_types.js'; // augments the two provide keys
|
|
23
|
+
*
|
|
24
|
+
* export default create_dual_spawn_global_setup({
|
|
25
|
+
* configs: {a: deno_backend_config, b: rust_backend_config},
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* The `.cross.test.ts` `inject`s both keys, rebuilds each with
|
|
30
|
+
* `reconstruct_bootstrapped_handle`, and asserts. Run this project in a
|
|
31
|
+
* later `groupOrder` than the single-backend projects (or with distinct
|
|
32
|
+
* ports) — it reuses both configs' ports, so it must not run concurrently
|
|
33
|
+
* with the per-backend projects.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import type { TestProject } from 'vitest/node';
|
|
38
|
+
import type { BackendConfig } from './backend_config.js';
|
|
39
|
+
export interface DualSpawnGlobalSetupOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The two backend config factories to spawn. `a` is spawned first; on
|
|
42
|
+
* its success `b` is spawned (with `a` torn down if `b` throws). Label
|
|
43
|
+
* them in the consuming test (e.g. via `assert_schema_snapshots_equal`'s labels).
|
|
44
|
+
*/
|
|
45
|
+
readonly configs: {
|
|
46
|
+
readonly a: () => BackendConfig;
|
|
47
|
+
readonly b: () => BackendConfig;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* `project.provide` keys for the two serialized handles (read by
|
|
51
|
+
* `inject` in the test). Default `parity_handle_a` / `parity_handle_b`
|
|
52
|
+
* (the primary parity use); override for other dual-spawn gates. Augment
|
|
53
|
+
* vitest's `ProvidedContext` for whichever keys you use.
|
|
54
|
+
*/
|
|
55
|
+
readonly provide_keys?: {
|
|
56
|
+
readonly a: string;
|
|
57
|
+
readonly b: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build a vitest `globalSetup` default export that spawns both backends.
|
|
62
|
+
*/
|
|
63
|
+
export declare const create_dual_spawn_global_setup: ({ configs, provide_keys, }: DualSpawnGlobalSetupOptions) => ((project: TestProject) => Promise<() => Promise<void>>);
|
|
64
|
+
//# sourceMappingURL=create_dual_spawn_global_setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create_dual_spawn_global_setup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/create_dual_spawn_global_setup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AAE7C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,MAAM,WAAW,2BAA2B;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE;QAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,aAAa,CAAC;QAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,aAAa,CAAA;KAAC,CAAC;IACrF;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC;CACjE;AAED;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAAI,4BAG5C,2BAA2B,KAAG,CAAC,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAmBvF,CAAC"}
|
|
@@ -4,7 +4,7 @@ import { serialize_bootstrapped_handle } from './setup.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Build a vitest `globalSetup` default export that spawns both backends.
|
|
6
6
|
*/
|
|
7
|
-
export const
|
|
7
|
+
export const create_dual_spawn_global_setup = ({ configs, provide_keys = { a: 'parity_handle_a', b: 'parity_handle_b' }, }) => {
|
|
8
8
|
return async (project) => {
|
|
9
9
|
const a = await bootstrap_backend(configs.a());
|
|
10
10
|
// Tear `a` down if `b` fails to spawn — no orphaned child process.
|
|
@@ -35,6 +35,18 @@ import '../assert_dev_env.js';
|
|
|
35
35
|
import type { BackendBootstrapConfig, BackendConfig } from './backend_config.js';
|
|
36
36
|
import type { BackendCapabilities, BackendShapeNotes } from './capabilities.js';
|
|
37
37
|
import { type TestBackendPaths } from './build_test_backend_paths.js';
|
|
38
|
+
/**
|
|
39
|
+
* Env var both spine binaries read to enable their login rate limiters
|
|
40
|
+
* (`'true'` on / unset off). The cross-language contract for the login-security
|
|
41
|
+
* cross project: the TS binary reads it via `runtime.env_get` (a test-only flag,
|
|
42
|
+
* not in `BaseServerEnv`); the Rust `testing_spine_stub` reads it via
|
|
43
|
+
* `std::env::var` — so one backend-config option drives both impls. Shared home
|
|
44
|
+
* here because both `ts_spine_backend_config` and `rust_spine_stub_backend_config`
|
|
45
|
+
* already import this module. (The spawned TS binary re-declares the literal
|
|
46
|
+
* locally — it can't import this module, which transitively pulls `vitest` —
|
|
47
|
+
* mirroring how `testing_spine_server_node.ts` re-declares `TS_SPINE_DIR_ENV`.)
|
|
48
|
+
*/
|
|
49
|
+
export declare const LOGIN_RATE_LIMIT_ENABLED_ENV = "FUZ_LOGIN_RATE_LIMIT_ENABLED";
|
|
38
50
|
/**
|
|
39
51
|
* Capabilities shared by TS-family backends — same canonical implementation,
|
|
40
52
|
* same feature set. The non-gating wiring facts (`bearer_auth` /
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAiBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAgBtC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBAIrC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
|
|
1
|
+
{"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAiBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAgBtC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBAIrC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
2
|
import { build_test_backend_paths } from './build_test_backend_paths.js';
|
|
3
3
|
import { default_test_bootstrap_token, default_test_cookie_keys, default_test_keeper_password, default_test_keeper_username, } from './default_secrets.js';
|
|
4
|
+
/**
|
|
5
|
+
* Env var both spine binaries read to enable their login rate limiters
|
|
6
|
+
* (`'true'` on / unset off). The cross-language contract for the login-security
|
|
7
|
+
* cross project: the TS binary reads it via `runtime.env_get` (a test-only flag,
|
|
8
|
+
* not in `BaseServerEnv`); the Rust `testing_spine_stub` reads it via
|
|
9
|
+
* `std::env::var` — so one backend-config option drives both impls. Shared home
|
|
10
|
+
* here because both `ts_spine_backend_config` and `rust_spine_stub_backend_config`
|
|
11
|
+
* already import this module. (The spawned TS binary re-declares the literal
|
|
12
|
+
* locally — it can't import this module, which transitively pulls `vitest` —
|
|
13
|
+
* mirroring how `testing_spine_server_node.ts` re-declares `TS_SPINE_DIR_ENV`.)
|
|
14
|
+
*/
|
|
15
|
+
export const LOGIN_RATE_LIMIT_ENABLED_ENV = 'FUZ_LOGIN_RATE_LIMIT_ENABLED';
|
|
4
16
|
/**
|
|
5
17
|
* Capabilities shared by TS-family backends — same canonical implementation,
|
|
6
18
|
* same feature set. The non-gating wiring facts (`bearer_auth` /
|
|
@@ -23,7 +23,7 @@ import '../assert_dev_env.js';
|
|
|
23
23
|
*
|
|
24
24
|
* @module
|
|
25
25
|
*/
|
|
26
|
-
import type { Logger } from '@fuzdev/fuz_util/log.
|
|
26
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
27
27
|
import type { NotificationSender } from '../../auth/role_grant_offer_notifications.js';
|
|
28
28
|
import { type RoleSchemaResult } from '../../auth/role_schema.js';
|
|
29
29
|
import { type SessionOptions } from '../../auth/session_cookie.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAGzE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAElG;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,gBAExB,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,gBAAgB,EACrB,UAAU,wBAAwB,KAChC,KAAK,CAAC,eAAe,CAQvB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,gBAAgB,KAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAGzE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAElG;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,gBAExB,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,gBAAgB,EACrB,UAAU,wBAAwB,KAChC,KAAK,CAAC,eAAe,CAQvB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,gBAAgB,KAAG,KAAK,CAAC,SAAS,CA+B/E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC;AAEjG;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,SAC6B,CAAC;AAE3F;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QAAO,cAM1C,CAAC"}
|
|
@@ -77,8 +77,15 @@ export const create_spine_route_specs = (ctx) => [
|
|
|
77
77
|
...prefix_route_specs('/api/account', [
|
|
78
78
|
...create_account_route_specs(ctx.deps, {
|
|
79
79
|
session_options: spine_session_options,
|
|
80
|
-
|
|
81
|
-
login_account_rate_limiter
|
|
80
|
+
// Honor the limiters the server was assembled with (on `AppServerContext`
|
|
81
|
+
// per the `ip_rate_limiter` / `login_account_rate_limiter` TSDoc) rather
|
|
82
|
+
// than hardcoding `null`. The standard spine binary passes `null` (the
|
|
83
|
+
// cross suites fire many loopback logins and must not trip a limiter), so
|
|
84
|
+
// this is behavior-preserving there; the dedicated login-security cross
|
|
85
|
+
// backend passes real limiters to exercise the 429 + `Retry-After` path
|
|
86
|
+
// and XFF-keyed bucketing over the wire (see `login_security.ts`).
|
|
87
|
+
ip_rate_limiter: ctx.ip_rate_limiter,
|
|
88
|
+
login_account_rate_limiter: ctx.login_account_rate_limiter,
|
|
82
89
|
login_fail_floor_ms: 0,
|
|
83
90
|
bootstrap_status: ctx.bootstrap_status,
|
|
84
91
|
}),
|
|
@@ -38,7 +38,7 @@ import '../assert_dev_env.js';
|
|
|
38
38
|
*/
|
|
39
39
|
import { describe, assert } from 'vitest';
|
|
40
40
|
import { z } from 'zod';
|
|
41
|
-
import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.
|
|
41
|
+
import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
42
42
|
import { CellCreateOutput } from '../../auth/cell_action_specs.js';
|
|
43
43
|
import { test_if } from './capabilities.js';
|
|
44
44
|
import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { SetupTest } from './setup.js';
|
|
3
|
+
/** Options for the login-security parity suite. */
|
|
4
|
+
export interface LoginSecurityCrossTestOptions {
|
|
5
|
+
/** Per-test fixture producer (cross-process only — see the module doc). */
|
|
6
|
+
readonly setup_test: SetupTest;
|
|
7
|
+
/** REST login route path. Default `/api/account/login` (the spine convention). */
|
|
8
|
+
readonly login_path?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const describe_login_security_cross_tests: (options: LoginSecurityCrossTestOptions) => void;
|
|
11
|
+
//# sourceMappingURL=login_security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login_security.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/login_security.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoD9B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAE1C,mDAAmD;AACnD,MAAM,WAAW,6BAA6B;IAC7C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAyBD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IAoGF,CAAC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for **login rate limiting + trusted-proxy
|
|
4
|
+
* (X-Forwarded-For) resolution** over real HTTP.
|
|
5
|
+
*
|
|
6
|
+
* Login throttling and client-IP resolution are wired-but-never-crossed: the
|
|
7
|
+
* in-process `describe_rate_limiting_tests` covers the limiter, and the
|
|
8
|
+
* `fuz_http` / proxy middleware unit tests cover XFF resolution, but no case
|
|
9
|
+
* exercised either over a real socket on both impls — the limiter is nulled
|
|
10
|
+
* on every standard cross backend and the resolved client IP has no
|
|
11
|
+
* wire-observable downstream there. This dedicated suite spawns a backend
|
|
12
|
+
* with the login limiters enabled + the loopback proxy trusted (see
|
|
13
|
+
* `global_setup_login_security.ts`), then pins two properties end-to-end:
|
|
14
|
+
*
|
|
15
|
+
* - **per-IP login limit fires** — the first `default_login_ip_rate_limit`
|
|
16
|
+
* failed logins from one forwarded IP each return `401`, and the next
|
|
17
|
+
* returns `429` with the canonical `{error: "rate_limit_exceeded",
|
|
18
|
+
* retry_after}` body **and** a `Retry-After: ceil(retry_after)` header. The
|
|
19
|
+
* 429 wire shape is the cross-impl contract — TS `rate_limit_exceeded_response`
|
|
20
|
+
* and Rust `route_response::rate_limit_exceeded` must agree.
|
|
21
|
+
* - **trusted-proxy / XFF resolution is honored** — distinct
|
|
22
|
+
* `X-Forwarded-For` IPs get independent buckets: after exhausting one
|
|
23
|
+
* forwarded IP to `429`, a *different* forwarded IP is unaffected (`401`,
|
|
24
|
+
* not `429`). A backend that ignored XFF and keyed on the (loopback) TCP
|
|
25
|
+
* peer would 429 the fresh-IP request too — so the `401` proves the limiter
|
|
26
|
+
* keys on the resolved `X-Forwarded-For` client IP.
|
|
27
|
+
*
|
|
28
|
+
* **Determinism without a limiter reset.** Limiter state is in-memory and the
|
|
29
|
+
* per-test `_testing_reset` wipes only the DB, never the buckets — so each
|
|
30
|
+
* case uses its own forwarded IP and its own (non-existent) username, keeping
|
|
31
|
+
* every bucket independent across cases and across the two impls (separate
|
|
32
|
+
* processes). The login floor is zeroed on both spines, so the failed-login
|
|
33
|
+
* loop stays fast.
|
|
34
|
+
*
|
|
35
|
+
* Both surfaces are flat REST (`POST /api/account/login`) on every spine, so
|
|
36
|
+
* this is an imperative suite (not a `conformance_table` row) — the sibling of
|
|
37
|
+
* `origin.ts` / `identity_parity.ts`. Cross-process only: the limiter+proxy
|
|
38
|
+
* wiring is the point, and the in-process counterparts already exist
|
|
39
|
+
* (`describe_rate_limiting_tests` + the proxy middleware tests). Cited
|
|
40
|
+
* property: `docs/security.md` §"Rate Limiting" + §"Trusted Proxy / Client IP".
|
|
41
|
+
*
|
|
42
|
+
* `$lib`-free by contract (relative specifiers only), like the sibling
|
|
43
|
+
* cross-backend suites.
|
|
44
|
+
*
|
|
45
|
+
* @module
|
|
46
|
+
*/
|
|
47
|
+
import { describe, test, assert } from 'vitest';
|
|
48
|
+
import { ERROR_RATE_LIMIT_EXCEEDED } from '../../http/error_schemas.js';
|
|
49
|
+
import { default_login_account_rate_limit, default_login_ip_rate_limit } from '../../rate_limiter.js';
|
|
50
|
+
/**
|
|
51
|
+
* RFC 5737 TEST-NET-3 (`203.0.113.0/24`) documentation addresses — guaranteed
|
|
52
|
+
* non-routable, so they never collide with a real client IP. One per bucket the
|
|
53
|
+
* suite needs; distinct across cases so no case inherits another's count (the
|
|
54
|
+
* limiter is not reset between cases). Spoofed via `X-Forwarded-For`; the
|
|
55
|
+
* loopback TCP peer is trusted, so the rightmost untrusted hop (these) is the
|
|
56
|
+
* resolved client IP.
|
|
57
|
+
*/
|
|
58
|
+
const XFF_IP_LIMIT = '203.0.113.1';
|
|
59
|
+
const XFF_SEGREGATION_EXHAUST = '203.0.113.2';
|
|
60
|
+
const XFF_SEGREGATION_FRESH = '203.0.113.3';
|
|
61
|
+
/** A non-routable, non-existent identifier — login resolves to the not-found path (records the bucket, returns 401). */
|
|
62
|
+
const PROBE_PASSWORD = 'login_security_wrong_password';
|
|
63
|
+
export const describe_login_security_cross_tests = (options) => {
|
|
64
|
+
const { setup_test } = options;
|
|
65
|
+
const login_path = options.login_path ?? '/api/account/login';
|
|
66
|
+
/** Per-IP cap shared by both impls (TS `default_login_ip_rate_limit`, Rust `DEFAULT_LOGIN_IP_RATE_LIMIT`). */
|
|
67
|
+
const ip_limit = default_login_ip_rate_limit.max_attempts;
|
|
68
|
+
// The XFF-segregation case enables both limiters (one flag drives both), so
|
|
69
|
+
// the per-account bucket is live too. After exhausting an IP, the shared
|
|
70
|
+
// username holds exactly `ip_limit` account records, so the fresh-IP probe
|
|
71
|
+
// only stays a 401 (not 429) while the account cap sits above the IP cap.
|
|
72
|
+
// Assert the ordering so a future cap change fails loud here instead of as a
|
|
73
|
+
// confusing 429-instead-of-401 mid-case.
|
|
74
|
+
assert(default_login_account_rate_limit.max_attempts > ip_limit, 'login-security XFF segregation needs the per-account cap above the per-IP cap');
|
|
75
|
+
/**
|
|
76
|
+
* POST a login with the given `X-Forwarded-For` on a fresh (cookie-jar-free)
|
|
77
|
+
* transport — wrong password against a non-existent username, so the handler
|
|
78
|
+
* clears origin + input validation, records the resolved-IP bucket, and
|
|
79
|
+
* returns `401` (or `429` once the bucket trips). Always reads the body so
|
|
80
|
+
* the cross-process socket releases.
|
|
81
|
+
*/
|
|
82
|
+
const attempt = async (fixture, forwarded_for, username) => {
|
|
83
|
+
const res = await fixture.fresh_transport()(login_path, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'content-type': 'application/json', 'x-forwarded-for': forwarded_for },
|
|
86
|
+
body: JSON.stringify({ username, password: PROBE_PASSWORD }),
|
|
87
|
+
});
|
|
88
|
+
const body = (await res.json().catch(() => undefined));
|
|
89
|
+
return {
|
|
90
|
+
status: res.status,
|
|
91
|
+
error: typeof body?.error === 'string' ? body.error : undefined,
|
|
92
|
+
retry_after: typeof body?.retry_after === 'number' ? body.retry_after : undefined,
|
|
93
|
+
retry_after_header: res.headers.get('retry-after'),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
describe('login rate limiting + trusted-proxy parity', () => {
|
|
97
|
+
test(`per-IP login limit fires 429 + Retry-After after ${ip_limit} attempts from one forwarded IP`, async () => {
|
|
98
|
+
const fixture = await setup_test();
|
|
99
|
+
// The first `ip_limit` failed logins from one forwarded IP each clear the
|
|
100
|
+
// limiter (a normal 401), recording against the resolved client IP.
|
|
101
|
+
for (let i = 0; i < ip_limit; i++) {
|
|
102
|
+
const r = await attempt(fixture, XFF_IP_LIMIT, 'probe_ip_limit');
|
|
103
|
+
assert.strictEqual(r.status, 401, `attempt ${i + 1}/${ip_limit} must be a normal 401, not rate-limited`);
|
|
104
|
+
}
|
|
105
|
+
// The next request from the SAME forwarded IP trips the per-IP bucket.
|
|
106
|
+
const limited = await attempt(fixture, XFF_IP_LIMIT, 'probe_ip_limit');
|
|
107
|
+
assert.strictEqual(limited.status, 429, `attempt ${ip_limit + 1} must be 429`);
|
|
108
|
+
assert.strictEqual(limited.error, ERROR_RATE_LIMIT_EXCEEDED, '429 body must carry the canonical rate_limit_exceeded reason');
|
|
109
|
+
assert(limited.retry_after !== undefined && limited.retry_after > 0, '429 body must carry a positive numeric retry_after');
|
|
110
|
+
assert(limited.retry_after_header !== null, 'Retry-After header must be present on the 429');
|
|
111
|
+
assert.strictEqual(Number(limited.retry_after_header), Math.ceil(limited.retry_after), 'Retry-After header must equal ceil(retry_after)');
|
|
112
|
+
});
|
|
113
|
+
test('distinct X-Forwarded-For IPs get independent buckets (XFF resolution honored)', async () => {
|
|
114
|
+
const fixture = await setup_test();
|
|
115
|
+
// Exhaust one forwarded IP to its cap (own username, so the per-account
|
|
116
|
+
// bucket never confounds — these stay well under the account cap).
|
|
117
|
+
for (let i = 0; i < ip_limit; i++) {
|
|
118
|
+
await attempt(fixture, XFF_SEGREGATION_EXHAUST, 'probe_xff');
|
|
119
|
+
}
|
|
120
|
+
const exhausted = await attempt(fixture, XFF_SEGREGATION_EXHAUST, 'probe_xff');
|
|
121
|
+
assert.strictEqual(exhausted.status, 429, 'the exhausted forwarded IP must be rate-limited');
|
|
122
|
+
// A DIFFERENT forwarded IP is a fresh bucket → not limited. If the backend
|
|
123
|
+
// ignored XFF and keyed on the loopback TCP peer, this would land in the
|
|
124
|
+
// already-exhausted peer bucket and 429 — so a 401 proves the limiter keys
|
|
125
|
+
// on the resolved X-Forwarded-For client IP.
|
|
126
|
+
const fresh = await attempt(fixture, XFF_SEGREGATION_FRESH, 'probe_xff');
|
|
127
|
+
assert.strictEqual(fresh.status, 401, "a fresh forwarded IP must not inherit another IP's limit");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
@@ -23,6 +23,23 @@ export interface SpineStubBackendConfigOptions {
|
|
|
23
23
|
* var. When neither is set the preset throws.
|
|
24
24
|
*/
|
|
25
25
|
readonly binary_path?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Enable the per-IP + per-account login rate limiters on the stub
|
|
28
|
+
* (`FUZ_LOGIN_RATE_LIMIT_ENABLED=true`). Off by default — the standard cross
|
|
29
|
+
* suites fire many loopback logins a live limiter would 429. Set `true` only
|
|
30
|
+
* for the dedicated login-security cross project (`global_setup_login_security.ts`).
|
|
31
|
+
* Pair with `trusted_proxies` so the limiter keys on the resolved
|
|
32
|
+
* `X-Forwarded-For` client IP. Mirrors `TsSpineBackendConfigOptions.enable_login_rate_limit`.
|
|
33
|
+
*/
|
|
34
|
+
readonly enable_login_rate_limit?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Comma-separated trusted-proxy allowlist passed as `FUZ_TRUSTED_PROXIES`
|
|
37
|
+
* (e.g. `'127.0.0.1,::1'`). Unset by default (the stub leaves XFF parsing
|
|
38
|
+
* off, keying on the raw TCP peer). The login-security project sets the
|
|
39
|
+
* loopback set so the limiter keys on the `X-Forwarded-For` client IP — the
|
|
40
|
+
* TS spine binary wires the equivalent set unconditionally.
|
|
41
|
+
*/
|
|
42
|
+
readonly trusted_proxies?: string;
|
|
26
43
|
}
|
|
27
44
|
/**
|
|
28
45
|
* Build the `BackendConfig` for `testing_spine_stub`. Resolves the binary
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AASvD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAenG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aA+CF,CAAC"}
|
|
@@ -36,7 +36,7 @@ import '../assert_dev_env.js';
|
|
|
36
36
|
import { fileURLToPath } from 'node:url';
|
|
37
37
|
import { build_test_backend_paths } from './build_test_backend_paths.js';
|
|
38
38
|
import { SPINE_EXPECTED_SCHEMA_URL } from './default_spine_surface.js';
|
|
39
|
-
import { make_default_rust_backend_config, rust_default_capabilities, } from './default_backend_configs.js';
|
|
39
|
+
import { LOGIN_RATE_LIMIT_ENABLED_ENV, make_default_rust_backend_config, rust_default_capabilities, } from './default_backend_configs.js';
|
|
40
40
|
/** Env var naming the prebuilt `testing_spine_stub` binary. Required when `binary_path` is omitted. */
|
|
41
41
|
export const RUST_SPINE_STUB_BIN_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_BIN';
|
|
42
42
|
/**
|
|
@@ -74,7 +74,7 @@ export const RUST_SPINE_STUB_DEFAULT_DATABASE_URL = 'postgres://localhost/fuz_ap
|
|
|
74
74
|
* @throws Error when no binary path is available.
|
|
75
75
|
*/
|
|
76
76
|
export const rust_spine_stub_backend_config = (options = {}) => {
|
|
77
|
-
const { port = RUST_SPINE_STUB_DEFAULT_PORT, database_url = RUST_SPINE_STUB_DEFAULT_DATABASE_URL, binary_path = process.env[RUST_SPINE_STUB_BIN_ENV], } = options;
|
|
77
|
+
const { port = RUST_SPINE_STUB_DEFAULT_PORT, database_url = RUST_SPINE_STUB_DEFAULT_DATABASE_URL, binary_path = process.env[RUST_SPINE_STUB_BIN_ENV], enable_login_rate_limit, trusted_proxies, } = options;
|
|
78
78
|
if (!binary_path) {
|
|
79
79
|
throw new Error(`rust_spine_stub_backend_config: no binary path — set ${RUST_SPINE_STUB_BIN_ENV} to a prebuilt ` +
|
|
80
80
|
'`testing_spine_stub` binary (build it with `cargo build -p testing_spine_stub --release`) ' +
|
|
@@ -105,6 +105,11 @@ export const rust_spine_stub_backend_config = (options = {}) => {
|
|
|
105
105
|
// file the TS spine reads, so the two backends share one cross-impl
|
|
106
106
|
// contract.
|
|
107
107
|
[RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH_ENV]: fileURLToPath(SPINE_EXPECTED_SCHEMA_URL),
|
|
108
|
+
// Login-security project opt-ins (off by default): enable the login
|
|
109
|
+
// limiters + trust the loopback proxy so the limiter keys on the
|
|
110
|
+
// resolved `X-Forwarded-For` IP. The stub reads both directly.
|
|
111
|
+
...(enable_login_rate_limit ? { [LOGIN_RATE_LIMIT_ENABLED_ENV]: 'true' } : {}),
|
|
112
|
+
...(trusted_proxies !== undefined ? { FUZ_TRUSTED_PROXIES: trusted_proxies } : {}),
|
|
108
113
|
},
|
|
109
114
|
});
|
|
110
115
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
-
import { Uuid } from '@fuzdev/fuz_util/id.
|
|
2
|
+
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
3
3
|
import type { TestAccount } from '../app_server.js';
|
|
4
4
|
import type { BackendCapabilities } from './capabilities.js';
|
|
5
5
|
import { type FetchTransport } from '../transports/fetch_transport.js';
|
|
@@ -22,7 +22,7 @@ import '../assert_dev_env.js';
|
|
|
22
22
|
* @module
|
|
23
23
|
*/
|
|
24
24
|
import { z } from 'zod';
|
|
25
|
-
import { Uuid } from '@fuzdev/fuz_util/id.
|
|
25
|
+
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
26
26
|
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
27
27
|
import { USERNAME_LENGTH_MAX } from '../../primitive_schemas.js';
|
|
28
28
|
import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
|
|
@@ -66,8 +66,8 @@ import '../assert_dev_env.js';
|
|
|
66
66
|
* @module
|
|
67
67
|
*/
|
|
68
68
|
import { z } from 'zod';
|
|
69
|
-
import { Uuid } from '@fuzdev/fuz_util/id.
|
|
70
|
-
import { fact_hash_bytes, FactHashSchema } from '@fuzdev/fuz_util/fact_hash.
|
|
69
|
+
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
70
|
+
import { fact_hash_bytes, FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
71
71
|
import { rpc_action } from '../../actions/action_rpc.js';
|
|
72
72
|
import { query_put_fact } from '../../db/fact_queries.js';
|
|
73
73
|
import { ROLE_ADMIN, ROLE_KEEPER } from '../../auth/role_schema.js';
|
|
@@ -28,7 +28,7 @@ import '../assert_dev_env.js';
|
|
|
28
28
|
*/
|
|
29
29
|
import type { Context, Hono } from 'hono';
|
|
30
30
|
import type { UpgradeWebSocket } from 'hono/ws';
|
|
31
|
-
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.
|
|
31
|
+
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.ts';
|
|
32
32
|
import type { RuntimeDeps } from '../../runtime/deps.js';
|
|
33
33
|
/**
|
|
34
34
|
* Adapter-built handle to a bound HTTP server.
|