@oneidentity/safeguard 8.0.1 → 8.0.2

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.d.cts CHANGED
@@ -30,7 +30,6 @@ interface PersistentListenerOptions {
30
30
  /** Maximum number of reconnect attempts before giving up. Default: unlimited. */
31
31
  maxRetries?: number;
32
32
  }
33
- //# sourceMappingURL=types.d.ts.map
34
33
  //#endregion
35
34
  //#region src/events/listener.d.ts
36
35
  /**
@@ -55,7 +54,6 @@ declare class SafeguardEventListener {
55
54
  */
56
55
  stop(): Promise<void>;
57
56
  }
58
- //# sourceMappingURL=listener.d.ts.map
59
57
  //#endregion
60
58
  //#region src/http/types.d.ts
61
59
  /**
@@ -82,7 +80,6 @@ interface HttpClient {
82
80
  request(options: HttpRequestOptions): Promise<HttpResponse>;
83
81
  dispose?(): void;
84
82
  }
85
- //# sourceMappingURL=types.d.ts.map
86
83
  //#endregion
87
84
  //#region src/storage/types.d.ts
88
85
  /**
@@ -94,7 +91,6 @@ interface StorageProvider {
94
91
  remove(key: string): void;
95
92
  clear(): void;
96
93
  }
97
- /** Well-known storage keys used by the SDK. */
98
94
  //#endregion
99
95
  //#region src/auth/types.d.ts
100
96
  /**
@@ -126,7 +122,6 @@ interface Auth {
126
122
  /** Human-readable description of this auth strategy (for logging). */
127
123
  readonly description: string;
128
124
  }
129
- //# sourceMappingURL=types.d.ts.map
130
125
  //#endregion
131
126
  //#region src/events/persistent.d.ts
132
127
  /**
@@ -159,8 +154,6 @@ declare class PersistentSafeguardEventListener {
159
154
  */
160
155
  stop(): Promise<void>;
161
156
  }
162
- //# sourceMappingURL=persistent.d.ts.map
163
-
164
157
  //#endregion
165
158
  export { type EventHandler, type EventListenerState, type PersistentListenerOptions, PersistentSafeguardEventListener, type SafeguardEvent, SafeguardEventListener, type StateChangeHandler };
166
- //# sourceMappingURL=events-zHsrmRBj.d.cts.map
159
+ //# sourceMappingURL=events.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.cts","names":[],"sources":["../src/events/types.ts","../src/events/listener.ts","../src/http/types.ts","../src/storage/types.ts","../src/secret.ts","../src/auth/types.ts","../src/events/persistent.ts"],"mappings":";;;;;;KAGY,kBAAA;;;;KAUA,YAAA,IAAgB,KAAqB,EAAd,cAAc;AAAjD;;;AAAA,KAKY,kBAAA,IAAsB,KAAyB,EAAlB,kBAAkB;AALV;AAKjD;;AALiD,UAUhC,cAAA;EACf,SAAA;EAAA,CACC,GAAW;AAAA;;;;UAMG,yBAAA;EAAA;EAEf,eAAA;;EAEA,UAAU;AAAA;;;;AAhCZ;;;cCIa,sBAAA;EAAA;cAKC,UAAA,EAAY,aAAA;EDCF;EAAA,ICuBlB,KAAA,CAAA,GAAS,kBAAA;EDvBoB;AAAc;AAKjD;ECyBE,EAAA,CAAG,OAAA,sBAA6B,OAAA,EAAS,YAAA;;;ADzBgB;ECiCnD,KAAA,CAAA,GAAS,OAAA;ED5Bc;;;ECqCvB,IAAA,CAAA,GAAQ,OAAA;AAAA;;;;;;UCxDC,kBAAA;EACf,GAAA;EACA,MAAA;EACA,OAAA,GAAU,MAAA;EACV,IAAA,YAAgB,MAAA;EAChB,MAAA,GAAS,WAAA;EACT,OAAA;AAAA;AAAA,UAGe,YAAA;EACf,MAAA;EACA,OAAA,EAAS,MAAM;EACf,IAAA;AAAA;;;;ADTF;UC6BiB,UAAA;EACf,OAAA,CAAQ,OAAA,EAAS,kBAAA,GAAqB,OAAA,CAAQ,YAAA;EAC9C,OAAA;AAAA;;;;;;UCnCe,eAAA;EACf,GAAA,CAAI,GAAA;EACJ,GAAA,CAAI,GAAA,UAAa,KAAA;EACjB,MAAA,CAAO,GAAA;EACP,KAAA;AAAA;;;;;;AHJF;;;;AAA8B;cIOjB,WAAA;EAAA;cAGC,KAAA;EJAqB;EIKjC,MAAA,CAAA;EAIA,QAAA,CAAA;EAIA,MAAA,CAAA;EAAA,KASK,MAAA,CAAO,WAAW;AAAA;;;AJhCzB;;;AAAA,UKIiB,QAAA;ELJa;EKM5B,WAAA,EAAa,WAAW;ELIF;EKFtB,SAAA;ELEiC;EKAjC,UAAA;AAAA;;;;UAMe,IAAA;ELIA;;;;AAEH;AAMd;EKLE,YAAA,CAAa,IAAA,UAAc,UAAA,EAAY,UAAA,EAAY,OAAA,EAAS,eAAA,GAAkB,OAAA,CAAQ,QAAA;;;ALS5E;EKJV,YAAA,EAAc,IAAA,UAAc,UAAA,EAAY,UAAA,EAAY,OAAA,EAAS,eAAA,GAAkB,OAAA,CAAQ,QAAA;;WAG9E,WAAA;AAAA;;;;;AL/BmB;AAU9B;;;;AAAiD;cMUpC,gCAAA;EAAA;cAoBT,UAAA,EAAY,aAAA,EACZ,IAAA,EAAM,IAAA,EACN,IAAA,UACA,UAAA,EAAY,UAAA,EACZ,OAAA,EAAS,eAAA,EACT,OAAA,GAAU,yBAAA,EACV,QAAA,GAAW,QAAA;EN/B0B;EAAA,IMkEnC,KAAA,CAAA,GAAS,kBAAA;EN7DE;;;EMoEf,EAAA,CAAG,OAAA,sBAA6B,OAAA,EAAS,YAAA;ENlE7B;AAMd;;EMoEE,aAAA,CAAc,OAAA,EAAS,kBAAA;ENlEvB;AAEU;;EMwEJ,KAAA,CAAA,GAAS,OAAA;;ALpGjB;;EK+GQ,IAAA,CAAA,GAAQ,OAAA;AAAA"}
package/dist/events.d.ts CHANGED
@@ -30,7 +30,6 @@ interface PersistentListenerOptions {
30
30
  /** Maximum number of reconnect attempts before giving up. Default: unlimited. */
31
31
  maxRetries?: number;
32
32
  }
