@fuzdev/fuz_app 0.71.0 → 0.72.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.
@@ -17,15 +17,23 @@ import type { SseStream, SseNotification, EventSpec } from './sse.js';
17
17
  /** SSE channel the audit-log stream route publishes on. */
18
18
  export declare const AUDIT_LOG_CHANNEL = "audit_log";
19
19
  /**
20
- * Audit event types that trigger SSE stream disconnection.
20
+ * Audit event types that trigger SSE stream disconnection — the union of
21
+ * access-invalidation events. Over-closing a one-way admin feed is cheap (the
22
+ * client reconnects if still authorized), so the SSE set is the full union.
21
23
  *
22
24
  * `role_grant_revoke` requires the revoked role to match the guard's `required_role`
23
25
  * (or is skipped entirely when `required_role` is `null` — useful for streams
24
- * not gated by any specific role_grant).
25
- * `session_revoke_all` and `password_change` close every stream for the target account.
26
+ * not gated by any specific role_grant). The WS half deliberately omits this
27
+ * event (per-message re-authorization picks role changes up there); a one-way
28
+ * SSE stream has no per-message recheck, so it must close here.
29
+ * `session_revoke_all` / `token_revoke_all` / `password_change` / `logout` close
30
+ * every stream for the target account.
26
31
  * `session_revoke` closes only the stream tied to the specific revoked session
27
32
  * (matched by the blake3 session hash in `event.metadata.session_id`) — closing
28
33
  * all of a user's streams for a single-session revoke would be over-aggressive.
34
+ * The single `token_revoke` the WS half handles is omitted: an SSE stream is
35
+ * opened under a cookie session, never an API token, so no stream is keyed by a
36
+ * single token id.
29
37
  */
30
38
  export declare const disconnect_event_types: ReadonlySet<string>;
31
39
  /**
@@ -33,8 +41,9 @@ export declare const disconnect_event_types: ReadonlySet<string>;
33
41
  *
34
42
  * Closes streams when:
35
43
  * - `role_grant_revoke` fires for the `required_role` targeting a connected subscriber
36
- * - `session_revoke_all` targets a connected subscriber (consistent invalidation)
37
- * - `password_change` targets a connected subscriber (sessions revoked implicitly)
44
+ * - `session_revoke` targets the specific revoked session (session-hash-scoped)
45
+ * - `session_revoke_all` / `token_revoke_all` / `password_change` / `logout`
46
+ * target a connected subscriber (account-wide)
38
47
  *
39
48
  * The registry must use `account_id` as the identity key when subscribing
40
49
  * (passed as the third argument to `registry.subscribe()`).
@@ -1 +1 @@
1
- {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAE,KAAK,gBAAgB,EAAC,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
1
+ {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAE,KAAK,gBAAgB,EAAC,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAOrD,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
@@ -15,29 +15,40 @@ import { SubscriberRegistry } from './subscriber_registry.js';
15
15
  /** SSE channel the audit-log stream route publishes on. */
16
16
  export const AUDIT_LOG_CHANNEL = 'audit_log';
17
17
  /**
18
- * Audit event types that trigger SSE stream disconnection.
18
+ * Audit event types that trigger SSE stream disconnection — the union of
19
+ * access-invalidation events. Over-closing a one-way admin feed is cheap (the
20
+ * client reconnects if still authorized), so the SSE set is the full union.
19
21
  *
20
22
  * `role_grant_revoke` requires the revoked role to match the guard's `required_role`
21
23
  * (or is skipped entirely when `required_role` is `null` — useful for streams
22
- * not gated by any specific role_grant).
23
- * `session_revoke_all` and `password_change` close every stream for the target account.
24
+ * not gated by any specific role_grant). The WS half deliberately omits this
25
+ * event (per-message re-authorization picks role changes up there); a one-way
26
+ * SSE stream has no per-message recheck, so it must close here.
27
+ * `session_revoke_all` / `token_revoke_all` / `password_change` / `logout` close
28
+ * every stream for the target account.
24
29
  * `session_revoke` closes only the stream tied to the specific revoked session
25
30
  * (matched by the blake3 session hash in `event.metadata.session_id`) — closing
26
31
  * all of a user's streams for a single-session revoke would be over-aggressive.
32
+ * The single `token_revoke` the WS half handles is omitted: an SSE stream is
33
+ * opened under a cookie session, never an API token, so no stream is keyed by a
34
+ * single token id.
27
35
  */
28
36
  export const disconnect_event_types = new Set([
29
37
  'role_grant_revoke', // role revoked — user lost access
30
38
  'session_revoke', // single session revoked — close only that stream
31
39
  'session_revoke_all', // all sessions invalidated — user should be kicked
40
+ 'token_revoke_all', // all API tokens invalidated — close the account's streams
32
41
  'password_change', // password changed — all sessions revoked implicitly
42
+ 'logout', // explicit logout — close the account's streams
33
43
  ]);
