@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.
- package/CHANGELOG.md +124 -0
- package/README.md +47 -28
- package/esm/actions/error-action.d.ts +7 -2
- package/esm/actions/error-action.d.ts.map +1 -1
- package/esm/actions/error-action.js +7 -2
- package/esm/actions/error-action.js.map +1 -1
- package/esm/actions/get-current-user.d.ts +1 -4
- package/esm/actions/get-current-user.d.ts.map +1 -1
- package/esm/actions/get-current-user.js +1 -4
- package/esm/actions/get-current-user.js.map +1 -1
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/is-authenticated.d.ts +3 -4
- package/esm/actions/is-authenticated.d.ts.map +1 -1
- package/esm/actions/is-authenticated.js +3 -4
- package/esm/actions/is-authenticated.js.map +1 -1
- package/esm/actions/is-authenticated.spec.js +10 -6
- package/esm/actions/is-authenticated.spec.js.map +1 -1
- package/esm/actions/logout-action.spec.js +11 -9
- package/esm/actions/logout-action.spec.js.map +1 -1
- package/esm/actions/logout.d.ts +2 -6
- package/esm/actions/logout.d.ts.map +1 -1
- package/esm/actions/logout.js +3 -7
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/not-found-action.d.ts +1 -3
- package/esm/actions/not-found-action.d.ts.map +1 -1
- package/esm/actions/not-found-action.js +1 -3
- package/esm/actions/not-found-action.js.map +1 -1
- package/esm/actions/password-login-action.d.ts +5 -12
- package/esm/actions/password-login-action.d.ts.map +1 -1
- package/esm/actions/password-login-action.js +6 -13
- package/esm/actions/password-login-action.js.map +1 -1
- package/esm/actions/password-login-action.spec.js +23 -28
- package/esm/actions/password-login-action.spec.js.map +1 -1
- package/esm/authenticate.d.ts +5 -11
- package/esm/authenticate.d.ts.map +1 -1
- package/esm/authenticate.js +6 -12
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authentication-providers/authentication-provider.d.ts +17 -0
- package/esm/authentication-providers/authentication-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.d.ts +7 -0
- package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.js +7 -0
- package/esm/authentication-providers/basic-auth-provider.js.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.js +8 -0
- package/esm/authentication-providers/cookie-auth-provider.js.map +1 -1
- package/esm/authorize.d.ts +5 -17
- package/esm/authorize.d.ts.map +1 -1
- package/esm/authorize.js +5 -17
- package/esm/authorize.js.map +1 -1
- package/esm/authorize.spec.js +34 -29
- package/esm/authorize.spec.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +8 -13
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts +17 -2
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +33 -20
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/get-schema-from-api.d.ts +1 -1
- package/esm/get-schema-from-api.d.ts.map +1 -1
- package/esm/get-schema-from-api.js +1 -0
- package/esm/get-schema-from-api.js.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +54 -88
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +90 -92
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.d.ts +2 -1
- package/esm/helpers.spec.d.ts.map +1 -1
- package/esm/helpers.spec.js +41 -38
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +56 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +27 -29
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-server-pool.d.ts +64 -0
- package/esm/http-server-pool.d.ts.map +1 -0
- package/esm/http-server-pool.js +95 -0
- package/esm/http-server-pool.js.map +1 -0
- package/esm/http-server-pool.spec.d.ts +2 -0
- package/esm/http-server-pool.spec.d.ts.map +1 -0
- package/esm/http-server-pool.spec.js +193 -0
- package/esm/http-server-pool.spec.js.map +1 -0
- package/esm/http-user-context.d.ts +34 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +75 -95
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +8 -2
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +226 -332
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/identity-cache-keys.d.ts +28 -0
- package/esm/identity-cache-keys.d.ts.map +1 -0
- package/esm/identity-cache-keys.js +13 -0
- package/esm/identity-cache-keys.js.map +1 -0
- package/esm/identity-cache-keys.spec.d.ts +2 -0
- package/esm/identity-cache-keys.spec.d.ts.map +1 -0
- package/esm/identity-cache-keys.spec.js +21 -0
- package/esm/identity-cache-keys.spec.js.map +1 -0
- package/esm/identity-event-bus.d.ts +102 -0
- package/esm/identity-event-bus.d.ts.map +1 -0
- package/esm/identity-event-bus.js +151 -0
- package/esm/identity-event-bus.js.map +1 -0
- package/esm/identity-event-bus.spec.d.ts +2 -0
- package/esm/identity-event-bus.spec.d.ts.map +1 -0
- package/esm/identity-event-bus.spec.js +392 -0
- package/esm/identity-event-bus.spec.js.map +1 -0
- package/esm/index.d.ts +9 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +9 -4
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +11 -10
- package/esm/login-response-strategy.d.ts.map +1 -1
- package/esm/login-response-strategy.js +7 -6
- package/esm/login-response-strategy.js.map +1 -1
- package/esm/login-response-strategy.spec.js +25 -29
- package/esm/login-response-strategy.spec.js.map +1 -1
- package/esm/openapi/generate-openapi-document.spec.js.map +1 -1
- package/esm/proxy-manager.spec.js +152 -176
- package/esm/proxy-manager.spec.js.map +1 -1
- package/esm/proxy-runtime.d.ts +27 -0
- package/esm/proxy-runtime.d.ts.map +1 -0
- package/esm/proxy-runtime.js +45 -0
- package/esm/proxy-runtime.js.map +1 -0
- package/esm/proxy-runtime.spec.d.ts +2 -0
- package/esm/proxy-runtime.spec.d.ts.map +1 -0
- package/esm/proxy-runtime.spec.js +39 -0
- package/esm/proxy-runtime.spec.js.map +1 -0
- package/esm/request-action-implementation.d.ts +25 -0
- package/esm/request-action-implementation.d.ts.map +1 -1
- package/esm/request-action-implementation.js +9 -0
- package/esm/request-action-implementation.js.map +1 -1
- package/esm/rest-api-runtime.d.ts +76 -0
- package/esm/rest-api-runtime.d.ts.map +1 -0
- package/esm/rest-api-runtime.js +131 -0
- package/esm/rest-api-runtime.js.map +1 -0
- package/esm/rest-api-runtime.spec.d.ts +2 -0
- package/esm/rest-api-runtime.spec.d.ts.map +1 -0
- package/esm/rest-api-runtime.spec.js +58 -0
- package/esm/rest-api-runtime.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts +1 -1
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +57 -80
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +26 -84
- package/esm/rest.integration.test.js.map +1 -1
- package/esm/server-telemetry.d.ts +49 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +25 -0
- package/esm/server-telemetry.js.map +1 -0
- package/esm/server-telemetry.spec.d.ts +2 -0
- package/esm/server-telemetry.spec.d.ts.map +1 -0
- package/esm/server-telemetry.spec.js +52 -0
- package/esm/server-telemetry.spec.js.map +1 -0
- package/esm/static-server-manager.spec.js +60 -125
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/static-site-runtime.d.ts +21 -0
- package/esm/static-site-runtime.d.ts.map +1 -0
- package/esm/static-site-runtime.js +48 -0
- package/esm/static-site-runtime.js.map +1 -0
- package/esm/static-site-runtime.spec.d.ts +2 -0
- package/esm/static-site-runtime.spec.d.ts.map +1 -0
- package/esm/static-site-runtime.spec.js +30 -0
- package/esm/static-site-runtime.spec.js.map +1 -0
- package/esm/user-resolution-cache.d.ts +65 -0
- package/esm/user-resolution-cache.d.ts.map +1 -0
- package/esm/user-resolution-cache.js +101 -0
- package/esm/user-resolution-cache.js.map +1 -0
- package/esm/user-resolution-cache.spec.d.ts +2 -0
- package/esm/user-resolution-cache.spec.d.ts.map +1 -0
- package/esm/user-resolution-cache.spec.js +220 -0
- package/esm/user-resolution-cache.spec.js.map +1 -0
- package/esm/user-store.d.ts +39 -0
- package/esm/user-store.d.ts.map +1 -0
- package/esm/user-store.js +62 -0
- package/esm/user-store.js.map +1 -0
- package/esm/user-store.spec.d.ts +2 -0
- package/esm/user-store.spec.d.ts.map +1 -0
- package/esm/user-store.spec.js +72 -0
- package/esm/user-store.spec.js.map +1 -0
- package/esm/validate.d.ts +13 -12
- package/esm/validate.d.ts.map +1 -1
- package/esm/validate.integration.spec.js +27 -28
- package/esm/validate.integration.spec.js.map +1 -1
- package/esm/validate.js +12 -0
- package/esm/validate.js.map +1 -1
- package/package.json +14 -12
- package/src/actions/error-action.ts +7 -3
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/get-current-user.ts +1 -4
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/is-authenticated.ts +3 -4
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +3 -7
- package/src/actions/not-found-action.ts +1 -3
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +6 -13
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +6 -12
- package/src/authentication-providers/authentication-provider.ts +17 -0
- package/src/authentication-providers/basic-auth-provider.ts +7 -0
- package/src/authentication-providers/cookie-auth-provider.ts +8 -0
- package/src/authorize.spec.ts +37 -39
- package/src/authorize.ts +6 -17
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
- package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
- package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
- package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -2
- package/src/endpoint-generators/create-get-schema-action.ts +1 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
- package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
- package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
- package/src/endpoint-generators/create-post-endpoint.ts +13 -23
- package/src/endpoint-generators/utils.ts +36 -28
- package/src/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +2 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +78 -24
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +246 -378
- package/src/http-user-context.ts +122 -92
- package/src/identity-cache-keys.spec.ts +24 -0
- package/src/identity-cache-keys.ts +31 -0
- package/src/identity-event-bus.spec.ts +285 -0
- package/src/identity-event-bus.ts +230 -0
- package/src/index.ts +9 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +14 -12
- package/src/openapi/generate-openapi-document.spec.ts +2 -2
- package/src/proxy-manager.spec.ts +158 -188
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/request-action-implementation.ts +25 -0
- package/src/rest-api-runtime.spec.ts +77 -0
- package/src/rest-api-runtime.ts +204 -0
- package/src/rest-service.integration.spec.ts +65 -85
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +43 -0
- package/src/static-server-manager.spec.ts +63 -151
- package/src/static-site-runtime.spec.ts +39 -0
- package/src/static-site-runtime.ts +75 -0
- package/src/user-resolution-cache.spec.ts +241 -0
- package/src/user-resolution-cache.ts +164 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +69 -0
- package/src/validate.integration.spec.ts +40 -35
- package/src/validate.ts +14 -12
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -179
- package/esm/api-manager.js.map +0 -1
- package/esm/proxy-manager.d.ts +0 -53
- package/esm/proxy-manager.d.ts.map +0 -1
- package/esm/proxy-manager.js +0 -84
- package/esm/proxy-manager.js.map +0 -1
- package/esm/server-manager.d.ts +0 -48
- package/esm/server-manager.d.ts.map +0 -1
- package/esm/server-manager.js +0 -100
- package/esm/server-manager.js.map +0 -1
- package/esm/static-server-manager.d.ts +0 -19
- package/esm/static-server-manager.d.ts.map +0 -1
- package/esm/static-server-manager.js +0 -65
- package/esm/static-server-manager.js.map +0 -1
- package/src/api-manager.ts +0 -304
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- 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-
|
|
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 './
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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('
|
|
32
|
-
await usingAsync(
|
|
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('
|
|
41
|
-
await usingAsync(
|
|
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('
|
|
50
|
-
await usingAsync(
|
|
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('
|
|
63
|
-
await usingAsync(
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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('
|
|
78
|
-
await usingAsync(
|
|
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}
|
|
13
|
+
* {@link ActionResult} carrying the session/token payload for the client.
|
|
13
14
|
*
|
|
14
|
-
* Pass a concrete strategy to action factories
|
|
15
|
-
* {@link createPasswordLoginAction} or `createGoogleLoginAction` to
|
|
16
|
-
*
|
|
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
|
|
19
|
-
* `{ accessToken
|
|
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
|
|
29
|
-
* DataSet
|
|
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
|
|
32
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
303
|
+
'/test': { path: '/test', isAuthenticated: false, schemaName: 'Empty', schema: null },
|
|
304
304
|
},
|
|
305
305
|
}
|
|
306
306
|
|