@fuzdev/fuz_app 0.128.0 → 0.130.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 +23 -6
- package/dist/actions/connection_closer.d.ts +3 -1
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/register_action_ws.d.ts +46 -1
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +105 -14
- package/dist/actions/transports.d.ts +2 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +2 -1
- package/dist/actions/transports_ws_backend.d.ts +37 -4
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +68 -4
- package/dist/auth/CLAUDE.md +1 -1
- package/dist/auth/actor_lookup_action_specs.d.ts +2 -2
- package/dist/auth/actor_lookup_action_specs.js +2 -2
- package/dist/auth/audit_emitter.d.ts +14 -7
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +6 -8
- package/dist/auth/audit_log_routes.d.ts +7 -3
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +48 -26
- package/dist/auth/bearer_auth.d.ts +3 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +3 -2
- package/dist/auth/request_context.d.ts +1 -1
- package/dist/auth/request_context.js +1 -1
- package/dist/auth/token_scope.d.ts +4 -2
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +10 -2
- package/dist/http/CLAUDE.md +4 -5
- package/dist/http/auth_shape.d.ts +3 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +3 -2
- package/dist/http/common_routes.d.ts +1 -14
- package/dist/http/common_routes.d.ts.map +1 -1
- package/dist/http/common_routes.js +1 -19
- package/dist/http/ip_canonical.d.ts +1 -1
- package/dist/http/ip_canonical.js +1 -1
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +7 -5
- package/dist/realtime/sse.d.ts +14 -1
- package/dist/realtime/sse.d.ts.map +1 -1
- package/dist/realtime/sse.js +77 -1
- package/dist/realtime/sse_auth_guard.d.ts +3 -1
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +4 -2
- package/dist/realtime/subscriber_registry.d.ts +44 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +64 -10
- package/dist/server/app_server.d.ts +122 -8
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +136 -91
- package/dist/server/surface_route.d.ts +32 -0
- package/dist/server/surface_route.d.ts.map +1 -0
- package/dist/server/surface_route.js +46 -0
- package/dist/testing/CLAUDE.md +33 -12
- package/dist/testing/app_server.d.ts +39 -4
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +84 -68
- package/dist/testing/audit_completeness.js +1 -1
- package/dist/testing/audit_drift_guard.d.ts +1 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts +2 -2
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +5 -2
- package/dist/testing/cross_backend/identity_parity.js +1 -1
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.js +4 -1
- package/dist/testing/cross_backend/method_coverage.d.ts +1 -1
- package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -1
- package/dist/testing/cross_backend/method_coverage.js +2 -2
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +4 -4
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.d.ts +4 -4
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/sse_round_trip.d.ts +1 -1
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +24 -14
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +40 -32
- package/dist/testing/cross_backend/testing_server_bun.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_bun.js +80 -64
- package/dist/testing/cross_backend/testing_server_core.d.ts +10 -4
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_deno.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_deno.js +1 -1
- package/dist/testing/cross_backend/testing_server_node.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_node.js +2 -2
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +20 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.js +177 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +1 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +11 -9
- package/dist/testing/stubs.d.ts +45 -7
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +60 -31
- package/dist/testing/ws_round_trip.d.ts +1 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/ui/AdminSurface.svelte +3 -1
- package/dist/ui/AdminSurface.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +3 -1
- package/dist/ui/async_slot.svelte.d.ts +4 -4
- package/dist/ui/async_slot.svelte.js +4 -4
- package/dist/ui/keyed_async_slot.svelte.d.ts +4 -4
- package/dist/ui/keyed_async_slot.svelte.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-process suite for the two refusals a WebSocket request can meet ahead
|
|
4
|
+
* of dispatch — the same on both spines, pinned over a real upgrade:
|
|
5
|
+
*
|
|
6
|
+
* - a request whose id names a request still running on the socket is
|
|
7
|
+
* answered `invalid_request` with `data.reason: 'duplicate_request_id'` and
|
|
8
|
+
* not dispatched; the running request keeps the id and answers as usual, and
|
|
9
|
+
* once it has answered the id is free again
|
|
10
|
+
* - past the per-socket in-flight ceiling (128 on both spines) a new request is
|
|
11
|
+
* answered `queue_overflow` with its id and not dispatched; the socket stays
|
|
12
|
+
* open, the running requests still answer, and the duplicate check comes
|
|
13
|
+
* first
|
|
14
|
+
*
|
|
15
|
+
* A request has to stay in flight for either to be observable, so the suite
|
|
16
|
+
* holds requests with `peer/ping`: the server's handler pings the client back
|
|
17
|
+
* and waits for the echo, and the client's responder withholds it until the
|
|
18
|
+
* test releases it. Gated on `capabilities.peer_request` for that reason.
|
|
19
|
+
*
|
|
20
|
+
* The wire literals are deliberate independent copies of the producers'
|
|
21
|
+
* constants (`ERROR_DUPLICATE_REQUEST_ID`,
|
|
22
|
+
* `MAX_INBOUND_DISPATCHES_PER_CONNECTION`, `WS_INBOUND_DISPATCH_OVERFLOW_MESSAGE`
|
|
23
|
+
* on TS; their Rust twins in `fuz_realtime`) — parity by test, so a value
|
|
24
|
+
* renamed on one side fails here instead of moving both at once.
|
|
25
|
+
*
|
|
26
|
+
* Cross-process only: `create_ws_transport` needs a real bound socket, so wire
|
|
27
|
+
* it from a `*.cross.test.ts` file.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { assert, describe } from 'vitest';
|
|
32
|
+
import { create_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
|
|
33
|
+
import { create_ws_transport } from '../transports/ws_transport.js';
|
|
34
|
+
import { is_response_for } from '../transports/ws_client.js';
|
|
35
|
+
import { test_if } from './capabilities.js';
|
|
36
|
+
/** `data.reason` of the duplicate-id refusal. */
|
|
37
|
+
const REASON_DUPLICATE_REQUEST_ID = 'duplicate_request_id';
|
|
38
|
+
/** The per-socket in-flight ceiling on both spines. */
|
|
39
|
+
const MAX_INBOUND_DISPATCHES = 128;
|
|
40
|
+
/** The `queue_overflow` message past the ceiling. */
|
|
41
|
+
const OVERFLOW_MESSAGE = 'too many concurrent requests on this connection';
|
|
42
|
+
const INVALID_REQUEST = -32600;
|
|
43
|
+
const QUEUE_OVERFLOW = -32009;
|
|
44
|
+
const PEER_PING_METHOD = 'peer/ping';
|
|
45
|
+
const HEARTBEAT_METHOD = 'heartbeat';
|
|
46
|
+
/** How long the test waits on a frame — above the server's peer-request deadline. */
|
|
47
|
+
const FRAME_TIMEOUT_MS = 12_000;
|
|
48
|
+
/**
|
|
49
|
+
* A client responder that withholds every server-initiated `peer/ping` echo
|
|
50
|
+
* until `release` — so each client `peer/ping` stays in flight on the server —
|
|
51
|
+
* and answers at once after it.
|
|
52
|
+
*/
|
|
53
|
+
const create_holding_responder = () => {
|
|
54
|
+
let release;
|
|
55
|
+
const released = new Promise((resolve) => {
|
|
56
|
+
release = resolve;
|
|
57
|
+
});
|
|
58
|
+
let ping_count = 0;
|
|
59
|
+
const ping_waiters = [];
|
|
60
|
+
const responder = async (request) => {
|
|
61
|
+
ping_count++;
|
|
62
|
+
for (let i = ping_waiters.length - 1; i >= 0; i--) {
|
|
63
|
+
const waiter = ping_waiters[i];
|
|
64
|
+
if (ping_count >= waiter.count) {
|
|
65
|
+
waiter.resolve();
|
|
66
|
+
ping_waiters.splice(i, 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await released;
|
|
70
|
+
const nonce = request.params?.nonce ?? 0;
|
|
71
|
+
return { result: { nonce, protocol_version: 1 } };
|
|
72
|
+
};
|
|
73
|
+
const pinged = (count) => ping_count >= count
|
|
74
|
+
? Promise.resolve()
|
|
75
|
+
: new Promise((resolve) => ping_waiters.push({ count, resolve }));
|
|
76
|
+
return { responder, release, pinged };
|
|
77
|
+
};
|
|
78
|
+
/** A response for `id` that carries a `peer/ping` result (`nonce` present). */
|
|
79
|
+
const is_ping_result_for = (id) => (msg) => is_response_for(id)(msg) &&
|
|
80
|
+
typeof msg.result === 'object' &&
|
|
81
|
+
msg.result !== null &&
|
|
82
|
+
'nonce' in msg.result;
|
|
83
|
+
/** A response for `id` that carries a `heartbeat` result (an object without `nonce`). */
|
|
84
|
+
const is_heartbeat_result_for = (id) => (msg) => is_response_for(id)(msg) &&
|
|
85
|
+
typeof msg.result === 'object' &&
|
|
86
|
+
msg.result !== null &&
|
|
87
|
+
!('nonce' in msg.result);
|
|
88
|
+
/** An error response for `id`. */
|
|
89
|
+
const is_error_for = (id) => (msg) => is_response_for(id)(msg) && 'error' in msg;
|
|
90
|
+
/**
|
|
91
|
+
* Register the duplicate-id and in-flight-ceiling cases. Gated on
|
|
92
|
+
* `capabilities.peer_request` (the cases hold requests with `peer/ping`).
|
|
93
|
+
*/
|
|
94
|
+
export const describe_ws_inbound_dispatch_cross_tests = (options) => {
|
|
95
|
+
const { setup_test, capabilities, base_url, ws_path } = options;
|
|
96
|
+
const open_ws = (cookie, on_request) => {
|
|
97
|
+
assert.ok(cookie, 'expected a session cookie for the WS upgrade');
|
|
98
|
+
return create_ws_transport({
|
|
99
|
+
base_url,
|
|
100
|
+
ws_path,
|
|
101
|
+
cookies: [cookie],
|
|
102
|
+
origin: base_url,
|
|
103
|
+
on_request,
|
|
104
|
+
default_timeout_ms: FRAME_TIMEOUT_MS
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
const send_ping = (ws, id, nonce) => ws.send(create_jsonrpc_request(PEER_PING_METHOD, { nonce }, id));
|
|
108
|
+
const send_heartbeat = (ws, id) => ws.send(create_jsonrpc_request(HEARTBEAT_METHOD, undefined, id));
|
|
109
|
+
const assert_duplicate_refusal = (frame, id) => {
|
|
110
|
+
assert.ok('error' in frame, `expected the duplicate to be refused: ${JSON.stringify(frame)}`);
|
|
111
|
+
assert.strictEqual(frame.id, id);
|
|
112
|
+
assert.strictEqual(frame.error.code, INVALID_REQUEST);
|
|
113
|
+
assert.strictEqual(frame.error.message, 'invalid request');
|
|
114
|
+
assert.deepStrictEqual(frame.error.data, { reason: REASON_DUPLICATE_REQUEST_ID });
|
|
115
|
+
};
|
|
116
|
+
describe('ws inbound dispatch refusals (cross-process)', () => {
|
|
117
|
+
test_if(capabilities.peer_request, 'a request reusing a live id is refused with that id; the first keeps it, then the id frees', async () => {
|
|
118
|
+
const fixture = await setup_test();
|
|
119
|
+
const { responder, release, pinged } = create_holding_responder();
|
|
120
|
+
const ws = await open_ws(fixture.create_session_headers().cookie, responder);
|
|
121
|
+
try {
|
|
122
|
+
const id = 'held';
|
|
123
|
+
await send_ping(ws, id, 41);
|
|
124
|
+
// the server pinged back: the first request is dispatched and waiting
|
|
125
|
+
await pinged(1);
|
|
126
|
+
await send_heartbeat(ws, id);
|
|
127
|
+
assert_duplicate_refusal(await ws.wait_for(is_error_for(id)), id);
|
|
128
|
+
// the string '7' and the number 7 are different ids on both spines
|
|
129
|
+
await send_ping(ws, 7, 7);
|
|
130
|
+
await pinged(2);
|
|
131
|
+
await send_heartbeat(ws, '7');
|
|
132
|
+
await ws.wait_for(is_heartbeat_result_for('7'));
|
|
133
|
+
release();
|
|
134
|
+
const first = await ws.wait_for(is_ping_result_for(id));
|
|
135
|
+
assert.strictEqual(first.result.nonce, 41, 'the first request answers under its id');
|
|
136
|
+
await ws.wait_for(is_ping_result_for(7));
|
|
137
|
+
// answered, the id is free again
|
|
138
|
+
await send_heartbeat(ws, id);
|
|
139
|
+
await ws.wait_for(is_heartbeat_result_for(id));
|
|
140
|
+
const refusals = ws.messages.filter(is_error_for(id));
|
|
141
|
+
assert.strictEqual(refusals.length, 1, 'only the duplicate was refused');
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
await ws.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
test_if(capabilities.peer_request, 'past the in-flight ceiling a request is shed with queue_overflow and the socket keeps serving', async () => {
|
|
148
|
+
const fixture = await setup_test();
|
|
149
|
+
const { responder, release } = create_holding_responder();
|
|
150
|
+
const ws = await open_ws(fixture.create_session_headers().cookie, responder);
|
|
151
|
+
try {
|
|
152
|
+
for (let n = 0; n < MAX_INBOUND_DISPATCHES; n++) {
|
|
153
|
+
await send_ping(ws, n, n);
|
|
154
|
+
}
|
|
155
|
+
// a duplicate at the ceiling is refused as a duplicate — that check is first
|
|
156
|
+
await send_heartbeat(ws, 0);
|
|
157
|
+
await send_heartbeat(ws, 'over');
|
|
158
|
+
assert_duplicate_refusal(await ws.wait_for(is_error_for(0)), 0);
|
|
159
|
+
const over = await ws.wait_for(is_response_for('over'));
|
|
160
|
+
assert.ok('error' in over, `expected the request to be shed: ${JSON.stringify(over)}`);
|
|
161
|
+
assert.strictEqual(over.error.code, QUEUE_OVERFLOW);
|
|
162
|
+
assert.strictEqual(over.error.message, OVERFLOW_MESSAGE);
|
|
163
|
+
assert.strictEqual(ws.close_code, null, 'shedding never closes the socket');
|
|
164
|
+
release();
|
|
165
|
+
for (let n = 0; n < MAX_INBOUND_DISPATCHES; n++) {
|
|
166
|
+
const frame = await ws.wait_for(is_ping_result_for(n));
|
|
167
|
+
assert.strictEqual(frame.result.nonce, n);
|
|
168
|
+
}
|
|
169
|
+
await send_heartbeat(ws, 'after');
|
|
170
|
+
await ws.wait_for(is_heartbeat_result_for('after'));
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await ws.close();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
2
|
import { type BackendCapabilities } from './capabilities.js';
|
|
3
3
|
import type { SetupTest } from './setup.js';
|
|
4
|
-
/** Configuration for
|
|
4
|
+
/** Configuration for `describe_cross_process_ws_tests`. */
|
|
5
5
|
export interface CrossProcessWsTestOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Per-test fixture producer (`default_cross_process_setup(handle)`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0G9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,2DAA2D;AAC3D,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IAkVpF,CAAC"}
|
|
@@ -19,14 +19,15 @@ import '../assert_dev_env.js';
|
|
|
19
19
|
* every WS endpoint — so the suite needs no knowledge of a consumer's domain
|
|
20
20
|
* WS methods. It validates the transport, not the domain.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* disallowed-origin upgrade
|
|
26
|
-
* upgrade time (not per message), so the
|
|
27
|
-
* itself rejects rather than a per-message
|
|
22
|
+
* Three cases mirror the upgrade stack `register_ws_endpoint` wires (origin
|
|
23
|
+
* check → `require_auth` → dispatch): an authenticated upgrade round-trips
|
|
24
|
+
* `heartbeat` (the first case), and an anonymous upgrade and a
|
|
25
|
+
* disallowed-origin upgrade are refused (after the spine-parity cases).
|
|
26
|
+
* Per-connection auth is enforced at upgrade time (not per message), so the
|
|
27
|
+
* negative cases assert the upgrade itself rejects rather than a per-message
|
|
28
|
+
* error frame.
|
|
28
29
|
*
|
|
29
|
-
* Spine-parity cases follow. Both spines admit a connection only after
|
|
30
|
+
* Spine-parity cases follow the first. Both spines admit a connection only after
|
|
30
31
|
* re-reading its credential, and requests that arrive first wait for that
|
|
31
32
|
* rather than being dropped: requests sent the moment the socket opens are all
|
|
32
33
|
* answered, and — gated on `capabilities.ws_handshake_pipelining`, with the
|
|
@@ -38,7 +39,8 @@ import '../assert_dev_env.js';
|
|
|
38
39
|
* `DEFAULT_WS_MAX_MESSAGE_BYTES`) closes the socket with
|
|
39
40
|
* `WS_CLOSE_MESSAGE_TOO_BIG`.
|
|
40
41
|
*
|
|
41
|
-
* The
|
|
42
|
+
* The cases after the refused upgrades (gated on `rpc_path`) cover
|
|
43
|
+
* server-initiated close.
|
|
42
44
|
* Per-message dispatch never re-checks credential validity, so an open socket
|
|
43
45
|
* ends only when a revocation closes it — which every revocation does, once
|
|
44
46
|
* its transaction commits. An authenticated socket is dropped when the
|
|
@@ -48,7 +50,7 @@ import '../assert_dev_env.js';
|
|
|
48
50
|
* `rpc_path` to skip them (consumers without the standard account and admin
|
|
49
51
|
* actions on their RPC endpoint).
|
|
50
52
|
*
|
|
51
|
-
*
|
|
53
|
+
* The last cases are gated on `capabilities.ws_account_actions` — the backend's
|
|
52
54
|
* WS endpoint mounts the self-service account actions: a socket that revokes
|
|
53
55
|
* its own sessions *over that socket* is closed with
|
|
54
56
|
* `WS_CLOSE_SESSION_REVOKED`, and — on a backend declaring
|
package/dist/testing/stubs.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { AppDeps } from '../auth/deps.js';
|
|
|
6
6
|
import type { AuditEmitter } from '../auth/audit_emitter.js';
|
|
7
7
|
import type { BootstrapServerOptions } from '../server/app_server.js';
|
|
8
8
|
import type { AppServerContext } from '../server/app_server_context.js';
|
|
9
|
+
import type { AppBackend } from '../server/app_backend.js';
|
|
9
10
|
import { Db } from '../db/db.js';
|
|
10
11
|
import { type RouteSpec } from '../http/route_spec.js';
|
|
11
12
|
import { type AppSurfaceSpec, type RpcEndpointSpec } from '../http/surface.js';
|
|
@@ -79,16 +80,33 @@ export declare const create_test_audit_emitter: () => AuditEmitter;
|
|
|
79
80
|
* `create_audit_log_sse` against `create_test_app`.
|
|
80
81
|
*/
|
|
81
82
|
export declare const create_stub_audit_sse: () => AuditLogSse;
|
|
82
|
-
/** Stub `AppDeps` for auth surface tests — throws on any method access. */
|
|
83
|
-
export declare const stub_app_deps: AppDeps;
|
|
84
83
|
/**
|
|
85
84
|
* Create no-op `AppDeps` for auth surface testing.
|
|
86
85
|
*/
|
|
87
86
|
export declare const create_stub_app_deps: () => AppDeps;
|
|
87
|
+
/** Options for `create_stub_app_backend`. */
|
|
88
|
+
export interface CreateStubAppBackendOptions {
|
|
89
|
+
/** The backend's deps. Default a fresh `create_stub_app_deps()`. */
|
|
90
|
+
deps?: AppDeps;
|
|
91
|
+
/** The backend's `close`. Default one that does nothing. */
|
|
92
|
+
close?: () => Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a stub `AppBackend` with no database behind it — no migration
|
|
96
|
+
* results, and a `close` that does nothing unless one is passed. For
|
|
97
|
+
* assembling a server (`create_app_server`) in tests that never reach the
|
|
98
|
+
* database; one whose `close` does nothing can host server after server.
|
|
99
|
+
*
|
|
100
|
+
* @param options - the deps and `close` to use in place of the defaults
|
|
101
|
+
*/
|
|
102
|
+
export declare const create_stub_app_backend: (options?: CreateStubAppBackendOptions) => AppBackend;
|
|
88
103
|
/**
|
|
89
104
|
* Create the API middleware stub array matching `create_auth_middleware_specs`
|
|
90
|
-
* output
|
|
91
|
-
*
|
|
105
|
+
* output — the same names, paths, order, and declared `errors` (the surface
|
|
106
|
+
* merges middleware errors into every route under the path, so a stub that
|
|
107
|
+
* declared more than the real stack would widen the test surface). Like the
|
|
108
|
+
* real factory's, each spec gets its own pass-through handler, so
|
|
109
|
+
* `assert_middleware_stack_mounted` can tell the layers apart by identity.
|
|
92
110
|
*/
|
|
93
111
|
export declare const create_stub_api_middleware: (options?: {
|
|
94
112
|
/** Include the daemon_token middleware layer. */
|
|
@@ -161,6 +179,23 @@ export interface CreateTestAppSurfaceSpecOptions {
|
|
|
161
179
|
* Surface assembly only reads `route_prefix` (default `'/api/account'`).
|
|
162
180
|
*/
|
|
163
181
|
bootstrap?: BootstrapServerOptions;
|
|
182
|
+
/**
|
|
183
|
+
* Symmetric with `AppServerOptions.surface_route` — `true` lists the
|
|
184
|
+
* admin-only `GET /api/surface`, which `create_app_server` mounts only when
|
|
185
|
+
* opted in. Pass the same value to both entry points so the surface tests
|
|
186
|
+
* probe the route exactly when the live server serves it.
|
|
187
|
+
*
|
|
188
|
+
* @default false
|
|
189
|
+
*/
|
|
190
|
+
surface_route?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* List the `daemon_token` middleware layer, as `create_app_server` mounts
|
|
193
|
+
* it when given a `daemon_token_state`. Pass `true` exactly when the live
|
|
194
|
+
* server gets one — `create_test_app` always passes one.
|
|
195
|
+
*
|
|
196
|
+
* @default false
|
|
197
|
+
*/
|
|
198
|
+
daemon_token?: boolean;
|
|
164
199
|
}
|
|
165
200
|
/**
|
|
166
201
|
* Create an `AppSurfaceSpec` for the standard testing suites.
|
|
@@ -172,13 +207,16 @@ export interface CreateTestAppSurfaceSpecOptions {
|
|
|
172
207
|
* suites consume the spec object this function returns, not the JSON
|
|
173
208
|
* file.
|
|
174
209
|
*
|
|
175
|
-
* Mirrors `create_app_server`'s route assembly: consumer
|
|
176
|
-
* factory-managed bootstrap routes
|
|
210
|
+
* Mirrors `create_app_server`'s route assembly, in its order: consumer
|
|
211
|
+
* routes, then the factory-managed bootstrap routes, RPC endpoint routes, and
|
|
212
|
+
* `GET /api/surface` (with `surface_route: true`), then surface
|
|
213
|
+
* generation — including its
|
|
177
214
|
* refusal of an `rpc_endpoints` or `ws_endpoints` path outside the auth
|
|
178
215
|
* middleware's scope (`assert_endpoint_in_auth_scope`) and of a
|
|
179
216
|
* `transform_middleware` that doesn't keep the proxy and auth stack mounted
|
|
180
217
|
* (`assert_middleware_stack_mounted`). The middleware list mirrors the real
|
|
181
|
-
* one: a stub `trusted_proxy` spec at `'*'`, then the stub auth stack
|
|
218
|
+
* one: a stub `trusted_proxy` spec at `'*'`, then the stub auth stack (with
|
|
219
|
+
* `daemon_token` when `options.daemon_token` is set). If
|
|
182
220
|
* `create_app_server` changes how it wires routes, update this helper
|
|
183
221
|
* to stay in sync (single source of truth for all consumers).
|
|
184
222
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAa7B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
|
package/dist/testing/stubs.js
CHANGED
|
@@ -8,12 +8,13 @@ import './assert_dev_env.js';
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
11
|
-
import { ApiError
|
|
11
|
+
import { ApiError } from '../http/error_schemas.js';
|
|
12
12
|
import { create_realtime_closer } from '../actions/connection_closer.js';
|
|
13
13
|
import { Db } from '../db/db.js';
|
|
14
14
|
import { prefix_route_specs } from '../http/route_spec.js';
|
|
15
15
|
import { bootstrap_route_shape } from '../auth/bootstrap_route_schema.js';
|
|
16
16
|
import { create_rpc_endpoint } from '../actions/action_rpc.js';
|
|
17
|
+
import { create_surface_route_spec } from '../server/surface_route.js';
|
|
17
18
|
import { create_app_surface_spec } from '../http/surface.js';
|
|
18
19
|
import { resolve_ws_endpoints } from '../actions/ws_endpoint_spec.js';
|
|
19
20
|
import { AUDIT_LOG_SSE_MAX_PER_SCOPE } from '../realtime/sse_auth_guard.js';
|
|
@@ -135,17 +136,6 @@ export const create_stub_audit_sse = () => {
|
|
|
135
136
|
registry
|
|
136
137
|
};
|
|
137
138
|
};
|
|
138
|
-
/** Stub `AppDeps` for auth surface tests — throws on any method access. */
|
|
139
|
-
export const stub_app_deps = {
|
|
140
|
-
read_secure_file: create_throwing_stub('read_secure_file'),
|
|
141
|
-
delete_file: create_throwing_stub('delete_file'),
|
|
142
|
-
keyring: create_throwing_stub('keyring'),
|
|
143
|
-
password: create_throwing_stub('password'),
|
|
144
|
-
db: create_throwing_stub('db'),
|
|
145
|
-
log: create_throwing_stub('log'),
|
|
146
|
-
audit: create_test_audit_emitter(),
|
|
147
|
-
connection_closer: create_realtime_closer()
|
|
148
|
-
};
|
|
149
139
|
/**
|
|
150
140
|
* Create no-op `AppDeps` for auth surface testing.
|
|
151
141
|
*/
|
|
@@ -161,12 +151,30 @@ export const create_stub_app_deps = () => ({
|
|
|
161
151
|
audit: create_test_audit_emitter(),
|
|
162
152
|
connection_closer: create_realtime_closer()
|
|
163
153
|
});
|
|
154
|
+
/**
|
|
155
|
+
* Create a stub `AppBackend` with no database behind it — no migration
|
|
156
|
+
* results, and a `close` that does nothing unless one is passed. For
|
|
157
|
+
* assembling a server (`create_app_server`) in tests that never reach the
|
|
158
|
+
* database; one whose `close` does nothing can host server after server.
|
|
159
|
+
*
|
|
160
|
+
* @param options - the deps and `close` to use in place of the defaults
|
|
161
|
+
*/
|
|
162
|
+
export const create_stub_app_backend = (options) => ({
|
|
163
|
+
db_type: 'pglite-memory',
|
|
164
|
+
db_name: 'test',
|
|
165
|
+
migration_results: [],
|
|
166
|
+
close: options?.close ?? (async () => { }),
|
|
167
|
+
deps: options?.deps ?? create_stub_app_deps()
|
|
168
|
+
});
|
|
164
169
|
/** A fresh pass-through middleware — a distinct handler per call, unlike the shared `stub_mw`. */
|
|
165
170
|
const create_stub_mw = () => async (_c, next) => next();
|
|
166
171
|
/**
|
|
167
172
|
* Create the API middleware stub array matching `create_auth_middleware_specs`
|
|
168
|
-
* output
|
|
169
|
-
*
|
|
173
|
+
* output — the same names, paths, order, and declared `errors` (the surface
|
|
174
|
+
* merges middleware errors into every route under the path, so a stub that
|
|
175
|
+
* declared more than the real stack would widen the test surface). Like the
|
|
176
|
+
* real factory's, each spec gets its own pass-through handler, so
|
|
177
|
+
* `assert_middleware_stack_mounted` can tell the layers apart by identity.
|
|
170
178
|
*/
|
|
171
179
|
export const create_stub_api_middleware = (options) => {
|
|
172
180
|
const specs = [
|
|
@@ -182,7 +190,7 @@ export const create_stub_api_middleware = (options) => {
|
|
|
182
190
|
name: 'bearer_auth',
|
|
183
191
|
path: AUTH_MIDDLEWARE_PATH,
|
|
184
192
|
handler: create_stub_mw(),
|
|
185
|
-
errors: {
|
|
193
|
+
errors: {}
|
|
186
194
|
}
|
|
187
195
|
];
|
|
188
196
|
if (options?.include_daemon_token) {
|
|
@@ -190,7 +198,7 @@ export const create_stub_api_middleware = (options) => {
|
|
|
190
198
|
name: 'daemon_token',
|
|
191
199
|
path: AUTH_MIDDLEWARE_PATH,
|
|
192
200
|
handler: create_stub_mw(),
|
|
193
|
-
errors: {
|
|
201
|
+
errors: {}
|
|
194
202
|
});
|
|
195
203
|
}
|
|
196
204
|
return specs;
|
|
@@ -204,16 +212,10 @@ export const create_stub_api_middleware = (options) => {
|
|
|
204
212
|
* @param session_options - consumer's session config (required — varies per app)
|
|
205
213
|
*/
|
|
206
214
|
export const create_stub_app_server_context = (session_options) => {
|
|
207
|
-
const
|
|
215
|
+
const backend = create_stub_app_backend();
|
|
208
216
|
return {
|
|
209
|
-
deps,
|
|
210
|
-
backend
|
|
211
|
-
deps,
|
|
212
|
-
db_type: 'pglite-memory',
|
|
213
|
-
db_name: 'test',
|
|
214
|
-
migration_results: [],
|
|
215
|
-
close: async () => { }
|
|
216
|
-
},
|
|
217
|
+
deps: backend.deps,
|
|
218
|
+
backend,
|
|
217
219
|
bootstrap_status: { available: false, token_path: null },
|
|
218
220
|
session_options,
|
|
219
221
|
login_ip_rate_limiter: null,
|
|
@@ -251,13 +253,16 @@ const to_self_resolved_ws_endpoint = (endpoint) => {
|
|
|
251
253
|
* suites consume the spec object this function returns, not the JSON
|
|
252
254
|
* file.
|
|
253
255
|
*
|
|
254
|
-
* Mirrors `create_app_server`'s route assembly: consumer
|
|
255
|
-
* factory-managed bootstrap routes
|
|
256
|
+
* Mirrors `create_app_server`'s route assembly, in its order: consumer
|
|
257
|
+
* routes, then the factory-managed bootstrap routes, RPC endpoint routes, and
|
|
258
|
+
* `GET /api/surface` (with `surface_route: true`), then surface
|
|
259
|
+
* generation — including its
|
|
256
260
|
* refusal of an `rpc_endpoints` or `ws_endpoints` path outside the auth
|
|
257
261
|
* middleware's scope (`assert_endpoint_in_auth_scope`) and of a
|
|
258
262
|
* `transform_middleware` that doesn't keep the proxy and auth stack mounted
|
|
259
263
|
* (`assert_middleware_stack_mounted`). The middleware list mirrors the real
|
|
260
|
-
* one: a stub `trusted_proxy` spec at `'*'`, then the stub auth stack
|
|
264
|
+
* one: a stub `trusted_proxy` spec at `'*'`, then the stub auth stack (with
|
|
265
|
+
* `daemon_token` when `options.daemon_token` is set). If
|
|
261
266
|
* `create_app_server` changes how it wires routes, update this helper
|
|
262
267
|
* to stay in sync (single source of truth for all consumers).
|
|
263
268
|
*
|
|
@@ -308,11 +313,33 @@ export const create_test_app_surface_spec = (options) => {
|
|
|
308
313
|
{ ...bootstrap_route_shape, handler: stub_handler }
|
|
309
314
|
])
|
|
310
315
|
: [];
|
|
311
|
-
|
|
316
|
+
// the surface route mirrors `create_app_server`'s opt-in mount — its handler
|
|
317
|
+
// serves the surface generated below, backfilled into the same ref
|
|
318
|
+
const surface_ref = {
|
|
319
|
+
surface: {
|
|
320
|
+
middleware: [],
|
|
321
|
+
routes: [],
|
|
322
|
+
rpc_endpoints: [],
|
|
323
|
+
ws_endpoints: [],
|
|
324
|
+
env: [],
|
|
325
|
+
events: [],
|
|
326
|
+
diagnostics: []
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const surface_route_specs = options.surface_route
|
|
330
|
+
? [create_surface_route_spec(surface_ref)]
|
|
331
|
+
: [];
|
|
332
|
+
// `create_app_server`'s order: consumer routes, then the factory routes
|
|
333
|
+
const route_specs = [
|
|
334
|
+
...consumer_routes,
|
|
335
|
+
...bootstrap_route_specs,
|
|
336
|
+
...rpc_route_specs,
|
|
337
|
+
...surface_route_specs
|
|
338
|
+
];
|
|
312
339
|
// mirrors `create_app_server`'s `[proxy_spec, ...auth_middleware]`
|
|
313
340
|
const stack_specs = [
|
|
314
341
|
{ name: 'trusted_proxy', path: '*', handler: create_stub_mw() },
|
|
315
|
-
...create_stub_api_middleware()
|
|
342
|
+
...create_stub_api_middleware({ include_daemon_token: options.daemon_token })
|
|
316
343
|
];
|
|
317
344
|
let middleware_specs = stack_specs;
|
|
318
345
|
if (options.transform_middleware) {
|
|
@@ -321,7 +348,7 @@ export const create_test_app_surface_spec = (options) => {
|
|
|
321
348
|
middleware_specs = options.transform_middleware(stack_specs.map((spec) => ({ ...spec })));
|
|
322
349
|
assert_middleware_stack_mounted(stack_specs, middleware_specs);
|
|
323
350
|
}
|
|
324
|
-
|
|
351
|
+
const surface_spec = create_app_surface_spec({
|
|
325
352
|
middleware_specs,
|
|
326
353
|
route_specs,
|
|
327
354
|
env_schema: options.env_schema ?? BaseServerEnv,
|
|
@@ -329,4 +356,6 @@ export const create_test_app_surface_spec = (options) => {
|
|
|
329
356
|
rpc_endpoints: resolved_rpc_endpoints,
|
|
330
357
|
ws_endpoints: resolved_ws_endpoints
|
|
331
358
|
});
|
|
359
|
+
surface_ref.surface = surface_spec.surface;
|
|
360
|
+
return surface_spec;
|
|
332
361
|
};
|
|
@@ -202,9 +202,7 @@ export interface WsTestHarness {
|
|
|
202
202
|
/**
|
|
203
203
|
* Open a mock connection. Resolves after `on_socket_open` (and the
|
|
204
204
|
* transport's `register_ws`) completes, so broadcasts issued
|
|
205
|
-
* immediately after the `await` reach the connection.
|
|
206
|
-
* revisions returned synchronously and required a `settle_open()`
|
|
207
|
-
* microtask drain — no longer necessary.
|
|
205
|
+
* immediately after the `await` reach the connection.
|
|
208
206
|
*
|
|
209
207
|
* Returns the shared `WsClient` interface — same surface the
|
|
210
208
|
* cross-process driver in `transports/ws_transport.ts` implements,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAQ,MAAM,MAAM,CAAC;AAC1C,OAAO,EACN,SAAS,EAET,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAEpG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,2BAA2B,CAAC;AAMnC;;;GAGG;AACH,MAAM,WAAW,MAAM;IACtB,EAAE,EAAE,SAAS,CAAC;IACd,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAO,MAajC,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACtC,eAAe,EAAE,cAAc,CAAC;IAChC,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,eAAe,CAAC,EAAE,cAAc,CAAC;CACjC;AAED;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,sBAAsB,KAAG,OAmBvE,CAAC;AAEF,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,CAAC,EAAE,OAAO,CAAC;IACX,8DAA8D;IAC9D,aAAa,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5D;AAED,mGAAmG;AACnG,MAAM,WAAW,WAAW;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtE;;;;;OAKG;IACH,QAAQ,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,QAAO,WAsBtC,CAAC;AAEF;;;;GAIG;AACH,qBAAa,wBAAyB,YAAW,sBAAsB;;IACtE,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAa;gBAEjC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC;IAGjD,qBAAqB,IAAI,SAAS;IAGlC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;CAG/D;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,YAAY,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,EAC9C,OAAO,YAAY,EACnB,IAAI,SAAS,KACX,OAAO,CAAC,IAAI,CAId,CAAC;AAMF,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IACjC,wEAAwE;IACxE,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,yFAAyF;IACzF,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sFAAsF;IACtF,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,4CAA4C;AAC5C,MAAM,WAAW,0BAA0B;IAC1C;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B,kEAAkE;IAClE,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;OAIG;IACH,SAAS,CAAC,EAAE,uBAAuB,CAAC,WAAW,CAAC,CAAC;IACjD,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,cAAc,CAAC,EAAE,uBAAuB,CAAC,gBAAgB,CAAC,CAAC;IAC3D,yDAAyD;IACzD,eAAe,CAAC,EAAE,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;IAC7D;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,yBAAyB,CAAC;IACrC;;;;;;;;OAQG;IACH,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC7D;AAiED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,0BAA0B,KAAG,aA+N5E,CAAC"}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Attack-surface viewer. Fetches `GET /api/surface` (REST — not RPC, since
|
|
4
4
|
* the surface dump exists outside the action surface) and delegates
|
|
5
5
|
* rendering to `SurfaceExplorer`. Surfaces a retry button on fetch
|
|
6
|
-
* failures.
|
|
6
|
+
* failures. The server must opt in to the route
|
|
7
|
+
* (`create_app_server({surface_route: true})`), which is admin-only and
|
|
8
|
+
* refused to a narrowed API token.
|
|
7
9
|
*
|
|
8
10
|
* @module
|
|
9
11
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminSurface.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminSurface.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminSurface.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminSurface.svelte"],"names":[],"mappings":"AAoEA,QAAA,MAAM,YAAY,2DAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
package/dist/ui/CLAUDE.md
CHANGED
|
@@ -326,7 +326,9 @@ destructive actions.
|
|
|
326
326
|
- `AdminSettings.svelte` — shell for `OpenSignupToggle` + the logged-in
|
|
327
327
|
account line + logout `ConfirmButton`. No direct RPC calls.
|
|
328
328
|
- `AdminSurface.svelte` — attack-surface viewer. Fetches
|
|
329
|
-
`/api/surface` (REST) and delegates to `SurfaceExplorer`.
|
|
329
|
+
`/api/surface` (REST) and delegates to `SurfaceExplorer`. The server must
|
|
330
|
+
opt in with `create_app_server({surface_route: true})`; the route is
|
|
331
|
+
admin-only and refused to a narrowed API token.
|
|
330
332
|
- `OpenSignupToggle.svelte` — single checkbox bound to
|
|
331
333
|
`AppSettingsState.settings.open_signup`. Consumes
|
|
332
334
|
`app_settings_rpc_context`.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* would otherwise need to compensate for.
|
|
29
29
|
* - **`AbortSignal` threaded to the callback** — RPC clients that accept
|
|
30
30
|
* a signal (or `fetch`) get cancellation for free; callers can also
|
|
31
|
-
* pass an external `signal` via
|
|
31
|
+
* pass an external `signal` via `RunOptions` to bind the slot's
|
|
32
32
|
* lifetime to a component / page.
|
|
33
33
|
* - **`preserve_error_on_retry`** — opt-in to keeping the previous error
|
|
34
34
|
* visible while a retry is pending (default clears at the start of
|
|
@@ -68,7 +68,7 @@ export interface AsyncSlotOptions<T, E = string> {
|
|
|
68
68
|
initial?: T;
|
|
69
69
|
/**
|
|
70
70
|
* Convert a caught throw into the error value stored in
|
|
71
|
-
*
|
|
71
|
+
* `AsyncSlot.error`. Default extracts `Error.message` (falling
|
|
72
72
|
* back to `'Request failed'` for non-Error throws). Pass
|
|
73
73
|
* `to_rpc_error_message` to unwrap JSON-RPC `data.reason` codes.
|
|
74
74
|
*/
|
|
@@ -87,7 +87,7 @@ export interface RunOptions {
|
|
|
87
87
|
/**
|
|
88
88
|
* External signal chained into the slot's internal controller. Aborts
|
|
89
89
|
* the in-flight run when fired (alongside automatic supersession by
|
|
90
|
-
* the next `run()` and manual
|
|
90
|
+
* the next `run()` and manual `AsyncSlot.abort` calls).
|
|
91
91
|
*/
|
|
92
92
|
signal?: AbortSignal;
|
|
93
93
|
}
|
|
@@ -96,7 +96,7 @@ export interface RunOptions {
|
|
|
96
96
|
*
|
|
97
97
|
* @typeParam T - The success payload type. Use `void` for write-only
|
|
98
98
|
* actions whose response isn't worth retaining.
|
|
99
|
-
* @typeParam E - The shape of
|
|
99
|
+
* @typeParam E - The shape of `AsyncSlot.error`. Defaults to
|
|
100
100
|
* `string` (set by the default `map_error`). Narrow to a structured
|
|
101
101
|
* type by providing a `map_error` that returns it.
|
|
102
102
|
*/
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* would otherwise need to compensate for.
|
|
29
29
|
* - **`AbortSignal` threaded to the callback** — RPC clients that accept
|
|
30
30
|
* a signal (or `fetch`) get cancellation for free; callers can also
|
|
31
|
-
* pass an external `signal` via
|
|
31
|
+
* pass an external `signal` via `RunOptions` to bind the slot's
|
|
32
32
|
* lifetime to a component / page.
|
|
33
33
|
* - **`preserve_error_on_retry`** — opt-in to keeping the previous error
|
|
34
34
|
* visible while a retry is pending (default clears at the start of
|
|
@@ -64,7 +64,7 @@ import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
|
64
64
|
*
|
|
65
65
|
* @typeParam T - The success payload type. Use `void` for write-only
|
|
66
66
|
* actions whose response isn't worth retaining.
|
|
67
|
-
* @typeParam E - The shape of
|
|
67
|
+
* @typeParam E - The shape of `AsyncSlot.error`. Defaults to
|
|
68
68
|
* `string` (set by the default `map_error`). Narrow to a structured
|
|
69
69
|
* type by providing a `map_error` that returns it.
|
|
70
70
|
*/
|
|
@@ -85,7 +85,7 @@ export class AsyncSlot {
|
|
|
85
85
|
#controller = null;
|
|
86
86
|
/**
|
|
87
87
|
* Tracks whether any `run()` or `set()` has ever produced a success
|
|
88
|
-
* result. Used by
|
|
88
|
+
* result. Used by `abort` to revert to `'success'` (vs `'initial'`)
|
|
89
89
|
* — explicit flag instead of inspecting `data` so the discriminator
|
|
90
90
|
* stays correct for `T = void` (where success-`data` is `undefined`)
|
|
91
91
|
* and for nullable `T`s where `null` is a legitimate success value.
|
|
@@ -183,7 +183,7 @@ export class AsyncSlot {
|
|
|
183
183
|
}
|
|
184
184
|
/**
|
|
185
185
|
* Abort the in-flight run (if any) and null out the controller field.
|
|
186
|
-
* Shared by
|
|
186
|
+
* Shared by `abort`, `set`, and `reset`.
|
|
187
187
|
*/
|
|
188
188
|
#clear_controller(reason) {
|
|
189
189
|
this.#controller?.abort(reason);
|