@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.
Files changed (69) hide show
  1. package/dist/actions/CLAUDE.md +7 -6
  2. package/dist/auth/CLAUDE.md +5 -2
  3. package/dist/auth/account_action_specs.d.ts.map +1 -1
  4. package/dist/auth/account_action_specs.js +2 -1
  5. package/dist/auth/account_route_schema.d.ts.map +1 -1
  6. package/dist/auth/account_route_schema.js +9 -2
  7. package/dist/auth/account_routes.d.ts +8 -0
  8. package/dist/auth/account_routes.d.ts.map +1 -1
  9. package/dist/auth/account_routes.js +12 -3
  10. package/dist/auth/request_context.d.ts +23 -0
  11. package/dist/auth/request_context.d.ts.map +1 -1
  12. package/dist/auth/request_context.js +32 -1
  13. package/dist/auth/token_scope.d.ts +22 -14
  14. package/dist/auth/token_scope.d.ts.map +1 -1
  15. package/dist/auth/token_scope.js +23 -14
  16. package/dist/db/CLAUDE.md +7 -3
  17. package/dist/db/fact_ddl.d.ts +1 -0
  18. package/dist/db/fact_ddl.d.ts.map +1 -1
  19. package/dist/db/fact_ddl.js +1 -0
  20. package/dist/db/fact_holder_queries.d.ts +8 -4
  21. package/dist/db/fact_holder_queries.d.ts.map +1 -1
  22. package/dist/db/fact_holder_queries.js +9 -5
  23. package/dist/db/fact_queries.d.ts +10 -7
  24. package/dist/db/fact_queries.d.ts.map +1 -1
  25. package/dist/db/fact_queries.js +31 -10
  26. package/dist/http/CLAUDE.md +2 -2
  27. package/dist/http/auth_shape.d.ts.map +1 -1
  28. package/dist/http/auth_shape.js +11 -6
  29. package/dist/http/proxy.d.ts +22 -21
  30. package/dist/http/proxy.d.ts.map +1 -1
  31. package/dist/http/proxy.js +28 -33
  32. package/dist/http/route_spec.d.ts +8 -0
  33. package/dist/http/route_spec.d.ts.map +1 -1
  34. package/dist/server/env.d.ts.map +1 -1
  35. package/dist/server/env.js +1 -1
  36. package/dist/server/fact_security_headers.d.ts +59 -0
  37. package/dist/server/fact_security_headers.d.ts.map +1 -0
  38. package/dist/server/fact_security_headers.js +58 -0
  39. package/dist/server/serve_fact_route.d.ts +21 -10
  40. package/dist/server/serve_fact_route.d.ts.map +1 -1
  41. package/dist/server/serve_fact_route.js +41 -50
  42. package/dist/server/x_accel.d.ts +135 -31
  43. package/dist/server/x_accel.d.ts.map +1 -1
  44. package/dist/server/x_accel.js +652 -88
  45. package/dist/testing/CLAUDE.md +73 -22
  46. package/dist/testing/cross_backend/capabilities.d.ts +0 -18
  47. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  48. package/dist/testing/cross_backend/capabilities.js +1 -4
  49. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  50. package/dist/testing/cross_backend/default_backend_configs.js +0 -7
  51. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  52. package/dist/testing/cross_backend/fact_serving.js +85 -12
  53. package/dist/testing/cross_backend/identity_parity.d.ts +11 -0
  54. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
  55. package/dist/testing/cross_backend/identity_parity.js +124 -9
  56. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/login_security.js +28 -0
  58. package/dist/testing/cross_backend/revocation_drivers.d.ts +70 -0
  59. package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -0
  60. package/dist/testing/cross_backend/revocation_drivers.js +104 -0
  61. package/dist/testing/cross_backend/sse_round_trip.d.ts +6 -1
  62. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  63. package/dist/testing/cross_backend/sse_round_trip.js +52 -16
  64. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/token_scope_surface.js +33 -0
  66. package/dist/testing/cross_backend/ws_round_trip.d.ts +13 -1
  67. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/ws_round_trip.js +155 -39
  69. 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
- * session revocation and account deletion closing the live socket, plus a
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;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"}
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. An authenticated socket is dropped when the
47
- * account's sessions are revoked over the keeper's session channel
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`). Omit
50
- * `rpc_path` to skip them (consumers without the standard account and admin
51
- * actions on their RPC endpoint).
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`, and — on a backend declaring
57
- * `capabilities.ws_self_revocation_reply` — reads the reply to its request
58
- * first. The TS spine queues the close behind the commit and sends the reply
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
- * session revocation and account deletion closing the live socket, plus a
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
- const closed = await client.wait_for_close(2000);
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
- const closed = await client.wait_for_close(2000);
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
- // A socket that revokes the session it is running on. It must be closed;
285
- // and where the backend declares `ws_self_revocation_reply`, the request
286
- // is answered first, the close following from behind the revocation's
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, capabilities.ws_self_revocation_reply
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
- const closed = await client.wait_for_close(2000);
302
- assert.ok(closed, 'socket did not close within 2s of revoking its own sessions');
303
- assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED);
304
- if (capabilities.ws_self_revocation_reply) {
305
- // frames arrive in wire order and nothing arrives after the
306
- // close, so a reply in hand was written ahead of it
307
- const reply = client.messages.find(is_response_for(revoke_id));
308
- assert.ok(reply, 'the reply must reach the caller before the close');
309
- assert.ok('result' in reply, JSON.stringify(reply));
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.144.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.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.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",