@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/README.md CHANGED
@@ -331,25 +331,72 @@ const raw: string = tokenSet.accessToken.expose(); // explicit opt-in
331
331
 
332
332
  ### TLS Verification
333
333
 
334
- TLS certificate verification is **enabled by default** and should never be
335
- disabled in production. For development environments with self-signed
336
- certificates:
334
+ TLS certificate verification is **enabled by default** in this SDK. The
335
+ Node-side `NodeHttpClient` constructs an `undici.Agent` with
336
+ `rejectUnauthorized: true`, so connections to an appliance whose certificate
337
+ chain does not validate are refused at the TLS layer.
338
+
339
+ #### Production: provide a custom CA bundle
340
+
341
+ The correct way to talk to an appliance whose certificate is issued by a
342
+ private / corporate CA is to provide the CA bundle to the SDK — not to
343
+ disable verification:
337
344
 
338
345
  ```typescript
339
- // Development only — never use in production
340
- const client = new SafeguardClient('dev-appliance.local', {
341
- auth: new PasswordAuth({ ... }),
342
- verify: false,
346
+ import { readFileSync } from 'node:fs';
347
+ import { SafeguardClient, NodeHttpClient, PasswordAuth } from '@oneidentity/safeguard';
348
+
349
+ const ca = readFileSync('/etc/ssl/corp-root-ca.pem');
350
+
351
+ const client = new SafeguardClient('safeguard.corp.example', {
352
+ auth: new PasswordAuth({ /* ... */ }),
343
353
  });
354
+ client.setHttpClient(new NodeHttpClient({ ca, rejectUnauthorized: true }));
355
+ await client.connect();
344
356
  ```
345
357
 
346
- For production with a custom CA:
358
+ `NodeHttpClient` accepts a PEM string or `Buffer` (single cert or
359
+ concatenated bundle). Verification stays on; only the trust anchor changes.
360
+
361
+ #### Development / lab appliances (self-signed)
362
+
363
+ For local appliances with self-signed certificates the supported opt-out is
364
+ the per-instance `rejectUnauthorized: false` flag on the HTTP client:
347
365
 
348
366
  ```typescript
349
- const client = new SafeguardClient('safeguard.corp', { auth });
350
- client.setHttpClient(new NodeHttpClient({ ca: '/path/to/ca-bundle.pem' }));
367
+ // Development only — never use in production
368
+ client.setHttpClient(new NodeHttpClient({ rejectUnauthorized: false }));
351
369
  ```
352
370
 
371
+ This affects only this `SafeguardClient` instance.
372
+
373
+ #### The `NODE_TLS_REJECT_UNAUTHORIZED` environment variable
374
+
375
+ Node.js honours the process-wide
376
+ [`NODE_TLS_REJECT_UNAUTHORIZED=0`](https://nodejs.org/api/cli.html#node_tls_reject_unauthorizedvalue)
377
+ environment variable at the TLS layer below `undici`. If that variable is
378
+ set when your program starts, **all TLS verification in the entire Node
379
+ process is disabled** — including this SDK's connections, and any other
380
+ HTTPS calls your application makes (telemetry, package mirrors, third-party
381
+ APIs, etc.). Node itself prints a one-time warning when the variable is
382
+ honoured.
383
+
384
+ This SDK deliberately does **not** override or unset this variable: it is a
385
+ documented Node.js mechanism that operators sometimes set intentionally in
386
+ CI or dev shells, and silently re-enabling verification from the library
387
+ would be surprising. But you should be aware that:
388
+
389
+ - It is **process-wide**, not SDK-specific. Setting it to bypass a lab
390
+ appliance also exposes every other outbound HTTPS call in the same
391
+ process.
392
+ - It cannot be re-enabled per-connection from JavaScript once set — even
393
+ `new NodeHttpClient({ rejectUnauthorized: true })` is overridden by the
394
+ env var.
395
+ - It must not be set in production. Prefer the custom-CA approach above.
396
+
397
+ If you find `NODE_TLS_REJECT_UNAUTHORIZED=0` in a deployment environment,
398
+ treat it as a finding to remediate, not as a working configuration.
399
+
353
400
  ### Host Validation
354
401
 
355
402
  The `SafeguardClient` constructor validates the `host` parameter to prevent
package/dist/browser.d.ts CHANGED
@@ -23,7 +23,6 @@ interface HttpClient {
23
23
  request(options: HttpRequestOptions): Promise<HttpResponse>;
24
24
  dispose?(): void;
25
25
  }
26
- //# sourceMappingURL=types.d.ts.map
27
26
  //#endregion
28
27
  //#region src/storage/types.d.ts
29
28
  /**
@@ -40,7 +39,6 @@ declare const StorageKeys: {
40
39
  readonly STATE: "safeguard_state";
41
40
  readonly CODE_VERIFIER: "safeguard_code_verifier";
42
41
  };
43
- //# sourceMappingURL=types.d.ts.map
44
42
  //#endregion
45
43
  //#region src/secret.d.ts
46
44
  /**
@@ -60,7 +58,6 @@ declare class SecretValue {
60
58
  toJSON(): string;
61
59
  get [Symbol.toStringTag](): string;
62
60
  }
63
- //# sourceMappingURL=secret.d.ts.map
64
61
  //#endregion
65
62
  //#region src/auth/types.d.ts
66
63
  /**
@@ -92,7 +89,6 @@ interface Auth {
92
89
  /** Human-readable description of this auth strategy (for logging). */
93
90
  readonly description: string;
94
91
  }
95
- //# sourceMappingURL=types.d.ts.map
96
92
  //#endregion
97
93
  //#region src/types.d.ts
98
94
  /**
@@ -104,7 +100,7 @@ declare enum Service {
104
100
  NOTIFICATION = "notification",
105
101
  A2A = "a2a",
106
102
  EVENT = "event",
107
- RSTS = "RSTS",
103
+ RSTS = "RSTS"
108
104
  }
109
105
  /**
110
106
  * HTTP methods supported by the Safeguard API.
@@ -114,7 +110,7 @@ declare enum HttpMethod {
114
110
  POST = "POST",
115
111
  PUT = "PUT",
116
112
  DELETE = "DELETE",
117
- PATCH = "PATCH",
113
+ PATCH = "PATCH"
118
114
  }
119
115
  /**
120
116
  * Full response from a Safeguard API call (returned when fullResponse: true).
@@ -160,7 +156,6 @@ interface SafeguardClientOptions {
160
156
  /** Auto-refresh tokens before expiry. Default: true. */
161
157
  autoRefresh?: boolean;
162
158
  }
163
- //# sourceMappingURL=types.d.ts.map
164
159
  //#endregion
165
160
  //#region src/errors.d.ts
166
161
  /**
@@ -211,7 +206,6 @@ declare class TransportError extends SafeguardError {
211
206
  declare class ConfigurationError extends SafeguardError {
212
207
  constructor(message: string, options?: ErrorOptions);
213
208
  }
214
- //# sourceMappingURL=errors.d.ts.map
215
209
  //#endregion
216
210
  //#region src/utils.d.ts
217
211
  /**
@@ -248,10 +242,6 @@ declare function generateCodeChallenge(verifier: string): Promise<string>;
248
242
  * Generates a random state parameter for OAuth flows.
249
243
  */
250
244
  declare function generateState(): string;
251
- /**
252
- * Extracts the `exp` claim from a JWT and returns expiresIn (seconds from now).
253
- * Returns undefined if the token is not a valid JWT or has no `exp` claim.
254
- */
255
245
  //#endregion
256
246
  //#region src/storage/memory.d.ts
257
247
  /**
@@ -265,7 +255,6 @@ declare class MemoryStorage implements StorageProvider {
265
255
  remove(key: string): void;
266
256
  clear(): void;
267
257
  }
268
- //# sourceMappingURL=memory.d.ts.map
269
258
  //#endregion
270
259
  //#region src/storage/session.d.ts
271
260
  /**
@@ -278,7 +267,6 @@ declare class BrowserSessionStorage implements StorageProvider {
278
267
  remove(key: string): void;
279
268
  clear(): void;
280
269
  }
281
- //# sourceMappingURL=session.d.ts.map
282
270
  //#endregion
283
271
  //#region src/http/browser.d.ts
284
272
  /**
@@ -292,7 +280,6 @@ declare class BrowserHttpClient implements HttpClient {
292
280
  });
293
281
  request(options: HttpRequestOptions): Promise<HttpResponse>;
294
282
  }
295
- //# sourceMappingURL=browser.d.ts.map
296
283
  //#endregion
297
284
  //#region src/auth/password.d.ts
298
285
  interface PasswordAuthOptions {
@@ -311,7 +298,6 @@ declare class PasswordAuth implements Auth {
311
298
  authenticate(host: string, httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet>;
312
299
  refreshToken(host: string, httpClient: HttpClient, storage: StorageProvider): Promise<TokenSet | null>;
313
300
  }
314
- //# sourceMappingURL=password.d.ts.map
315
301
  //#endregion
316
302
  //#region src/auth/pkce.d.ts
317
303
  interface PkceAuthOptions {
@@ -334,7 +320,6 @@ declare class PkceAuth implements Auth {
334
320
  */
335
321
  authenticate(host: string, httpClient: HttpClient, storage: StorageProvider): Promise<TokenSet>;
336
322
  }
337
- //# sourceMappingURL=pkce.d.ts.map
338
323
  //#endregion
339
324
  //#region src/auth/pkce-callback.d.ts
