@furystack/rest-service 13.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 (311) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +47 -28
  3. package/esm/actions/error-action.d.ts +7 -2
  4. package/esm/actions/error-action.d.ts.map +1 -1
  5. package/esm/actions/error-action.js +7 -2
  6. package/esm/actions/error-action.js.map +1 -1
  7. package/esm/actions/get-current-user.d.ts +1 -4
  8. package/esm/actions/get-current-user.d.ts.map +1 -1
  9. package/esm/actions/get-current-user.js +1 -4
  10. package/esm/actions/get-current-user.js.map +1 -1
  11. package/esm/actions/get-current-user.spec.js +10 -6
  12. package/esm/actions/get-current-user.spec.js.map +1 -1
  13. package/esm/actions/is-authenticated.d.ts +3 -4
  14. package/esm/actions/is-authenticated.d.ts.map +1 -1
  15. package/esm/actions/is-authenticated.js +3 -4
  16. package/esm/actions/is-authenticated.js.map +1 -1
  17. package/esm/actions/is-authenticated.spec.js +10 -6
  18. package/esm/actions/is-authenticated.spec.js.map +1 -1
  19. package/esm/actions/logout-action.spec.js +11 -9
  20. package/esm/actions/logout-action.spec.js.map +1 -1
  21. package/esm/actions/logout.d.ts +2 -6
  22. package/esm/actions/logout.d.ts.map +1 -1
  23. package/esm/actions/logout.js +3 -7
  24. package/esm/actions/logout.js.map +1 -1
  25. package/esm/actions/not-found-action.d.ts +1 -3
  26. package/esm/actions/not-found-action.d.ts.map +1 -1
  27. package/esm/actions/not-found-action.js +1 -3
  28. package/esm/actions/not-found-action.js.map +1 -1
  29. package/esm/actions/password-login-action.d.ts +5 -12
  30. package/esm/actions/password-login-action.d.ts.map +1 -1
  31. package/esm/actions/password-login-action.js +6 -13
  32. package/esm/actions/password-login-action.js.map +1 -1
  33. package/esm/actions/password-login-action.spec.js +23 -28
  34. package/esm/actions/password-login-action.spec.js.map +1 -1
  35. package/esm/authenticate.d.ts +5 -11
  36. package/esm/authenticate.d.ts.map +1 -1
  37. package/esm/authenticate.js +6 -12
  38. package/esm/authenticate.js.map +1 -1
  39. package/esm/authenticate.spec.js +41 -33
  40. package/esm/authenticate.spec.js.map +1 -1
  41. package/esm/authentication-providers/authentication-provider.d.ts +17 -0
  42. package/esm/authentication-providers/authentication-provider.d.ts.map +1 -1
  43. package/esm/authentication-providers/basic-auth-provider.d.ts +7 -0
  44. package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -1
  45. package/esm/authentication-providers/basic-auth-provider.js +7 -0
  46. package/esm/authentication-providers/basic-auth-provider.js.map +1 -1
  47. package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -1
  48. package/esm/authentication-providers/cookie-auth-provider.js +8 -0
  49. package/esm/authentication-providers/cookie-auth-provider.js.map +1 -1
  50. package/esm/authorize.d.ts +5 -17
  51. package/esm/authorize.d.ts.map +1 -1
  52. package/esm/authorize.js +5 -17
  53. package/esm/authorize.js.map +1 -1
  54. package/esm/authorize.spec.js +34 -29
  55. package/esm/authorize.spec.js.map +1 -1
  56. package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
  57. package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
  58. package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
  59. package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
  60. package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
  61. package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
  62. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
  63. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
  64. package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
  65. package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
  66. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
  67. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
  68. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
  69. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
  70. package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
  71. package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
  72. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
  73. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  74. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -1
  75. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
  76. package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -1
  77. package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
  78. package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
  79. package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
  80. package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
  81. package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
  82. package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
  83. package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
  84. package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
  85. package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
  86. package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  87. package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
  88. package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
  89. package/esm/endpoint-generators/create-post-endpoint.js +8 -13
  90. package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
  91. package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
  92. package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  93. package/esm/endpoint-generators/utils.d.ts +17 -2
  94. package/esm/endpoint-generators/utils.d.ts.map +1 -1
  95. package/esm/endpoint-generators/utils.js +33 -20
  96. package/esm/endpoint-generators/utils.js.map +1 -1
  97. package/esm/get-schema-from-api.d.ts +1 -1
  98. package/esm/get-schema-from-api.d.ts.map +1 -1
  99. package/esm/get-schema-from-api.js +1 -0
  100. package/esm/get-schema-from-api.js.map +1 -1
  101. package/esm/get-schema-from-api.spec.js.map +1 -1
  102. package/esm/helpers.d.ts +54 -88
  103. package/esm/helpers.d.ts.map +1 -1
  104. package/esm/helpers.js +90 -92
  105. package/esm/helpers.js.map +1 -1
  106. package/esm/helpers.spec.d.ts +2 -1
  107. package/esm/helpers.spec.d.ts.map +1 -1
  108. package/esm/helpers.spec.js +41 -38
  109. package/esm/helpers.spec.js.map +1 -1
  110. package/esm/http-authentication-settings.d.ts +56 -12
  111. package/esm/http-authentication-settings.d.ts.map +1 -1
  112. package/esm/http-authentication-settings.js +27 -29
  113. package/esm/http-authentication-settings.js.map +1 -1
  114. package/esm/http-server-pool.d.ts +64 -0
  115. package/esm/http-server-pool.d.ts.map +1 -0
  116. package/esm/http-server-pool.js +95 -0
  117. package/esm/http-server-pool.js.map +1 -0
  118. package/esm/http-server-pool.spec.d.ts +2 -0
  119. package/esm/http-server-pool.spec.d.ts.map +1 -0
  120. package/esm/http-server-pool.spec.js +193 -0
  121. package/esm/http-server-pool.spec.js.map +1 -0
  122. package/esm/http-user-context.d.ts +34 -44
  123. package/esm/http-user-context.d.ts.map +1 -1
  124. package/esm/http-user-context.js +75 -95
  125. package/esm/http-user-context.js.map +1 -1
  126. package/esm/http-user-context.spec.d.ts +8 -2
  127. package/esm/http-user-context.spec.d.ts.map +1 -1
  128. package/esm/http-user-context.spec.js +226 -332
  129. package/esm/http-user-context.spec.js.map +1 -1
  130. package/esm/identity-cache-keys.d.ts +28 -0
  131. package/esm/identity-cache-keys.d.ts.map +1 -0
  132. package/esm/identity-cache-keys.js +13 -0
  133. package/esm/identity-cache-keys.js.map +1 -0
  134. package/esm/identity-cache-keys.spec.d.ts +2 -0
  135. package/esm/identity-cache-keys.spec.d.ts.map +1 -0
  136. package/esm/identity-cache-keys.spec.js +21 -0
  137. package/esm/identity-cache-keys.spec.js.map +1 -0
  138. package/esm/identity-event-bus.d.ts +102 -0
  139. package/esm/identity-event-bus.d.ts.map +1 -0
  140. package/esm/identity-event-bus.js +151 -0
  141. package/esm/identity-event-bus.js.map +1 -0
  142. package/esm/identity-event-bus.spec.d.ts +2 -0
  143. package/esm/identity-event-bus.spec.d.ts.map +1 -0
  144. package/esm/identity-event-bus.spec.js +392 -0
  145. package/esm/identity-event-bus.spec.js.map +1 -0
  146. package/esm/index.d.ts +9 -4
  147. package/esm/index.d.ts.map +1 -1
  148. package/esm/index.js +9 -4
  149. package/esm/index.js.map +1 -1
  150. package/esm/login-response-strategy.d.ts +11 -10
  151. package/esm/login-response-strategy.d.ts.map +1 -1
  152. package/esm/login-response-strategy.js +7 -6
  153. package/esm/login-response-strategy.js.map +1 -1
  154. package/esm/login-response-strategy.spec.js +25 -29
  155. package/esm/login-response-strategy.spec.js.map +1 -1
  156. package/esm/openapi/generate-openapi-document.spec.js.map +1 -1
  157. package/esm/proxy-manager.spec.js +152 -176
  158. package/esm/proxy-manager.spec.js.map +1 -1
  159. package/esm/proxy-runtime.d.ts +27 -0
  160. package/esm/proxy-runtime.d.ts.map +1 -0
  161. package/esm/proxy-runtime.js +45 -0
  162. package/esm/proxy-runtime.js.map +1 -0
  163. package/esm/proxy-runtime.spec.d.ts +2 -0
  164. package/esm/proxy-runtime.spec.d.ts.map +1 -0
  165. package/esm/proxy-runtime.spec.js +39 -0
  166. package/esm/proxy-runtime.spec.js.map +1 -0
  167. package/esm/request-action-implementation.d.ts +25 -0
  168. package/esm/request-action-implementation.d.ts.map +1 -1
  169. package/esm/request-action-implementation.js +9 -0
  170. package/esm/request-action-implementation.js.map +1 -1
  171. package/esm/rest-api-runtime.d.ts +76 -0
  172. package/esm/rest-api-runtime.d.ts.map +1 -0
  173. package/esm/rest-api-runtime.js +131 -0
  174. package/esm/rest-api-runtime.js.map +1 -0
  175. package/esm/rest-api-runtime.spec.d.ts +2 -0
  176. package/esm/rest-api-runtime.spec.d.ts.map +1 -0
  177. package/esm/rest-api-runtime.spec.js +58 -0
  178. package/esm/rest-api-runtime.spec.js.map +1 -0
  179. package/esm/rest-service.integration.spec.d.ts +1 -1
  180. package/esm/rest-service.integration.spec.d.ts.map +1 -1
  181. package/esm/rest-service.integration.spec.js +57 -80
  182. package/esm/rest-service.integration.spec.js.map +1 -1
  183. package/esm/rest.integration.test.js +26 -84
  184. package/esm/rest.integration.test.js.map +1 -1
  185. package/esm/server-telemetry.d.ts +49 -0
  186. package/esm/server-telemetry.d.ts.map +1 -0
  187. package/esm/server-telemetry.js +25 -0
  188. package/esm/server-telemetry.js.map +1 -0
  189. package/esm/server-telemetry.spec.d.ts +2 -0
  190. package/esm/server-telemetry.spec.d.ts.map +1 -0
  191. package/esm/server-telemetry.spec.js +52 -0
  192. package/esm/server-telemetry.spec.js.map +1 -0
  193. package/esm/static-server-manager.spec.js +60 -125
  194. package/esm/static-server-manager.spec.js.map +1 -1
  195. package/esm/static-site-runtime.d.ts +21 -0
  196. package/esm/static-site-runtime.d.ts.map +1 -0
  197. package/esm/static-site-runtime.js +48 -0
  198. package/esm/static-site-runtime.js.map +1 -0
  199. package/esm/static-site-runtime.spec.d.ts +2 -0
  200. package/esm/static-site-runtime.spec.d.ts.map +1 -0
  201. package/esm/static-site-runtime.spec.js +30 -0
  202. package/esm/static-site-runtime.spec.js.map +1 -0
  203. package/esm/user-resolution-cache.d.ts +65 -0
  204. package/esm/user-resolution-cache.d.ts.map +1 -0
  205. package/esm/user-resolution-cache.js +101 -0
  206. package/esm/user-resolution-cache.js.map +1 -0
  207. package/esm/user-resolution-cache.spec.d.ts +2 -0
  208. package/esm/user-resolution-cache.spec.d.ts.map +1 -0
  209. package/esm/user-resolution-cache.spec.js +220 -0
  210. package/esm/user-resolution-cache.spec.js.map +1 -0
  211. package/esm/user-store.d.ts +39 -0
  212. package/esm/user-store.d.ts.map +1 -0
  213. package/esm/user-store.js +62 -0
  214. package/esm/user-store.js.map +1 -0
  215. package/esm/user-store.spec.d.ts +2 -0
  216. package/esm/user-store.spec.d.ts.map +1 -0
  217. package/esm/user-store.spec.js +72 -0
  218. package/esm/user-store.spec.js.map +1 -0
  219. package/esm/validate.d.ts +13 -12
  220. package/esm/validate.d.ts.map +1 -1
  221. package/esm/validate.integration.spec.js +27 -28
  222. package/esm/validate.integration.spec.js.map +1 -1
  223. package/esm/validate.js +12 -0
  224. package/esm/validate.js.map +1 -1
  225. package/package.json +14 -12
  226. package/src/actions/error-action.ts +7 -3
  227. package/src/actions/get-current-user.spec.ts +11 -10
  228. package/src/actions/get-current-user.ts +1 -4
  229. package/src/actions/is-authenticated.spec.ts +12 -7
  230. package/src/actions/is-authenticated.ts +3 -4
  231. package/src/actions/logout-action.spec.ts +12 -12
  232. package/src/actions/logout.ts +3 -7
  233. package/src/actions/not-found-action.ts +1 -3
  234. package/src/actions/password-login-action.spec.ts +30 -30
  235. package/src/actions/password-login-action.ts +6 -13
  236. package/src/authenticate.spec.ts +45 -51
  237. package/src/authenticate.ts +6 -12
  238. package/src/authentication-providers/authentication-provider.ts +17 -0
  239. package/src/authentication-providers/basic-auth-provider.ts +7 -0
  240. package/src/authentication-providers/cookie-auth-provider.ts +8 -0
  241. package/src/authorize.spec.ts +37 -39
  242. package/src/authorize.ts +6 -17
  243. package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
  244. package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
  245. package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
  246. package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
  247. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
  248. package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
  249. package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -2
  250. package/src/endpoint-generators/create-get-schema-action.ts +1 -1
  251. package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
  252. package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
  253. package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
  254. package/src/endpoint-generators/create-post-endpoint.ts +13 -23
  255. package/src/endpoint-generators/utils.ts +36 -28
  256. package/src/get-schema-from-api.spec.ts +1 -1
  257. package/src/get-schema-from-api.ts +2 -1
  258. package/src/helpers.spec.ts +51 -41
  259. package/src/helpers.ts +140 -109
  260. package/src/http-authentication-settings.ts +78 -24
  261. package/src/http-server-pool.spec.ts +220 -0
  262. package/src/http-server-pool.ts +169 -0
  263. package/src/http-user-context.spec.ts +246 -378
  264. package/src/http-user-context.ts +122 -92
  265. package/src/identity-cache-keys.spec.ts +24 -0
  266. package/src/identity-cache-keys.ts +31 -0
  267. package/src/identity-event-bus.spec.ts +285 -0
  268. package/src/identity-event-bus.ts +230 -0
  269. package/src/index.ts +9 -4
  270. package/src/login-response-strategy.spec.ts +32 -31
  271. package/src/login-response-strategy.ts +14 -12
  272. package/src/openapi/generate-openapi-document.spec.ts +2 -2
  273. package/src/proxy-manager.spec.ts +158 -188
  274. package/src/proxy-runtime.spec.ts +50 -0
  275. package/src/proxy-runtime.ts +75 -0
  276. package/src/request-action-implementation.ts +25 -0
  277. package/src/rest-api-runtime.spec.ts +77 -0
  278. package/src/rest-api-runtime.ts +204 -0
  279. package/src/rest-service.integration.spec.ts +65 -85
  280. package/src/rest.integration.test.ts +27 -87
  281. package/src/server-telemetry.spec.ts +56 -0
  282. package/src/server-telemetry.ts +43 -0
  283. package/src/static-server-manager.spec.ts +63 -151
  284. package/src/static-site-runtime.spec.ts +39 -0
  285. package/src/static-site-runtime.ts +75 -0
  286. package/src/user-resolution-cache.spec.ts +241 -0
  287. package/src/user-resolution-cache.ts +164 -0
  288. package/src/user-store.spec.ts +86 -0
  289. package/src/user-store.ts +69 -0
  290. package/src/validate.integration.spec.ts +40 -35
  291. package/src/validate.ts +14 -12
  292. package/esm/api-manager.d.ts +0 -104
  293. package/esm/api-manager.d.ts.map +0 -1
  294. package/esm/api-manager.js +0 -179
  295. package/esm/api-manager.js.map +0 -1
  296. package/esm/proxy-manager.d.ts +0 -53
  297. package/esm/proxy-manager.d.ts.map +0 -1
  298. package/esm/proxy-manager.js +0 -84
  299. package/esm/proxy-manager.js.map +0 -1
  300. package/esm/server-manager.d.ts +0 -48
  301. package/esm/server-manager.d.ts.map +0 -1
  302. package/esm/server-manager.js +0 -100
  303. package/esm/server-manager.js.map +0 -1
  304. package/esm/static-server-manager.d.ts +0 -19
  305. package/esm/static-server-manager.d.ts.map +0 -1
  306. package/esm/static-server-manager.js +0 -65
  307. package/esm/static-server-manager.js.map +0 -1
  308. package/src/api-manager.ts +0 -304
  309. package/src/proxy-manager.ts +0 -102
  310. package/src/server-manager.ts +0 -135
  311. package/src/static-server-manager.ts +0 -90
