@furystack/rest-service 14.0.0 → 14.1.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 (143) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/esm/actions/error-action.d.ts +7 -2
  3. package/esm/actions/error-action.d.ts.map +1 -1
  4. package/esm/actions/error-action.js +7 -2
  5. package/esm/actions/error-action.js.map +1 -1
  6. package/esm/actions/get-current-user.d.ts +1 -4
  7. package/esm/actions/get-current-user.d.ts.map +1 -1
  8. package/esm/actions/get-current-user.js +1 -4
  9. package/esm/actions/get-current-user.js.map +1 -1
  10. package/esm/actions/is-authenticated.d.ts +3 -4
  11. package/esm/actions/is-authenticated.d.ts.map +1 -1
  12. package/esm/actions/is-authenticated.js +3 -4
  13. package/esm/actions/is-authenticated.js.map +1 -1
  14. package/esm/actions/logout.d.ts +2 -6
  15. package/esm/actions/logout.d.ts.map +1 -1
  16. package/esm/actions/logout.js +2 -6
  17. package/esm/actions/logout.js.map +1 -1
  18. package/esm/actions/not-found-action.d.ts +1 -3
  19. package/esm/actions/not-found-action.d.ts.map +1 -1
  20. package/esm/actions/not-found-action.js +1 -3
  21. package/esm/actions/not-found-action.js.map +1 -1
  22. package/esm/actions/password-login-action.d.ts +5 -12
  23. package/esm/actions/password-login-action.d.ts.map +1 -1
  24. package/esm/actions/password-login-action.js +5 -12
  25. package/esm/actions/password-login-action.js.map +1 -1
  26. package/esm/authenticate.d.ts +5 -11
  27. package/esm/authenticate.d.ts.map +1 -1
  28. package/esm/authenticate.js +5 -11
  29. package/esm/authenticate.js.map +1 -1
  30. package/esm/authentication-providers/authentication-provider.d.ts +17 -0
  31. package/esm/authentication-providers/authentication-provider.d.ts.map +1 -1
  32. package/esm/authentication-providers/basic-auth-provider.d.ts +7 -0
  33. package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -1
  34. package/esm/authentication-providers/basic-auth-provider.js +7 -0
  35. package/esm/authentication-providers/basic-auth-provider.js.map +1 -1
  36. package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -1
  37. package/esm/authentication-providers/cookie-auth-provider.js +8 -0
  38. package/esm/authentication-providers/cookie-auth-provider.js.map +1 -1
  39. package/esm/authorize.d.ts +5 -17
  40. package/esm/authorize.d.ts.map +1 -1
  41. package/esm/authorize.js +5 -17
  42. package/esm/authorize.js.map +1 -1
  43. package/esm/get-schema-from-api.d.ts.map +1 -1
  44. package/esm/get-schema-from-api.js +1 -0
  45. package/esm/get-schema-from-api.js.map +1 -1
  46. package/esm/helpers.d.ts +6 -6
  47. package/esm/helpers.d.ts.map +1 -1
  48. package/esm/helpers.js +3 -4
  49. package/esm/helpers.js.map +1 -1
  50. package/esm/http-authentication-settings.d.ts +27 -6
  51. package/esm/http-authentication-settings.d.ts.map +1 -1
  52. package/esm/http-authentication-settings.js +6 -0
  53. package/esm/http-authentication-settings.js.map +1 -1
  54. package/esm/http-server-pool.spec.js.map +1 -1
  55. package/esm/http-user-context.d.ts +8 -5
  56. package/esm/http-user-context.d.ts.map +1 -1
  57. package/esm/http-user-context.js +26 -27
  58. package/esm/http-user-context.js.map +1 -1
  59. package/esm/http-user-context.spec.js +48 -6
  60. package/esm/http-user-context.spec.js.map +1 -1
  61. package/esm/identity-cache-keys.d.ts +28 -0
  62. package/esm/identity-cache-keys.d.ts.map +1 -0
  63. package/esm/identity-cache-keys.js +13 -0
  64. package/esm/identity-cache-keys.js.map +1 -0
  65. package/esm/identity-cache-keys.spec.d.ts +2 -0
  66. package/esm/identity-cache-keys.spec.d.ts.map +1 -0
  67. package/esm/identity-cache-keys.spec.js +21 -0
  68. package/esm/identity-cache-keys.spec.js.map +1 -0
  69. package/esm/identity-event-bus.d.ts +102 -0
  70. package/esm/identity-event-bus.d.ts.map +1 -0
  71. package/esm/identity-event-bus.js +151 -0
  72. package/esm/identity-event-bus.js.map +1 -0
  73. package/esm/identity-event-bus.spec.d.ts +2 -0
  74. package/esm/identity-event-bus.spec.d.ts.map +1 -0
  75. package/esm/identity-event-bus.spec.js +392 -0
  76. package/esm/identity-event-bus.spec.js.map +1 -0
  77. package/esm/index.d.ts +3 -0
  78. package/esm/index.d.ts.map +1 -1
  79. package/esm/index.js +3 -0
  80. package/esm/index.js.map +1 -1
  81. package/esm/login-response-strategy.d.ts.map +1 -1
  82. package/esm/login-response-strategy.js.map +1 -1
  83. package/esm/openapi/generate-openapi-document.spec.js.map +1 -1
  84. package/esm/proxy-manager.spec.js.map +1 -1
  85. package/esm/request-action-implementation.d.ts +25 -0
  86. package/esm/request-action-implementation.d.ts.map +1 -1
  87. package/esm/request-action-implementation.js +9 -0
  88. package/esm/request-action-implementation.js.map +1 -1
  89. package/esm/rest-api-runtime.spec.js.map +1 -1
  90. package/esm/server-telemetry.d.ts +6 -14
  91. package/esm/server-telemetry.d.ts.map +1 -1
  92. package/esm/server-telemetry.js +2 -3
  93. package/esm/server-telemetry.js.map +1 -1
  94. package/esm/user-resolution-cache.d.ts +65 -0
  95. package/esm/user-resolution-cache.d.ts.map +1 -0
  96. package/esm/user-resolution-cache.js +101 -0
  97. package/esm/user-resolution-cache.js.map +1 -0
  98. package/esm/user-resolution-cache.spec.d.ts +2 -0
  99. package/esm/user-resolution-cache.spec.d.ts.map +1 -0
  100. package/esm/user-resolution-cache.spec.js +220 -0
  101. package/esm/user-resolution-cache.spec.js.map +1 -0
  102. package/esm/user-store.d.ts.map +1 -1
  103. package/esm/user-store.js.map +1 -1
  104. package/esm/validate.d.ts +13 -12
  105. package/esm/validate.d.ts.map +1 -1
  106. package/esm/validate.integration.spec.js +1 -0
  107. package/esm/validate.integration.spec.js.map +1 -1
  108. package/esm/validate.js +12 -0
  109. package/esm/validate.js.map +1 -1
  110. package/package.json +14 -12
  111. package/src/actions/error-action.ts +7 -3
  112. package/src/actions/get-current-user.ts +1 -4
  113. package/src/actions/is-authenticated.ts +3 -4
  114. package/src/actions/logout.ts +2 -6
  115. package/src/actions/not-found-action.ts +1 -3
  116. package/src/actions/password-login-action.ts +5 -12
  117. package/src/authenticate.ts +5 -11
  118. package/src/authentication-providers/authentication-provider.ts +17 -0
  119. package/src/authentication-providers/basic-auth-provider.ts +7 -0
  120. package/src/authentication-providers/cookie-auth-provider.ts +8 -0
  121. package/src/authorize.ts +6 -17
  122. package/src/get-schema-from-api.ts +1 -0
  123. package/src/helpers.ts +11 -11
  124. package/src/http-authentication-settings.ts +34 -6
  125. package/src/http-server-pool.spec.ts +1 -1
  126. package/src/http-user-context.spec.ts +59 -11
  127. package/src/http-user-context.ts +38 -31
  128. package/src/identity-cache-keys.spec.ts +24 -0
  129. package/src/identity-cache-keys.ts +31 -0
  130. package/src/identity-event-bus.spec.ts +285 -0
  131. package/src/identity-event-bus.ts +230 -0
  132. package/src/index.ts +3 -0
  133. package/src/login-response-strategy.ts +1 -0
  134. package/src/openapi/generate-openapi-document.spec.ts +2 -2
  135. package/src/proxy-manager.spec.ts +6 -6
  136. package/src/request-action-implementation.ts +25 -0
  137. package/src/rest-api-runtime.spec.ts +1 -1
  138. package/src/server-telemetry.ts +6 -14
  139. package/src/user-resolution-cache.spec.ts +241 -0
  140. package/src/user-resolution-cache.ts +164 -0
  141. package/src/user-store.ts +1 -0
  142. package/src/validate.integration.spec.ts +1 -0
  143. package/src/validate.ts +14 -12
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Single source of truth for the cache key and cache tag shapes used by the
3
+ * identity-resolution path. Producer (`cookie-auth-provider.getCacheKey`),
4
+ * local invalidation (`HttpUserContext.cookieLogout`,
5
+ * `UserResolutionCache.invalidateByUser`), and any future
6
+ * `IdentityEventBus` subscribers all derive their strings here so the
7
+ * local-vs-replicated invalidation paths cannot drift.
8
+ */
9
+ /**
10
+ * Cache key shape for the cookie-authentication provider. Keying by
11
+ * session id is safe because session invalidation drops the session
12
+ * row and the {@link UserResolutionCache} TTL bounds staleness.
13
+ */
14
+ export type SessionCacheKey = `cookie:${string}`;
15
+ /**
16
+ * Tag attached to every {@link UserResolutionCache} entry, derived from
17
+ * the resolved user's `username`. Enables `removeByTag` invalidation
18
+ * across every cached session that resolved to a given user — used by
19
+ * `invalidateByUser` and (later) by `IdentityEventBus` subscribers
20
+ * acting on `userRolesChanged` / `userDeleted` / `passwordChanged`
21
+ * events.
22
+ */
23
+ export type UserCacheTag = `user:${string}`;
24
+ /** Builds the cookie-auth cache key for `sessionId`. */
25
+ export declare const sessionCacheKey: (sessionId: string) => SessionCacheKey;
26
+ /** Builds the user-resolution cache tag for `username`. */
27
+ export declare const userCacheTag: (username: string) => UserCacheTag;
28
+ //# sourceMappingURL=identity-cache-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-cache-keys.d.ts","sourceRoot":"","sources":["../src/identity-cache-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,MAAM,EAAE,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,MAAM,EAAE,CAAA;AAE3C,wDAAwD;AACxD,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,KAAG,eAAwC,CAAA;AAE5F,2DAA2D;AAC3D,eAAO,MAAM,YAAY,GAAI,UAAU,MAAM,KAAG,YAAkC,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Single source of truth for the cache key and cache tag shapes used by the
3
+ * identity-resolution path. Producer (`cookie-auth-provider.getCacheKey`),
4
+ * local invalidation (`HttpUserContext.cookieLogout`,
5
+ * `UserResolutionCache.invalidateByUser`), and any future
6
+ * `IdentityEventBus` subscribers all derive their strings here so the
7
+ * local-vs-replicated invalidation paths cannot drift.
8
+ */
9
+ /** Builds the cookie-auth cache key for `sessionId`. */
10
+ export const sessionCacheKey = (sessionId) => `cookie:${sessionId}`;
11
+ /** Builds the user-resolution cache tag for `username`. */
12
+ export const userCacheTag = (username) => `user:${username}`;
13
+ //# sourceMappingURL=identity-cache-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-cache-keys.js","sourceRoot":"","sources":["../src/identity-cache-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmBH,wDAAwD;AACxD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,SAAiB,EAAmB,EAAE,CAAC,UAAU,SAAS,EAAE,CAAA;AAE5F,2DAA2D;AAC3D,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAgB,EAAE,CAAC,QAAQ,QAAQ,EAAE,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=identity-cache-keys.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-cache-keys.spec.d.ts","sourceRoot":"","sources":["../src/identity-cache-keys.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { sessionCacheKey, userCacheTag } from './identity-cache-keys.js';
3
+ describe('identity-cache-keys', () => {
4
+ describe('sessionCacheKey', () => {
5
+ it('prefixes the session id with `cookie:`', () => {
6
+ expect(sessionCacheKey('abc')).toBe('cookie:abc');
7
+ });
8
+ it('preserves the empty string suffix shape', () => {
9
+ expect(sessionCacheKey('')).toBe('cookie:');
10
+ });
11
+ });
12
+ describe('userCacheTag', () => {
13
+ it('prefixes the username with `user:`', () => {
14
+ expect(userCacheTag('alice')).toBe('user:alice');
15
+ });
16
+ it('preserves the empty string suffix shape', () => {
17
+ expect(userCacheTag('')).toBe('user:');
18
+ });
19
+ });
20
+ });
21
+ //# sourceMappingURL=identity-cache-keys.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-cache-keys.spec.js","sourceRoot":"","sources":["../src/identity-cache-keys.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAExE,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;YACjD,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAC7C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;YACjD,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACxC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,102 @@
1
+ import { type Token } from '@furystack/inject';
2
+ import { UserResolutionCache } from './user-resolution-cache.js';
3
+ /**
4
+ * Wire topic the facade publishes to and subscribes from on the underlying
5
+ * {@link CrossNodeBus}. A single topic is used for all event types — internal
6
+ * dispatch happens by `event.type`. Keeping one topic minimises broker
7
+ * subscriptions on networked adapters (Redis Streams, NATS) and matches the
8
+ * EventHub-style fan-out apps already use.
9
+ */
10
+ export declare const IDENTITY_EVENT_TOPIC = "identity/events";
11
+ /**
12
+ * Discriminated union of cross-node identity events. Adding a new variant
13
+ * **must** also extend {@link applyLocalIdentityInvalidation} so the local
14
+ * cache stays in sync with the published intent.
15
+ *
16
+ * - `userLoggedOut` / `sessionInvalidated` invalidate the cache entry for a
17
+ * single session id (same shape — `sessionInvalidated` is meant for
18
+ * admin / forced flows where a session row is dropped server-side).
19
+ * - `userRolesChanged` / `userDeleted` / `passwordChanged` invalidate every
20
+ * cached entry that resolved to the named user, regardless of which
21
+ * session originally populated it.
22
+ */
23
+ export type IdentityEvent = {
24
+ type: 'userLoggedOut';
25
+ sessionId: string;
26
+ } | {
27
+ type: 'sessionInvalidated';
28
+ sessionId: string;
29
+ } | {
30
+ type: 'userRolesChanged';
31
+ username: string;
32
+ } | {
33
+ type: 'userDeleted';
34
+ username: string;
35
+ } | {
36
+ type: 'passwordChanged';
37
+ username: string;
38
+ };
39
+ /**
40
+ * Cross-node identity event facade. Bridges the per-process
41
+ * {@link UserResolutionCache} to the shared {@link CrossNodeBus} so that
42
+ * logouts, role flips, password resets and account deletions invalidate
43
+ * cached identities across every node, not just the one that originated
44
+ * the change.
45
+ *
46
+ * Publishing collapses the staleness window from `userCacheTtlMs`
47
+ * (default 30 s) to bus latency (single-digit ms with Redis pub/sub).
48
+ *
49
+ * **Self-delivery contract.** {@link IdentityEventBus.publish} applies the
50
+ * matching local invalidation and fires local subscribers synchronously
51
+ * **before** awaiting the bus. Sibling nodes receive the message via a
52
+ * `subscribeRemoteOnly` bridge — own publishes never round-trip through the
53
+ * transport, so duplicate work is impossible.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import type { Injector } from '@furystack/inject'
58
+ * import { IdentityEventBus } from '@furystack/rest-service'
59
+ *
60
+ * declare const injector: Injector
61
+ * const bus = injector.get(IdentityEventBus)
62
+ *
63
+ * using sub = bus.subscribe('userRolesChanged', ({ username }) => {
64
+ * console.log('roles changed for', username)
65
+ * })
66
+ *
67
+ * await bus.publish({ type: 'userRolesChanged', username: 'alice' })
68
+ * ```
69
+ */
70
+ export interface IdentityEventBus extends Disposable {
71
+ /**
72
+ * Publishes a typed identity event. Applies the matching local cache
73
+ * invalidation synchronously, fires local subscribers synchronously, then
74
+ * awaits the underlying bus broadcast. Resolves once the message has been
75
+ * accepted by the transport.
76
+ */
77
+ publish(event: IdentityEvent): Promise<void>;
78
+ /**
79
+ * Subscribes to a single event type. Local emits and bus-delivered messages
80
+ * from sibling nodes both fire the handler. Returns a `Disposable` that
81
+ * removes the listener on dispose.
82
+ */
83
+ subscribe<TType extends IdentityEvent['type']>(type: TType, handler: (event: Extract<IdentityEvent, {
84
+ type: TType;
85
+ }>) => void): Disposable;
86
+ }
87
+ /**
88
+ * Applies the cache invalidation that corresponds to `event` against
89
+ * `userCache`. Centralised so the publish path and the remote-receive path
90
+ * cannot drift — both call this single helper. Exported for the rare
91
+ * subsystem (e.g. websocket-api logout-driven socket close) that consumes
92
+ * an {@link IdentityEvent} and needs to mirror the cache effect.
93
+ */
94
+ export declare const applyLocalIdentityInvalidation: (event: IdentityEvent, userCache: UserResolutionCache) => void;
95
+ /**
96
+ * Singleton DI token for the {@link IdentityEventBus}. Resolves a facade that
97
+ * shares the injector's {@link CrossNodeBus} binding — apps that have not
98
+ * bound a transport adapter run against the in-process default and
99
+ * single-node deployments work without configuration.
100
+ */
101
+ export declare const IdentityEventBus: Token<IdentityEventBus, 'singleton'>;
102
+ //# sourceMappingURL=identity-event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-event-bus.d.ts","sourceRoot":"","sources":["../src/identity-event-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAE7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAA;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE5C;;;;OAIG;IACH,SAAS,CAAC,KAAK,SAAS,aAAa,CAAC,MAAM,CAAC,EAC3C,IAAI,EAAE,KAAK,EACX,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE,CAAC,KAAK,IAAI,GAChE,UAAU,CAAA;CACd;AAED;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,GAAI,OAAO,aAAa,EAAE,WAAW,mBAAmB,KAAG,IAmBrG,CAAA;AAoGD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,gBAAgB,EAAE,WAAW,CAahE,CAAA"}
@@ -0,0 +1,151 @@
1
+ import { CrossNodeBus } from '@furystack/cross-node-bus';
2
+ import { defineService } from '@furystack/inject';
3
+ import { sessionCacheKey } from './identity-cache-keys.js';
4
+ import { UserResolutionCache } from './user-resolution-cache.js';
5
+ /**
6
+ * Wire topic the facade publishes to and subscribes from on the underlying
7
+ * {@link CrossNodeBus}. A single topic is used for all event types — internal
8
+ * dispatch happens by `event.type`. Keeping one topic minimises broker
9
+ * subscriptions on networked adapters (Redis Streams, NATS) and matches the
10
+ * EventHub-style fan-out apps already use.
11
+ */
12
+ export const IDENTITY_EVENT_TOPIC = 'identity/events';
13
+ /**
14
+ * Applies the cache invalidation that corresponds to `event` against
15
+ * `userCache`. Centralised so the publish path and the remote-receive path
16
+ * cannot drift — both call this single helper. Exported for the rare
17
+ * subsystem (e.g. websocket-api logout-driven socket close) that consumes
18
+ * an {@link IdentityEvent} and needs to mirror the cache effect.
19
+ */
20
+ export const applyLocalIdentityInvalidation = (event, userCache) => {
21
+ switch (event.type) {
22
+ case 'userLoggedOut':
23
+ case 'sessionInvalidated':
24
+ userCache.invalidate(sessionCacheKey(event.sessionId));
25
+ return;
26
+ case 'userRolesChanged':
27
+ case 'userDeleted':
28
+ case 'passwordChanged':
29
+ userCache.invalidateByUser(event.username);
30
+ return;
31
+ default: {
32
+ // Exhaustiveness check: every variant of {@link IdentityEvent} above
33
+ // must be handled. Adding a new variant without updating this switch
34
+ // will fail to compile.
35
+ const _exhaustive = event;
36
+ void _exhaustive;
37
+ }
38
+ }
39
+ };
40
+ const isIdentityEvent = (value) => {
41
+ if (typeof value !== 'object' || value === null)
42
+ return false;
43
+ const candidate = value;
44
+ switch (candidate.type) {
45
+ case 'userLoggedOut':
46
+ case 'sessionInvalidated':
47
+ return typeof candidate.sessionId === 'string';
48
+ case 'userRolesChanged':
49
+ case 'userDeleted':
50
+ case 'passwordChanged':
51
+ return typeof candidate.username === 'string';
52
+ default:
53
+ return false;
54
+ }
55
+ };
56
+ class IdentityEventBusImpl {
57
+ #bus;
58
+ #userCache;
59
+ #handlers = new Map();
60
+ #busSubscription;
61
+ #disposed = false;
62
+ constructor(bus, userCache) {
63
+ this.#bus = bus;
64
+ this.#userCache = userCache;
65
+ // subscribeRemoteOnly: own publishes invalidate locally inline, so we
66
+ // only handle messages originating on sibling nodes here.
67
+ this.#busSubscription = bus.subscribeRemoteOnly(IDENTITY_EVENT_TOPIC, (message) => this.#receive(message));
68
+ }
69
+ async publish(event) {
70
+ if (this.#disposed) {
71
+ throw new Error('IdentityEventBus has been disposed');
72
+ }
73
+ applyLocalIdentityInvalidation(event, this.#userCache);
74
+ this.#emit(event);
75
+ await this.#bus.publish(IDENTITY_EVENT_TOPIC, event);
76
+ }
77
+ subscribe(type, handler) {
78
+ let handlers = this.#handlers.get(type);
79
+ if (!handlers) {
80
+ handlers = new Set();
81
+ this.#handlers.set(type, handlers);
82
+ }
83
+ const wrapped = (event) => handler(event);
84
+ handlers.add(wrapped);
85
+ return {
86
+ [Symbol.dispose]: () => {
87
+ const current = this.#handlers.get(type);
88
+ if (!current)
89
+ return;
90
+ current.delete(wrapped);
91
+ if (current.size === 0)
92
+ this.#handlers.delete(type);
93
+ },
94
+ };
95
+ }
96
+ [Symbol.dispose]() {
97
+ if (this.#disposed)
98
+ return;
99
+ this.#disposed = true;
100
+ this.#busSubscription[Symbol.dispose]();
101
+ this.#handlers.clear();
102
+ }
103
+ #receive(message) {
104
+ const { payload } = message;
105
+ if (!isIdentityEvent(payload)) {
106
+ // Foreign or future-version messages are silently ignored — older nodes
107
+ // see new event types as no-ops, which is the documented dual-accept
108
+ // behaviour from the cross-node-bus PRD §12.
109
+ return;
110
+ }
111
+ applyLocalIdentityInvalidation(payload, this.#userCache);
112
+ this.#emit(payload);
113
+ }
114
+ #emit(event) {
115
+ const handlers = this.#handlers.get(event.type);
116
+ if (!handlers)
117
+ return;
118
+ // Snapshot so a handler that disposes its own subscription mid-emit can't
119
+ // mutate the iterator. Listener errors are isolated per-handler and
120
+ // logged — one throwing handler must not poison its peers.
121
+ for (const handler of [...handlers]) {
122
+ try {
123
+ handler(event);
124
+ }
125
+ catch (error) {
126
+ console.error('IdentityEventBus listener threw', { type: event.type, error });
127
+ }
128
+ }
129
+ }
130
+ }
131
+ /**
132
+ * Singleton DI token for the {@link IdentityEventBus}. Resolves a facade that
133
+ * shares the injector's {@link CrossNodeBus} binding — apps that have not
134
+ * bound a transport adapter run against the in-process default and
135
+ * single-node deployments work without configuration.
136
+ */
137
+ export const IdentityEventBus = defineService({
138
+ name: 'furystack/rest-service/IdentityEventBus',
139
+ lifetime: 'singleton',
140
+ factory: ({ inject, onDispose }) => {
141
+ const bus = inject(CrossNodeBus);
142
+ const userCache = inject(UserResolutionCache);
143
+ const impl = new IdentityEventBusImpl(bus, userCache);
144
+ onDispose(() => {
145
+ // eslint-disable-next-line furystack/prefer-using-wrapper -- onDispose is the teardown hook
146
+ impl[Symbol.dispose]();
147
+ });
148
+ return impl;
149
+ },
150
+ });
151
+ //# sourceMappingURL=identity-event-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-event-bus.js","sourceRoot":"","sources":["../src/identity-event-bus.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=identity-event-bus.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-event-bus.spec.d.ts","sourceRoot":"","sources":["../src/identity-event-bus.spec.ts"],"names":[],"mappings":""}