@fuzdev/fuz_app 0.144.0 → 0.146.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 +7 -6
- package/dist/auth/CLAUDE.md +5 -2
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +2 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +9 -2
- package/dist/auth/account_routes.d.ts +8 -0
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +12 -3
- package/dist/auth/request_context.d.ts +23 -0
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +32 -1
- package/dist/auth/token_scope.d.ts +22 -14
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +23 -14
- package/dist/db/CLAUDE.md +7 -3
- package/dist/db/fact_ddl.d.ts +1 -0
- package/dist/db/fact_ddl.d.ts.map +1 -1
- package/dist/db/fact_ddl.js +1 -0
- package/dist/db/fact_holder_queries.d.ts +8 -4
- package/dist/db/fact_holder_queries.d.ts.map +1 -1
- package/dist/db/fact_holder_queries.js +9 -5
- package/dist/db/fact_queries.d.ts +10 -7
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +31 -10
- package/dist/http/CLAUDE.md +2 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +11 -6
- package/dist/http/proxy.d.ts +22 -21
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +28 -33
- package/dist/http/route_spec.d.ts +8 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +1 -1
- package/dist/server/fact_security_headers.d.ts +59 -0
- package/dist/server/fact_security_headers.d.ts.map +1 -0
- package/dist/server/fact_security_headers.js +58 -0
- package/dist/server/serve_fact_route.d.ts +21 -10
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +41 -50
- package/dist/server/x_accel.d.ts +135 -31
- package/dist/server/x_accel.d.ts.map +1 -1
- package/dist/server/x_accel.js +652 -88
- package/dist/testing/CLAUDE.md +73 -22
- package/dist/testing/cross_backend/capabilities.d.ts +0 -18
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +1 -4
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +0 -7
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +85 -12
- package/dist/testing/cross_backend/identity_parity.d.ts +11 -0
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/identity_parity.js +124 -9
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +28 -0
- package/dist/testing/cross_backend/revocation_drivers.d.ts +70 -0
- package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -0
- package/dist/testing/cross_backend/revocation_drivers.js +104 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +6 -1
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +52 -16
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +33 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +13 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +155 -39
- package/package.json +3 -3
|
@@ -31,13 +31,25 @@ export interface CrossProcessWsTestOptions {
|
|
|
31
31
|
* the server cap.
|
|
32
32
|
*/
|
|
33
33
|
readonly max_message_bytes?: number;
|
|
34
|
+
/** REST logout route path, for the logout close case. Default `/api/account/logout`. */
|
|
35
|
+
readonly logout_path?: string;
|
|
36
|
+
/** REST password-change route path, for its close case. Default `/api/account/password`. */
|
|
37
|
+
readonly password_path?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The backend's per-account API token cap, which the token-cap close case
|
|
40
|
+
* mints past. Defaults to `DEFAULT_MAX_TOKENS`; `null` (a backend with the
|
|
41
|
+
* cap disabled) skips the case.
|
|
42
|
+
*/
|
|
43
|
+
readonly max_tokens?: number | null;
|
|
34
44
|
}
|
|
35
45
|
/**
|
|
36
46
|
* Register the cross-process WS round-trip suite over a real upgrade: authed
|
|
37
47
|
* `heartbeat` round-trip, `heartbeat`'s parameterless-shape contract, an
|
|
38
48
|
* oversized message closing with `WS_CLOSE_MESSAGE_TOO_BIG`, anonymous-upgrade
|
|
39
49
|
* refusal, disallowed-origin refusal, and — when `rpc_path` is supplied —
|
|
40
|
-
*
|
|
50
|
+
* each listed revocation (session and token revokes, admin revoke-all, logout,
|
|
51
|
+
* password change, token-cap eviction, account delete and purge) closing the
|
|
52
|
+
* live socket, plus a
|
|
41
53
|
* self-revoking socket reading its reply before its close, and a WS mutation's
|
|
42
54
|
* audit row read back after `_testing_drain_effects`.
|
|
43
55
|
*/
|
|
@@ -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;AAyH9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAGtE,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,YAAY,CAAC;AAK7E,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;IACpC,wFAAwF;IACxF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,4FAA4F;IAC5F,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IA4epF,CAAC"}
|
|
@@ -43,21 +43,30 @@ import '../assert_dev_env.js';
|
|
|
43
43
|
* server-initiated close.
|
|
44
44
|
* Per-message dispatch never re-checks credential validity, so an open socket
|
|
45
45
|
* ends only when a revocation closes it — which every revocation does, once
|
|
46
|
-
* its transaction commits
|
|
47
|
-
*
|
|
46
|
+
* its transaction commits, and every close here is asserted with
|
|
47
|
+
* `WS_CLOSE_SESSION_REVOKED` and `WS_CLOSE_SESSION_REVOKED_REASON`, so a
|
|
48
|
+
* dropped connection can't pass for one. An authenticated socket is dropped
|
|
49
|
+
* when the account's sessions are revoked over the keeper's session channel
|
|
48
50
|
* (`account_session_revoke_all`), and when its account is deleted
|
|
49
|
-
* (`account_delete`, also gated on `capabilities.account_lifecycle`).
|
|
50
|
-
*
|
|
51
|
-
*
|
|
51
|
+
* (`account_delete`, also gated on `capabilities.account_lifecycle`). The
|
|
52
|
+
* rest of the revocation table follows, each closing a second account's
|
|
53
|
+
* socket:
|
|
54
|
+
* `account_token_revoke` closes the revoked token's bearer socket and leaves
|
|
55
|
+
* a socket on the account's other token open; `admin_token_revoke_all`,
|
|
56
|
+
* `POST /logout`, `POST /password`, and `account_purge` (also gated on
|
|
57
|
+
* `capabilities.account_lifecycle`) close the account's bearer and session
|
|
58
|
+
* sockets — the rows `create_account_revocations` shares with the SSE suite;
|
|
59
|
+
* and a token the per-account cap evicts (`max_tokens` further mints) has its
|
|
60
|
+
* bearer socket closed. Omit `rpc_path` to skip them
|
|
61
|
+
* (consumers without the standard account and admin actions on their RPC
|
|
62
|
+
* endpoint).
|
|
52
63
|
*
|
|
53
64
|
* The last cases are gated on `capabilities.ws_account_actions` — the backend's
|
|
54
65
|
* WS endpoint mounts the self-service account actions: a socket that revokes
|
|
55
66
|
* its own sessions *over that socket* is closed with
|
|
56
|
-
* `WS_CLOSE_SESSION_REVOKED
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* ahead of it, so the caller is never left without an answer; the Rust spine
|
|
60
|
-
* does not answer yet, which that flag records. Under the same gate (and
|
|
67
|
+
* `WS_CLOSE_SESSION_REVOKED` after reading the reply to its request — both
|
|
68
|
+
* spines answer the revoking request ahead of the close, so the caller is
|
|
69
|
+
* never left without an answer. Under the same gate (and
|
|
61
70
|
* `rpc_path`), a socket mints an API token over WS and the token's
|
|
62
71
|
* `token_create` audit row is read back after `_testing_drain_effects` — the
|
|
63
72
|
* barrier a WS mutation needs, since a WS reply can precede its audit write.
|
|
@@ -73,16 +82,19 @@ import '../assert_dev_env.js';
|
|
|
73
82
|
import { assert, describe } from 'vitest';
|
|
74
83
|
import { assert_rejects } from '@fuzdev/fuz_util/testing.ts';
|
|
75
84
|
import { heartbeat_action_spec } from '../../actions/heartbeat.js';
|
|
76
|
-
import { DEFAULT_WS_MAX_MESSAGE_BYTES, WS_CLOSE_MESSAGE_TOO_BIG, WS_CLOSE_SESSION_REVOKED } from '../../actions/transports.js';
|
|
77
|
-
import { account_session_revoke_all_action_spec, account_token_create_action_spec } from '../../auth/account_action_specs.js';
|
|
85
|
+
import { DEFAULT_WS_MAX_MESSAGE_BYTES, WS_CLOSE_MESSAGE_TOO_BIG, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON } from '../../actions/transports.js';
|
|
86
|
+
import { account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_revoke_action_spec, TokenCreateOutput, TokenRevokeOutput } from '../../auth/account_action_specs.js';
|
|
87
|
+
import { DEFAULT_MAX_TOKENS } from '../../auth/account_route_schema.js';
|
|
78
88
|
import { account_delete_action_spec, audit_log_list_action_spec, AuditLogListOutput } from '../../auth/admin_action_specs.js';
|
|
79
89
|
import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
|
|
80
90
|
import { is_response_for } from '../transports/ws_client.js';
|
|
81
|
-
import { create_ws_transport } from '../transports/ws_transport.js';
|
|
91
|
+
import { create_admitted_ws_transport, create_ws_transport } from '../transports/ws_transport.js';
|
|
82
92
|
import { connect_raw_ws, is_raw_ws_close, RAW_WS_OPCODE_TEXT } from '../transports/ws_raw_client.js';
|
|
83
93
|
import { create_rpc_post_init } from '../rpc_helpers.js';
|
|
94
|
+
import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
|
|
84
95
|
import { test_if } from './capabilities.js';
|
|
85
96
|
import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
|
|
97
|
+
import { create_account_revocations } from './revocation_drivers.js';
|
|
86
98
|
/** Origin guaranteed to fail the `http://localhost:*` allowlist the test backends run with. */
|
|
87
99
|
const DISALLOWED_ORIGIN = 'http://disallowed.example';
|
|
88
100
|
/**
|
|
@@ -90,12 +102,15 @@ const DISALLOWED_ORIGIN = 'http://disallowed.example';
|
|
|
90
102
|
* `heartbeat` round-trip, `heartbeat`'s parameterless-shape contract, an
|
|
91
103
|
* oversized message closing with `WS_CLOSE_MESSAGE_TOO_BIG`, anonymous-upgrade
|
|
92
104
|
* refusal, disallowed-origin refusal, and — when `rpc_path` is supplied —
|
|
93
|
-
*
|
|
105
|
+
* each listed revocation (session and token revokes, admin revoke-all, logout,
|
|
106
|
+
* password change, token-cap eviction, account delete and purge) closing the
|
|
107
|
+
* live socket, plus a
|
|
94
108
|
* self-revoking socket reading its reply before its close, and a WS mutation's
|
|
95
109
|
* audit row read back after `_testing_drain_effects`.
|
|
96
110
|
*/
|
|
97
111
|
export const describe_cross_process_ws_tests = (options) => {
|
|
98
|
-
const { setup_test, capabilities, base_url, ws_path, origin, rpc_path, max_message_bytes = DEFAULT_WS_MAX_MESSAGE_BYTES } = options;
|
|
112
|
+
const { setup_test, capabilities, base_url, ws_path, origin, rpc_path, max_message_bytes = DEFAULT_WS_MAX_MESSAGE_BYTES, logout_path = '/api/account/logout', password_path = '/api/account/password' } = options;
|
|
113
|
+
const max_tokens = options.max_tokens === undefined ? DEFAULT_MAX_TOKENS : options.max_tokens;
|
|
99
114
|
const open_authed = async () => {
|
|
100
115
|
const fixture = await setup_test();
|
|
101
116
|
return create_ws_transport({ base_url, ws_path, cookies: fixture.transport.cookies(), origin });
|
|
@@ -229,6 +244,54 @@ export const describe_cross_process_ws_tests = (options) => {
|
|
|
229
244
|
test_if(capabilities.ws, 'disallowed-origin upgrade is refused', async () => {
|
|
230
245
|
await assert_rejects(() => create_ws_transport({ base_url, ws_path, cookies: [], origin: DISALLOWED_ORIGIN }));
|
|
231
246
|
});
|
|
247
|
+
// Every revocation closes with 4001 "Session revoked" — the code and its
|
|
248
|
+
// reason, so a dropped connection (1006) or a crash can't pass for one.
|
|
249
|
+
const assert_revoked_close = async (client, label) => {
|
|
250
|
+
assert.ok(await client.wait_for_close(2000), `socket did not close within 2s after ${label}`);
|
|
251
|
+
assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED, label);
|
|
252
|
+
assert.strictEqual(client.close_reason, WS_CLOSE_SESSION_REVOKED_REASON, label);
|
|
253
|
+
};
|
|
254
|
+
// A bearer socket opens outside a browser context (no Origin, no cookie),
|
|
255
|
+
// as a CLI client's would.
|
|
256
|
+
const open_bearer = (token) => create_admitted_ws_transport({
|
|
257
|
+
base_url,
|
|
258
|
+
ws_path,
|
|
259
|
+
cookies: [],
|
|
260
|
+
origin: null,
|
|
261
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
262
|
+
});
|
|
263
|
+
const open_session = (account) => {
|
|
264
|
+
const { cookie } = account.create_session_headers();
|
|
265
|
+
assert.ok(cookie, 'expected a session cookie');
|
|
266
|
+
return create_admitted_ws_transport({ base_url, ws_path, cookies: [cookie], origin });
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Open each socket in turn, run `fn` over them, and close every socket
|
|
270
|
+
* that opened — also when a later open fails, so a refused upgrade leaves
|
|
271
|
+
* no socket registered on the shared backend for the rest of the run.
|
|
272
|
+
*
|
|
273
|
+
* Every close is awaited whatever the others do. When the open or `fn`
|
|
274
|
+
* failed, that error propagates and a close error is dropped, so it never
|
|
275
|
+
* hides the case's own failure; when they succeeded, the first close error
|
|
276
|
+
* is thrown, so a transport error surfacing at close still fails the case.
|
|
277
|
+
*/
|
|
278
|
+
const with_sockets = async (opens, fn) => {
|
|
279
|
+
const clients = [];
|
|
280
|
+
try {
|
|
281
|
+
for (const open of opens)
|
|
282
|
+
clients.push(await open());
|
|
283
|
+
await fn(...clients);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await Promise.allSettled(clients.map((c) => c.close()));
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
const closes = await Promise.allSettled(clients.map((c) => c.close()));
|
|
290
|
+
for (const close of closes) {
|
|
291
|
+
if (close.status === 'rejected')
|
|
292
|
+
throw close.reason;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
232
295
|
// Per-message dispatch never re-checks credential validity, so a live
|
|
233
296
|
// socket drops only when a revocation closes it. Revoke the keeper's
|
|
234
297
|
// sessions over its own session channel → the handler's post-commit
|
|
@@ -248,8 +311,7 @@ export const describe_cross_process_ws_tests = (options) => {
|
|
|
248
311
|
await client.request(1, heartbeat_action_spec.method, {});
|
|
249
312
|
const res = await fixture.transport(rpc_path, create_rpc_post_init(account_session_revoke_all_action_spec.method));
|
|
250
313
|
assert.strictEqual(res.status, 200, `account_session_revoke_all RPC failed (status=${res.status})`);
|
|
251
|
-
|
|
252
|
-
assert.ok(closed, 'socket did not close within 2s after session_revoke_all');
|
|
314
|
+
await assert_revoked_close(client, 'account_session_revoke_all');
|
|
253
315
|
}
|
|
254
316
|
finally {
|
|
255
317
|
await client.close();
|
|
@@ -273,22 +335,80 @@ export const describe_cross_process_ws_tests = (options) => {
|
|
|
273
335
|
account_id: target.account.id
|
|
274
336
|
}));
|
|
275
337
|
assert.strictEqual(res.status, 200, `account_delete RPC failed (status=${res.status})`);
|
|
276
|
-
|
|
277
|
-
assert.ok(closed, 'socket did not close within 2s after account_delete');
|
|
278
|
-
assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED);
|
|
338
|
+
await assert_revoked_close(client, 'account_delete');
|
|
279
339
|
}
|
|
280
340
|
finally {
|
|
281
341
|
await client.close();
|
|
282
342
|
}
|
|
283
343
|
});
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
344
|
+
// The rest of the revocation table (docs/security.md §Closing on
|
|
345
|
+
// Revocation), one case per revocation, each closing a socket of a second
|
|
346
|
+
// account with 4001 "Session revoked".
|
|
347
|
+
/** Mint a token for `account` over its own session. */
|
|
348
|
+
const mint_token = async (fixture, account, name) => expect_output(await cross_rpc_call(fixture.fresh_transport(), rpc_path, account_token_create_action_spec.method, { name, scope: { kind: 'full' }, lifetime: { kind: 'eternal' } }, account.create_session_headers()), TokenCreateOutput);
|
|
349
|
+
// `account_token_revoke` closes the revoked token's connections and no
|
|
350
|
+
// others: a socket on the account's other token stays open.
|
|
351
|
+
test_if(capabilities.ws && rpc_path !== undefined, "account_token_revoke closes the revoked token's bearer socket only", async () => {
|
|
352
|
+
const fixture = await setup_test();
|
|
353
|
+
const target = await fixture.create_account({ username: 'ws_token_revoke' });
|
|
354
|
+
const minted = await mint_token(fixture, target, 'ws revoke probe');
|
|
355
|
+
await with_sockets([() => open_bearer(minted.token), () => open_bearer(target.api_token)], async (revoked, other) => {
|
|
356
|
+
const res = expect_output(await cross_rpc_call(fixture.fresh_transport(), rpc_path, account_token_revoke_action_spec.method, { token_id: minted.id }, target.create_session_headers()), TokenRevokeOutput);
|
|
357
|
+
assert.strictEqual(res.revoked, true);
|
|
358
|
+
await assert_revoked_close(revoked, 'account_token_revoke');
|
|
359
|
+
// the account's other token keeps its socket
|
|
360
|
+
assert.deepStrictEqual(await other.request(2, heartbeat_action_spec.method, {}), {});
|
|
361
|
+
assert.strictEqual(other.close_code, null);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
// The account-wide revocations (`revocation_drivers.ts`, shared with the
|
|
365
|
+
// SSE suite) close the account's connections — its bearer socket, and its
|
|
366
|
+
// session socket too.
|
|
367
|
+
// Without `rpc_path` the rows still register, as skipped cases, and never
|
|
368
|
+
// run — so the empty path is never sent.
|
|
369
|
+
const revocations = create_account_revocations({
|
|
370
|
+
capabilities,
|
|
371
|
+
rpc_path: rpc_path ?? '',
|
|
372
|
+
logout_path,
|
|
373
|
+
password_path
|
|
374
|
+
});
|
|
375
|
+
for (const { label, slug, gate, revoke } of revocations) {
|
|
376
|
+
test_if(capabilities.ws && gate && rpc_path !== undefined, `${label} closes the account's bearer and session sockets`, async () => {
|
|
377
|
+
const fixture = await setup_test();
|
|
378
|
+
const target = await fixture.create_account({
|
|
379
|
+
username: `ws_revoke_${slug}`,
|
|
380
|
+
password_value: DEFAULT_TEST_PASSWORD
|
|
381
|
+
});
|
|
382
|
+
await with_sockets([() => open_bearer(target.api_token), () => open_session(target)], async (bearer, session) => {
|
|
383
|
+
await revoke(fixture, target);
|
|
384
|
+
await assert_revoked_close(bearer, `${label} (bearer socket)`);
|
|
385
|
+
await assert_revoked_close(session, `${label} (session socket)`);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// `max_tokens` further mints leave the watched token outside the newest
|
|
390
|
+
// `max_tokens`, so the cap evicts it whatever tokens the account held
|
|
391
|
+
// before it, and its socket closes once the evicting mint commits.
|
|
392
|
+
test_if(capabilities.ws && rpc_path !== undefined && max_tokens !== null, 'a token the cap evicts has its bearer socket closed', async () => {
|
|
393
|
+
const fixture = await setup_test();
|
|
394
|
+
const target = await fixture.create_account({ username: 'ws_token_cap' });
|
|
395
|
+
const watched = await mint_token(fixture, target, 'ws cap victim');
|
|
396
|
+
const client = await open_bearer(watched.token);
|
|
397
|
+
try {
|
|
398
|
+
for (let i = 0; i < max_tokens; i++) {
|
|
399
|
+
await mint_token(fixture, target, `ws cap filler ${i}`);
|
|
400
|
+
}
|
|
401
|
+
await assert_revoked_close(client, `${max_tokens} further token mints`);
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
await client.close();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
// A socket that revokes the session it is running on. Its request is
|
|
408
|
+
// answered first, the close following from behind the revocation's
|
|
287
409
|
// commit. A caller left with only the close cannot tell a completed
|
|
288
410
|
// revocation from a dropped connection.
|
|
289
|
-
test_if(capabilities.ws && capabilities.ws_account_actions,
|
|
290
|
-
? 'a socket that revokes its own sessions reads the reply, then the revocation close'
|
|
291
|
-
: 'a socket that revokes its own sessions is closed with the revocation code', async () => {
|
|
411
|
+
test_if(capabilities.ws && capabilities.ws_account_actions, 'a socket that revokes its own sessions reads the reply, then the revocation close', async () => {
|
|
292
412
|
const client = await open_authed();
|
|
293
413
|
try {
|
|
294
414
|
await client.request(1, heartbeat_action_spec.method, {});
|
|
@@ -298,19 +418,15 @@ export const describe_cross_process_ws_tests = (options) => {
|
|
|
298
418
|
id: revoke_id,
|
|
299
419
|
method: account_session_revoke_all_action_spec.method
|
|
300
420
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const result = reply.result;
|
|
311
|
-
assert.strictEqual(result.ok, true);
|
|
312
|
-
assert.ok((result.count ?? 0) >= 1, "at least the socket's own session was revoked");
|
|
313
|
-
}
|
|
421
|
+
await assert_revoked_close(client, 'revoking its own sessions');
|
|
422
|
+
// frames arrive in wire order and nothing arrives after the
|
|
423
|
+
// close, so a reply in hand was written ahead of it
|
|
424
|
+
const reply = client.messages.find(is_response_for(revoke_id));
|
|
425
|
+
assert.ok(reply, 'the reply must reach the caller before the close');
|
|
426
|
+
assert.ok('result' in reply, JSON.stringify(reply));
|
|
427
|
+
const result = reply.result;
|
|
428
|
+
assert.strictEqual(result.ok, true);
|
|
429
|
+
assert.ok((result.count ?? 0) >= 1, "at least the socket's own session was revoked");
|
|
314
430
|
}
|
|
315
431
|
finally {
|
|
316
432
|
await client.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.146.0",
|
|
4
4
|
"description": "fullstack app library",
|
|
5
5
|
"glyph": "🗝",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@fuzdev/fuz_css": ">=0.70.0",
|
|
37
37
|
"@fuzdev/fuz_schema": "^0.5.0",
|
|
38
38
|
"@fuzdev/fuz_util": ">=0.77.0",
|
|
39
|
-
"@fuzdev/pglet-wasm": ">=0.1.
|
|
39
|
+
"@fuzdev/pglet-wasm": ">=0.1.2",
|
|
40
40
|
"@hono/bun": ">=1",
|
|
41
41
|
"@hono/deno": ">=1",
|
|
42
42
|
"@hono/node-server": ">=1",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"@fuzdev/fuz_util": "^0.77.0",
|
|
99
99
|
"@fuzdev/gro": "^0.220.0",
|
|
100
100
|
"@fuzdev/mdz": "^0.6.0",
|
|
101
|
-
"@fuzdev/pglet-wasm": "^0.1.
|
|
101
|
+
"@fuzdev/pglet-wasm": "^0.1.2",
|
|
102
102
|
"@hono/bun": "^1.0.1",
|
|
103
103
|
"@hono/deno": "^1.0.3",
|
|
104
104
|
"@hono/node-server": "^1.19.14",
|