@@ -0,0 +1,230 @@
1
+ import { CrossNodeBus, type BusMessage } from '@furystack/cross-node-bus'
2
+ import { defineService, type Token } from '@furystack/inject'
3
+ import { sessionCacheKey } from './identity-cache-keys.js'
4
+ import { UserResolutionCache } from './user-resolution-cache.js'
5
+
6
+ /**
7
+ * Wire topic the facade publishes to and subscribes from on the underlying
8
+ * {@link CrossNodeBus}. A single topic is used for all event types — internal
9
+ * dispatch happens by `event.type`. Keeping one topic minimises broker
10
+ * subscriptions on networked adapters (Redis Streams, NATS) and matches the
11
+ * EventHub-style fan-out apps already use.
12
+ */
13
+ export const IDENTITY_EVENT_TOPIC = 'identity/events'
14
+
15
+ /**
16
+ * Discriminated union of cross-node identity events. Adding a new variant
17
+ * **must** also extend {@link applyLocalIdentityInvalidation} so the local
18
+ * cache stays in sync with the published intent.
19
+ *
20
+ * - `userLoggedOut` / `sessionInvalidated` invalidate the cache entry for a
21
+ * single session id (same shape — `sessionInvalidated` is meant for
22
+ * admin / forced flows where a session row is dropped server-side).
23
+ * - `userRolesChanged` / `userDeleted` / `passwordChanged` invalidate every
24
+ * cached entry that resolved to the named user, regardless of which
25
+ * session originally populated it.
26
+ */
27
+ export type IdentityEvent =
28
+ | { type: 'userLoggedOut'; sessionId: string }
29
+ | { type: 'sessionInvalidated'; sessionId: string }
30
+ | { type: 'userRolesChanged'; username: string }
31
+ | { type: 'userDeleted'; username: string }
32
+ | { type: 'passwordChanged'; username: string }
33
+
34
+ /**
35
+ * Cross-node identity event facade. Bridges the per-process
36
+ * {@link UserResolutionCache} to the shared {@link CrossNodeBus} so that
37
+ * logouts, role flips, password resets and account deletions invalidate
38
+ * cached identities across every node, not just the one that originated
39
+ * the change.
40
+ *
41
+ * Publishing collapses the staleness window from `userCacheTtlMs`
42
+ * (default 30 s) to bus latency (single-digit ms with Redis pub/sub).
43
+ *
44
+ * **Self-delivery contract.** {@link IdentityEventBus.publish} applies the
45
+ * matching local invalidation and fires local subscribers synchronously
46
+ * **before** awaiting the bus. Sibling nodes receive the message via a
47
+ * `subscribeRemoteOnly` bridge — own publishes never round-trip through the
48
+ * transport, so duplicate work is impossible.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import type { Injector } from '@furystack/inject'
53
+ * import { IdentityEventBus } from '@furystack/rest-service'
54
+ *
55
+ * declare const injector: Injector
56
+ * const bus = injector.get(IdentityEventBus)
57
+ *
58
+ * using sub = bus.subscribe('userRolesChanged', ({ username }) => {
59
+ * console.log('roles changed for', username)
60
+ * })
61
+ *
62
+ * await bus.publish({ type: 'userRolesChanged', username: 'alice' })
63
+ * ```
64
+ */
65
+ export interface IdentityEventBus extends Disposable {
66
+ /**
67
+ * Publishes a typed identity event. Applies the matching local cache
68
+ * invalidation synchronously, fires local subscribers synchronously, then
69
+ * awaits the underlying bus broadcast. Resolves once the message has been
70
+ * accepted by the transport.
71
+ */
72
+ publish(event: IdentityEvent): Promise<void>
73
+
74
+ /**
75
+ * Subscribes to a single event type. Local emits and bus-delivered messages
76
+ * from sibling nodes both fire the handler. Returns a `Disposable` that
77
+ * removes the listener on dispose.
78
+ */
79
+ subscribe<TType extends IdentityEvent['type']>(
80
+ type: TType,
81
+ handler: (event: Extract<IdentityEvent, { type: TType }>) => void,
82
+ ): Disposable
83
+ }
84
+
85
+ /**
86
+ * Applies the cache invalidation that corresponds to `event` against
87
+ * `userCache`. Centralised so the publish path and the remote-receive path
88
+ * cannot drift — both call this single helper. Exported for the rare
89
+ * subsystem (e.g. websocket-api logout-driven socket close) that consumes
90
+ * an {@link IdentityEvent} and needs to mirror the cache effect.
91
+ */
92
+ export const applyLocalIdentityInvalidation = (event: IdentityEvent, userCache: UserResolutionCache): void => {
93
+ switch (event.type) {
94
+ case 'userLoggedOut':
95
+ case 'sessionInvalidated':
96
+ userCache.invalidate(sessionCacheKey(event.sessionId))
97
+ return
98
+ case 'userRolesChanged':
99
+ case 'userDeleted':
100
+ case 'passwordChanged':
101
+ userCache.invalidateByUser(event.username)
102
+ return
103
+ default: {
104
+ // Exhaustiveness check: every variant of {@link IdentityEvent} above
105
+ // must be handled. Adding a new variant without updating this switch
106
+ // will fail to compile.
107
+ const _exhaustive: never = event
108
+ void _exhaustive
109
+ }
110
+ }
111
+ }
112
+
113
+ const isIdentityEvent = (value: unknown): value is IdentityEvent => {
114
+ if (typeof value !== 'object' || value === null) return false
115
+ const candidate = value as { type?: unknown; sessionId?: unknown; username?: unknown }
116
+ switch (candidate.type) {
117
+ case 'userLoggedOut':
118
+ case 'sessionInvalidated':
119
+ return typeof candidate.sessionId === 'string'
120
+ case 'userRolesChanged':
121
+ case 'userDeleted':
122
+ case 'passwordChanged':
123
+ return typeof candidate.username === 'string'
124
+ default:
125
+ return false
126
+ }
127
+ }
128
+
129
+ type AnyIdentityHandler = (event: IdentityEvent) => void
130
+
131
+ class IdentityEventBusImpl implements IdentityEventBus {
132
+ readonly #bus: CrossNodeBus
133
+ readonly #userCache: UserResolutionCache
134
+ readonly #handlers: Map<IdentityEvent['type'], Set<AnyIdentityHandler>> = new Map()
135
+ readonly #busSubscription: Disposable
136
+ #disposed = false
137
+
138
+ constructor(bus: CrossNodeBus, userCache: UserResolutionCache) {
139
+ this.#bus = bus
140
+ this.#userCache = userCache
141
+ // subscribeRemoteOnly: own publishes invalidate locally inline, so we
142
+ // only handle messages originating on sibling nodes here.
143
+ this.#busSubscription = bus.subscribeRemoteOnly(IDENTITY_EVENT_TOPIC, (message) => this.#receive(message))
144
+ }
145
+
146
+ public async publish(event: IdentityEvent): Promise<void> {
147
+ if (this.#disposed) {
148
+ throw new Error('IdentityEventBus has been disposed')
149
+ }
150
+ applyLocalIdentityInvalidation(event, this.#userCache)
151
+ this.#emit(event)
152
+ await this.#bus.publish(IDENTITY_EVENT_TOPIC, event)
153
+ }
154
+
155
+ public subscribe<TType extends IdentityEvent['type']>(
156
+ type: TType,
157
+ handler: (event: Extract<IdentityEvent, { type: TType }>) => void,
158
+ ): Disposable {
159
+ let handlers = this.#handlers.get(type)
160
+ if (!handlers) {
161
+ handlers = new Set()
162
+ this.#handlers.set(type, handlers)
163
+ }
164
+ const wrapped: AnyIdentityHandler = (event) => handler(event as Extract<IdentityEvent, { type: TType }>)
165
+ handlers.add(wrapped)
166
+ return {
167
+ [Symbol.dispose]: () => {
168
+ const current = this.#handlers.get(type)
169
+ if (!current) return
170
+ current.delete(wrapped)
171
+ if (current.size === 0) this.#handlers.delete(type)
172
+ },
173
+ }
174
+ }
175
+
176
+ public [Symbol.dispose](): void {
177
+ if (this.#disposed) return
178
+ this.#disposed = true
179
+ this.#busSubscription[Symbol.dispose]()
180
+ this.#handlers.clear()
181
+ }
182
+
183
+ #receive(message: BusMessage): void {
184
+ const { payload } = message
185
+ if (!isIdentityEvent(payload)) {
186
+ // Foreign or future-version messages are silently ignored — older nodes
187
+ // see new event types as no-ops, which is the documented dual-accept
188
+ // behaviour from the cross-node-bus PRD §12.
189
+ return
190
+ }
191
+ applyLocalIdentityInvalidation(payload, this.#userCache)
192
+ this.#emit(payload)
193
+ }
194
+
195
+ #emit(event: IdentityEvent): void {
196
+ const handlers = this.#handlers.get(event.type)
197
+ if (!handlers) return
198
+ // Snapshot so a handler that disposes its own subscription mid-emit can't
199
+ // mutate the iterator. Listener errors are isolated per-handler and
200
+ // logged — one throwing handler must not poison its peers.
201
+ for (const handler of [...handlers]) {
202
+ try {
203
+ handler(event)
204
+ } catch (error) {
205
+ console.error('IdentityEventBus listener threw', { type: event.type, error })
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Singleton DI token for the {@link IdentityEventBus}. Resolves a facade that
213
+ * shares the injector's {@link CrossNodeBus} binding — apps that have not
214
+ * bound a transport adapter run against the in-process default and
215
+ * single-node deployments work without configuration.
216
+ */
217
+ export const IdentityEventBus: Token<IdentityEventBus, 'singleton'> = defineService({
218
+ name: 'furystack/rest-service/IdentityEventBus',
219
+ lifetime: 'singleton',
220
+ factory: ({ inject, onDispose }): IdentityEventBus => {
221
+ const bus = inject(CrossNodeBus)
222
+ const userCache = inject(UserResolutionCache)
223
+ const impl = new IdentityEventBusImpl(bus, userCache)
224
+ onDispose(() => {
225
+ // eslint-disable-next-line furystack/prefer-using-wrapper -- onDispose is the teardown hook
226
+ impl[Symbol.dispose]()
227
+ })
228
+ return impl
229
+ },
230
+ })
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from './actions/index.js'
2
2
  export * from './add-cors-header.js'
3
- export * from './api-manager.js'
4
3
  export * from './authenticate.js'
5
4
  export * from './authentication-providers/index.js'
6
5
  export * from './authorize.js'
@@ -8,15 +7,21 @@ export * from './endpoint-generators/index.js'
8
7
  export * from './get-schema-from-api.js'
9
8
  export * from './helpers.js'
10
9
  export * from './http-authentication-settings.js'
10
+ export * from './http-server-pool.js'
11
11
  export * from './http-user-context.js'
12
+ export * from './identity-cache-keys.js'
13
+ export * from './identity-event-bus.js'
12
14
  export * from './login-response-strategy.js'
13
15
  export * from './mime-types.js'
14
16
  export * from './models/index.js'
15
- export * from './proxy-manager.js'
17
+ export * from './proxy-runtime.js'
16
18
  export * from './read-post-body.js'
17
19
  export * from './request-action-implementation.js'
20
+ export * from './rest-api-runtime.js'
18
21
  export * from './schema-validator/index.js'
19
- export * from './server-manager.js'
20
22
  export * from './server-response-extensions.js'
21
- export * from './static-server-manager.js'
23
+ export * from './server-telemetry.js'
24
+ export * from './static-site-runtime.js'
25
+ export * from './user-resolution-cache.js'
26
+ export * from './user-store.js'
22
27
  export * from './validate.js'
@@ -1,25 +1,25 @@
1
- import { InMemoryStore, User, addStore } from '@furystack/core'
2
- import { Injector } from '@furystack/inject'
3
- import { getRepository } from '@furystack/repository'
4
- import { PasswordCredential, PasswordResetToken, usePasswordPolicy } from '@furystack/security'
1
+ import type { User } from '@furystack/core'
2
+ import { InMemoryStore, User as UserModel, useSystemIdentityContext } from '@furystack/core'
3
+ import { createInjector, type Injector } from '@furystack/inject'
4
+ import {
5
+ PasswordCredential,
6
+ PasswordCredentialStore,
7
+ PasswordResetToken,
8
+ PasswordResetTokenStore,
9
+ usePasswordPolicy,
10
+ } from '@furystack/security'
5
11
  import { usingAsync } from '@furystack/utils'
6
12
  import { describe, expect, it } from 'vitest'
7
-
8
13
  import { useHttpAuthentication } from './helpers.js'
9
14
  import { createCookieLoginStrategy } from './login-response-strategy.js'
10
15
  import { DefaultSession } from './models/default-session.js'
16
+ import { SessionDataSet, SessionStore, UserStore } from './user-store.js'
11
17
 
12
18
  const setupInjector = (i: Injector) => {
13
- addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' }))
14
- .addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
15
- .addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
16
- .addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
17
-
18
- const repo = getRepository(i)
19
- repo.createDataSet(User, 'username')
20
- repo.createDataSet(DefaultSession, 'sessionId')
21
- repo.createDataSet(PasswordCredential, 'userName')
22
- repo.createDataSet(PasswordResetToken, 'token')
19
+ i.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
20
+ i.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
21
+ i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
22
+ i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
23
23
 
24
24
  usePasswordPolicy(i)
25
25
  useHttpAuthentication(i)
@@ -28,8 +28,8 @@ const setupInjector = (i: Injector) => {
28
28
  describe('createCookieLoginStrategy', () => {
29
29
  const testUser: User = { username: 'testuser', roles: ['admin'] }
30
30
 
31
- it('Should return the user as the response body', async () => {
32
- await usingAsync(new Injector(), async (i) => {
31
+ it('returns the user as the response body', async () => {
32
+ await usingAsync(createInjector(), async (i) => {
33
33
  setupInjector(i)
34
34
  const strategy = createCookieLoginStrategy(i)
35
35
  const result = await strategy.createLoginResponse(testUser, i)
@@ -37,8 +37,8 @@ describe('createCookieLoginStrategy', () => {
37
37
  })
38
38
  })
39
39
 
40
- it('Should return status code 200', async () => {
41
- await usingAsync(new Injector(), async (i) => {
40
+ it('returns status code 200', async () => {
41
+ await usingAsync(createInjector(), async (i) => {
42
42
  setupInjector(i)
43
43
  const strategy = createCookieLoginStrategy(i)
44
44
  const result = await strategy.createLoginResponse(testUser, i)
@@ -46,8 +46,8 @@ describe('createCookieLoginStrategy', () => {
46
46
  })
47
47
  })
48
48
 
49
- it('Should include a Set-Cookie header with the session ID', async () => {
50
- await usingAsync(new Injector(), async (i) => {
49
+ it('emits a Set-Cookie header with the generated session id', async () => {
50
+ await usingAsync(createInjector(), async (i) => {
51
51
  setupInjector(i)
52
52
  const strategy = createCookieLoginStrategy(i)
53
53
  const result = await strategy.createLoginResponse(testUser, i)
@@ -59,23 +59,24 @@ describe('createCookieLoginStrategy', () => {
59
59
  })
60
60
  })
61
61
 
62
- it('Should persist the session in the DataSet', async () => {
63
- await usingAsync(new Injector(), async (i) => {
62
+ it('persists the session via SessionDataSet', async () => {
63
+ await usingAsync(createInjector(), async (i) => {
64
64
  setupInjector(i)
65
65
  const strategy = createCookieLoginStrategy(i)
66
66
  await strategy.createLoginResponse(testUser, i)
67
67
 
68
- const repo = getRepository(i)
69
- const sessionDataSet = repo.getDataSetFor(DefaultSession, 'sessionId')
70
- const sessions = await sessionDataSet.find(i, { filter: { username: { $eq: 'testuser' } } })
71
- expect(sessions).toHaveLength(1)
72
- expect(sessions[0].username).toBe('testuser')
73
- expect(sessions[0].sessionId).toBeTruthy()
68
+ await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
69
+ const sessionDataSet = systemScope.get(SessionDataSet)
70
+ const sessions = await sessionDataSet.find(systemScope, { filter: { username: { $eq: 'testuser' } } })
71
+ expect(sessions).toHaveLength(1)
72
+ expect(sessions[0].username).toBe('testuser')
73
+ expect(sessions[0].sessionId).toBeTruthy()
74
+ })
74
75
  })
75
76
  })
76
77
 
77
- it('Should create unique session IDs for each call', async () => {
78
- await usingAsync(new Injector(), async (i) => {
78
+ it('creates unique session ids for each call', async () => {
79
+ await usingAsync(createInjector(), async (i) => {
79
80
  setupInjector(i)
80
81
  const strategy = createCookieLoginStrategy(i)
81
82
 
@@ -3,20 +3,21 @@ import { useSystemIdentityContext } from '@furystack/core'
3
3
  import type { Injector } from '@furystack/inject'
4
4
  import { randomBytes } from 'crypto'
5
5
 
6
+ import type { createPasswordLoginAction } from './actions/password-login-action.js'
6
7
  import { HttpAuthenticationSettings } from './http-authentication-settings.js'
7
8
  import type { ActionResult } from './request-action-implementation.js'
8
9
  import { JsonResult } from './request-action-implementation.js'
9
10
 
10
11
  /**
11
12
  * A pluggable strategy that turns an authenticated {@link User} into an
12
- * {@link ActionResult} containing the session/token data for the client.
13
+ * {@link ActionResult} carrying the session/token payload for the client.
13
14
  *
14
- * Pass a concrete strategy to action factories like
15
- * {@link createPasswordLoginAction} or `createGoogleLoginAction` to
16
- * decouple the authentication mechanism from the session/token mechanism.
15
+ * Pass a concrete strategy to action factories such as
16
+ * {@link createPasswordLoginAction} or `createGoogleLoginAction` to decouple
17
+ * the authentication mechanism from the session/token mechanism.
17
18
  *
18
- * @typeParam TResult The shape of the response body (e.g. `User` for cookies,
19
- * `{ accessToken: string; refreshToken: string }` for JWT)
19
+ * @typeParam TResult The shape of the response body — e.g. `User` for
20
+ * cookies, `{ accessToken, refreshToken }` for JWT.
20
21
  */
21
22
  export type LoginResponseStrategy<TResult> = {
22
23
  createLoginResponse: (user: User, injector: Injector) => Promise<ActionResult<TResult>>
@@ -25,19 +26,20 @@ export type LoginResponseStrategy<TResult> = {
25
26
  /**
26
27
  * Creates a cookie-based {@link LoginResponseStrategy}.
27
28
  *
28
- * On each login it generates a random session ID, persists it in the session
29
- * DataSet, and returns the user with a `Set-Cookie` header.
29
+ * On each login it generates a random session id, persists it via the
30
+ * configured session DataSet and returns the user with a `Set-Cookie`
31
+ * response header.
30
32
  *
31
- * @param injector The root injector (must have {@link HttpAuthenticationSettings} configured)
32
- * @returns A strategy that returns `ActionResult<User>`
33
+ * @param injector - The injector scope (typically the root) that carries
34
+ * `HttpAuthenticationSettings` and a persistent session store.
33
35
  */
34
36
  export const createCookieLoginStrategy = (injector: Injector): LoginResponseStrategy<User> => {
35
- const settings = injector.getInstance(HttpAuthenticationSettings)
37
+ const settings = injector.get(HttpAuthenticationSettings)
36
38
  const systemInjector = useSystemIdentityContext({ injector, username: 'CookieLoginStrategy' })
39
+ const sessionDataSet = systemInjector.get(settings.sessionDataSet)
37
40
 
38
41
  return {
39
42
  createLoginResponse: async (user) => {
40
- const sessionDataSet = settings.getSessionDataSet(systemInjector)
41
43
  const sessionId = randomBytes(32).toString('hex')
42
44
  await sessionDataSet.add(systemInjector, { sessionId, username: user.username })
43
45
  return JsonResult(user, 200, {
@@ -275,7 +275,7 @@ describe('generateOpenApiDocument', () => {
275
275
  it('Should use generic object schema when no schemaName', () => {
276
276
  const api: ApiEndpointSchema['endpoints'] = {
277
277
  GET: {
278
- '/test': { path: '/test', isAuthenticated: false, schemaName: '', schema: null as unknown },
278
+ '/test': { path: '/test', isAuthenticated: false, schemaName: '', schema: null },
279
279
  },
280
280
  }
281
281
 
@@ -300,7 +300,7 @@ describe('generateOpenApiDocument', () => {
300
300
  it('Should not add schema to components when schema is falsy', () => {
301
301
  const api: ApiEndpointSchema['endpoints'] = {
302
302
  GET: {
303
- '/test': { path: '/test', isAuthenticated: false, schemaName: 'Empty', schema: null as unknown },
303
+ '/test': { path: '/test', isAuthenticated: false, schemaName: 'Empty', schema: null },
304
304
  },
305
305
  }
306
306