33
- //# sourceMappingURL=types.d.ts.map
34
33
  //#endregion
35
34
  //#region src/events/listener.d.ts
36
35
  /**
@@ -55,7 +54,6 @@ declare class SafeguardEventListener {
55
54
  */
56
55
  stop(): Promise<void>;
57
56
  }
58
- //# sourceMappingURL=listener.d.ts.map
59
57
  //#endregion
60
58
  //#region src/http/types.d.ts
61
59
  /**
@@ -82,7 +80,6 @@ interface HttpClient {
82
80
  request(options: HttpRequestOptions): Promise<HttpResponse>;
83
81
  dispose?(): void;
84
82
  }
85
- //# sourceMappingURL=types.d.ts.map
86
83
  //#endregion
87
84
  //#region src/storage/types.d.ts
88
85
  /**
@@ -94,7 +91,6 @@ interface StorageProvider {
94
91
  remove(key: string): void;
95
92
  clear(): void;
96
93
  }
97
- /** Well-known storage keys used by the SDK. */
98
94
  //#endregion
99
95
  //#region src/auth/types.d.ts
100
96
  /**
@@ -126,7 +122,6 @@ interface Auth {
126
122
  /** Human-readable description of this auth strategy (for logging). */
127
123
  readonly description: string;
128
124
  }
129
- //# sourceMappingURL=types.d.ts.map
130
125
  //#endregion
131
126
  //#region src/events/persistent.d.ts
132
127
  /**
@@ -159,8 +154,6 @@ declare class PersistentSafeguardEventListener {
159
154
  */
160
155
  stop(): Promise<void>;
161
156
  }
162
- //# sourceMappingURL=persistent.d.ts.map
163
-
164
157
  //#endregion
165
158
  export { type EventHandler, type EventListenerState, type PersistentListenerOptions, PersistentSafeguardEventListener, type SafeguardEvent, SafeguardEventListener, type StateChangeHandler };