34
44
  /**
35
45
  * Create an audit event handler that closes SSE streams on auth changes.
36
46
  *
37
47
  * Closes streams when:
38
48
  * - `role_grant_revoke` fires for the `required_role` targeting a connected subscriber
39
- * - `session_revoke_all` targets a connected subscriber (consistent invalidation)
40
- * - `password_change` targets a connected subscriber (sessions revoked implicitly)
49
+ * - `session_revoke` targets the specific revoked session (session-hash-scoped)
50
+ * - `session_revoke_all` / `token_revoke_all` / `password_change` / `logout`
51
+ * target a connected subscriber (account-wide)
41
52
  *
42
53
  * The registry must use `account_id` as the identity key when subscribing
43
54
  * (passed as the third argument to `registry.subscribe()`).
@@ -1 +1 @@
1
- {"version":3,"file":"spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,0FAA0F;AAC1F,eAAO,MAAM,+BAA+B,iDAAiD,CAAC;AAE9F,MAAM,WAAW,6BAA6B;IAC7C,yDAAyD;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GACrC,UAAS,6BAAkC,KACzC,aAkCF,CAAC"}
1
+ {"version":3,"file":"spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAOvD,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,0FAA0F;AAC1F,eAAO,MAAM,+BAA+B,iDAAiD,CAAC;AAE9F,MAAM,WAAW,6BAA6B;IAC7C,yDAAyD;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GACrC,UAAS,6BAAkC,KACzC,aAsCF,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import '../assert_dev_env.js';
2
2
  import { build_test_backend_paths } from './build_test_backend_paths.js';
3
- import { make_default_rust_backend_config } from './default_backend_configs.js';
3
+ import { make_default_rust_backend_config, rust_default_capabilities, } from './default_backend_configs.js';
4
4
  /** Env var naming the prebuilt `testing_spine_stub` binary. Required when `binary_path` is omitted. */
5
5
  export const SPINE_STUB_BIN_ENV = 'FUZ_TESTING_SPINE_STUB_BIN';
6
6
  /** Default listening port — slots beside zzz's 1175/1176; matches the binary's `DEFAULT_PORT`. */
@@ -35,6 +35,10 @@ export const spine_stub_backend_config = (options = {}) => {
35
35
  // is the lower-precedence fallback — both carry the same value.
36
36
  start_command: [binary_path, '--port', String(port)],
37
37
  database_url,
38
+ // The stub now serves `GET /api/admin/audit/stream` (the spine
39
+ // `fuz_realtime::SseRegistry` + audit listener), so it advertises `sse`
40
+ // like the TS spines — the cross-process SSE suite's three cases run.
41
+ capabilities: { ...rust_default_capabilities, sse: true },
38
42
  port_env_var: 'FUZ_SPINE_STUB_PORT',
39
43
  rust_log: 'info,testing_spine_stub=info',
40
44
  paths,
@@ -29,9 +29,9 @@ export interface CrossProcessSseTestOptions {
29
29
  readonly origin?: string;
30
30
  }
31
31
  /**
32
- * Register the cross-process SSE round-trip suite. Up to three cases over a
33
- * real streaming `fetch`: connected-comment, audit data frame, and
34
- * close-on-revoke.
32
+ * Register the cross-process SSE round-trip suite. Up to four cases over a
33
+ * real streaming `fetch`: connected-comment, audit data frame, account-wide
34
+ * close-on-revoke, and session-scoped close-on-revoke.
35
35
  */
36
36
  export declare const describe_cross_process_sse_tests: (options: CrossProcessSseTestOptions) => void;
37
37
  //# sourceMappingURL=sse_round_trip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8C9B,OAAO,EAAC,KAAK,mBAAmB,EAAU,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAK1C,kEAAkE;AAClE,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IAwGtF,CAAC"}
1
+ {"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+D9B,OAAO,EAAC,KAAK,mBAAmB,EAAU,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAK1C,kEAAkE;AAClE,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IAqKtF,CAAC"}
@@ -11,7 +11,7 @@ import '../assert_dev_env.js';
11
11
  * (`describe_standard_cross_process_tests`) omits SSE by design, so consumers
12
12
  * call this alongside it (paralleling `describe_cross_process_ws_tests`).
13
13
  *
14
- * Three cases, mirroring the in-process SSE self-test against fuz_app's
14
+ * Four cases, mirroring the in-process SSE self-test against fuz_app's
15
15
  * standard audit-log stream:
16
16
  *
17
17
  * 1. **connects** — the stream opens and emits the `: connected` comment.
@@ -22,10 +22,23 @@ import '../assert_dev_env.js';
22
22
  * the secondary, not the subscriber). The secondary is minted *before* the
23
23
  * stream opens so `create_account`'s own audit events (invite / signup /
24
24
  * login / token) don't land on it.
25
- * 3. **close-on-revoke** (gated on `rpc_path`) — the subscriber's *own*
26
- * sessions are revoked (`account_session_revoke_all`), so the
25
+ * 3. **close-on-revoke, account-wide** (gated on `rpc_path`) — the subscriber's
26
+ * *own* sessions are revoked (`account_session_revoke_all`), so the
27
27
  * `session_revoke_all` event targets the keeper and the audit guard drops
