@fuzdev/fuz_app 0.134.0 → 0.136.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 +93 -11
- package/dist/actions/register_action_ws.d.ts +23 -3
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +57 -37
- package/dist/actions/transports.d.ts +23 -0
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +23 -0
- package/dist/actions/transports_ws_backend.d.ts +133 -10
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +237 -34
- package/dist/actions/ws_endpoint_spec.d.ts +9 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/actions/ws_outbound_writer.d.ts +206 -0
- package/dist/actions/ws_outbound_writer.d.ts.map +1 -0
- package/dist/actions/ws_outbound_writer.js +431 -0
- package/dist/auth/CLAUDE.md +10 -4
- package/dist/auth/account_action_specs.d.ts +7 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +11 -1
- package/dist/auth/audit_emitter.d.ts +4 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +18 -2
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +3 -1
- package/dist/auth/cell_action_specs.d.ts +10 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +17 -3
- package/dist/auth/cell_actions.js +1 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +7 -2
- package/dist/auth/role_schema.d.ts +14 -3
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +14 -2
- package/dist/auth/scope_kind_schema.d.ts +10 -0
- package/dist/auth/scope_kind_schema.d.ts.map +1 -1
- package/dist/auth/scope_kind_schema.js +11 -0
- package/dist/db/migrate.d.ts +1 -1
- package/dist/db/migrate.js +2 -2
- package/dist/env/load.d.ts +1 -1
- package/dist/env/load.js +1 -1
- package/dist/http/CLAUDE.md +2 -1
- package/dist/http/body_limit.d.ts +74 -0
- package/dist/http/body_limit.d.ts.map +1 -0
- package/dist/http/body_limit.js +100 -0
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +5 -3
- package/dist/http/error_schemas.d.ts +2 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +2 -0
- package/dist/realtime/outbound_budget.d.ts +91 -0
- package/dist/realtime/outbound_budget.d.ts.map +1 -0
- package/dist/realtime/outbound_budget.js +108 -0
- package/dist/realtime/sse.d.ts +36 -3
- package/dist/realtime/sse.d.ts.map +1 -1
- package/dist/realtime/sse.js +58 -6
- package/dist/realtime/sse_constants.d.ts +12 -0
- package/dist/realtime/sse_constants.d.ts.map +1 -1
- package/dist/realtime/sse_constants.js +12 -0
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +3 -6
- package/dist/testing/CLAUDE.md +96 -19
- package/dist/testing/admin_integration.js +1 -1
- package/dist/testing/audit_completeness.d.ts +1 -1
- package/dist/testing/audit_completeness.js +1 -1
- package/dist/testing/audit_drift_guard.d.ts +19 -6
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +25 -9
- package/dist/testing/cross_backend/body_size.d.ts +18 -5
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +233 -34
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +0 -12
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +103 -108
- package/dist/testing/cross_backend/capabilities.d.ts +3 -21
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +1 -3
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +31 -2
- package/dist/testing/cross_backend/conformance_case.d.ts +3 -3
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_header_robustness.js +18 -54
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +2 -8
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts +6 -0
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_role_grammar.js +117 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +2 -3
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +2 -3
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +10 -4
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +87 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +106 -2
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +34 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +8 -13
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +29 -0
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_outbound_overflow.js +276 -0
- package/dist/testing/integration.js +1 -1
- package/dist/testing/schema_introspect.d.ts +30 -2
- package/dist/testing/schema_introspect.d.ts.map +1 -1
- package/dist/testing/schema_introspect.js +38 -7
- package/dist/testing/transports/raw_http.d.ts +78 -0
- package/dist/testing/transports/raw_http.d.ts.map +1 -0
- package/dist/testing/transports/raw_http.js +141 -0
- package/dist/testing/transports/ws_transport.d.ts +4 -2
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +2 -1
- package/dist/testing/ws_round_trip.d.ts +32 -2
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +26 -4
- package/package.json +1 -1
- package/dist/env/resolve.d.ts +0 -163
- package/dist/env/resolve.d.ts.map +0 -1
- package/dist/env/resolve.js +0 -267
|
@@ -122,25 +122,6 @@ export interface BackendCapabilities {
|
|
|
122
122
|
* explicitly (a backend that deliberately omits the route).
|
|
123
123
|
*/
|
|
124
124
|
readonly account_status: boolean;
|
|
125
|
-
/**
|
|
126
|
-
* On an oversized-body `413` reject the backend **closes the connection
|
|
127
|
-
* without reading the body** (the defense-in-depth posture), rather than
|
|
128
|
-
* draining the declared `Content-Length` and keeping the socket alive.
|
|
129
|
-
* Gates the strong half of `describe_body_size_smuggling_cross_tests`: when
|
|
130
|
-
* `true`, the pipelined GET is never reached (at most one response); when
|
|
131
|
-
* `false`, the suite instead asserts the weaker but still-load-bearing
|
|
132
|
-
* no-desync property (the body is framed on `Content-Length`, not reparsed
|
|
133
|
-
* as request bytes).
|
|
134
|
-
*
|
|
135
|
-
* `true` for the Node / Deno (`@hono/node-server` graceful close) and Rust
|
|
136
|
-
* (hyper RST) backends; `false` for Bun — `Bun.serve` reads the full body
|
|
137
|
-
* and processes the correctly-framed pipelined request even when the `413`
|
|
138
|
-
* carries `Connection: close`. Bun is not insecure (no desync — it answers
|
|
139
|
-
* the cleanly-delimited GET with a proper `400`); the flag records the
|
|
140
|
-
* connection-handling divergence so the suite stays green without losing the
|
|
141
|
-
* smuggle detector. See `docs/security.md` §"Body Size Limiting".
|
|
142
|
-
*/
|
|
143
|
-
readonly oversized_reject_closes_connection: boolean;
|
|
144
125
|
/**
|
|
145
126
|
* The backend can **initiate** a JSON-RPC request to a connected client
|
|
146
127
|
* and await its typed reply (the server→client request/response direction
|
|
@@ -171,8 +152,9 @@ export interface BackendCapabilities {
|
|
|
171
152
|
* anything.
|
|
172
153
|
*
|
|
173
154
|
* `true` for Node (`ws`), Deno, and the Rust spine (hyper hands the
|
|
174
|
-
* leftover bytes to the upgraded stream); `false` for Bun
|
|
175
|
-
*
|
|
155
|
+
* leftover bytes to the upgraded stream); `false` for a Bun backend that
|
|
156
|
+
* upgrades after an asynchronous check, as one behind auth does — `Bun.serve`
|
|
157
|
+
* answers such a request `400` (observed on Bun 1.4.3). A backend
|
|
176
158
|
* without it still runs the suite's client-library case (requests sent the
|
|
177
159
|
* moment the socket opens), which pins the same contract without forcing
|
|
178
160
|
* the ordering.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2C9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAqBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
|
|
@@ -22,8 +22,7 @@ import '../assert_dev_env.js';
|
|
|
22
22
|
* a capability is a compile error until each family declares it).
|
|
23
23
|
* - `ts_spine_capabilities` / `ts_spine_bun_capabilities` — fuz_app's own TS
|
|
24
24
|
* spine presets, in `ts_spine_backend_config.ts` (deltas off the family
|
|
25
|
-
* default; Bun flips `
|
|
26
|
-
* `ws_handshake_pipelining`).
|
|
25
|
+
* default; Bun flips `ws_handshake_pipelining`).
|
|
27
26
|
* - `rust_spine_stub_capabilities` — fuz_app's Rust spine-stub preset, in
|
|
28
27
|
* `rust_spine_stub_backend_config.ts` (delta off the rust family default).
|
|
29
28
|
*
|
|
@@ -61,7 +60,6 @@ export const in_process_capabilities = Object.freeze({
|
|
|
61
60
|
fact_serving: true,
|
|
62
61
|
ready: true,
|
|
63
62
|
account_status: true,
|
|
64
|
-
oversized_reject_closes_connection: true,
|
|
65
63
|
peer_request: false,
|
|
66
64
|
// Cross-process-only, like `peer_request`: the case needs a real bound socket
|
|
67
65
|
// to write raw bytes to.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IA6fF,CAAC"}
|
|
@@ -32,7 +32,10 @@ import '../assert_dev_env.js';
|
|
|
32
32
|
* `kind` inside `data` still gets the authz error (404 / 403), never the
|
|
33
33
|
* kind error — authz runs before the input-shape checks, on both spines;
|
|
34
34
|
* - an empty-string `kind` is rejected on create (`cell_kind_empty`) and is
|
|
35
|
-
* accepted as a list filter that matches nothing
|
|
35
|
+
* accepted as a list filter that matches nothing;
|
|
36
|
+
* - a `kind` of exactly `CELL_KIND_LENGTH_MAX` UTF-16 code units is stored
|
|
37
|
+
* whole, and one unit past it is `invalid_params` on create and as a list
|
|
38
|
+
* filter.
|
|
36
39
|
*
|
|
37
40
|
* The visibility-manage-tier 403 (`cell_visibility_manage_only`) needs a
|
|
38
41
|
* non-owner editor, which only a `cell_grant` can produce, so it lives in
|
|
@@ -42,7 +45,7 @@ import '../assert_dev_env.js';
|
|
|
42
45
|
* @module
|
|
43
46
|
*/
|
|
44
47
|
import { describe, assert } from 'vitest';
|
|
45
|
-
import { CellCreateOutput, CellDeleteOutput, CellGetOutput, CellListOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
|
|
48
|
+
import { CELL_KIND_LENGTH_MAX, CellCreateOutput, CellDeleteOutput, CellGetOutput, CellListOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
|
|
46
49
|
import { test_if } from './capabilities.js';
|
|
47
50
|
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
48
51
|
import { assert_iso8601_seconds, assert_iso8601_seconds_nullable } from './wire_shapes.js';
|
|
@@ -239,5 +242,31 @@ export const describe_cell_crud_cross_tests = (options) => {
|
|
|
239
242
|
const listed = expect_output(await cross_rpc_call(t, rpc_path, 'cell_list', { kind: '' }, owner_headers), CellListOutput);
|
|
240
243
|
assert.ok(!listed.cells.some((c) => c.id === note.id), 'empty-kind filter matched a non-empty-kind cell');
|
|
241
244
|
});
|
|
245
|
+
test_if(capabilities.cell_crud, 'kind length cap: stored whole at the cap; one unit past it is invalid_params on create and list', async () => {
|
|
246
|
+
const fixture = await setup_test();
|
|
247
|
+
const owner = await fixture.create_account({ username: 'cell_kind_cap' });
|
|
248
|
+
const t = fixture.fresh_transport();
|
|
249
|
+
const owner_headers = owner.create_session_headers();
|
|
250
|
+
// an astral character costs two UTF-16 code units on both spines
|
|
251
|
+
const astral_at_cap = '\u{1F600}'.repeat(CELL_KIND_LENGTH_MAX / 2);
|
|
252
|
+
for (const kind of ['k'.repeat(CELL_KIND_LENGTH_MAX), astral_at_cap]) {
|
|
253
|
+
assert.strictEqual(kind.length, CELL_KIND_LENGTH_MAX);
|
|
254
|
+
const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind, data: {} }, owner_headers), CellCreateOutput).cell;
|
|
255
|
+
assert.strictEqual(created.kind, kind, 'a kind at the cap must be stored whole');
|
|
256
|
+
const listed = expect_output(await cross_rpc_call(t, rpc_path, 'cell_list', { kind }, owner_headers), CellListOutput);
|
|
257
|
+
assert.ok(listed.cells.some((c) => c.id === created.id), 'a kind filter at the cap must match the cell that carries it');
|
|
258
|
+
}
|
|
259
|
+
for (const kind of ['k'.repeat(CELL_KIND_LENGTH_MAX + 1), astral_at_cap + 'k']) {
|
|
260
|
+
for (const [method, params] of [
|
|
261
|
+
['cell_create', { kind, data: {} }],
|
|
262
|
+
['cell_list', { kind }]
|
|
263
|
+
]) {
|
|
264
|
+
const refused = await cross_rpc_call(t, rpc_path, method, params, owner_headers);
|
|
265
|
+
assert.ok(!refused.ok, `${method} admitted a ${kind.length}-unit kind`);
|
|
266
|
+
assert.strictEqual(refused.error?.code, -32602);
|
|
267
|
+
assert.strictEqual(error_reason(refused), undefined, 'a schema refusal carries no reason');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
242
271
|
});
|
|
243
272
|
};
|
|
@@ -92,12 +92,12 @@ import { z } from 'zod';
|
|
|
92
92
|
export declare const ConformancePrincipal: z.ZodEnum<{
|
|
93
93
|
keeper: "keeper";
|
|
94
94
|
token: "token";
|
|
95
|
+
anonymous: "anonymous";
|
|
95
96
|
daemon: "daemon";
|
|
96
97
|
invalid_daemon: "invalid_daemon";
|
|
97
98
|
daemon_browser: "daemon_browser";
|
|
98
99
|
bearer_browser: "bearer_browser";
|
|
99
100
|
scoped_token: "scoped_token";
|
|
100
|
-
anonymous: "anonymous";
|
|
101
101
|
fresh_non_admin: "fresh_non_admin";
|
|
102
102
|
role_holder: "role_holder";
|
|
103
103
|
wrong_role: "wrong_role";
|
|
@@ -122,12 +122,12 @@ export declare const ConformanceCaseRequest: z.ZodObject<{
|
|
|
122
122
|
as: z.ZodEnum<{
|
|
123
123
|
keeper: "keeper";
|
|
124
124
|
token: "token";
|
|
125
|
+
anonymous: "anonymous";
|
|
125
126
|
daemon: "daemon";
|
|
126
127
|
invalid_daemon: "invalid_daemon";
|
|
127
128
|
daemon_browser: "daemon_browser";
|
|
128
129
|
bearer_browser: "bearer_browser";
|
|
129
130
|
scoped_token: "scoped_token";
|
|
130
|
-
anonymous: "anonymous";
|
|
131
131
|
fresh_non_admin: "fresh_non_admin";
|
|
132
132
|
role_holder: "role_holder";
|
|
133
133
|
wrong_role: "wrong_role";
|
|
@@ -179,12 +179,12 @@ export declare const ConformanceCase: z.ZodObject<{
|
|
|
179
179
|
as: z.ZodEnum<{
|
|
180
180
|
keeper: "keeper";
|
|
181
181
|
token: "token";
|
|
182
|
+
anonymous: "anonymous";
|
|
182
183
|
daemon: "daemon";
|
|
183
184
|
invalid_daemon: "invalid_daemon";
|
|
184
185
|
daemon_browser: "daemon_browser";
|
|
185
186
|
bearer_browser: "bearer_browser";
|
|
186
187
|
scoped_token: "scoped_token";
|
|
187
|
-
anonymous: "anonymous";
|
|
188
188
|
fresh_non_admin: "fresh_non_admin";
|
|
189
189
|
role_holder: "role_holder";
|
|
190
190
|
wrong_role: "wrong_role";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential_header_robustness.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/credential_header_robustness.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"credential_header_robustness.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/credential_header_robustness.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4D9B,qGAAqG;AACrG,MAAM,WAAW,0CAA0C;IAC1D,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAgDD,eAAO,MAAM,iDAAiD,GAC7D,SAAS,0CAA0C,KACjD,IAkHF,CAAC"}
|
|
@@ -46,55 +46,19 @@ import '../assert_dev_env.js';
|
|
|
46
46
|
*
|
|
47
47
|
* @module
|
|
48
48
|
*/
|
|
49
|
-
import { connect } from 'node:net';
|
|
50
49
|
import { describe, test, assert } from 'vitest';
|
|
51
50
|
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
51
|
+
import { raw_http_count_responses, raw_http_exchange, raw_http_first_status } from '../transports/raw_http.js';
|
|
52
52
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
53
53
|
/** A well-formed UUID that never names a real row. */
|
|
54
54
|
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
55
55
|
/** A value that is never a real credential — used for the conflicting / garbage copies. */
|
|
56
56
|
const GARBAGE_TOKEN = 'not-a-real-credential-value';
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* `body_size_smuggling.ts`'s `send_raw`.
|
|
63
|
-
*/
|
|
64
|
-
const send_raw = (base_url, request_bytes, read_timeout_ms) => new Promise((resolve) => {
|
|
65
|
-
const url = new URL(base_url);
|
|
66
|
-
const port = Number(url.port) || (url.protocol === 'https:' ? 443 : 80);
|
|
67
|
-
const socket = connect({ host: url.hostname, port });
|
|
68
|
-
let received = '';
|
|
69
|
-
let settled = false;
|
|
70
|
-
const finish = () => {
|
|
71
|
-
if (settled)
|
|
72
|
-
return;
|
|
73
|
-
settled = true;
|
|
74
|
-
clearTimeout(timer);
|
|
75
|
-
socket.destroy();
|
|
76
|
-
resolve(received);
|
|
77
|
-
};
|
|
78
|
-
const timer = setTimeout(finish, read_timeout_ms);
|
|
79
|
-
socket.setEncoding('latin1');
|
|
80
|
-
socket.on('connect', () => socket.write(request_bytes));
|
|
81
|
-
socket.on('data', (chunk) => {
|
|
82
|
-
received += chunk;
|
|
83
|
-
});
|
|
84
|
-
socket.on('error', () => { }); // EPIPE/ECONNRESET on mid-write close is expected
|
|
85
|
-
socket.on('close', finish);
|
|
86
|
-
});
|
|
87
|
-
/**
|
|
88
|
-
* Count HTTP response status lines in a raw byte stream. Unanchored so a
|
|
89
|
-
* second (smuggled) response concatenated after the first body — which carries
|
|
90
|
-
* no trailing newline — is still counted; see `body_size_smuggling.ts`.
|
|
91
|
-
*/
|
|
92
|
-
const count_responses = (raw) => (raw.match(/HTTP\/1\.[01] \d{3}/g) ?? []).length;
|
|
93
|
-
/** The status code of the first HTTP response in a raw stream, or `null` if none. */
|
|
94
|
-
const first_status = (raw) => {
|
|
95
|
-
const match = /HTTP\/1\.[01] (\d{3})/.exec(raw);
|
|
96
|
-
return match ? Number(match[1]) : null;
|
|
97
|
-
};
|
|
57
|
+
/** How long each raw exchange waits for the server to finish answering. */
|
|
58
|
+
const RAW_READ_TIMEOUT_MS = 2000;
|
|
59
|
+
/** Write `request_bytes` to a fresh raw socket and read until the server closes. */
|
|
60
|
+
const send_raw = async (base_url, request_bytes) => (await raw_http_exchange(base_url, request_bytes, { read_timeout_ms: RAW_READ_TIMEOUT_MS }))
|
|
61
|
+
.received;
|
|
98
62
|
const is_success_status = (status) => status >= 200 && status < 300;
|
|
99
63
|
/**
|
|
100
64
|
* Frame a raw HTTP/1.1 POST with the given extra header lines + JSON body.
|
|
@@ -124,18 +88,18 @@ export const describe_credential_header_robustness_cross_tests = (options) => {
|
|
|
124
88
|
describe('credential-header robustness', () => {
|
|
125
89
|
test('duplicate X-Daemon-Token (valid + conflicting) never escalates → one non-2xx response', async () => {
|
|
126
90
|
const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${daemon_token}`, `${DAEMON_TOKEN_HEADER}: ${GARBAGE_TOKEN}`], purge_body('dup-daemon'));
|
|
127
|
-
const raw = await send_raw(base_url, req
|
|
128
|
-
assert.strictEqual(
|
|
129
|
-
const status =
|
|
91
|
+
const raw = await send_raw(base_url, req);
|
|
92
|
+
assert.strictEqual(raw_http_count_responses(raw), 1, `a duplicated X-Daemon-Token must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
|
|
93
|
+
const status = raw_http_first_status(raw);
|
|
130
94
|
assert.ok(status !== null, `expected an HTTP response. Raw head: ${raw.slice(0, 120)}`);
|
|
131
95
|
assert.ok(!is_success_status(status), `a duplicated daemon token must not perform a keeper operation; whichever copy wins, ` +
|
|
132
96
|
`the result must be non-2xx (got ${status})`);
|
|
133
97
|
});
|
|
134
98
|
test('duplicate Authorization bearer headers (both garbage) do not authenticate → one non-2xx response', async () => {
|
|
135
99
|
const req = rpc_post(host, rpc_path, [`Authorization: Bearer ${GARBAGE_TOKEN}`, `Authorization: Bearer ${GARBAGE_TOKEN}-2`], purge_body('dup-bearer'));
|
|
136
|
-
const raw = await send_raw(base_url, req
|
|
137
|
-
assert.strictEqual(
|
|
138
|
-
const status =
|
|
100
|
+
const raw = await send_raw(base_url, req);
|
|
101
|
+
assert.strictEqual(raw_http_count_responses(raw), 1, `duplicate Authorization headers must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
|
|
102
|
+
const status = raw_http_first_status(raw);
|
|
139
103
|
assert.ok(status !== null, `expected an HTTP response. Raw head: ${raw.slice(0, 120)}`);
|
|
140
104
|
assert.ok(!is_success_status(status), `garbage bearer tokens (even duplicated) must not authenticate (got ${status})`);
|
|
141
105
|
});
|
|
@@ -154,10 +118,10 @@ export const describe_credential_header_robustness_cross_tests = (options) => {
|
|
|
154
118
|
];
|
|
155
119
|
for (const { label, value } of malformed) {
|
|
156
120
|
const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${value}`], purge_body('ctrl-char'));
|
|
157
|
-
const raw = await send_raw(base_url, req
|
|
158
|
-
assert.ok(
|
|
121
|
+
const raw = await send_raw(base_url, req);
|
|
122
|
+
assert.ok(raw_http_count_responses(raw) <= 1, `${label}: a control-char-injected credential header must not frame a second ` +
|
|
159
123
|
`request (no smuggle). Raw head: ${raw.slice(0, 120)}`);
|
|
160
|
-
const status =
|
|
124
|
+
const status = raw_http_first_status(raw);
|
|
161
125
|
assert.ok(status === null || !is_success_status(status), `${label}: a malformed credential header must never authenticate (got ${status})`);
|
|
162
126
|
}
|
|
163
127
|
});
|
|
@@ -166,12 +130,12 @@ export const describe_credential_header_robustness_cross_tests = (options) => {
|
|
|
166
130
|
// rejected (4xx / 431 / connection close). Its own outcome is don't-care.
|
|
167
131
|
const oversized = 'a'.repeat(64 * 1024);
|
|
168
132
|
const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${oversized}`], purge_body('oversized'));
|
|
169
|
-
await send_raw(base_url, req
|
|
133
|
+
await send_raw(base_url, req);
|
|
170
134
|
// The property: the server survives it. A normal request on a fresh
|
|
171
135
|
// connection must still get a response (no crash, no wedged accept loop).
|
|
172
136
|
const probe = `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`;
|
|
173
|
-
const raw = await send_raw(base_url, probe
|
|
174
|
-
assert.ok(
|
|
137
|
+
const raw = await send_raw(base_url, probe);
|
|
138
|
+
assert.ok(raw_http_first_status(raw) !== null, `server must still respond after an oversized-header request (no crash / wedge). Raw head: ${raw.slice(0, 120)}`);
|
|
175
139
|
});
|
|
176
140
|
});
|
|
177
141
|
};
|
|
@@ -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,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAYhG,2CAA2C;AAC3C,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,yBAAyB,KAChC,MAAM,CAAC,MAAM,EAAE,MAAM,CAKtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,
|
|
1
|
+
{"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"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"}
|
|
@@ -32,10 +32,6 @@ export const ts_default_capabilities = Object.freeze({
|
|
|
32
32
|
// `GET /api/account/status` is bundled into `create_account_route_specs`, so
|
|
33
33
|
// every TS backend mounting account routes serves it.
|
|
34
34
|
account_status: true,
|
|
35
|
-
// Node/Deno close the socket on an oversized-body reject (the default
|
|
36
|
-
// posture). A Bun-served consumer overrides to `false` (see the bun spine
|
|
37
|
-
// config) — fail-loud rather than silently skipping the smuggle detector.
|
|
38
|
-
oversized_reject_closes_connection: true,
|
|
39
35
|
// Off by default like `sse`/`ready`: the WS backend transport *can* initiate
|
|
40
36
|
// server→client requests (`BackendWebsocketTransport.request_connection`),
|
|
41
37
|
// but driving the full `peer/ping` suite also needs `peer/ping` mounted on
|
|
@@ -44,7 +40,8 @@ export const ts_default_capabilities = Object.freeze({
|
|
|
44
40
|
peer_request: false,
|
|
45
41
|
// Node (`ws`) and Deno hand frames pipelined with the upgrade request to the
|
|
46
42
|
// socket. A Bun-served consumer overrides to `false` (see the bun spine
|
|
47
|
-
// config) — `Bun.serve` answers such a request `400
|
|
43
|
+
// config) — `Bun.serve` answers such a request `400` when the upgrade
|
|
44
|
+
// follows an asynchronous check.
|
|
48
45
|
ws_handshake_pipelining: true,
|
|
49
46
|
// Off by default: a consumer's WS endpoint mounts its own domain's actions.
|
|
50
47
|
// fuz_app's spine configs opt in (the spine binary mounts the account
|
|
@@ -76,9 +73,6 @@ export const rust_default_capabilities = Object.freeze({
|
|
|
76
73
|
// The Rust `account_router` bundles `/status` into the account routes, so
|
|
77
74
|
// every Rust spine serving the account surface serves it.
|
|
78
75
|
account_status: true,
|
|
79
|
-
// hyper sends an RST on the oversized-body reject — the connection closes
|
|
80
|
-
// and the pipelined request is never reached.
|
|
81
|
-
oversized_reject_closes_connection: true,
|
|
82
76
|
// Server-initiated requests landed Rust-first canonical — the Rust spine
|
|
83
77
|
// drives the `peer/ping` round-trip, so the family opts into the suite.
|
|
84
78
|
peer_request: true,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { RpcPathCrossSuiteOptions } from './setup.js';
|
|
3
|
+
/** Options for the role-grammar suite (the standard RPC-dispatched shape). */
|
|
4
|
+
export type RoleGrantRoleGrammarCrossTestOptions = RpcPathCrossSuiteOptions;
|
|
5
|
+
export declare const describe_role_grant_role_grammar_cross_tests: (options: RoleGrantRoleGrammarCrossTestOptions) => void;
|
|
6
|
+
//# sourceMappingURL=role_grant_role_grammar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role_grant_role_grammar.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_role_grammar.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,8EAA8E;AAC9E,MAAM,MAAM,oCAAoC,GAAG,wBAAwB,CAAC;AAoC5E,eAAO,MAAM,4CAA4C,GACxD,SAAS,oCAAoC,KAC3C,IAoGF,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for an **off-grammar `role`** on the grant verbs.
|
|
4
|
+
*
|
|
5
|
+
* `role_grant_assign` and `role_grant_offer_create` declare `role` as
|
|
6
|
+
* `RoleName` (lowercase letters and underscores, no leading or trailing
|
|
7
|
+
* underscore, at most 64 characters). A role string outside that grammar is refused at input
|
|
8
|
+
* validation — JSON-RPC `invalid_params`, with no `data.reason` — and writes
|
|
9
|
+
* no audit row: a role is caller-chosen text until it passes the grammar, and
|
|
10
|
+
* the grant verbs record the role they refuse in their failure rows.
|
|
11
|
+
*
|
|
12
|
+
* - **refused at validation** — each off-grammar role, on each verb, answers
|
|
13
|
+
* `invalid_params` with no reason, so the refusal is the input schema's and
|
|
14
|
+
* not a grant-path or authorizer one. One of them is all lowercase letters
|
|
15
|
+
* and one character past the length cap: the cap is part of the grammar, so
|
|
16
|
+
* a failure row never carries an arbitrarily long caller-chosen string.
|
|
17
|
+
* - **no audit row** — after the refusals, no grant audit row carries an
|
|
18
|
+
* off-grammar role. The control keeps that read from being vacuous: an
|
|
19
|
+
* on-grammar role no registry holds is refused past validation with
|
|
20
|
+
* `role_not_web_grantable`, the refusal a failure row records — as is one
|
|
21
|
+
* exactly at the length cap, which pins the boundary.
|
|
22
|
+
*
|
|
23
|
+
* Exists because the action-manifest parity gate captures no param schemas
|
|
24
|
+
* while the Rust spine parses params by hand — a spine that let an off-grammar
|
|
25
|
+
* role reach its grant path would answer with a different error and write the
|
|
26
|
+
* caller's text into the audit log. Both verbs are on every spine's standard
|
|
27
|
+
* surface, so the suite is ungated. Cross-process only: it reads the audit log
|
|
28
|
+
* through `_testing_drain_effects`, which the spine binaries mount.
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
import { assert, describe, test } from 'vitest';
|
|
33
|
+
import { ERROR_ROLE_NOT_WEB_GRANTABLE } from '../../http/error_schemas.js';
|
|
34
|
+
import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
|
|
35
|
+
import { cross_rpc_call, error_reason } from './cell_cross_helpers.js';
|
|
36
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
37
|
+
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
38
|
+
/**
|
|
39
|
+
* The longest role name the grammar admits — an independent copy of the
|
|
40
|
+
* producers' `ROLE_NAME_LENGTH_MAX`, so a cap moved on one spine fails here.
|
|
41
|
+
*/
|
|
42
|
+
const ROLE_NAME_LENGTH_MAX = 64;
|
|
43
|
+
/** All lowercase letters, so only its length is off the grammar. */
|
|
44
|
+
const OVER_LENGTH_ROLE = 'a'.repeat(ROLE_NAME_LENGTH_MAX + 1);
|
|
45
|
+
/** On the grammar at exactly the length cap, held by no registry. */
|
|
46
|
+
const AT_LENGTH_ROLE = 'a'.repeat(ROLE_NAME_LENGTH_MAX);
|
|
47
|
+
/** Each breaks one rule of the `RoleName` grammar. */
|
|
48
|
+
const OFF_GRAMMAR_ROLES = [
|
|
49
|
+
'Admin', // uppercase
|
|
50
|
+
'role-name', // hyphen
|
|
51
|
+
'role1', // digit
|
|
52
|
+
'_role', // leading underscore
|
|
53
|
+
'role_', // trailing underscore
|
|
54
|
+
'role name', // space
|
|
55
|
+
'', // empty
|
|
56
|
+
OVER_LENGTH_ROLE // on the alphabet, one past the length cap
|
|
57
|
+
];
|
|
58
|
+
/** On the grammar, held by no registry. */
|
|
59
|
+
const UNREGISTERED_ROLE = 'not_a_registered_role';
|
|
60
|
+
const GRANT_METHODS = ['role_grant_assign', 'role_grant_offer_create'];
|
|
61
|
+
/** The audit event types the grant verbs write. */
|
|
62
|
+
const GRANT_EVENT_TYPES = ['role_grant_create', 'role_grant_offer_create'];
|
|
63
|
+
export const describe_role_grant_role_grammar_cross_tests = (options) => {
|
|
64
|
+
const { setup_test } = options;
|
|
65
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
66
|
+
describe('grant verbs refuse an off-grammar role at validation', { timeout: 30_000 }, () => {
|
|
67
|
+
for (const method of GRANT_METHODS) {
|
|
68
|
+
test(`${method}: an off-grammar role is invalid_params with no reason`, async () => {
|
|
69
|
+
const fixture = await setup_test();
|
|
70
|
+
const t = fixture.fresh_transport();
|
|
71
|
+
for (const role of OFF_GRAMMAR_ROLES) {
|
|
72
|
+
const res = await cross_rpc_call(t, rpc_path, method, { to_account_id: NIL_UUID, role }, fixture.create_session_headers());
|
|
73
|
+
const label = `${method} role=${JSON.stringify(role)}`;
|
|
74
|
+
assert.strictEqual(res.ok, false, `${label}: expected a refusal`);
|
|
75
|
+
assert.strictEqual(res.error?.code, JSONRPC_ERROR_CODES.invalid_params, label);
|
|
76
|
+
// a validation refusal, not a grant-path or authorizer one
|
|
77
|
+
assert.strictEqual(error_reason(res), undefined, `${label}: no data.reason`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
test('an off-grammar role writes no audit row; an unregistered on-grammar one is refused past validation', async () => {
|
|
82
|
+
const fixture = await setup_test();
|
|
83
|
+
const t = fixture.fresh_transport();
|
|
84
|
+
const session = fixture.create_session_headers();
|
|
85
|
+
for (const method of GRANT_METHODS) {
|
|
86
|
+
for (const role of OFF_GRAMMAR_ROLES) {
|
|
87
|
+
const res = await cross_rpc_call(t, rpc_path, method, { to_account_id: NIL_UUID, role }, session);
|
|
88
|
+
assert.strictEqual(res.error?.code, JSONRPC_ERROR_CODES.invalid_params, `${method} ${JSON.stringify(role)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// control: the same call with an on-grammar role reaches the grant-path gate
|
|
92
|
+
const control = await cross_rpc_call(t, rpc_path, 'role_grant_assign', { to_account_id: NIL_UUID, role: UNREGISTERED_ROLE }, session);
|
|
93
|
+
assert.strictEqual(error_reason(control), ERROR_ROLE_NOT_WEB_GRANTABLE);
|
|
94
|
+
// and the length cap is inclusive: a role exactly at it is on the grammar
|
|
95
|
+
const at_length = await cross_rpc_call(t, rpc_path, 'role_grant_assign', { to_account_id: NIL_UUID, role: AT_LENGTH_ROLE }, session);
|
|
96
|
+
assert.strictEqual(error_reason(at_length), ERROR_ROLE_NOT_WEB_GRANTABLE, 'a role at the length cap passes validation');
|
|
97
|
+
// a barrier before reading the log: both spines write audit rows off
|
|
98
|
+
// the request path
|
|
99
|
+
const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
|
|
100
|
+
assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
|
|
101
|
+
for (const event_type of GRANT_EVENT_TYPES) {
|
|
102
|
+
const listed = await cross_rpc_call(t, rpc_path, 'audit_log_list', { event_type }, session);
|
|
103
|
+
assert.ok(listed.ok, `audit_log_list failed: ${JSON.stringify(listed.error)}`);
|
|
104
|
+
const events = listed.result.events;
|
|
105
|
+
const roles = events.map((event) => event.metadata?.role);
|
|
106
|
+
for (const role of roles) {
|
|
107
|
+
assert.ok(typeof role !== 'string' || !OFF_GRAMMAR_ROLES.includes(role), `${event_type}: an audit row records the off-grammar role ${JSON.stringify(role)}`);
|
|
108
|
+
}
|
|
109
|
+
if (event_type === 'role_grant_create') {
|
|
110
|
+
// the controls' failure rows are in the same read, so it is not vacuous
|
|
111
|
+
assert.ok(roles.includes(UNREGISTERED_ROLE), 'the control refusal wrote its failure row');
|
|
112
|
+
assert.ok(roles.includes(AT_LENGTH_ROLE), 'the at-cap refusal wrote its failure row');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
};
|
|
@@ -5,9 +5,8 @@ export declare const RUST_SPINE_STUB_BIN_ENV = "FUZ_TESTING_RUST_SPINE_STUB_BIN"
|
|
|
5
5
|
/**
|
|
6
6
|
* Env var overriding the stub's Postgres URL when `database_url` is omitted —
|
|
7
7
|
* e.g. a scratch cluster on a spare port, keeping the run off the machine's
|
|
8
|
-
* default server.
|
|
9
|
-
*
|
|
10
|
-
* names.
|
|
8
|
+
* default server. A runner that creates the stub's database before the spawn
|
|
9
|
+
* must create the one this URL names, on the server it names.
|
|
11
10
|
*/
|
|
12
11
|
export declare const RUST_SPINE_STUB_DATABASE_URL_ENV = "FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL";
|
|
13
12
|
/**
|
|
@@ -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;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE
|
|
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;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAqBnG,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;;;OAGG;IACH,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;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAmDF,CAAC"}
|
|
@@ -45,9 +45,8 @@ export const RUST_SPINE_STUB_BIN_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_BIN';
|
|
|
45
45
|
/**
|
|
46
46
|
* Env var overriding the stub's Postgres URL when `database_url` is omitted —
|
|
47
47
|
* e.g. a scratch cluster on a spare port, keeping the run off the machine's
|
|
48
|
-
* default server.
|
|
49
|
-
*
|
|
50
|
-
* names.
|
|
48
|
+
* default server. A runner that creates the stub's database before the spawn
|
|
49
|
+
* must create the one this URL names, on the server it names.
|
|
51
50
|
*/
|
|
52
51
|
export const RUST_SPINE_STUB_DATABASE_URL_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL';
|
|
53
52
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AA+ExE,kEAAkE;AAClE,MAAM,WAAW,+BAAgC,SAAQ,wBAAwB;IAChF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE;QACb,2EAA2E;QAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,sDAAsD;QACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACF;AAED,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAyEF,CAAC"}
|
|
@@ -4,10 +4,11 @@ import '../assert_dev_env.js';
|
|
|
4
4
|
* actions.
|
|
5
5
|
*
|
|
6
6
|
* `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
|
|
7
|
-
* `
|
|
8
|
-
* `_testing_action_manifest` are privileged
|
|
9
|
-
* wire never exposes —
|
|
10
|
-
* row, raw fact insert)
|
|
7
|
+
* `_testing_flood_notifications` / `_testing_schema_snapshot` /
|
|
8
|
+
* `_testing_migration_tracker` / `_testing_action_manifest` are privileged
|
|
9
|
+
* test-binary actions the production wire never exposes — direct DB writes
|
|
10
|
+
* (full auth wipe, forged session row, raw fact insert), a flood of the
|
|
11
|
+
* caller's own connection, and introspection reads (the live schema, the
|
|
11
12
|
* `schema_version` migration tracker, and the live RPC registry — the highest
|
|
12
13
|
* info-leak of the set were the gate to break). Their only structural fence
|
|
13
14
|
* is the **daemon-token** credential gate on
|
|
@@ -76,8 +77,13 @@ const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
|
76
77
|
*/
|
|
77
78
|
const backdoor_methods = [
|
|
78
79
|
{ method: '_testing_reset', params: {} },
|
|
80
|
+
// The audit barrier — its input is void, so the call carries no params.
|
|
81
|
+
{ method: '_testing_drain_effects', params: undefined },
|
|
79
82
|
{ method: '_testing_mint_session', params: { account_id: NIL_UUID, expires_in_seconds: -60 } },
|
|
80
83
|
{ method: '_testing_put_fact', params: { content: 'backdoor-probe' } },
|
|
84
|
+
// An empty burst — valid input, so it clears the 400 phase; a refused
|
|
85
|
+
// caller never reaches the handler that would queue it.
|
|
86
|
+
{ method: '_testing_flood_notifications', params: { count: 0, size: 0 } },
|
|
81
87
|
// The schema-dump read — `exclude_tables` is optional, so `{}` is valid
|
|
82
88
|
// and clears the 400 phase like the writes above.
|
|
83
89
|
{ method: '_testing_schema_snapshot', params: {} },
|