166
- //# sourceMappingURL=events-RcqBZBsR.d.ts.map
159
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","names":[],"sources":["../src/events/types.ts","../src/events/listener.ts","../src/http/types.ts","../src/storage/types.ts","../src/secret.ts","../src/auth/types.ts","../src/events/persistent.ts"],"mappings":";;;;;;KAGY,kBAAA;;;;KAUA,YAAA,IAAgB,KAAqB,EAAd,cAAc;AAAjD;;;AAAA,KAKY,kBAAA,IAAsB,KAAyB,EAAlB,kBAAkB;AALV;AAKjD;;AALiD,UAUhC,cAAA;EACf,SAAA;EAAA,CACC,GAAW;AAAA;;;;UAMG,yBAAA;EAAA;EAEf,eAAA;;EAEA,UAAU;AAAA;;;;AAhCZ;;;cCIa,sBAAA;EAAA;cAKC,UAAA,EAAY,aAAA;EDCF;EAAA,ICuBlB,KAAA,CAAA,GAAS,kBAAA;EDvBoB;AAAc;AAKjD;ECyBE,EAAA,CAAG,OAAA,sBAA6B,OAAA,EAAS,YAAA;;;ADzBgB;ECiCnD,KAAA,CAAA,GAAS,OAAA;ED5Bc;;;ECqCvB,IAAA,CAAA,GAAQ,OAAA;AAAA;;;;;;UCxDC,kBAAA;EACf,GAAA;EACA,MAAA;EACA,OAAA,GAAU,MAAA;EACV,IAAA,YAAgB,MAAA;EAChB,MAAA,GAAS,WAAA;EACT,OAAA;AAAA;AAAA,UAGe,YAAA;EACf,MAAA;EACA,OAAA,EAAS,MAAM;EACf,IAAA;AAAA;;;;ADTF;UC6BiB,UAAA;EACf,OAAA,CAAQ,OAAA,EAAS,kBAAA,GAAqB,OAAA,CAAQ,YAAA;EAC9C,OAAA;AAAA;;;;;;UCnCe,eAAA;EACf,GAAA,CAAI,GAAA;EACJ,GAAA,CAAI,GAAA,UAAa,KAAA;EACjB,MAAA,CAAO,GAAA;EACP,KAAA;AAAA;;;;;;AHJF;;;;AAA8B;cIOjB,WAAA;EAAA;cAGC,KAAA;EJAqB;EIKjC,MAAA,CAAA;EAIA,QAAA,CAAA;EAIA,MAAA,CAAA;EAAA,KASK,MAAA,CAAO,WAAW;AAAA;;;AJhCzB;;;AAAA,UKIiB,QAAA;ELJa;EKM5B,WAAA,EAAa,WAAW;ELIF;EKFtB,SAAA;ELEiC;EKAjC,UAAA;AAAA;;;;UAMe,IAAA;ELIA;;;;AAEH;AAMd;EKLE,YAAA,CAAa,IAAA,UAAc,UAAA,EAAY,UAAA,EAAY,OAAA,EAAS,eAAA,GAAkB,OAAA,CAAQ,QAAA;;;ALS5E;EKJV,YAAA,EAAc,IAAA,UAAc,UAAA,EAAY,UAAA,EAAY,OAAA,EAAS,eAAA,GAAkB,OAAA,CAAQ,QAAA;;WAG9E,WAAA;AAAA;;;;;AL/BmB;AAU9B;;;;AAAiD;cMUpC,gCAAA;EAAA;cAoBT,UAAA,EAAY,aAAA,EACZ,IAAA,EAAM,IAAA,EACN,IAAA,UACA,UAAA,EAAY,UAAA,EACZ,OAAA,EAAS,eAAA,EACT,OAAA,GAAU,yBAAA,EACV,QAAA,GAAW,QAAA;EN/B0B;EAAA,IMkEnC,KAAA,CAAA,GAAS,kBAAA;EN7DE;;;EMoEf,EAAA,CAAG,OAAA,sBAA6B,OAAA,EAAS,YAAA;ENlE7B;AAMd;;EMoEE,aAAA,CAAc,OAAA,EAAS,kBAAA;ENlEvB;AAEU;;EMwEJ,KAAA,CAAA,GAAS,OAAA;;ALpGjB;;EK+GQ,IAAA,CAAA,GAAQ,OAAA;AAAA"}
package/dist/events.js CHANGED
@@ -79,6 +79,8 @@ var PersistentSafeguardEventListener = class {
79
79
  #manualStop = false;
80
80
  #retryCount = 0;
81
81
  #retryTimer;
82
+ /** Single-flight token refresh promise to prevent thundering herd. */
83
+ #tokenRefreshPromise;
82
84
  constructor(connection, auth, host, httpClient, storage, options, tokenSet) {
83
85
  this.#connection = connection;
84
86
  this.#auth = auth;
@@ -162,11 +164,7 @@ var PersistentSafeguardEventListener = class {
162
164
  if (this.#manualStop) return;
163
165
  this.#retryCount++;
164
166
  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);
167
+ if (this.#isTokenExpired()) await this.#refreshTokenSingleFlight();
170
168
  await this.#connection.start();
171
169
  this.#retryCount = 0;
172
170
  this.#setState("connected");
@@ -174,6 +172,27 @@ var PersistentSafeguardEventListener = class {
174
172
  this.#scheduleReconnect();
175
173
  }
176
174
  }
175
+ async #refreshTokenSingleFlight() {
176
+ if (this.#tokenRefreshPromise) {
177
+ await this.#tokenRefreshPromise;
178
+ return;
179
+ }
180
+ this.#tokenRefreshPromise = (async () => {
181
+ if (this.#auth.refreshToken) {
182
+ const refreshed = await this.#auth.refreshToken(this.#host, this.#httpClient, this.#storage);
183
+ if (refreshed) {
184
+ this.#tokenSet = refreshed;
185
+ return;
186
+ }
187
+ }
188
+ this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
189
+ })();
190
+ try {
191
+ await this.#tokenRefreshPromise;
192
+ } finally {
193
+ this.#tokenRefreshPromise = void 0;
194
+ }
195
+ }
177
196
  #isTokenExpired() {