340
325
  interface PkceCallbackResult {
@@ -353,7 +338,6 @@ interface PkceCallbackResult {
353
338
  * @returns The parsed code and state, or null if not a valid callback
354
339
  */
355
340
  declare function handlePkceCallback(storage: StorageProvider, url?: string): PkceCallbackResult | null;
356
- //# sourceMappingURL=pkce-callback.d.ts.map
357
341
  //#endregion
358
342
  //#region src/auth/token.d.ts
359
343
  interface TokenAuthOptions {
@@ -370,7 +354,6 @@ declare class TokenAuth implements Auth {
370
354
  get description(): string;
371
355
  authenticate(_host: string, _httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet>;
372
356
  }
373
- //# sourceMappingURL=token.d.ts.map
374
357
  //#endregion
375
358
  //#region src/auth/anonymous.d.ts
376
359
  /**
@@ -381,7 +364,6 @@ declare class AnonymousAuth implements Auth {
381
364
  get description(): string;
382
365
  authenticate(_host: string, _httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet>;
383
366
  }
384
- //# sourceMappingURL=anonymous.d.ts.map
385
367
  //#endregion
386
368
  //#region src/client.d.ts
387
369
  /**
@@ -440,8 +422,6 @@ declare class SafeguardClient {
440
422
  */
441
423
  invoke<T = unknown>(service: Service, method: HttpMethod, relativeUrl: string, options?: RequestOptions): Promise<T>;
442
424
  }
443
- //# sourceMappingURL=client.d.ts.map
444
-
445
425
  //#endregion
446
426
  export { AnonymousAuth, ApiError, type ApiErrorBody, type Auth, AuthenticationError, AuthorizationError, BrowserHttpClient, BrowserSessionStorage, ConfigurationError, type HttpClient, HttpMethod, type HttpRequestOptions, type HttpResponse, MemoryStorage, NotFoundError, PasswordAuth, type PasswordAuthOptions, PkceAuth, type PkceAuthOptions, type PkceCallbackResult, type RequestOptions, SafeguardClient, type SafeguardClientOptions, SafeguardError, type SafeguardResponse, SecretValue, Service, StorageKeys, type StorageProvider, TokenAuth, type TokenAuthOptions, type TokenSet, TransportError, base64UrlEncode, buildRequestUrl, buildServiceUrl, generateCodeChallenge, generateCodeVerifier, generateState, handlePkceCallback };
447
- //# sourceMappingURL=browser-Df-ljVNJ.d.ts.map
427
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","names":[],"sources":["../src/http/types.ts","../src/storage/types.ts","../src/secret.ts","../src/auth/types.ts","../src/types.ts","../src/errors.ts","../src/utils.ts","../src/storage/memory.ts","../src/storage/session.ts","../src/http/browser.ts","../src/auth/password.ts","../src/auth/pkce.ts","../src/auth/pkce-callback.ts","../src/auth/token.ts","../src/auth/anonymous.ts","../src/client.ts"],"mappings":"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"}
package/dist/browser.js CHANGED
@@ -603,6 +603,8 @@ var SafeguardClient = class {
603
603
  #storage;
604
604
  #tokenSet;
605
605
  #connected = false;
606
+ /** Single-flight refresh promise to prevent thundering herd on token expiry. */
607
+ #tokenRefreshPromise;
606
608
  constructor(host, options) {
607
609
  if (!host) throw new ConfigurationError("SafeguardClient requires a host");
608
610
  if (!options.auth) throw new ConfigurationError("SafeguardClient requires an auth strategy");
@@ -738,7 +740,12 @@ var SafeguardClient = class {
738
740
  if (!this.#tokenSet.expiresIn) return;
739
741
  const elapsed = Date.now() - this.#tokenSet.acquiredAt;
740
742
  const expiresInMs = this.#tokenSet.expiresIn * 1e3;
741
- if (elapsed + TOKEN_REFRESH_MARGIN_MS >= expiresInMs) {
743
+ if (elapsed + TOKEN_REFRESH_MARGIN_MS < expiresInMs) return;
744
+ if (this.#tokenRefreshPromise) {
745
+ await this.#tokenRefreshPromise;
746
+ return;
747
+ }
748
+ this.#tokenRefreshPromise = (async () => {
742
749
  if (this.#auth.refreshToken) {
743
750
  const refreshed = await this.#auth.refreshToken(this.#host, this.#httpClient, this.#storage);
744
751
  if (refreshed) {
@@ -747,6 +754,11 @@ var SafeguardClient = class {
747
754
  }
748
755
  }
749
756
  this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
757
+ })();
758
+ try {
759
+ await this.#tokenRefreshPromise;
760
+ } finally {
761
+ this.#tokenRefreshPromise = void 0;
750
762
  }
751
763
  }
752
764
  };
@@ -1 +1 @@
1
- {"version":3,"file":"browser.js","names":["#value","#store","#credentials","#doFetch","#username","#password","#provider","#getRstsToken","#exchangeForUserToken","#redirectUri","#provider","#exchangeCode","#initiateRedirect","#accessToken","#host","#auth","#apiVersion","#timeout","#autoRefresh","#ca","#verify","#storage","#connected","#httpClient","#tokenSet","#ensureValidToken"],"sources":["../src/types.ts","../src/errors.ts","../src/secret.ts","../src/utils.ts","../src/storage/memory.ts","../src/storage/session.ts","../src/storage/types.ts","../src/http/browser.ts","../src/auth/password.ts","../src/auth/pkce.ts","../src/auth/pkce-callback.ts","../src/auth/token.ts","../src/auth/anonymous.ts","../src/client.ts"],"sourcesContent":["/**\n * Safeguard appliance service endpoints.\n */\nexport enum Service {\n CORE = 'core',\n APPLIANCE = 'appliance',\n NOTIFICATION = 'notification',\n A2A = 'a2a',\n EVENT = 'event',\n RSTS = 'RSTS',\n}\n\n/**\n * HTTP methods supported by the Safeguard API.\n */\nexport enum HttpMethod {\n GET = 'GET',\n POST = 'POST',\n PUT = 'PUT',\n DELETE = 'DELETE',\n PATCH = 'PATCH',\n}\n\n/**\n * Full response from a Safeguard API call (returned when fullResponse: true).\n */\nexport interface SafeguardResponse<T = unknown> {\n data: T;\n status: number;\n headers: Record<string, string>;\n}\n\n/**\n * Options for individual API requests.\n */\nexport interface RequestOptions {\n /** Request body as JSON-serializable object. */\n json?: unknown;\n /** Raw request body. */\n body?: BodyInit | null;\n /** Additional HTTP headers. */\n headers?: Record<string, string>;\n /** URL query parameters. */\n query?: Record<string, string | number | boolean>;\n /** AbortSignal for request cancellation. */\n signal?: AbortSignal;\n /** Request timeout in milliseconds (overrides client default). */\n timeout?: number;\n /** If true, returns full response with status and headers. */\n fullResponse?: boolean;\n}\n\n/**\n * Options for constructing a SafeguardClient.\n */\nexport interface SafeguardClientOptions {\n /** Authentication strategy. */\n auth: import('./auth/types.js').Auth;\n /** Custom CA certificate (PEM string or Buffer). Node.js only. */\n ca?: string | Buffer;\n /** Whether to verify TLS certificates. Default: true. */\n verify?: boolean;\n /** Safeguard API version prefix. Default: 'v4'. */\n apiVersion?: string;\n /** Request timeout in milliseconds. Default: 300_000 (5 min). */\n timeout?: number;\n /** Auto-refresh tokens before expiry. Default: true. */\n autoRefresh?: boolean;\n}\n","/**\n * Error hierarchy for the Safeguard SDK.\n *\n * SafeguardError\n * ├── ApiError (HTTP errors from Safeguard API)\n * │ ├── AuthenticationError (401)\n * │ ├── AuthorizationError (403)\n * │ └── NotFoundError (404)\n * ├── TransportError (Network/connection failures)\n * └── ConfigurationError (Invalid client options)\n */\n\nexport class SafeguardError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'SafeguardError';\n }\n}\n\nexport interface ApiErrorBody {\n Code?: string;\n Message?: string;\n InnerError?: { Message?: string };\n [key: string]: unknown;\n}\n\nexport class ApiError extends SafeguardError {\n readonly status: number;\n readonly body: ApiErrorBody | null;\n\n constructor(message: string, status: number, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ApiError';\n this.status = status;\n this.body = body;\n }\n\n /**\n * Creates an appropriate ApiError subclass based on HTTP status code.\n */\n static fromResponse(status: number, body: string | ApiErrorBody | null): ApiError {\n const parsed = ApiError.parseBody(body);\n const message = ApiError.buildMessage(status, parsed);\n\n switch (status) {\n case 401:\n return new AuthenticationError(message, parsed);\n case 403:\n return new AuthorizationError(message, parsed);\n case 404:\n return new NotFoundError(message, parsed);\n default:\n return new ApiError(message, status, parsed);\n }\n }\n\n private static parseBody(body: string | ApiErrorBody | null): ApiErrorBody | null {\n if (body === null || body === undefined) return null;\n if (typeof body === 'object') return body;\n try {\n return JSON.parse(body) as ApiErrorBody;\n } catch {\n return { Message: body };\n }\n }\n\n private static buildMessage(status: number, body: ApiErrorBody | null): string {\n const apiMessage = body?.Message ?? body?.InnerError?.Message;\n if (apiMessage) {\n return `HTTP ${String(status)}: ${apiMessage}`;\n }\n return `HTTP ${String(status)}`;\n }\n}\n\nexport class AuthenticationError extends ApiError {\n constructor(message: string, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, 401, body, options);\n this.name = 'AuthenticationError';\n }\n}\n\nexport class AuthorizationError extends ApiError {\n constructor(message: string, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, 403, body, options);\n this.name = 'AuthorizationError';\n }\n}\n\nexport class NotFoundError extends ApiError {\n constructor(message: string, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, 404, body, options);\n this.name = 'NotFoundError';\n }\n}\n\nexport class TransportError extends SafeguardError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'TransportError';\n }\n}\n\nexport class ConfigurationError extends SafeguardError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ConfigurationError';\n }\n}\n","const REDACTED = '[REDACTED]';\n\n/**\n * A wrapper that prevents accidental logging of sensitive values.\n *\n * - `toString()` returns \"[REDACTED]\"\n * - `toJSON()` returns \"[REDACTED]\"\n * - `console.log(secretValue)` prints \"[REDACTED]\"\n * - `.expose()` returns the raw value when intentionally needed\n */\nexport class SecretValue {\n readonly #value: string;\n\n constructor(value: string) {\n this.#value = value;\n }\n\n /** Returns the raw secret. Use only when you intentionally need the plaintext. */\n expose(): string {\n return this.#value;\n }\n\n toString(): string {\n return REDACTED;\n }\n\n toJSON(): string {\n return REDACTED;\n }\n\n /** Node.js custom inspect (prevents leaking in console.log / util.inspect). */\n [Symbol.for('nodejs.util.inspect.custom')](): string {\n return REDACTED;\n }\n\n get [Symbol.toStringTag](): string {\n return 'SecretValue';\n }\n}\n","import { Service } from './types.js';\nimport { ConfigurationError } from './errors.js';\n\n/**\n * Validates that a host string is a bare hostname or IP address.\n * Rejects URLs, paths, query strings, userinfo, and embedded ports.\n *\n * @throws {ConfigurationError} if the host contains disallowed characters\n */\nexport function validateHost(host: string): string {\n if (!host) throw new ConfigurationError('Host must not be empty');\n\n const trimmed = host.trim();\n if (trimmed !== host) {\n throw new ConfigurationError('Host must not contain leading or trailing whitespace');\n }\n\n // Reject protocol prefixes\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(host)) {\n throw new ConfigurationError(\n `Host must be a bare hostname or IP, not a URL. Remove the protocol prefix (e.g., use \"${host.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '')}\")`,\n );\n }\n\n // Reject path, query, fragment, userinfo characters\n if (/[/?#@]/.test(host)) {\n throw new ConfigurationError(\n 'Host must be a bare hostname or IP address — must not contain /, ?, #, or @',\n );\n }\n\n // Reject port suffix (e.g. \"host:8443\") — SDK always uses HTTPS/443\n if (/:\\d+$/.test(host) && !host.startsWith('[')) {\n throw new ConfigurationError(\n 'Host must not include a port number. The SDK always connects over HTTPS (port 443).',\n );\n }\n\n return host;\n}\n\n/**\n * Builds the base URL for a Safeguard service endpoint.\n *\n * @example\n * buildServiceUrl('appliance.example.com', Service.CORE)\n * // => 'https://appliance.example.com/service/core/v4'\n */\nexport function buildServiceUrl(host: string, service: Service, apiVersion: string = 'v4'): string {\n if (service === Service.RSTS) {\n return `https://${host}/RSTS`;\n }\n return `https://${host}/service/${service}/${apiVersion}`;\n}\n\n/**\n * Builds a full API URL including path and optional query parameters.\n *\n * @example\n * buildRequestUrl('appliance.example.com', Service.CORE, 'Users', { filter: 'Name eq \"admin\"' })\n * // => 'https://appliance.example.com/service/core/v4/Users?filter=Name+eq+%22admin%22'\n */\nexport function buildRequestUrl(\n host: string,\n service: Service,\n relativeUrl: string,\n query?: Record<string, string | number | boolean>,\n apiVersion: string = 'v4',\n): string {\n const base = buildServiceUrl(host, service, apiVersion);\n const path = relativeUrl.startsWith('/') ? relativeUrl.slice(1) : relativeUrl;\n const url = new URL(`${base}/${path}`);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, String(value));\n }\n }\n\n return url.toString();\n}\n\n/**\n * Encodes bytes to a URL-safe base64 string (no padding).\n */\nexport function base64UrlEncode(bytes: Uint8Array): string {\n const binary = String.fromCharCode(...bytes);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/**\n * Generates a cryptographically random code verifier for PKCE.\n * Returns a 43-character base64url string (32 random bytes).\n */\nexport function generateCodeVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Computes the S256 code challenge from a code verifier.\n * Uses Web Crypto API (available in Node 20+ and all modern browsers).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(new Uint8Array(digest));\n}\n\n/**\n * Generates a random state parameter for OAuth flows.\n */\nexport function generateState(): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Extracts the `exp` claim from a JWT and returns expiresIn (seconds from now).\n * Returns undefined if the token is not a valid JWT or has no `exp` claim.\n */\nexport function getTokenExpiresIn(jwt: string): number | undefined {\n try {\n const parts = jwt.split('.');\n if (parts.length !== 3) return undefined;\n // Base64url decode the payload\n const payload = parts[1]!.replace(/-/g, '+').replace(/_/g, '/');\n const json = atob(payload);\n const claims = JSON.parse(json) as { exp?: number };\n if (typeof claims.exp !== 'number') return undefined;\n const now = Math.floor(Date.now() / 1000);\n return Math.max(0, claims.exp - now);\n } catch {\n return undefined;\n }\n}\n","import type { StorageProvider } from './types.js';\n\n/**\n * In-memory storage provider. Default for Node.js environments.\n * Data does not persist across process restarts.\n */\nexport class MemoryStorage implements StorageProvider {\n readonly #store = new Map<string, string>();\n\n get(key: string): string | null {\n return this.#store.get(key) ?? null;\n }\n\n set(key: string, value: string): void {\n this.#store.set(key, value);\n }\n\n remove(key: string): void {\n this.#store.delete(key);\n }\n\n clear(): void {\n this.#store.clear();\n }\n}\n","import type { StorageProvider } from './types.js';\n\n/**\n * Browser sessionStorage adapter. Data persists for the browser tab lifetime.\n * Requires a DOM environment (browser or jsdom).\n */\nexport class BrowserSessionStorage implements StorageProvider {\n get(key: string): string | null {\n return sessionStorage.getItem(key);\n }\n\n set(key: string, value: string): void {\n sessionStorage.setItem(key, value);\n }\n\n remove(key: string): void {\n sessionStorage.removeItem(key);\n }\n\n clear(): void {\n sessionStorage.clear();\n }\n}\n","/**\n * Interface for token/state storage backends.\n */\nexport interface StorageProvider {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n clear(): void;\n}\n\n/** Well-known storage keys used by the SDK. */\nexport const StorageKeys = {\n STATE: 'safeguard_state',\n CODE_VERIFIER: 'safeguard_code_verifier',\n} as const;\n","import type { HttpClient, HttpRequestOptions, HttpResponse } from './types.js';\n\n/**\n * Browser HTTP client using native fetch API.\n * No TLS configuration needed — browser handles certificates.\n */\nexport class BrowserHttpClient implements HttpClient {\n readonly #credentials: RequestCredentials;\n\n constructor(options?: { credentials?: RequestCredentials }) {\n this.#credentials = options?.credentials ?? 'include';\n }\n\n async request(options: HttpRequestOptions): Promise<HttpResponse> {\n const { url, method, headers, body, signal, timeout } = options;\n\n const requestInit: RequestInit = {\n method,\n credentials: this.#credentials,\n };\n\n if (headers) requestInit.headers = headers;\n if (body != null) requestInit.body = body as BodyInit;\n if (signal || timeout) {\n const controller = new AbortController();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (timeout) {\n timeoutId = setTimeout(() => controller.abort(new Error('Request timed out')), timeout);\n }\n\n requestInit.signal = signal\n ? AbortSignal.any([signal, controller.signal])\n : controller.signal;\n\n try {\n return await this.#doFetch(url, requestInit);\n } finally {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n }\n }\n\n return this.#doFetch(url, requestInit);\n }\n\n async #doFetch(url: string, init: RequestInit): Promise<HttpResponse> {\n const response = await fetch(url, init);\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n const responseBody = await response.text();\n\n return {\n status: response.status,\n headers: responseHeaders,\n body: responseBody,\n };\n }\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { ConfigurationError } from '../errors.js';\nimport { SecretValue } from '../secret.js';\nimport { getTokenExpiresIn } from '../utils.js';\n\nexport interface PasswordAuthOptions {\n username: string;\n password: string;\n /** Identity provider name. Default: 'local'. */\n provider?: string;\n}\n\n/**\n * Authenticates using username/password via RSTS Resource Owner grant.\n */\nexport class PasswordAuth implements Auth {\n readonly #username: string;\n readonly #password: SecretValue;\n readonly #provider: string;\n\n constructor(options: PasswordAuthOptions) {\n if (!options.username) throw new ConfigurationError('PasswordAuth requires username');\n if (!options.password) throw new ConfigurationError('PasswordAuth requires password');\n if (!options.provider) throw new ConfigurationError('PasswordAuth requires provider');\n this.#username = options.username;\n this.#password = new SecretValue(options.password);\n this.#provider = options.provider;\n }\n\n get description(): string {\n return `PasswordAuth(${this.#username}@${this.#provider})`;\n }\n\n async authenticate(host: string, httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet> {\n // Step 1: Get RSTS token\n const rstsToken = await this.#getRstsToken(host, httpClient);\n\n // Step 2: Exchange RSTS token for Safeguard user token\n const userToken = await this.#exchangeForUserToken(host, httpClient, rstsToken);\n\n return userToken;\n }\n\n async refreshToken(host: string, httpClient: HttpClient, storage: StorageProvider): Promise<TokenSet | null> {\n // Password auth can always re-authenticate\n return this.authenticate(host, httpClient, storage);\n }\n\n async #getRstsToken(host: string, httpClient: HttpClient): Promise<string> {\n const body = JSON.stringify({\n grant_type: 'password',\n username: this.#username,\n password: this.#password.expose(),\n scope: 'rsts:sts:primaryproviderid:' + this.#provider.toLowerCase(),\n });\n\n const response = await httpClient.request({\n url: `https://${host}/RSTS/oauth2/token`,\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (response.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = JSON.parse(response.body) as { access_token: string };\n return data.access_token;\n }\n\n async #exchangeForUserToken(host: string, httpClient: HttpClient, rstsToken: string): Promise<TokenSet> {\n const response = await httpClient.request({\n url: `https://${host}/service/core/v4/Token/LoginResponse`,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${rstsToken}`,\n },\n body: JSON.stringify({ StsAccessToken: rstsToken }),\n });\n\n if (response.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = JSON.parse(response.body) as { UserToken: string; ExpiresIn?: number };\n const tokenSet: TokenSet = {\n accessToken: new SecretValue(data.UserToken),\n acquiredAt: Date.now(),\n };\n if (data.ExpiresIn != null) {\n tokenSet.expiresIn = data.ExpiresIn;\n } else {\n const expiresIn = getTokenExpiresIn(data.UserToken);\n if (expiresIn != null) tokenSet.expiresIn = expiresIn;\n }\n return tokenSet;\n }\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { ConfigurationError } from '../errors.js';\nimport { SecretValue } from '../secret.js';\nimport { StorageKeys } from '../storage/types.js';\nimport { generateCodeVerifier, generateCodeChallenge, generateState } from '../utils.js';\n\nexport interface PkceAuthOptions {\n /** OAuth redirect URI (must match RSTS configuration). */\n redirectUri: string;\n /** Identity provider name. Default: 'local'. */\n provider?: string;\n}\n\n/**\n * Authenticates using PKCE (Proof Key for Code Exchange) with browser redirect.\n * For browser environments where the user is redirected to RSTS login.\n */\nexport class PkceAuth implements Auth {\n readonly #redirectUri: string;\n readonly #provider: string;\n\n constructor(options: PkceAuthOptions) {\n if (!options.redirectUri) throw new ConfigurationError('PkceAuth requires redirectUri');\n if (!options.provider) throw new ConfigurationError('PkceAuth requires provider');\n this.#redirectUri = options.redirectUri;\n this.#provider = options.provider;\n }\n\n get description(): string {\n return `PKCE(redirect=${this.#redirectUri})`;\n }\n\n /**\n * Initiates the PKCE flow. If a stored authorization code exists (from callback),\n * exchanges it for a token. Otherwise, redirects to RSTS login.\n */\n async authenticate(host: string, httpClient: HttpClient, storage: StorageProvider): Promise<TokenSet> {\n // Check for authorization code (set by handlePkceCallback)\n const code = storage.get('safeguard_auth_code');\n if (code) {\n const verifier = storage.get(StorageKeys.CODE_VERIFIER);\n if (!verifier) {\n throw new ConfigurationError('PKCE code verifier not found in storage');\n }\n storage.remove('safeguard_auth_code');\n storage.remove(StorageKeys.CODE_VERIFIER);\n storage.remove(StorageKeys.STATE);\n return this.#exchangeCode(host, httpClient, code, verifier);\n }\n\n // No code — initiate redirect\n await this.#initiateRedirect(host, storage);\n\n // This point is never reached in a browser (redirect happens).\n // For testability, throw rather than hang.\n throw new ConfigurationError(\n 'PKCE redirect initiated. Call handlePkceCallback() on the callback page.',\n );\n }\n\n async #initiateRedirect(host: string, storage: StorageProvider): Promise<void> {\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n const state = generateState();\n\n storage.set(StorageKeys.CODE_VERIFIER, verifier);\n storage.set(StorageKeys.STATE, state);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: 'safeguard.js',\n redirect_uri: this.#redirectUri,\n scope: 'rsts:sts:primaryproviderid:' + this.#provider,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state,\n });\n\n const authUrl = `https://${host}/RSTS/Login?${params.toString()}`;\n\n // In browser environment, redirect\n if (typeof window !== 'undefined' && window.location) {\n window.location.href = authUrl;\n }\n }\n\n async #exchangeCode(\n host: string,\n httpClient: HttpClient,\n code: string,\n verifier: string,\n ): Promise<TokenSet> {\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.#redirectUri,\n client_id: 'safeguard.js',\n code_verifier: verifier,\n });\n\n const response = await httpClient.request({\n url: `https://${host}/RSTS/oauth2/token`,\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (response.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = JSON.parse(response.body) as { access_token: string; expires_in?: number };\n\n // Exchange RSTS token for Safeguard user token\n const userTokenResponse = await httpClient.request({\n url: `https://${host}/service/core/v4/Token/LoginResponse`,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${data.access_token}`,\n },\n body: JSON.stringify({ StsAccessToken: data.access_token }),\n });\n\n if (userTokenResponse.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(userTokenResponse.status, userTokenResponse.body);\n }\n\n const userData = JSON.parse(userTokenResponse.body) as { UserToken: string; ExpiresIn?: number };\n const tokenSet: TokenSet = {\n accessToken: new SecretValue(userData.UserToken),\n acquiredAt: Date.now(),\n };\n if (userData.ExpiresIn != null) tokenSet.expiresIn = userData.ExpiresIn;\n\n // Token stays in-memory only (SafeguardClient.#tokenSet). Not persisted to\n // sessionStorage to avoid XSS exfiltration of high-privilege PAM tokens.\n return tokenSet;\n }\n}\n","import { StorageKeys } from '../storage/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport { ConfigurationError } from '../errors.js';\n\nexport interface PkceCallbackResult {\n code: string;\n state: string;\n}\n\n/**\n * Parses the PKCE callback URL and stores the authorization code.\n * Call this on the callback/redirect page to complete the PKCE flow.\n *\n * This is the explicit replacement for the legacy `safeguardJsAccessTokenChecker.js`\n * script that ran as a side-effect on import.\n *\n * @param storage - StorageProvider to persist the code\n * @param url - The callback URL to parse (defaults to window.location.href)\n * @returns The parsed code and state, or null if not a valid callback\n */\nexport function handlePkceCallback(\n storage: StorageProvider,\n url?: string,\n): PkceCallbackResult | null {\n const callbackUrl = url ?? (typeof window !== 'undefined' ? window.location.href : '');\n if (!callbackUrl) return null;\n\n const parsed = new URL(callbackUrl);\n const code = parsed.searchParams.get('code');\n const state = parsed.searchParams.get('state');\n\n if (!code || !state) return null;\n\n // Validate state matches what we stored\n const storedState = storage.get(StorageKeys.STATE);\n if (storedState && storedState !== state) {\n throw new ConfigurationError('PKCE state mismatch — possible CSRF attack');\n }\n\n // Store the code for the PkceAuth strategy to pick up\n storage.set('safeguard_auth_code', code);\n\n return { code, state };\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { ConfigurationError } from '../errors.js';\nimport { SecretValue } from '../secret.js';\n\nexport interface TokenAuthOptions {\n /** Pre-existing Safeguard access token. */\n accessToken: string;\n}\n\n/**\n * Uses a pre-existing access token. No authentication flow — the token is assumed valid.\n * Useful when tokens are obtained externally or for service-to-service scenarios.\n */\nexport class TokenAuth implements Auth {\n readonly #accessToken: SecretValue;\n\n constructor(options: TokenAuthOptions) {\n if (!options.accessToken) throw new ConfigurationError('TokenAuth requires accessToken');\n this.#accessToken = new SecretValue(options.accessToken);\n }\n\n get description(): string {\n return 'TokenAuth(pre-existing)';\n }\n\n async authenticate(_host: string, _httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet> {\n return {\n accessToken: this.#accessToken,\n acquiredAt: Date.now(),\n };\n }\n\n // No refresh possible with a pre-existing token\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { SecretValue } from '../secret.js';\n\n/**\n * Anonymous authentication — no credentials.\n * Used for accessing public Safeguard endpoints (e.g., appliance status).\n */\nexport class AnonymousAuth implements Auth {\n get description(): string {\n return 'AnonymousAuth';\n }\n\n async authenticate(_host: string, _httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet> {\n return {\n accessToken: new SecretValue(''),\n acquiredAt: Date.now(),\n };\n }\n}\n","import { Service, HttpMethod } from './types.js';\nimport type { RequestOptions, SafeguardClientOptions } from './types.js';\nimport type { Auth, TokenSet } from './auth/types.js';\nimport type { HttpClient } from './http/types.js';\nimport type { StorageProvider } from './storage/types.js';\nimport { MemoryStorage } from './storage/memory.js';\nimport { buildRequestUrl, validateHost } from './utils.js';\nimport { AuthenticationError, ConfigurationError, ApiError } from './errors.js';\n\nconst DEFAULT_TIMEOUT = 300_000;\nconst DEFAULT_API_VERSION = 'v4';\nconst TOKEN_REFRESH_MARGIN_MS = 60_000; // refresh 60s before expiry\n\n/**\n * Main entry point for the Safeguard SDK.\n * Instance-based — no global state. Multiple clients to different appliances are supported.\n */\nexport class SafeguardClient {\n readonly #host: string;\n readonly #auth: Auth;\n readonly #apiVersion: string;\n readonly #timeout: number;\n readonly #autoRefresh: boolean;\n readonly #ca: string | Buffer | undefined;\n readonly #verify: boolean;\n\n #httpClient: HttpClient | undefined;\n #storage: StorageProvider;\n #tokenSet: TokenSet | undefined;\n #connected = false;\n\n constructor(host: string, options: SafeguardClientOptions) {\n if (!host) throw new ConfigurationError('SafeguardClient requires a host');\n if (!options.auth) throw new ConfigurationError('SafeguardClient requires an auth strategy');\n this.#host = validateHost(host);\n this.#auth = options.auth;\n this.#apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;\n this.#timeout = options.timeout ?? DEFAULT_TIMEOUT;\n this.#autoRefresh = options.autoRefresh !== false;\n this.#ca = options.ca;\n this.#verify = options.verify !== false;\n this.#storage = new MemoryStorage();\n }\n\n /** The appliance hostname this client connects to. */\n get host(): string {\n return this.#host;\n }\n\n /** The API version prefix (e.g. 'v4'). */\n get apiVersion(): string {\n return this.#apiVersion;\n }\n\n /** Custom CA certificate, if provided. */\n get ca(): string | Buffer | undefined {\n return this.#ca;\n }\n\n /** Whether TLS verification is enabled. */\n get verify(): boolean {\n return this.#verify;\n }\n\n /** Whether the client has an active authenticated session. */\n get isConnected(): boolean {\n return this.#connected;\n }\n\n /**\n * Sets a custom StorageProvider (e.g., BrowserSessionStorage).\n * Must be called before connect().\n */\n setStorage(storage: StorageProvider): this {\n this.#storage = storage;\n return this;\n }\n\n /**\n * Sets the HttpClient implementation. If not called, must be set before connect().\n * The factory pattern allows platform-specific client injection:\n * - Node: `new NodeHttpClient({ ca, rejectUnauthorized })`\n * - Browser: `new BrowserHttpClient()`\n */\n setHttpClient(httpClient: HttpClient): this {\n this.#httpClient = httpClient;\n return this;\n }\n\n /**\n * Authenticate and establish a session.\n * @throws {AuthenticationError} if authentication fails\n * @throws {ConfigurationError} if httpClient not set\n */\n async connect(): Promise<void> {\n if (!this.#httpClient) {\n throw new ConfigurationError(\n 'HttpClient not set. Call setHttpClient() before connect(), or use SafeguardClient.createNode() / SafeguardClient.createBrowser().',\n );\n }\n\n this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);\n if (!this.#tokenSet.accessToken.expose()) {\n throw new AuthenticationError('Authentication returned empty access token');\n }\n this.#connected = true;\n }\n\n /**\n * Disconnect — dispose HTTP client and clear token.\n */\n async disconnect(): Promise<void> {\n this.#tokenSet = undefined;\n this.#connected = false;\n this.#httpClient?.dispose?.();\n }\n\n /**\n * Returns seconds until the current access token expires, or -1 if unknown.\n */\n getAccessTokenLifetimeRemaining(): number {\n if (!this.#tokenSet?.expiresIn) return -1;\n const elapsed = (Date.now() - this.#tokenSet.acquiredAt) / 1000;\n return Math.max(0, this.#tokenSet.expiresIn - elapsed);\n }\n\n // ─── Convenience HTTP verbs ─────────────────────────────────────────\n\n async get<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.GET, relativeUrl, options);\n }\n\n async post<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.POST, relativeUrl, options);\n }\n\n async put<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.PUT, relativeUrl, options);\n }\n\n async delete<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.DELETE, relativeUrl, options);\n }\n\n async patch<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.PATCH, relativeUrl, options);\n }\n\n // ─── Core invoke ────────────────────────────────────────────────────\n\n /**\n * Low-level request method. Convenience verbs delegate here.\n *\n * When `options.fullResponse` is true, returns SafeguardResponse<T> with status + headers.\n * Otherwise returns just the parsed response body as T.\n */\n async invoke<T = unknown>(\n service: Service,\n method: HttpMethod,\n relativeUrl: string,\n options?: RequestOptions,\n ): Promise<T> {\n if (!this.#connected || !this.#httpClient) {\n throw new ConfigurationError('Client not connected. Call connect() first.');\n }\n\n // Auto-refresh token if needed\n if (this.#autoRefresh) {\n await this.#ensureValidToken();\n }\n\n const url = buildRequestUrl(this.#host, service, relativeUrl, options?.query, this.#apiVersion);\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.#tokenSet!.accessToken.expose()}`,\n Accept: 'application/json',\n ...options?.headers,\n };\n\n let body: string | null = null;\n if (options?.json != null) {\n body = JSON.stringify(options.json);\n headers['Content-Type'] = 'application/json';\n } else if (options?.body != null) {\n body = options.body as string;\n }\n\n const requestOpts: import('./http/types.js').HttpRequestOptions = {\n url,\n method,\n headers,\n body,\n timeout: options?.timeout ?? this.#timeout,\n };\n if (options?.signal) requestOpts.signal = options.signal;\n\n const response = await this.#httpClient.request(requestOpts);\n\n if (response.status >= 400) {\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = response.body ? (JSON.parse(response.body) as T) : (undefined as T);\n\n if (options?.fullResponse) {\n return { data, status: response.status, headers: response.headers } as unknown as T;\n }\n\n return data;\n }\n\n // ─── Private helpers ────────────────────────────────────────────────\n\n async #ensureValidToken(): Promise<void> {\n if (!this.#tokenSet) return;\n if (!this.#tokenSet.expiresIn) return; // can't determine expiry\n\n const elapsed = Date.now() - this.#tokenSet.acquiredAt;\n const expiresInMs = this.#tokenSet.expiresIn * 1000;\n\n if (elapsed + TOKEN_REFRESH_MARGIN_MS >= expiresInMs) {\n // Token is expired or about to expire — refresh\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 // No refresh support or refresh failed — re-authenticate\n this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient!, this.#storage);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"browser.js","names":["#value","#store","#credentials","#doFetch","#username","#password","#provider","#getRstsToken","#exchangeForUserToken","#redirectUri","#provider","#exchangeCode","#initiateRedirect","#accessToken","#host","#auth","#apiVersion","#timeout","#autoRefresh","#ca","#verify","#storage","#connected","#httpClient","#tokenSet","#ensureValidToken","#tokenRefreshPromise"],"sources":["../src/types.ts","../src/errors.ts","../src/secret.ts","../src/utils.ts","../src/storage/memory.ts","../src/storage/session.ts","../src/storage/types.ts","../src/http/browser.ts","../src/auth/password.ts","../src/auth/pkce.ts","../src/auth/pkce-callback.ts","../src/auth/token.ts","../src/auth/anonymous.ts","../src/client.ts"],"sourcesContent":["/**\n * Safeguard appliance service endpoints.\n */\nexport enum Service {\n CORE = 'core',\n APPLIANCE = 'appliance',\n NOTIFICATION = 'notification',\n A2A = 'a2a',\n EVENT = 'event',\n RSTS = 'RSTS',\n}\n\n/**\n * HTTP methods supported by the Safeguard API.\n */\nexport enum HttpMethod {\n GET = 'GET',\n POST = 'POST',\n PUT = 'PUT',\n DELETE = 'DELETE',\n PATCH = 'PATCH',\n}\n\n/**\n * Full response from a Safeguard API call (returned when fullResponse: true).\n */\nexport interface SafeguardResponse<T = unknown> {\n data: T;\n status: number;\n headers: Record<string, string>;\n}\n\n/**\n * Options for individual API requests.\n */\nexport interface RequestOptions {\n /** Request body as JSON-serializable object. */\n json?: unknown;\n /** Raw request body. */\n body?: BodyInit | null;\n /** Additional HTTP headers. */\n headers?: Record<string, string>;\n /** URL query parameters. */\n query?: Record<string, string | number | boolean>;\n /** AbortSignal for request cancellation. */\n signal?: AbortSignal;\n /** Request timeout in milliseconds (overrides client default). */\n timeout?: number;\n /** If true, returns full response with status and headers. */\n fullResponse?: boolean;\n}\n\n/**\n * Options for constructing a SafeguardClient.\n */\nexport interface SafeguardClientOptions {\n /** Authentication strategy. */\n auth: import('./auth/types.js').Auth;\n /** Custom CA certificate (PEM string or Buffer). Node.js only. */\n ca?: string | Buffer;\n /** Whether to verify TLS certificates. Default: true. */\n verify?: boolean;\n /** Safeguard API version prefix. Default: 'v4'. */\n apiVersion?: string;\n /** Request timeout in milliseconds. Default: 300_000 (5 min). */\n timeout?: number;\n /** Auto-refresh tokens before expiry. Default: true. */\n autoRefresh?: boolean;\n}\n","/**\n * Error hierarchy for the Safeguard SDK.\n *\n * SafeguardError\n * ├── ApiError (HTTP errors from Safeguard API)\n * │ ├── AuthenticationError (401)\n * │ ├── AuthorizationError (403)\n * │ └── NotFoundError (404)\n * ├── TransportError (Network/connection failures)\n * └── ConfigurationError (Invalid client options)\n */\n\nexport class SafeguardError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'SafeguardError';\n }\n}\n\nexport interface ApiErrorBody {\n Code?: string;\n Message?: string;\n InnerError?: { Message?: string };\n [key: string]: unknown;\n}\n\nexport class ApiError extends SafeguardError {\n readonly status: number;\n readonly body: ApiErrorBody | null;\n\n constructor(message: string, status: number, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ApiError';\n this.status = status;\n this.body = body;\n }\n\n /**\n * Creates an appropriate ApiError subclass based on HTTP status code.\n */\n static fromResponse(status: number, body: string | ApiErrorBody | null): ApiError {\n const parsed = ApiError.parseBody(body);\n const message = ApiError.buildMessage(status, parsed);\n\n switch (status) {\n case 401:\n return new AuthenticationError(message, parsed);\n case 403:\n return new AuthorizationError(message, parsed);\n case 404:\n return new NotFoundError(message, parsed);\n default:\n return new ApiError(message, status, parsed);\n }\n }\n\n private static parseBody(body: string | ApiErrorBody | null): ApiErrorBody | null {\n if (body === null || body === undefined) return null;\n if (typeof body === 'object') return body;\n try {\n return JSON.parse(body) as ApiErrorBody;\n } catch {\n return { Message: body };\n }\n }\n\n private static buildMessage(status: number, body: ApiErrorBody | null): string {\n const apiMessage = body?.Message ?? body?.InnerError?.Message;\n if (apiMessage) {\n return `HTTP ${String(status)}: ${apiMessage}`;\n }\n return `HTTP ${String(status)}`;\n }\n}\n\nexport class AuthenticationError extends ApiError {\n constructor(message: string, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, 401, body, options);\n this.name = 'AuthenticationError';\n }\n}\n\nexport class AuthorizationError extends ApiError {\n constructor(message: string, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, 403, body, options);\n this.name = 'AuthorizationError';\n }\n}\n\nexport class NotFoundError extends ApiError {\n constructor(message: string, body: ApiErrorBody | null = null, options?: ErrorOptions) {\n super(message, 404, body, options);\n this.name = 'NotFoundError';\n }\n}\n\nexport class TransportError extends SafeguardError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'TransportError';\n }\n}\n\nexport class ConfigurationError extends SafeguardError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ConfigurationError';\n }\n}\n","const REDACTED = '[REDACTED]';\n\n/**\n * A wrapper that prevents accidental logging of sensitive values.\n *\n * - `toString()` returns \"[REDACTED]\"\n * - `toJSON()` returns \"[REDACTED]\"\n * - `console.log(secretValue)` prints \"[REDACTED]\"\n * - `.expose()` returns the raw value when intentionally needed\n */\nexport class SecretValue {\n readonly #value: string;\n\n constructor(value: string) {\n this.#value = value;\n }\n\n /** Returns the raw secret. Use only when you intentionally need the plaintext. */\n expose(): string {\n return this.#value;\n }\n\n toString(): string {\n return REDACTED;\n }\n\n toJSON(): string {\n return REDACTED;\n }\n\n /** Node.js custom inspect (prevents leaking in console.log / util.inspect). */\n [Symbol.for('nodejs.util.inspect.custom')](): string {\n return REDACTED;\n }\n\n get [Symbol.toStringTag](): string {\n return 'SecretValue';\n }\n}\n","import { Service } from './types.js';\nimport { ConfigurationError } from './errors.js';\n\n/**\n * Validates that a host string is a bare hostname or IP address.\n * Rejects URLs, paths, query strings, userinfo, and embedded ports.\n *\n * @throws {ConfigurationError} if the host contains disallowed characters\n */\nexport function validateHost(host: string): string {\n if (!host) throw new ConfigurationError('Host must not be empty');\n\n const trimmed = host.trim();\n if (trimmed !== host) {\n throw new ConfigurationError('Host must not contain leading or trailing whitespace');\n }\n\n // Reject protocol prefixes\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(host)) {\n throw new ConfigurationError(\n `Host must be a bare hostname or IP, not a URL. Remove the protocol prefix (e.g., use \"${host.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '')}\")`,\n );\n }\n\n // Reject path, query, fragment, userinfo characters\n if (/[/?#@]/.test(host)) {\n throw new ConfigurationError(\n 'Host must be a bare hostname or IP address — must not contain /, ?, #, or @',\n );\n }\n\n // Reject port suffix (e.g. \"host:8443\") — SDK always uses HTTPS/443\n if (/:\\d+$/.test(host) && !host.startsWith('[')) {\n throw new ConfigurationError(\n 'Host must not include a port number. The SDK always connects over HTTPS (port 443).',\n );\n }\n\n return host;\n}\n\n/**\n * Builds the base URL for a Safeguard service endpoint.\n *\n * @example\n * buildServiceUrl('appliance.example.com', Service.CORE)\n * // => 'https://appliance.example.com/service/core/v4'\n */\nexport function buildServiceUrl(host: string, service: Service, apiVersion: string = 'v4'): string {\n if (service === Service.RSTS) {\n return `https://${host}/RSTS`;\n }\n return `https://${host}/service/${service}/${apiVersion}`;\n}\n\n/**\n * Builds a full API URL including path and optional query parameters.\n *\n * @example\n * buildRequestUrl('appliance.example.com', Service.CORE, 'Users', { filter: 'Name eq \"admin\"' })\n * // => 'https://appliance.example.com/service/core/v4/Users?filter=Name+eq+%22admin%22'\n */\nexport function buildRequestUrl(\n host: string,\n service: Service,\n relativeUrl: string,\n query?: Record<string, string | number | boolean>,\n apiVersion: string = 'v4',\n): string {\n const base = buildServiceUrl(host, service, apiVersion);\n const path = relativeUrl.startsWith('/') ? relativeUrl.slice(1) : relativeUrl;\n const url = new URL(`${base}/${path}`);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, String(value));\n }\n }\n\n return url.toString();\n}\n\n/**\n * Encodes bytes to a URL-safe base64 string (no padding).\n */\nexport function base64UrlEncode(bytes: Uint8Array): string {\n const binary = String.fromCharCode(...bytes);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/**\n * Generates a cryptographically random code verifier for PKCE.\n * Returns a 43-character base64url string (32 random bytes).\n */\nexport function generateCodeVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Computes the S256 code challenge from a code verifier.\n * Uses Web Crypto API (available in Node 20+ and all modern browsers).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(new Uint8Array(digest));\n}\n\n/**\n * Generates a random state parameter for OAuth flows.\n */\nexport function generateState(): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Extracts the `exp` claim from a JWT and returns expiresIn (seconds from now).\n * Returns undefined if the token is not a valid JWT or has no `exp` claim.\n */\nexport function getTokenExpiresIn(jwt: string): number | undefined {\n try {\n const parts = jwt.split('.');\n if (parts.length !== 3) return undefined;\n // Base64url decode the payload\n const payload = parts[1]!.replace(/-/g, '+').replace(/_/g, '/');\n const json = atob(payload);\n const claims = JSON.parse(json) as { exp?: number };\n if (typeof claims.exp !== 'number') return undefined;\n const now = Math.floor(Date.now() / 1000);\n return Math.max(0, claims.exp - now);\n } catch {\n return undefined;\n }\n}\n","import type { StorageProvider } from './types.js';\n\n/**\n * In-memory storage provider. Default for Node.js environments.\n * Data does not persist across process restarts.\n */\nexport class MemoryStorage implements StorageProvider {\n readonly #store = new Map<string, string>();\n\n get(key: string): string | null {\n return this.#store.get(key) ?? null;\n }\n\n set(key: string, value: string): void {\n this.#store.set(key, value);\n }\n\n remove(key: string): void {\n this.#store.delete(key);\n }\n\n clear(): void {\n this.#store.clear();\n }\n}\n","import type { StorageProvider } from './types.js';\n\n/**\n * Browser sessionStorage adapter. Data persists for the browser tab lifetime.\n * Requires a DOM environment (browser or jsdom).\n */\nexport class BrowserSessionStorage implements StorageProvider {\n get(key: string): string | null {\n return sessionStorage.getItem(key);\n }\n\n set(key: string, value: string): void {\n sessionStorage.setItem(key, value);\n }\n\n remove(key: string): void {\n sessionStorage.removeItem(key);\n }\n\n clear(): void {\n sessionStorage.clear();\n }\n}\n","/**\n * Interface for token/state storage backends.\n */\nexport interface StorageProvider {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n clear(): void;\n}\n\n/** Well-known storage keys used by the SDK. */\nexport const StorageKeys = {\n STATE: 'safeguard_state',\n CODE_VERIFIER: 'safeguard_code_verifier',\n} as const;\n","import type { HttpClient, HttpRequestOptions, HttpResponse } from './types.js';\n\n/**\n * Browser HTTP client using native fetch API.\n * No TLS configuration needed — browser handles certificates.\n */\nexport class BrowserHttpClient implements HttpClient {\n readonly #credentials: RequestCredentials;\n\n constructor(options?: { credentials?: RequestCredentials }) {\n this.#credentials = options?.credentials ?? 'include';\n }\n\n async request(options: HttpRequestOptions): Promise<HttpResponse> {\n const { url, method, headers, body, signal, timeout } = options;\n\n const requestInit: RequestInit = {\n method,\n credentials: this.#credentials,\n };\n\n if (headers) requestInit.headers = headers;\n if (body != null) requestInit.body = body as BodyInit;\n if (signal || timeout) {\n const controller = new AbortController();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (timeout) {\n timeoutId = setTimeout(() => controller.abort(new Error('Request timed out')), timeout);\n }\n\n requestInit.signal = signal\n ? AbortSignal.any([signal, controller.signal])\n : controller.signal;\n\n try {\n return await this.#doFetch(url, requestInit);\n } finally {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n }\n }\n\n return this.#doFetch(url, requestInit);\n }\n\n async #doFetch(url: string, init: RequestInit): Promise<HttpResponse> {\n const response = await fetch(url, init);\n\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n const responseBody = await response.text();\n\n return {\n status: response.status,\n headers: responseHeaders,\n body: responseBody,\n };\n }\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { ConfigurationError } from '../errors.js';\nimport { SecretValue } from '../secret.js';\nimport { getTokenExpiresIn } from '../utils.js';\n\nexport interface PasswordAuthOptions {\n username: string;\n password: string;\n /** Identity provider name. Default: 'local'. */\n provider?: string;\n}\n\n/**\n * Authenticates using username/password via RSTS Resource Owner grant.\n */\nexport class PasswordAuth implements Auth {\n readonly #username: string;\n readonly #password: SecretValue;\n readonly #provider: string;\n\n constructor(options: PasswordAuthOptions) {\n if (!options.username) throw new ConfigurationError('PasswordAuth requires username');\n if (!options.password) throw new ConfigurationError('PasswordAuth requires password');\n if (!options.provider) throw new ConfigurationError('PasswordAuth requires provider');\n this.#username = options.username;\n this.#password = new SecretValue(options.password);\n this.#provider = options.provider;\n }\n\n get description(): string {\n return `PasswordAuth(${this.#username}@${this.#provider})`;\n }\n\n async authenticate(host: string, httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet> {\n // Step 1: Get RSTS token\n const rstsToken = await this.#getRstsToken(host, httpClient);\n\n // Step 2: Exchange RSTS token for Safeguard user token\n const userToken = await this.#exchangeForUserToken(host, httpClient, rstsToken);\n\n return userToken;\n }\n\n async refreshToken(host: string, httpClient: HttpClient, storage: StorageProvider): Promise<TokenSet | null> {\n // Password auth can always re-authenticate\n return this.authenticate(host, httpClient, storage);\n }\n\n async #getRstsToken(host: string, httpClient: HttpClient): Promise<string> {\n const body = JSON.stringify({\n grant_type: 'password',\n username: this.#username,\n password: this.#password.expose(),\n scope: 'rsts:sts:primaryproviderid:' + this.#provider.toLowerCase(),\n });\n\n const response = await httpClient.request({\n url: `https://${host}/RSTS/oauth2/token`,\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (response.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = JSON.parse(response.body) as { access_token: string };\n return data.access_token;\n }\n\n async #exchangeForUserToken(host: string, httpClient: HttpClient, rstsToken: string): Promise<TokenSet> {\n const response = await httpClient.request({\n url: `https://${host}/service/core/v4/Token/LoginResponse`,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${rstsToken}`,\n },\n body: JSON.stringify({ StsAccessToken: rstsToken }),\n });\n\n if (response.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = JSON.parse(response.body) as { UserToken: string; ExpiresIn?: number };\n const tokenSet: TokenSet = {\n accessToken: new SecretValue(data.UserToken),\n acquiredAt: Date.now(),\n };\n if (data.ExpiresIn != null) {\n tokenSet.expiresIn = data.ExpiresIn;\n } else {\n const expiresIn = getTokenExpiresIn(data.UserToken);\n if (expiresIn != null) tokenSet.expiresIn = expiresIn;\n }\n return tokenSet;\n }\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { ConfigurationError } from '../errors.js';\nimport { SecretValue } from '../secret.js';\nimport { StorageKeys } from '../storage/types.js';\nimport { generateCodeVerifier, generateCodeChallenge, generateState } from '../utils.js';\n\nexport interface PkceAuthOptions {\n /** OAuth redirect URI (must match RSTS configuration). */\n redirectUri: string;\n /** Identity provider name. Default: 'local'. */\n provider?: string;\n}\n\n/**\n * Authenticates using PKCE (Proof Key for Code Exchange) with browser redirect.\n * For browser environments where the user is redirected to RSTS login.\n */\nexport class PkceAuth implements Auth {\n readonly #redirectUri: string;\n readonly #provider: string;\n\n constructor(options: PkceAuthOptions) {\n if (!options.redirectUri) throw new ConfigurationError('PkceAuth requires redirectUri');\n if (!options.provider) throw new ConfigurationError('PkceAuth requires provider');\n this.#redirectUri = options.redirectUri;\n this.#provider = options.provider;\n }\n\n get description(): string {\n return `PKCE(redirect=${this.#redirectUri})`;\n }\n\n /**\n * Initiates the PKCE flow. If a stored authorization code exists (from callback),\n * exchanges it for a token. Otherwise, redirects to RSTS login.\n */\n async authenticate(host: string, httpClient: HttpClient, storage: StorageProvider): Promise<TokenSet> {\n // Check for authorization code (set by handlePkceCallback)\n const code = storage.get('safeguard_auth_code');\n if (code) {\n const verifier = storage.get(StorageKeys.CODE_VERIFIER);\n if (!verifier) {\n throw new ConfigurationError('PKCE code verifier not found in storage');\n }\n storage.remove('safeguard_auth_code');\n storage.remove(StorageKeys.CODE_VERIFIER);\n storage.remove(StorageKeys.STATE);\n return this.#exchangeCode(host, httpClient, code, verifier);\n }\n\n // No code — initiate redirect\n await this.#initiateRedirect(host, storage);\n\n // This point is never reached in a browser (redirect happens).\n // For testability, throw rather than hang.\n throw new ConfigurationError(\n 'PKCE redirect initiated. Call handlePkceCallback() on the callback page.',\n );\n }\n\n async #initiateRedirect(host: string, storage: StorageProvider): Promise<void> {\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n const state = generateState();\n\n storage.set(StorageKeys.CODE_VERIFIER, verifier);\n storage.set(StorageKeys.STATE, state);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: 'safeguard.js',\n redirect_uri: this.#redirectUri,\n scope: 'rsts:sts:primaryproviderid:' + this.#provider,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state,\n });\n\n const authUrl = `https://${host}/RSTS/Login?${params.toString()}`;\n\n // In browser environment, redirect\n if (typeof window !== 'undefined' && window.location) {\n window.location.href = authUrl;\n }\n }\n\n async #exchangeCode(\n host: string,\n httpClient: HttpClient,\n code: string,\n verifier: string,\n ): Promise<TokenSet> {\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.#redirectUri,\n client_id: 'safeguard.js',\n code_verifier: verifier,\n });\n\n const response = await httpClient.request({\n url: `https://${host}/RSTS/oauth2/token`,\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (response.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = JSON.parse(response.body) as { access_token: string; expires_in?: number };\n\n // Exchange RSTS token for Safeguard user token\n const userTokenResponse = await httpClient.request({\n url: `https://${host}/service/core/v4/Token/LoginResponse`,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${data.access_token}`,\n },\n body: JSON.stringify({ StsAccessToken: data.access_token }),\n });\n\n if (userTokenResponse.status !== 200) {\n const { ApiError } = await import('../errors.js');\n throw ApiError.fromResponse(userTokenResponse.status, userTokenResponse.body);\n }\n\n const userData = JSON.parse(userTokenResponse.body) as { UserToken: string; ExpiresIn?: number };\n const tokenSet: TokenSet = {\n accessToken: new SecretValue(userData.UserToken),\n acquiredAt: Date.now(),\n };\n if (userData.ExpiresIn != null) tokenSet.expiresIn = userData.ExpiresIn;\n\n // Token stays in-memory only (SafeguardClient.#tokenSet). Not persisted to\n // sessionStorage to avoid XSS exfiltration of high-privilege PAM tokens.\n return tokenSet;\n }\n}\n","import { StorageKeys } from '../storage/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport { ConfigurationError } from '../errors.js';\n\nexport interface PkceCallbackResult {\n code: string;\n state: string;\n}\n\n/**\n * Parses the PKCE callback URL and stores the authorization code.\n * Call this on the callback/redirect page to complete the PKCE flow.\n *\n * This is the explicit replacement for the legacy `safeguardJsAccessTokenChecker.js`\n * script that ran as a side-effect on import.\n *\n * @param storage - StorageProvider to persist the code\n * @param url - The callback URL to parse (defaults to window.location.href)\n * @returns The parsed code and state, or null if not a valid callback\n */\nexport function handlePkceCallback(\n storage: StorageProvider,\n url?: string,\n): PkceCallbackResult | null {\n const callbackUrl = url ?? (typeof window !== 'undefined' ? window.location.href : '');\n if (!callbackUrl) return null;\n\n const parsed = new URL(callbackUrl);\n const code = parsed.searchParams.get('code');\n const state = parsed.searchParams.get('state');\n\n if (!code || !state) return null;\n\n // Validate state matches what we stored\n const storedState = storage.get(StorageKeys.STATE);\n if (storedState && storedState !== state) {\n throw new ConfigurationError('PKCE state mismatch — possible CSRF attack');\n }\n\n // Store the code for the PkceAuth strategy to pick up\n storage.set('safeguard_auth_code', code);\n\n return { code, state };\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { ConfigurationError } from '../errors.js';\nimport { SecretValue } from '../secret.js';\n\nexport interface TokenAuthOptions {\n /** Pre-existing Safeguard access token. */\n accessToken: string;\n}\n\n/**\n * Uses a pre-existing access token. No authentication flow — the token is assumed valid.\n * Useful when tokens are obtained externally or for service-to-service scenarios.\n */\nexport class TokenAuth implements Auth {\n readonly #accessToken: SecretValue;\n\n constructor(options: TokenAuthOptions) {\n if (!options.accessToken) throw new ConfigurationError('TokenAuth requires accessToken');\n this.#accessToken = new SecretValue(options.accessToken);\n }\n\n get description(): string {\n return 'TokenAuth(pre-existing)';\n }\n\n async authenticate(_host: string, _httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet> {\n return {\n accessToken: this.#accessToken,\n acquiredAt: Date.now(),\n };\n }\n\n // No refresh possible with a pre-existing token\n}\n","import type { HttpClient } from '../http/types.js';\nimport type { StorageProvider } from '../storage/types.js';\nimport type { Auth, TokenSet } from './types.js';\nimport { SecretValue } from '../secret.js';\n\n/**\n * Anonymous authentication — no credentials.\n * Used for accessing public Safeguard endpoints (e.g., appliance status).\n */\nexport class AnonymousAuth implements Auth {\n get description(): string {\n return 'AnonymousAuth';\n }\n\n async authenticate(_host: string, _httpClient: HttpClient, _storage: StorageProvider): Promise<TokenSet> {\n return {\n accessToken: new SecretValue(''),\n acquiredAt: Date.now(),\n };\n }\n}\n","import { Service, HttpMethod } from './types.js';\nimport type { RequestOptions, SafeguardClientOptions } from './types.js';\nimport type { Auth, TokenSet } from './auth/types.js';\nimport type { HttpClient } from './http/types.js';\nimport type { StorageProvider } from './storage/types.js';\nimport { MemoryStorage } from './storage/memory.js';\nimport { buildRequestUrl, validateHost } from './utils.js';\nimport { AuthenticationError, ConfigurationError, ApiError } from './errors.js';\n\nconst DEFAULT_TIMEOUT = 300_000;\nconst DEFAULT_API_VERSION = 'v4';\nconst TOKEN_REFRESH_MARGIN_MS = 60_000; // refresh 60s before expiry\n\n/**\n * Main entry point for the Safeguard SDK.\n * Instance-based — no global state. Multiple clients to different appliances are supported.\n */\nexport class SafeguardClient {\n readonly #host: string;\n readonly #auth: Auth;\n readonly #apiVersion: string;\n readonly #timeout: number;\n readonly #autoRefresh: boolean;\n readonly #ca: string | Buffer | undefined;\n readonly #verify: boolean;\n\n #httpClient: HttpClient | undefined;\n #storage: StorageProvider;\n #tokenSet: TokenSet | undefined;\n #connected = false;\n /** Single-flight refresh promise to prevent thundering herd on token expiry. */\n #tokenRefreshPromise: Promise<void> | undefined;\n\n constructor(host: string, options: SafeguardClientOptions) {\n if (!host) throw new ConfigurationError('SafeguardClient requires a host');\n if (!options.auth) throw new ConfigurationError('SafeguardClient requires an auth strategy');\n this.#host = validateHost(host);\n this.#auth = options.auth;\n this.#apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;\n this.#timeout = options.timeout ?? DEFAULT_TIMEOUT;\n this.#autoRefresh = options.autoRefresh !== false;\n this.#ca = options.ca;\n this.#verify = options.verify !== false;\n this.#storage = new MemoryStorage();\n }\n\n /** The appliance hostname this client connects to. */\n get host(): string {\n return this.#host;\n }\n\n /** The API version prefix (e.g. 'v4'). */\n get apiVersion(): string {\n return this.#apiVersion;\n }\n\n /** Custom CA certificate, if provided. */\n get ca(): string | Buffer | undefined {\n return this.#ca;\n }\n\n /** Whether TLS verification is enabled. */\n get verify(): boolean {\n return this.#verify;\n }\n\n /** Whether the client has an active authenticated session. */\n get isConnected(): boolean {\n return this.#connected;\n }\n\n /**\n * Sets a custom StorageProvider (e.g., BrowserSessionStorage).\n * Must be called before connect().\n */\n setStorage(storage: StorageProvider): this {\n this.#storage = storage;\n return this;\n }\n\n /**\n * Sets the HttpClient implementation. If not called, must be set before connect().\n * The factory pattern allows platform-specific client injection:\n * - Node: `new NodeHttpClient({ ca, rejectUnauthorized })`\n * - Browser: `new BrowserHttpClient()`\n */\n setHttpClient(httpClient: HttpClient): this {\n this.#httpClient = httpClient;\n return this;\n }\n\n /**\n * Authenticate and establish a session.\n * @throws {AuthenticationError} if authentication fails\n * @throws {ConfigurationError} if httpClient not set\n */\n async connect(): Promise<void> {\n if (!this.#httpClient) {\n throw new ConfigurationError(\n 'HttpClient not set. Call setHttpClient() before connect(), or use SafeguardClient.createNode() / SafeguardClient.createBrowser().',\n );\n }\n\n this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);\n if (!this.#tokenSet.accessToken.expose()) {\n throw new AuthenticationError('Authentication returned empty access token');\n }\n this.#connected = true;\n }\n\n /**\n * Disconnect — dispose HTTP client and clear token.\n */\n async disconnect(): Promise<void> {\n this.#tokenSet = undefined;\n this.#connected = false;\n this.#httpClient?.dispose?.();\n }\n\n /**\n * Returns seconds until the current access token expires, or -1 if unknown.\n */\n getAccessTokenLifetimeRemaining(): number {\n if (!this.#tokenSet?.expiresIn) return -1;\n const elapsed = (Date.now() - this.#tokenSet.acquiredAt) / 1000;\n return Math.max(0, this.#tokenSet.expiresIn - elapsed);\n }\n\n // ─── Convenience HTTP verbs ─────────────────────────────────────────\n\n async get<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.GET, relativeUrl, options);\n }\n\n async post<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.POST, relativeUrl, options);\n }\n\n async put<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.PUT, relativeUrl, options);\n }\n\n async delete<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.DELETE, relativeUrl, options);\n }\n\n async patch<T = unknown>(service: Service, relativeUrl: string, options?: RequestOptions): Promise<T> {\n return this.invoke<T>(service, HttpMethod.PATCH, relativeUrl, options);\n }\n\n // ─── Core invoke ────────────────────────────────────────────────────\n\n /**\n * Low-level request method. Convenience verbs delegate here.\n *\n * When `options.fullResponse` is true, returns SafeguardResponse<T> with status + headers.\n * Otherwise returns just the parsed response body as T.\n */\n async invoke<T = unknown>(\n service: Service,\n method: HttpMethod,\n relativeUrl: string,\n options?: RequestOptions,\n ): Promise<T> {\n if (!this.#connected || !this.#httpClient) {\n throw new ConfigurationError('Client not connected. Call connect() first.');\n }\n\n // Auto-refresh token if needed\n if (this.#autoRefresh) {\n await this.#ensureValidToken();\n }\n\n const url = buildRequestUrl(this.#host, service, relativeUrl, options?.query, this.#apiVersion);\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.#tokenSet!.accessToken.expose()}`,\n Accept: 'application/json',\n ...options?.headers,\n };\n\n let body: string | null = null;\n if (options?.json != null) {\n body = JSON.stringify(options.json);\n headers['Content-Type'] = 'application/json';\n } else if (options?.body != null) {\n body = options.body as string;\n }\n\n const requestOpts: import('./http/types.js').HttpRequestOptions = {\n url,\n method,\n headers,\n body,\n timeout: options?.timeout ?? this.#timeout,\n };\n if (options?.signal) requestOpts.signal = options.signal;\n\n const response = await this.#httpClient.request(requestOpts);\n\n if (response.status >= 400) {\n throw ApiError.fromResponse(response.status, response.body);\n }\n\n const data = response.body ? (JSON.parse(response.body) as T) : (undefined as T);\n\n if (options?.fullResponse) {\n return { data, status: response.status, headers: response.headers } as unknown as T;\n }\n\n return data;\n }\n\n // ─── Private helpers ────────────────────────────────────────────────\n\n async #ensureValidToken(): Promise<void> {\n if (!this.#tokenSet) return;\n if (!this.#tokenSet.expiresIn) return; // can't determine expiry\n\n const elapsed = Date.now() - this.#tokenSet.acquiredAt;\n const expiresInMs = this.#tokenSet.expiresIn * 1000;\n\n if (elapsed + TOKEN_REFRESH_MARGIN_MS < expiresInMs) {\n return; // token still fresh\n }\n\n // Single-flight: if a refresh is already running, all concurrent callers\n // await the same promise so the auth server only sees one exchange and no\n // two callers race the storage provider.\n if (this.#tokenRefreshPromise) {\n await this.#tokenRefreshPromise;\n return;\n }\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\n try {\n await this.#tokenRefreshPromise;\n } finally {\n // Always clear the slot — failures must not be cached, otherwise\n // every subsequent invoke would inherit the same rejection forever.\n this.#tokenRefreshPromise = undefined;\n }\n }\n}\n"],"mappings":"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"}
package/dist/events.cjs CHANGED
@@ -80,6 +80,8 @@ var PersistentSafeguardEventListener = class {
80
80
  #manualStop = false;
81
81
  #retryCount = 0;
82
82
  #retryTimer;
83
+ /** Single-flight token refresh promise to prevent thundering herd. */
84
+ #tokenRefreshPromise;
83
85
  constructor(connection, auth, host, httpClient, storage, options, tokenSet) {
84
86
  this.#connection = connection;
85
87
  this.#auth = auth;
@@ -163,11 +165,7 @@ var PersistentSafeguardEventListener = class {
163
165
  if (this.#manualStop) return;
164
166
  this.#retryCount++;
165
167
  try {
166
- if (this.#isTokenExpired()) if (this.#auth.refreshToken) {
167
- const refreshed = await this.#auth.refreshToken(this.#host, this.#httpClient, this.#storage);
168
- if (refreshed) this.#tokenSet = refreshed;
169
- else this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
170
- } else this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
168
+ if (this.#isTokenExpired()) await this.#refreshTokenSingleFlight();
171
169
  await this.#connection.start();
172
170
  this.#retryCount = 0;
173
171
  this.#setState("connected");
@@ -175,6 +173,27 @@ var PersistentSafeguardEventListener = class {
175
173
  this.#scheduleReconnect();
176
174
  }
177
175
  }
176
+ async #refreshTokenSingleFlight() {
177
+ if (this.#tokenRefreshPromise) {
178
+ await this.#tokenRefreshPromise;
179
+ return;
180
+ }
181
+ this.#tokenRefreshPromise = (async () => {
182
+ if (this.#auth.refreshToken) {
183
+ const refreshed = await this.#auth.refreshToken(this.#host, this.#httpClient, this.#storage);
184
+ if (refreshed) {
185
+ this.#tokenSet = refreshed;
186
+ return;
187
+ }
188
+ }
189
+ this.#tokenSet = await this.#auth.authenticate(this.#host, this.#httpClient, this.#storage);
190
+ })();
191
+ try {
192
+ await this.#tokenRefreshPromise;
193
+ } finally {
194
+ this.#tokenRefreshPromise = void 0;
195
+ }
196
+ }
178
197
  #isTokenExpired() {
179
198
  if (!this.#tokenSet) return true;
180
199
  if (!this.#tokenSet.expiresIn) return true;
@@ -1 +1 @@
1
- {"version":3,"file":"events.cjs","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.cjs","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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"}