@oneidentity/safeguard 7.4.0 → 8.0.0-pre36948

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/dist/events.js ADDED
@@ -0,0 +1,188 @@
1
+ //#region src/events/listener.ts
2
+ /**
3
+ * One-shot event listener wrapping a SignalR HubConnection.
4
+ * For persistent auto-reconnect behavior, use PersistentSafeguardEventListener.
5
+ */
6
+ var SafeguardEventListener = class {
7
+ #connection;
8
+ #handlers = [];
9
+ #state = "stopped";
10
+ constructor(connection) {
11
+ this.#connection = connection;
12
+ this.#connection.onclose(() => {
13
+ this.#setState("disconnected");
14
+ });
15
+ this.#connection.onreconnecting(() => {
16
+ this.#setState("reconnecting");
17
+ });
18
+ this.#connection.onreconnected(() => {
19
+ this.#setState("connected");
20
+ });
21
+ this.#connection.on("NotifyEventAsync", (event) => {
22
+ for (const handler of this.#handlers) handler(event);
23
+ });
24
+ }
25
+ /** Current connection state. */
26
+ get state() {
27
+ return this.#state;
28
+ }
29
+ /**
30
+ * Register an event handler for Safeguard notifications.
31
+ */
32
+ on(_method, handler) {
33
+ this.#handlers.push(handler);
34
+ return this;
35
+ }
36
+ /**
37
+ * Start the SignalR connection.
38
+ */
39
+ async start() {
40
+ this.#setState("starting");
41
+ await this.#connection.start();
42
+ this.#setState("connected");
43
+ }
44
+ /**
45
+ * Stop the SignalR connection.
46
+ */
47
+ async stop() {
48
+ await this.#connection.stop();
49
+ this.#setState("stopped");
50
+ }
51
+ #setState(state) {
52
+ this.#state = state;
53
+ }
54
+ };
55
+ //#endregion
56
+ //#region src/events/persistent.ts
57
+ const DEFAULT_RETRY_INTERVAL_MS = 5e3;
58
+ const TOKEN_REFRESH_MARGIN_MS = 6e4;
59
+ /**
60
+ * Persistent event listener with automatic reconnection and token refresh.
61
+ *
62
+ * Wraps SignalR's built-in `withAutomaticReconnect()` AND adds:
63
+ * - Token refresh on reconnect (re-authenticates if needed)
64
+ * - State change event notifications
65
+ * - Manual `stop()` that prevents further auto-reconnect
66
+ */
67
+ var PersistentSafeguardEventListener = class {
68
+ #connection;
69
+ #auth;
70
+ #host;
71
+ #httpClient;
72
+ #storage;
73
+ #retryIntervalMs;
74
+ #maxRetries;
75
+ #handlers = [];
76
+ #stateChangeHandlers = [];
77
+ #state = "stopped";
78
+ #tokenSet;
79
+ #manualStop = false;
80
+ #retryCount = 0;
81
+ #retryTimer;
82
+ constructor(connection, auth, host, httpClient, storage, options, tokenSet) {
83
+ this.#connection = connection;
84
+ this.#auth = auth;
85
+ this.#host = host;
86
+ this.#httpClient = httpClient;
87
+ this.#storage = storage;
88
+ this.#retryIntervalMs = options?.retryIntervalMs ?? DEFAULT_RETRY_INTERVAL_MS;
89
+ this.#maxRetries = options?.maxRetries;
90
+ this.#tokenSet = tokenSet;
91
+ this.#connection.onclose(() => {
92
+ this.#setState("disconnected");
93
+ if (!this.#manualStop) this.#scheduleReconnect();
94
+ });
95
+ this.#connection.onreconnecting(() => {
96
+ this.#setState("reconnecting");
97
+ });
98
+ this.#connection.onreconnected(() => {
99
+ this.#retryCount = 0;
100
+ this.#setState("connected");
101
+ });
102
+ this.#connection.on("NotifyEventAsync", (event) => {
103
+ for (const handler of this.#handlers) handler(event);
104
+ });
105
+ }
106
+ /** Current connection state. */
107
+ get state() {
108
+ return this.#state;
109
+ }
110
+ /**
111
+ * Register an event handler for Safeguard notifications.
112
+ */
113
+ on(_method, handler) {
114
+ this.#handlers.push(handler);
115
+ return this;
116
+ }
117
+ /**
118
+ * Register a state change handler.
119
+ */
120
+ onStateChange(handler) {
121
+ this.#stateChangeHandlers.push(handler);
122
+ return this;
123
+ }
124
+ /**
125
+ * Start the persistent connection.
126
+ */
127
+ async start() {
128
+ this.#manualStop = false;
129
+ this.#retryCount = 0;
130
+ this.#setState("starting");
131
+ await this.#connection.start();
132
+ this.#setState("connected");
133
+ }
134
+ /**
135
+ * Stop the connection. Prevents auto-reconnect.
136
+ */
137
+ async stop() {
138
+ this.#manualStop = true;
139
+ if (this.#retryTimer != null) {
140
+ clearTimeout(this.#retryTimer);
141
+ this.#retryTimer = void 0;
142
+ }
143
+ await this.#connection.stop();
144
+ this.#setState("stopped");
145
+ }
146
+ #setState(state) {
147
+ this.#state = state;
148
+ for (const handler of this.#stateChangeHandlers) handler(state);
149
+ }
150
+ async #scheduleReconnect() {
151
+ if (this.#manualStop) return;
152
+ if (this.#maxRetries != null && this.#retryCount >= this.#maxRetries) {
153
+ this.#setState("stopped");
154
+ return;
155
+ }
156
+ this.#setState("reconnecting");
157
+ this.#retryTimer = setTimeout(() => {
158
+ this.#reconnect();
159
+ }, this.#retryIntervalMs);
160
+ }
161
+ async #reconnect() {
162
+ if (this.#manualStop) return;
163
+ this.#retryCount++;
164
+ try {
165
+ if (this.#isTokenExpired()) if (this.#auth.refreshToken) {
166
+ const refreshed = await this.#auth.refreshToken(this.#host, this.#httpClient, this.#storage);
167
+ if (refreshed) this.#tokenSet = refreshed;
168
+ else this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
169
+ } else this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
170
+ await this.#connection.start();
171
+ this.#retryCount = 0;
172
+ this.#setState("connected");
173
+ } catch {
174
+ this.#scheduleReconnect();
175
+ }
176
+ }
177
+ #isTokenExpired() {
178
+ if (!this.#tokenSet) return true;
179
+ if (!this.#tokenSet.expiresIn) return true;
180
+ const elapsed = Date.now() - this.#tokenSet.acquiredAt;
181
+ const expiresInMs = this.#tokenSet.expiresIn * 1e3;
182
+ return elapsed + TOKEN_REFRESH_MARGIN_MS >= expiresInMs;
183
+ }
184
+ };
185
+ //#endregion
186
+ export { PersistentSafeguardEventListener, SafeguardEventListener };
187
+
188
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","names":["#connection","#handlers","#setState","#state","#connection","#auth","#host","#httpClient","#storage","#retryIntervalMs","#maxRetries","#handlers","#stateChangeHandlers","#tokenSet","#setState","#manualStop","#scheduleReconnect","#retryCount","#state","#retryTimer","#reconnect","#isTokenExpired"],"sources":["../src/events/listener.ts","../src/events/persistent.ts"],"sourcesContent":["import type { HubConnection } from '@microsoft/signalr';\nimport type { EventHandler, EventListenerState, SafeguardEvent } from './types.js';\n\n/**\n * One-shot event listener wrapping a SignalR HubConnection.\n * For persistent auto-reconnect behavior, use PersistentSafeguardEventListener.\n */\nexport class SafeguardEventListener {\n readonly #connection: HubConnection;\n readonly #handlers: EventHandler[] = [];\n #state: EventListenerState = 'stopped';\n\n constructor(connection: HubConnection) {\n this.#connection = connection;\n\n this.#connection.onclose(() => {\n this.#setState('disconnected');\n });\n\n this.#connection.onreconnecting(() => {\n this.#setState('reconnecting');\n });\n\n this.#connection.onreconnected(() => {\n this.#setState('connected');\n });\n\n // Register for the Safeguard event method\n this.#connection.on('NotifyEventAsync', (event: SafeguardEvent) => {\n for (const handler of this.#handlers) {\n handler(event);\n }\n });\n }\n\n /** Current connection state. */\n get state(): EventListenerState {\n return this.#state;\n }\n\n /**\n * Register an event handler for Safeguard notifications.\n */\n on(_method: 'NotifyEventAsync', handler: EventHandler): this {\n this.#handlers.push(handler);\n return this;\n }\n\n /**\n * Start the SignalR connection.\n */\n async start(): Promise<void> {\n this.#setState('starting');\n await this.#connection.start();\n this.#setState('connected');\n }\n\n /**\n * Stop the SignalR connection.\n */\n async stop(): Promise<void> {\n await this.#connection.stop();\n this.#setState('stopped');\n }\n\n #setState(state: EventListenerState): void {\n this.#state = state;\n }\n}\n","import type { HubConnection } from '@microsoft/signalr';\nimport type { Auth, TokenSet } from '../auth/types.js';\nimport type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type {\n EventHandler,\n EventListenerState,\n PersistentListenerOptions,\n SafeguardEvent,\n StateChangeHandler,\n} from './types.js';\n\nconst DEFAULT_RETRY_INTERVAL_MS = 5_000;\nconst TOKEN_REFRESH_MARGIN_MS = 60_000; // refresh 60s before expiry\n\n/**\n * Persistent event listener with automatic reconnection and token refresh.\n *\n * Wraps SignalR's built-in `withAutomaticReconnect()` AND adds:\n * - Token refresh on reconnect (re-authenticates if needed)\n * - State change event notifications\n * - Manual `stop()` that prevents further auto-reconnect\n */\nexport class PersistentSafeguardEventListener {\n readonly #connection: HubConnection;\n readonly #auth: Auth;\n readonly #host: string;\n readonly #httpClient: HttpClient;\n readonly #storage: StorageProvider;\n readonly #retryIntervalMs: number;\n readonly #maxRetries: number | undefined;\n readonly #handlers: EventHandler[] = [];\n readonly #stateChangeHandlers: StateChangeHandler[] = [];\n\n #state: EventListenerState = 'stopped';\n #tokenSet: TokenSet | undefined;\n #manualStop = false;\n #retryCount = 0;\n #retryTimer: ReturnType<typeof setTimeout> | undefined;\n\n constructor(\n connection: HubConnection,\n auth: Auth,\n host: string,\n httpClient: HttpClient,\n storage: StorageProvider,\n options?: PersistentListenerOptions,\n tokenSet?: TokenSet,\n ) {\n this.#connection = connection;\n this.#auth = auth;\n this.#host = host;\n this.#httpClient = httpClient;\n this.#storage = storage;\n this.#retryIntervalMs = options?.retryIntervalMs ?? DEFAULT_RETRY_INTERVAL_MS;\n this.#maxRetries = options?.maxRetries;\n this.#tokenSet = tokenSet;\n\n this.#connection.onclose(() => {\n this.#setState('disconnected');\n if (!this.#manualStop) {\n void this.#scheduleReconnect();\n }\n });\n\n this.#connection.onreconnecting(() => {\n this.#setState('reconnecting');\n });\n\n this.#connection.onreconnected(() => {\n this.#retryCount = 0;\n this.#setState('connected');\n });\n\n this.#connection.on('NotifyEventAsync', (event: SafeguardEvent) => {\n for (const handler of this.#handlers) {\n handler(event);\n }\n });\n }\n\n /** Current connection state. */\n get state(): EventListenerState {\n return this.#state;\n }\n\n /**\n * Register an event handler for Safeguard notifications.\n */\n on(_method: 'NotifyEventAsync', handler: EventHandler): this {\n this.#handlers.push(handler);\n return this;\n }\n\n /**\n * Register a state change handler.\n */\n onStateChange(handler: StateChangeHandler): this {\n this.#stateChangeHandlers.push(handler);\n return this;\n }\n\n /**\n * Start the persistent connection.\n */\n async start(): Promise<void> {\n this.#manualStop = false;\n this.#retryCount = 0;\n this.#setState('starting');\n await this.#connection.start();\n this.#setState('connected');\n }\n\n /**\n * Stop the connection. Prevents auto-reconnect.\n */\n async stop(): Promise<void> {\n this.#manualStop = true;\n if (this.#retryTimer != null) {\n clearTimeout(this.#retryTimer);\n this.#retryTimer = undefined;\n }\n await this.#connection.stop();\n this.#setState('stopped');\n }\n\n // ─── Private ────────────────────────────────────────────────────────\n\n #setState(state: EventListenerState): void {\n this.#state = state;\n for (const handler of this.#stateChangeHandlers) {\n handler(state);\n }\n }\n\n async #scheduleReconnect(): Promise<void> {\n if (this.#manualStop) return;\n if (this.#maxRetries != null && this.#retryCount >= this.#maxRetries) {\n this.#setState('stopped');\n return;\n }\n\n this.#setState('reconnecting');\n this.#retryTimer = setTimeout(() => {\n void this.#reconnect();\n }, this.#retryIntervalMs);\n }\n\n async #reconnect(): Promise<void> {\n if (this.#manualStop) return;\n this.#retryCount++;\n\n try {\n // Only refresh the token if it's expired or near-expiry\n if (this.#isTokenExpired()) {\n if (this.#auth.refreshToken) {\n const refreshed = await this.#auth.refreshToken(this.#host, this.#httpClient, this.#storage);\n if (refreshed) {\n this.#tokenSet = refreshed;\n } else {\n // Refresh failed — full re-auth\n this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);\n }\n } else {\n this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);\n }\n }\n\n await this.#connection.start();\n this.#retryCount = 0;\n this.#setState('connected');\n } catch {\n // Reconnect failed — try again\n void this.#scheduleReconnect();\n }\n }\n\n #isTokenExpired(): boolean {\n if (!this.#tokenSet) return true; // no token at all — must authenticate\n if (!this.#tokenSet.expiresIn) return true; // can't determine lifetime — refresh to be safe\n\n const elapsed = Date.now() - this.#tokenSet.acquiredAt;\n const expiresInMs = this.#tokenSet.expiresIn * 1000;\n return elapsed + TOKEN_REFRESH_MARGIN_MS >= expiresInMs;\n }\n}\n"],"mappings":"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"}