178
197
  if (!this.#tokenSet) return true;
179
198
  if (!this.#tokenSet.expiresIn) return true;
@@ -1 +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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"}
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","#refreshTokenSingleFlight","#tokenRefreshPromise"],"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 /** Single-flight token refresh promise to prevent thundering herd. */\n #tokenRefreshPromise: Promise<void> | 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. Single-flight\n // so overlapping reconnect attempts don't double-call refreshToken or\n // trample the storage provider.\n if (this.#isTokenExpired()) {\n await this.#refreshTokenSingleFlight();\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 async #refreshTokenSingleFlight(): Promise<void> {\n if (this.#tokenRefreshPromise) {\n await this.#tokenRefreshPromise;\n return;\n }\n this.#tokenRefreshPromise = (async (): Promise<void> => {\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 return;\n }\n }\n this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);\n })();\n try {\n await this.#tokenRefreshPromise;\n } finally {\n this.#tokenRefreshPromise = undefined;\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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"}
package/dist/index.cjs CHANGED
@@ -684,7 +684,7 @@ var PkceNonInteractiveAuth = class {
684
684
  const state = generateState();
685
685
  const redirectUri = `http://localhost:${String(this.#callbackPort)}/callback`;
686
686
  const server = createServer();
687
- server.listen(this.#callbackPort);
687
+ server.listen(this.#callbackPort, "127.0.0.1");
688
688
  const authUrl = `https://${host}/RSTS/Login?${new URLSearchParams({
689
689
  response_type: "code",
690
690
  client_id: "safeguard.js",
@@ -875,6 +875,8 @@ var SafeguardClient = class {
875
875
  #storage;
876
876
  #tokenSet;
877
877
  #connected = false;
878
+ /** Single-flight refresh promise to prevent thundering herd on token expiry. */
879
+ #tokenRefreshPromise;
878
880
  constructor(host, options) {
879
881
  if (!host) throw new ConfigurationError("SafeguardClient requires a host");
880
882
  if (!options.auth) throw new ConfigurationError("SafeguardClient requires an auth strategy");
@@ -1010,7 +1012,12 @@ var SafeguardClient = class {
1010
1012
  if (!this.#tokenSet.expiresIn) return;
1011
1013
  const elapsed = Date.now() - this.#tokenSet.acquiredAt;
1012
1014
  const expiresInMs = this.#tokenSet.expiresIn * 1e3;
1013
- if (elapsed + TOKEN_REFRESH_MARGIN_MS >= expiresInMs) {
1015
+ if (elapsed + TOKEN_REFRESH_MARGIN_MS < expiresInMs) return;
1016
+ if (this.#tokenRefreshPromise) {
1017
+ await this.#tokenRefreshPromise;
1018
+ return;
1019
+ }
1020
+ this.#tokenRefreshPromise = (async () => {
1014
1021
  if (this.#auth.refreshToken) {
1015
1022
  const refreshed = await this.#auth.refreshToken(this.#host, this.#httpClient, this.#storage);
1016
1023
  if (refreshed) {
@@ -1019,6 +1026,11 @@ var SafeguardClient = class {
1019
1026
  }
1020
1027
  }
1021
1028
  this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
1029
+ })();
1030
+ try {
1031
+ await this.#tokenRefreshPromise;
1032
+ } finally {
1033
+ this.#tokenRefreshPromise = void 0;
1022
1034
  }
1023
1035
  }
1024
1036
  };