28
- * the live stream. Asserted via `SseTransport.wait_for_close`.
28
+ * the live stream via the account-wide `close_for_account` path. Asserted
29
+ * via `SseTransport.wait_for_close`.
30
+ * 4. **close-on-revoke, session-scoped** (gated on `rpc_path`) — the
31
+ * subscriber's *own* single session is revoked (`account_session_revoke`),
32
+ * so the `session_revoke` event drops the stream via the session-hash-scoped
33
+ * `close_for_session` path (the distinct primitive cases 2–3 don't reach).
34
+ *
35
+ * The close-on-revoke matrix is layered: cases 3–4 exercise the account-wide
36
+ * and session-scoped paths cross-process; the remaining union events
37
+ * (`token_revoke_all` / `logout` / `password_change`, all account-wide; and
38
+ * `role_grant_revoke`, role-matched) are covered by the spine's `fuz_realtime`
39
+ * SSE-registry unit tests and the in-process guard self-test, so a cross-process
40
+ * `token_revoke_all`-with-zero-tokens case (which may emit no audit row) stays
41
+ * out to keep the spawned-backend suite non-flaky.
29
42
  *
30
43
  * Gated on `capabilities.sse` — backends without an end-to-end SSE stream
31
44
  * skip (the cases still surface as `.skip` in the report). Cross-process
@@ -35,7 +48,7 @@ import '../assert_dev_env.js';
35
48
  * @module
36
49
  */
37
50
  import { assert, describe } from 'vitest';
38
- import { account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
51
+ import { account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, } from '../../auth/account_action_specs.js';
39
52
  import { admin_session_revoke_all_action_spec } from '../../auth/admin_action_specs.js';
40
53
  import { audit_log_event_specs } from '../../realtime/sse_auth_guard.js';
41
54
  import { SSE_CONNECTED_COMMENT } from '../../realtime/sse.js';
@@ -60,9 +73,9 @@ const assert_audit_data_frame = (frame) => {
60
73
  assert.ok(result.success, `audit data frame params mismatch for '${String(payload.method)}': ${result.success ? '' : JSON.stringify(result.error.issues)}`);
61
74
  };
62
75
  /**
63
- * Register the cross-process SSE round-trip suite. Up to three cases over a
64
- * real streaming `fetch`: connected-comment, audit data frame, and
65
- * close-on-revoke.
76
+ * Register the cross-process SSE round-trip suite. Up to four cases over a
77
+ * real streaming `fetch`: connected-comment, audit data frame, account-wide
78
+ * close-on-revoke, and session-scoped close-on-revoke.
66
79
  */
67
80
  export const describe_cross_process_sse_tests = (options) => {
68
81
  const { setup_test, capabilities, base_url, rpc_path, origin } = options;
@@ -133,5 +146,39 @@ export const describe_cross_process_sse_tests = (options) => {
133
146
  await sse.close();
134
147
  }
135
148
  });
149
+ // Single `session_revoke` of the subscriber's OWN session → the
150
+ // session-hash-scoped close path (`close_for_session` / the TS guard's
151
+ // `close_by_identity(session_id)`). The keeper holds exactly one session,
152
+ // so revoking it by its blake3 hash drops the stream opened under it.
153
+ // This is the close-on-revoke path the account-wide cases above don't
154
+ // exercise; the remaining union events (`token_revoke_all` / `logout` /
155
+ // `password_change`) share the account-wide `close_for_account` path the
156
+ // `session_revoke_all` case already covers, and `role_grant_revoke`'s
157
+ // role-matched path is covered by `fuz_realtime`'s SSE registry unit tests.
158
+ test_if(capabilities.sse && rpc_path !== undefined, 'stream closes on a single session_revoke of the subscriber session', async () => {
159
+ const fixture = await setup_test();
160
+ const sse = await create_sse_transport({
161
+ base_url,
162
+ sse_path,
163
+ cookies: fixture.transport.cookies(),
164
+ origin,
165
+ });
166
+ try {
167
+ const first = await sse.read_frame();
168
+ assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, 'first frame must be the connected comment');
169
+ const list_res = await fixture.transport(rpc_path, create_rpc_post_init(account_session_list_action_spec.method));
170
+ assert.strictEqual(list_res.status, 200, `account_session_list RPC failed (status=${list_res.status})`);
171
+ const list_body = (await list_res.json());
172
+ const session_id = list_body.result?.sessions?.[0]?.id;
173
+ assert.ok(typeof session_id === 'string' && session_id.length > 0, 'expected the subscriber session id (blake3 hash) to revoke');
174
+ const revoke_res = await fixture.transport(rpc_path, create_rpc_post_init(account_session_revoke_action_spec.method, { session_id }));
175
+ assert.strictEqual(revoke_res.status, 200, `account_session_revoke RPC failed (status=${revoke_res.status})`);
176
+ const closed = await sse.wait_for_close(2000);
177
+ assert.ok(closed, 'stream did not close within 2s after session_revoke');
178
+ }
179
+ finally {
180
+ await sse.close();
181
+ }
182
+ });
136
183
  });
137
184
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",