@dloizides/bff-web-client 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to `@dloizides/bff-web-client` are documented here.
4
4
 
5
+ ## [1.1.0] - 2026-07-05
6
+
7
+ ### Added
8
+ - `registerWarmupRetryInterceptor` — retries transient cold-start `502/503/504`
9
+ gateway failures (upstream not ready = request not processed) with exponential
10
+ backoff, so the first `/bff/me` probe after idle no longer surfaces an
11
+ auth-error / anonymous flash (P1-06). Wired FIRST in the response chain by
12
+ `registerInterceptors` so it resolves before any downstream interceptor sees
13
+ the error. Opt out or tune via the new `warmupRetry` port
14
+ (`WarmupRetryConfig | false`; defaults: 3 retries, 400ms base, [502,503,504]).
15
+ `401`/`4xx`/`500` are never retried.
16
+
5
17
  ## [1.0.0] - 2026-06-14
6
18
 
7
19
  ### Added
package/dist/index.d.mts CHANGED
@@ -32,6 +32,21 @@ type EmitToast = (message: string, severity: ErrorSeverity) => void;
32
32
  * {@link registerInterceptors} in the correct chain position.
33
33
  */
34
34
  type InterceptorRegistrar = (instance: AxiosInstance) => void;
35
+ /**
36
+ * Warm-up retry tuning for {@link RegisterInterceptorsPorts.warmupRetry} — the
37
+ * P1-06 cold-start 502/503/504 retry. All fields optional; defaults are
38
+ * 3 retries, 400ms base (exponential backoff), statuses [502, 503, 504].
39
+ */
40
+ interface WarmupRetryConfig {
41
+ /** Max retry attempts after the first failure. Default 3. */
42
+ maxRetries?: number;
43
+ /** Base backoff in ms; attempt N waits `base * 2^(N-1)`. Default 400. */
44
+ baseDelayMs?: number;
45
+ /** Statuses treated as transient warm-up failures. Default [502, 503, 504]. */
46
+ retryableStatuses?: readonly number[];
47
+ /** Backoff sleeper — injectable for tests. Defaults to `setTimeout`. */
48
+ sleep?: (ms: number) => Promise<void>;
49
+ }
35
50
  /**
36
51
  * Options for {@link createBffAxiosClient}.
37
52
  */
@@ -66,6 +81,11 @@ interface RegisterInterceptorsPorts {
66
81
  * not handle 401 session death in the HTTP layer.
67
82
  */
68
83
  onSessionExpiry?: InterceptorRegistrar;
84
+ /**
85
+ * Warm-up retry tuning (P1-06). Transient cold-start `502/503/504`s are
86
+ * retried with backoff by default; pass overrides here, or `false` to disable.
87
+ */
88
+ warmupRetry?: WarmupRetryConfig | false;
69
89
  }
70
90
 
71
91
  /**
@@ -93,10 +113,15 @@ declare function createBffAxiosClient(options: BffAxiosClientOptions): AxiosInst
93
113
  * 2. csrf (registered second, runs first = attaches the CSRF header)
94
114
  *
95
115
  * Response interceptors run in ORDER of registration, so:
96
- * 1. logging (logs response/error first)
97
- * 2. normalizer (emits success toast)
98
- * 3. session expiry (handles 401 -> clear session, app-supplied port)
99
- * 4. error classifier (classifies remaining errors)
116
+ * 1. warm-up retry (retries transient 502/503/504 upstream-cold failures)
117
+ * 2. logging (logs response/error)
118
+ * 3. normalizer (emits success toast)
119
+ * 4. session expiry (handles 401 -> clear session, app-supplied port)
120
+ * 5. error classifier (classifies remaining errors)
121
+ *
122
+ * The warm-up retry runs FIRST so a cold-start 502/503/504 is re-issued and
123
+ * (usually) resolves before any downstream interceptor logs or surfaces it as
124
+ * an error — the P1-06 "auth-error/anonymous flash on first load" fix.
100
125
  *
101
126
  * The package owns the logging, normalizer, and error-classifier bodies. The
102
127
  * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns
@@ -191,4 +216,34 @@ declare function attachCsrfHeader(config: InternalAxiosRequestConfig): InternalA
191
216
  */
192
217
  declare function registerDefaultCsrfInterceptor(instance: AxiosInstance): number;
193
218
 
194
- export { type BffAxiosClientOptions, type BffLogger, type EmitToast, type InterceptorRegistrar, type RegisterInterceptorsPorts, attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer };
219
+ /**
220
+ * Response error interceptor: transparently retries transient warm-up failures.
221
+ *
222
+ * WHY (P1-06): on a resource-tight, scale-to-zero prod node the BFF (or its
223
+ * upstream API) can be cold on the FIRST request after idle — the gateway answers
224
+ * `502/503/504` for a few seconds while the upstream comes up, then settles to a
225
+ * correct `401/200`. The unguarded `/bff/me` probe surfaced that transient as an
226
+ * auth error / anonymous flash on first load. These statuses mean the request was
227
+ * NOT processed (upstream unavailable), so retrying is safe for any method.
228
+ *
229
+ * This interceptor catches those statuses and re-issues the SAME request up to
230
+ * `maxRetries` times with exponential backoff, BEFORE the error classifier turns
231
+ * it into a surfaced error — so it must be registered FIRST in the response
232
+ * chain. `4xx` (incl. 401) and `500` fall straight through: a 500 is a real
233
+ * server error, not a warm-up, and must not be retried.
234
+ *
235
+ * Deterministic + timer-injectable: the backoff `sleep` is a port (defaults to
236
+ * `setTimeout`) so unit tests resolve instantly and no jitter/`Math.random` is
237
+ * used.
238
+ */
239
+
240
+ /** Gateway/upstream-not-ready statuses that mean "request not processed → safe to retry". */
241
+ declare const DEFAULT_RETRYABLE_STATUSES: readonly number[];
242
+ declare const DEFAULT_MAX_RETRIES = 3;
243
+ declare const DEFAULT_BASE_DELAY_MS = 400;
244
+ /**
245
+ * Registers the warm-up retry interceptor. Returns the interceptor ID for ejection.
246
+ */
247
+ declare function registerWarmupRetryInterceptor(instance: AxiosInstance, logger: BffLogger, config?: WarmupRetryConfig): number;
248
+
249
+ export { type BffAxiosClientOptions, type BffLogger, DEFAULT_BASE_DELAY_MS, DEFAULT_MAX_RETRIES, DEFAULT_RETRYABLE_STATUSES, type EmitToast, type InterceptorRegistrar, type RegisterInterceptorsPorts, type WarmupRetryConfig, attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer, registerWarmupRetryInterceptor };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,21 @@ type EmitToast = (message: string, severity: ErrorSeverity) => void;
32
32
  * {@link registerInterceptors} in the correct chain position.
33
33
  */
34
34
  type InterceptorRegistrar = (instance: AxiosInstance) => void;
35
+ /**
36
+ * Warm-up retry tuning for {@link RegisterInterceptorsPorts.warmupRetry} — the
37
+ * P1-06 cold-start 502/503/504 retry. All fields optional; defaults are
38
+ * 3 retries, 400ms base (exponential backoff), statuses [502, 503, 504].
39
+ */
40
+ interface WarmupRetryConfig {
41
+ /** Max retry attempts after the first failure. Default 3. */
42
+ maxRetries?: number;
43
+ /** Base backoff in ms; attempt N waits `base * 2^(N-1)`. Default 400. */
44
+ baseDelayMs?: number;
45
+ /** Statuses treated as transient warm-up failures. Default [502, 503, 504]. */
46
+ retryableStatuses?: readonly number[];
47
+ /** Backoff sleeper — injectable for tests. Defaults to `setTimeout`. */
48
+ sleep?: (ms: number) => Promise<void>;
49
+ }
35
50
  /**
36
51
  * Options for {@link createBffAxiosClient}.
37
52
  */
@@ -66,6 +81,11 @@ interface RegisterInterceptorsPorts {
66
81
  * not handle 401 session death in the HTTP layer.
67
82
  */
68
83
  onSessionExpiry?: InterceptorRegistrar;
84
+ /**
85
+ * Warm-up retry tuning (P1-06). Transient cold-start `502/503/504`s are
86
+ * retried with backoff by default; pass overrides here, or `false` to disable.
87
+ */
88
+ warmupRetry?: WarmupRetryConfig | false;
69
89
  }
70
90
 
71
91
  /**
@@ -93,10 +113,15 @@ declare function createBffAxiosClient(options: BffAxiosClientOptions): AxiosInst
93
113
  * 2. csrf (registered second, runs first = attaches the CSRF header)
94
114
  *
95
115
  * Response interceptors run in ORDER of registration, so:
96
- * 1. logging (logs response/error first)
97
- * 2. normalizer (emits success toast)
98
- * 3. session expiry (handles 401 -> clear session, app-supplied port)
99
- * 4. error classifier (classifies remaining errors)
116
+ * 1. warm-up retry (retries transient 502/503/504 upstream-cold failures)
117
+ * 2. logging (logs response/error)
118
+ * 3. normalizer (emits success toast)
119
+ * 4. session expiry (handles 401 -> clear session, app-supplied port)
120
+ * 5. error classifier (classifies remaining errors)
121
+ *
122
+ * The warm-up retry runs FIRST so a cold-start 502/503/504 is re-issued and
123
+ * (usually) resolves before any downstream interceptor logs or surfaces it as
124
+ * an error — the P1-06 "auth-error/anonymous flash on first load" fix.
100
125
  *
101
126
  * The package owns the logging, normalizer, and error-classifier bodies. The
102
127
  * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns
@@ -191,4 +216,34 @@ declare function attachCsrfHeader(config: InternalAxiosRequestConfig): InternalA
191
216
  */
192
217
  declare function registerDefaultCsrfInterceptor(instance: AxiosInstance): number;
193
218
 
194
- export { type BffAxiosClientOptions, type BffLogger, type EmitToast, type InterceptorRegistrar, type RegisterInterceptorsPorts, attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer };
219
+ /**
220
+ * Response error interceptor: transparently retries transient warm-up failures.
221
+ *
222
+ * WHY (P1-06): on a resource-tight, scale-to-zero prod node the BFF (or its
223
+ * upstream API) can be cold on the FIRST request after idle — the gateway answers
224
+ * `502/503/504` for a few seconds while the upstream comes up, then settles to a
225
+ * correct `401/200`. The unguarded `/bff/me` probe surfaced that transient as an
226
+ * auth error / anonymous flash on first load. These statuses mean the request was
227
+ * NOT processed (upstream unavailable), so retrying is safe for any method.
228
+ *
229
+ * This interceptor catches those statuses and re-issues the SAME request up to
230
+ * `maxRetries` times with exponential backoff, BEFORE the error classifier turns
231
+ * it into a surfaced error — so it must be registered FIRST in the response
232
+ * chain. `4xx` (incl. 401) and `500` fall straight through: a 500 is a real
233
+ * server error, not a warm-up, and must not be retried.
234
+ *
235
+ * Deterministic + timer-injectable: the backoff `sleep` is a port (defaults to
236
+ * `setTimeout`) so unit tests resolve instantly and no jitter/`Math.random` is
237
+ * used.
238
+ */
239
+
240
+ /** Gateway/upstream-not-ready statuses that mean "request not processed → safe to retry". */
241
+ declare const DEFAULT_RETRYABLE_STATUSES: readonly number[];
242
+ declare const DEFAULT_MAX_RETRIES = 3;
243
+ declare const DEFAULT_BASE_DELAY_MS = 400;
244
+ /**
245
+ * Registers the warm-up retry interceptor. Returns the interceptor ID for ejection.
246
+ */
247
+ declare function registerWarmupRetryInterceptor(instance: AxiosInstance, logger: BffLogger, config?: WarmupRetryConfig): number;
248
+
249
+ export { type BffAxiosClientOptions, type BffLogger, DEFAULT_BASE_DELAY_MS, DEFAULT_MAX_RETRIES, DEFAULT_RETRYABLE_STATUSES, type EmitToast, type InterceptorRegistrar, type RegisterInterceptorsPorts, type WarmupRetryConfig, attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer, registerWarmupRetryInterceptor };
package/dist/index.js CHANGED
@@ -264,15 +264,68 @@ function registerResponseNormalizer(instance, emitToast, logger) {
264
264
  );
265
265
  }
266
266
 
267
+ // src/interceptors/warmupRetryInterceptor.ts
268
+ var LOG_CONTEXT4 = "warmupRetry";
269
+ var DEFAULT_RETRYABLE_STATUSES = [502, 503, 504];
270
+ var DEFAULT_MAX_RETRIES = 3;
271
+ var DEFAULT_BASE_DELAY_MS = 400;
272
+ var BACKOFF_FACTOR = 2;
273
+ function defaultSleep(ms) {
274
+ return new Promise((resolve) => setTimeout(resolve, ms));
275
+ }
276
+ function isAxiosError2(value) {
277
+ return typeof value === "object" && value !== null && "isAxiosError" in value;
278
+ }
279
+ function registerWarmupRetryInterceptor(instance, logger, config = {}) {
280
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
281
+ const baseDelayMs = config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
282
+ const retryableStatuses = config.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES;
283
+ const sleep = config.sleep ?? defaultSleep;
284
+ return instance.interceptors.response.use(
285
+ (response) => response,
286
+ async (error) => {
287
+ if (!isAxiosError2(error) || error.response === void 0) {
288
+ return Promise.reject(error);
289
+ }
290
+ if (!retryableStatuses.includes(error.response.status)) {
291
+ return Promise.reject(error);
292
+ }
293
+ const requestConfig = error.config;
294
+ if (requestConfig === void 0) {
295
+ return Promise.reject(error);
296
+ }
297
+ const attempt = (requestConfig.__warmupRetryCount ?? 0) + 1;
298
+ if (attempt > maxRetries) {
299
+ logger.warn(LOG_CONTEXT4, `gave up after ${maxRetries} warm-up retries`, {
300
+ url: requestConfig.url,
301
+ status: error.response.status
302
+ });
303
+ return Promise.reject(error);
304
+ }
305
+ requestConfig.__warmupRetryCount = attempt;
306
+ const delayMs = baseDelayMs * BACKOFF_FACTOR ** (attempt - 1);
307
+ logger.debug(LOG_CONTEXT4, `upstream warming up \u2014 retry ${attempt}/${maxRetries} in ${delayMs}ms`, {
308
+ url: requestConfig.url,
309
+ status: error.response.status
310
+ });
311
+ await sleep(delayMs);
312
+ return instance.request(requestConfig);
313
+ }
314
+ );
315
+ }
316
+
267
317
  // src/registerInterceptors.ts
268
318
  function registerInterceptors(instance, ports) {
269
- const { logger, emitToast, csrf, onSessionExpiry } = ports;
319
+ const { logger, emitToast, csrf, onSessionExpiry, warmupRetry } = ports;
270
320
  registerLoggingInterceptor(instance, logger);
271
321
  if (csrf) {
272
322
  csrf(instance);
273
323
  } else {
274
324
  registerDefaultCsrfInterceptor(instance);
275
325
  }
326
+ if (warmupRetry !== false) {
327
+ registerWarmupRetryInterceptor(instance, logger, warmupRetry ?? {});
328
+ }
276
329
  registerResponseNormalizer(instance, emitToast, logger);
277
330
  if (onSessionExpiry) {
278
331
  onSessionExpiry(instance);
@@ -280,6 +333,9 @@ function registerInterceptors(instance, ports) {
280
333
  registerErrorClassifier(instance, logger);
281
334
  }
282
335
 
336
+ exports.DEFAULT_BASE_DELAY_MS = DEFAULT_BASE_DELAY_MS;
337
+ exports.DEFAULT_MAX_RETRIES = DEFAULT_MAX_RETRIES;
338
+ exports.DEFAULT_RETRYABLE_STATUSES = DEFAULT_RETRYABLE_STATUSES;
283
339
  exports.attachCsrfHeader = attachCsrfHeader;
284
340
  exports.classifyError = classifyError;
285
341
  exports.createBffAxiosClient = createBffAxiosClient;
@@ -289,5 +345,6 @@ exports.registerErrorClassifier = registerErrorClassifier;
289
345
  exports.registerInterceptors = registerInterceptors;
290
346
  exports.registerLoggingInterceptor = registerLoggingInterceptor;
291
347
  exports.registerResponseNormalizer = registerResponseNormalizer;
348
+ exports.registerWarmupRetryInterceptor = registerWarmupRetryInterceptor;
292
349
  //# sourceMappingURL=index.js.map
293
350
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/createBffAxiosClient.ts","../src/interceptors/defaultCsrfInterceptor.ts","../src/interceptors/errorClassifierInterceptor.ts","../src/interceptors/loggingInterceptor.ts","../src/interceptors/responseNormalizer.ts","../src/registerInterceptors.ts"],"names":["axios","isValueDefined","HttpMethod","LOG_CONTEXT","isRecord","ErrorSeverity"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Clean axios instance factory for the BFF era.\n *\n * Produces an axios instance with the BFF defaults (JSON, XHR marker,\n * credentialed) and no interceptors. Interceptors are wired separately via\n * {@link registerInterceptors} so the app controls the chain composition.\n */\n\nimport axios from 'axios';\n\nimport type { BffAxiosClientOptions } from './types';\nimport type { AxiosInstance } from 'axios';\n\nconst BFF_DEFAULT_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n};\n\n/**\n * Creates a credentialed axios instance with the shared BFF defaults. No\n * interceptors are registered here — call {@link registerInterceptors} once at\n * bootstrap to install the chain.\n */\nfunction createBffAxiosClient(options: BffAxiosClientOptions): AxiosInstance {\n return axios.create({\n timeout: options.timeoutMs,\n baseURL: options.baseURL,\n headers: { ...BFF_DEFAULT_HEADERS, ...(options.headers ?? {}) },\n withCredentials: true,\n });\n}\n\nexport { createBffAxiosClient };\n","/**\n * Default CSRF request interceptor.\n *\n * After a BFF cutover, an SPA authenticates via a cookie. Cookie auth\n * reintroduces CSRF risk, so the BFF's `Bff.AspNetCore` anti-forgery\n * middleware requires a custom header on every state-changing request — a\n * request a cross-site form POST cannot forge. This default attaches\n * `X-BFF-Csrf: 1` to all mutating methods.\n *\n * This is the **default implementation of the `csrf` port**. Apps that need a\n * different CSRF strategy supply their own registrar to\n * {@link registerInterceptors}; this body is what diverged per app and is kept\n * overridable on purpose.\n */\n\nimport type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';\n\n/** Header name + value the `Bff.AspNetCore` anti-forgery middleware checks. */\nconst CSRF_HEADER = 'X-BFF-Csrf';\nconst CSRF_HEADER_VALUE = '1';\n\n/** Methods the BFF anti-forgery middleware treats as state-changing. */\nconst STATE_CHANGING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\n\nfunction isStateChanging(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return STATE_CHANGING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Adds `X-BFF-Csrf` to every state-changing request. Safe (GET/HEAD) requests\n * are left untouched — the BFF only enforces the header on mutations.\n */\nfunction attachCsrfHeader(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {\n if (isStateChanging(config.method)) {\n config.headers.set(CSRF_HEADER, CSRF_HEADER_VALUE);\n }\n return config;\n}\n\n/**\n * Registers the default CSRF request interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerDefaultCsrfInterceptor(instance: AxiosInstance): number {\n return instance.interceptors.request.use(attachCsrfHeader);\n}\n\nexport { registerDefaultCsrfInterceptor, attachCsrfHeader };\n","/**\n * Response error interceptor: classifies axios errors.\n *\n * Converts raw AxiosError objects into {@link ClassifiedError} instances with\n * full context (status, url, method, errorCode, message, etc), logs them\n * through the app-supplied {@link BffLogger}, and re-rejects so callers and\n * downstream interceptors can react.\n */\n\nimport { HttpMethod } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { ClassifiedError } from '@dloizides/api-client-base';\nimport type { AxiosError, AxiosInstance, AxiosResponse } from 'axios';\n\nconst LOG_CONTEXT = 'errorClassifier';\nconst TIMEOUT_ERROR_CODE = 'ECONNABORTED';\nconst NETWORK_ERROR_STATUS = 0;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction isAxiosError(value: unknown): value is AxiosError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'isAxiosError' in value;\n}\n\nfunction extractErrorCode(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const code = data.errorCode ?? data.code ?? data.error;\n if (typeof code === 'string' && code.length > 0) {\n return code;\n }\n\n return undefined;\n}\n\nfunction extractErrorMessage(data: unknown, fallback: string): string {\n if (!isRecord(data)) {\n return fallback;\n }\n\n const message = data.message ?? data.detail ?? data.error ?? data.title;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n return fallback;\n}\n\nfunction extractStringHeader(headers: Record<string, unknown>, key: string): string | undefined {\n const value: unknown = headers[key];\n if (typeof value === 'string' && value.length > 0) {\n return value;\n }\n return undefined;\n}\n\nfunction extractRequestId(response: AxiosResponse | undefined): string | undefined {\n if (!isValueDefined(response)) {\n return undefined;\n }\n\n const headers = response.headers;\n if (!isRecord(headers)) {\n return undefined;\n }\n\n return extractStringHeader(headers, 'x-request-id') ?? extractStringHeader(headers, 'x-correlation-id');\n}\n\nfunction resolveHttpMethod(method: string | undefined): HttpMethod {\n if (typeof method !== 'string') {\n return HttpMethod.Get;\n }\n const upper = method.toUpperCase();\n const methodMap: Record<string, HttpMethod | undefined> = {\n GET: HttpMethod.Get,\n POST: HttpMethod.Post,\n PUT: HttpMethod.Put,\n PATCH: HttpMethod.Patch,\n DELETE: HttpMethod.Delete,\n };\n return methodMap[upper] ?? HttpMethod.Get;\n}\n\n/**\n * Converts an AxiosError into a {@link ClassifiedError} with full context.\n */\nfunction classifyError(error: AxiosError): ClassifiedError {\n const response = error.response;\n const hasResponse = isValueDefined(response);\n\n const status = hasResponse ? response.status : NETWORK_ERROR_STATUS;\n const url = error.config?.url ?? 'unknown';\n const method = resolveHttpMethod(error.config?.method);\n const body = hasResponse ? response.data : undefined;\n\n const isTimeout = error.code === TIMEOUT_ERROR_CODE;\n const defaultMessage = isTimeout ? 'Request timed out' : 'Network error';\n const errorCode = extractErrorCode(body) ?? (isTimeout ? TIMEOUT_ERROR_CODE : undefined);\n const message = hasResponse ? extractErrorMessage(body, error.message) : defaultMessage;\n\n return {\n status,\n url,\n method,\n errorCode,\n message,\n body,\n originalError: error,\n timestamp: Date.now(),\n requestId: extractRequestId(response),\n };\n}\n\n/**\n * Handles response errors by classifying them and logging.\n */\nasync function handleResponseError(error: unknown, logger: BffLogger): Promise<never> {\n if (!isAxiosError(error)) {\n return Promise.reject(error);\n }\n\n const classified = classifyError(error);\n\n logger.warn(LOG_CONTEXT, `HTTP ${classified.method} ${classified.url} failed`, {\n status: classified.status,\n errorCode: classified.errorCode,\n message: classified.message,\n });\n\n return Promise.reject(error);\n}\n\n/**\n * Registers the error classifier interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerErrorClassifier(instance: AxiosInstance, logger: BffLogger): number {\n return instance.interceptors.response.use(\n (response) => response,\n (error: unknown) => handleResponseError(error, logger),\n );\n}\n\nexport { registerErrorClassifier, classifyError, handleResponseError };\n","/**\n * Request and response logging interceptor.\n *\n * Logs HTTP method, URL, status, and duration through the app-supplied\n * {@link BffLogger}. Only active in non-production environments. Request\n * bodies are NOT logged for security reasons.\n */\n\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';\n\nconst LOG_CONTEXT = 'http';\nconst REQUEST_START_HEADER = 'x-request-start-time';\n\nfunction isProduction(): boolean {\n return process.env.NODE_ENV === 'production';\n}\n\n/**\n * Logs the outgoing request and stamps the start time for duration tracking.\n */\nfunction logRequest(\n config: InternalAxiosRequestConfig,\n logger: BffLogger,\n): InternalAxiosRequestConfig {\n if (isProduction()) {\n return config;\n }\n\n const method = (config.method ?? 'GET').toUpperCase();\n const url = config.url ?? 'unknown';\n\n logger.debug(LOG_CONTEXT, `-> ${method} ${url}`);\n\n config.headers.set(REQUEST_START_HEADER, String(Date.now()));\n\n return config;\n}\n\nfunction calculateDurationMs(config: InternalAxiosRequestConfig): number | undefined {\n const startRaw = config.headers.get(REQUEST_START_HEADER);\n if (typeof startRaw !== 'string' || startRaw.length === 0) {\n return undefined;\n }\n\n const start = Number(startRaw);\n if (Number.isNaN(start)) {\n return undefined;\n }\n\n return Date.now() - start;\n}\n\n/**\n * Logs successful responses with status and duration.\n */\nfunction logResponse(response: AxiosResponse, logger: BffLogger): AxiosResponse {\n if (isProduction()) {\n return response;\n }\n\n const method = (response.config.method ?? 'GET').toUpperCase();\n const url = response.config.url ?? 'unknown';\n const status = response.status;\n const duration = calculateDurationMs(response.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.debug(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return response;\n}\n\ninterface AxiosLikeError {\n config: InternalAxiosRequestConfig;\n response?: AxiosResponse;\n message?: string;\n}\n\nfunction isAxiosLikeError(value: unknown): value is AxiosLikeError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'config' in value;\n}\n\n/**\n * Logs error responses with status and duration.\n */\nasync function logErrorResponse(error: unknown, logger: BffLogger): Promise<never> {\n if (isProduction()) {\n return Promise.reject(error);\n }\n if (!isAxiosLikeError(error)) {\n return Promise.reject(error);\n }\n\n const method = (error.config.method ?? 'GET').toUpperCase();\n const url = error.config.url ?? 'unknown';\n const status = error.response?.status ?? 0;\n const duration = calculateDurationMs(error.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.warn(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return Promise.reject(error);\n}\n\n/**\n * Registers request and response logging interceptors on an axios instance.\n * No-ops in production.\n * @returns An object with both interceptor IDs for potential ejection.\n */\nfunction registerLoggingInterceptor(\n instance: AxiosInstance,\n logger: BffLogger,\n): { request: number; response: number } {\n const requestId = instance.interceptors.request.use((config) => logRequest(config, logger));\n const responseId = instance.interceptors.response.use(\n (response) => logResponse(response, logger),\n (error: unknown) => logErrorResponse(error, logger),\n );\n return { request: requestId, response: responseId };\n}\n\nexport { registerLoggingInterceptor };\n","/**\n * Response interceptor: normalizes successful API responses.\n *\n * For successful mutation requests (POST/PUT/PATCH/DELETE), emits a toast via\n * the app-supplied {@link EmitToast} port so the UI can display a success\n * notification without coupling the package to a specific UI bus.\n */\n\nimport { ErrorSeverity } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger, EmitToast } from '../types';\nimport type { AxiosInstance, AxiosResponse } from 'axios';\n\nconst MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\nconst DEFAULT_SUCCESS_MESSAGE = 'Saved successfully.';\nconst LOG_CONTEXT = 'responseNormalizer';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction extractMessageFromBody(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const message = data.message;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n const detail = data.detail;\n if (typeof detail === 'string' && detail.length > 0) {\n return detail;\n }\n\n return undefined;\n}\n\nfunction isMutatingMethod(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return MUTATING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Handles a successful response by emitting a toast for mutations.\n *\n * Always returns the original `response` unchanged — an axios response\n * interceptor must pass the response through. The invariant return is the\n * required contract, not a code smell.\n */\n// eslint-disable-next-line sonarjs/no-invariant-returns\nfunction handleSuccessResponse(\n response: AxiosResponse,\n emitToast: EmitToast,\n logger: BffLogger,\n): AxiosResponse {\n try {\n const method = response.config.method;\n if (!isMutatingMethod(method)) {\n return response;\n }\n\n const message = extractMessageFromBody(response.data) ?? DEFAULT_SUCCESS_MESSAGE;\n emitToast(String(message), ErrorSeverity.Info);\n } catch (emitError) {\n logger.warn(LOG_CONTEXT, 'Failed to emit success notification', emitError);\n }\n\n return response;\n}\n\n/**\n * Registers the response normalizer interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerResponseNormalizer(\n instance: AxiosInstance,\n emitToast: EmitToast,\n logger: BffLogger,\n): number {\n return instance.interceptors.response.use((response) =>\n handleSuccessResponse(response, emitToast, logger),\n );\n}\n\nexport { registerResponseNormalizer };\n","/**\n * Interceptor registration — BFF era.\n *\n * Wires the interceptor chain onto an axios instance in the correct order.\n *\n * Request interceptors run in REVERSE order of registration, so:\n * 1. logging (registered first, runs last = logs the FINAL config)\n * 2. csrf (registered second, runs first = attaches the CSRF header)\n *\n * Response interceptors run in ORDER of registration, so:\n * 1. logging (logs response/error first)\n * 2. normalizer (emits success toast)\n * 3. session expiry (handles 401 -> clear session, app-supplied port)\n * 4. error classifier (classifies remaining errors)\n *\n * The package owns the logging, normalizer, and error-classifier bodies. The\n * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns\n * its CSRF strategy and its session store); `csrf` falls back to the package\n * default when omitted.\n */\n\nimport { registerDefaultCsrfInterceptor } from './interceptors/defaultCsrfInterceptor';\nimport { registerErrorClassifier } from './interceptors/errorClassifierInterceptor';\nimport { registerLoggingInterceptor } from './interceptors/loggingInterceptor';\nimport { registerResponseNormalizer } from './interceptors/responseNormalizer';\n\nimport type { RegisterInterceptorsPorts } from './types';\nimport type { AxiosInstance } from 'axios';\n\n/**\n * Registers the full BFF interceptor chain on the provided axios instance.\n * Call this once during application bootstrap after creating the instance.\n */\nfunction registerInterceptors(instance: AxiosInstance, ports: RegisterInterceptorsPorts): void {\n const { logger, emitToast, csrf, onSessionExpiry } = ports;\n\n // Request interceptors (registered order = reverse execution order)\n registerLoggingInterceptor(instance, logger);\n if (csrf) {\n csrf(instance);\n } else {\n registerDefaultCsrfInterceptor(instance);\n }\n\n // Response interceptors (registered order = execution order)\n registerResponseNormalizer(instance, emitToast, logger);\n if (onSessionExpiry) {\n onSessionExpiry(instance);\n }\n registerErrorClassifier(instance, logger);\n}\n\nexport { registerInterceptors };\n"]}
1
+ {"version":3,"sources":["../src/createBffAxiosClient.ts","../src/interceptors/defaultCsrfInterceptor.ts","../src/interceptors/errorClassifierInterceptor.ts","../src/interceptors/loggingInterceptor.ts","../src/interceptors/responseNormalizer.ts","../src/interceptors/warmupRetryInterceptor.ts","../src/registerInterceptors.ts"],"names":["axios","isValueDefined","HttpMethod","LOG_CONTEXT","isRecord","ErrorSeverity","isAxiosError"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Clean axios instance factory for the BFF era.\n *\n * Produces an axios instance with the BFF defaults (JSON, XHR marker,\n * credentialed) and no interceptors. Interceptors are wired separately via\n * {@link registerInterceptors} so the app controls the chain composition.\n */\n\nimport axios from 'axios';\n\nimport type { BffAxiosClientOptions } from './types';\nimport type { AxiosInstance } from 'axios';\n\nconst BFF_DEFAULT_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n};\n\n/**\n * Creates a credentialed axios instance with the shared BFF defaults. No\n * interceptors are registered here — call {@link registerInterceptors} once at\n * bootstrap to install the chain.\n */\nfunction createBffAxiosClient(options: BffAxiosClientOptions): AxiosInstance {\n return axios.create({\n timeout: options.timeoutMs,\n baseURL: options.baseURL,\n headers: { ...BFF_DEFAULT_HEADERS, ...(options.headers ?? {}) },\n withCredentials: true,\n });\n}\n\nexport { createBffAxiosClient };\n","/**\n * Default CSRF request interceptor.\n *\n * After a BFF cutover, an SPA authenticates via a cookie. Cookie auth\n * reintroduces CSRF risk, so the BFF's `Bff.AspNetCore` anti-forgery\n * middleware requires a custom header on every state-changing request — a\n * request a cross-site form POST cannot forge. This default attaches\n * `X-BFF-Csrf: 1` to all mutating methods.\n *\n * This is the **default implementation of the `csrf` port**. Apps that need a\n * different CSRF strategy supply their own registrar to\n * {@link registerInterceptors}; this body is what diverged per app and is kept\n * overridable on purpose.\n */\n\nimport type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';\n\n/** Header name + value the `Bff.AspNetCore` anti-forgery middleware checks. */\nconst CSRF_HEADER = 'X-BFF-Csrf';\nconst CSRF_HEADER_VALUE = '1';\n\n/** Methods the BFF anti-forgery middleware treats as state-changing. */\nconst STATE_CHANGING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\n\nfunction isStateChanging(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return STATE_CHANGING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Adds `X-BFF-Csrf` to every state-changing request. Safe (GET/HEAD) requests\n * are left untouched — the BFF only enforces the header on mutations.\n */\nfunction attachCsrfHeader(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {\n if (isStateChanging(config.method)) {\n config.headers.set(CSRF_HEADER, CSRF_HEADER_VALUE);\n }\n return config;\n}\n\n/**\n * Registers the default CSRF request interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerDefaultCsrfInterceptor(instance: AxiosInstance): number {\n return instance.interceptors.request.use(attachCsrfHeader);\n}\n\nexport { registerDefaultCsrfInterceptor, attachCsrfHeader };\n","/**\n * Response error interceptor: classifies axios errors.\n *\n * Converts raw AxiosError objects into {@link ClassifiedError} instances with\n * full context (status, url, method, errorCode, message, etc), logs them\n * through the app-supplied {@link BffLogger}, and re-rejects so callers and\n * downstream interceptors can react.\n */\n\nimport { HttpMethod } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { ClassifiedError } from '@dloizides/api-client-base';\nimport type { AxiosError, AxiosInstance, AxiosResponse } from 'axios';\n\nconst LOG_CONTEXT = 'errorClassifier';\nconst TIMEOUT_ERROR_CODE = 'ECONNABORTED';\nconst NETWORK_ERROR_STATUS = 0;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction isAxiosError(value: unknown): value is AxiosError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'isAxiosError' in value;\n}\n\nfunction extractErrorCode(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const code = data.errorCode ?? data.code ?? data.error;\n if (typeof code === 'string' && code.length > 0) {\n return code;\n }\n\n return undefined;\n}\n\nfunction extractErrorMessage(data: unknown, fallback: string): string {\n if (!isRecord(data)) {\n return fallback;\n }\n\n const message = data.message ?? data.detail ?? data.error ?? data.title;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n return fallback;\n}\n\nfunction extractStringHeader(headers: Record<string, unknown>, key: string): string | undefined {\n const value: unknown = headers[key];\n if (typeof value === 'string' && value.length > 0) {\n return value;\n }\n return undefined;\n}\n\nfunction extractRequestId(response: AxiosResponse | undefined): string | undefined {\n if (!isValueDefined(response)) {\n return undefined;\n }\n\n const headers = response.headers;\n if (!isRecord(headers)) {\n return undefined;\n }\n\n return extractStringHeader(headers, 'x-request-id') ?? extractStringHeader(headers, 'x-correlation-id');\n}\n\nfunction resolveHttpMethod(method: string | undefined): HttpMethod {\n if (typeof method !== 'string') {\n return HttpMethod.Get;\n }\n const upper = method.toUpperCase();\n const methodMap: Record<string, HttpMethod | undefined> = {\n GET: HttpMethod.Get,\n POST: HttpMethod.Post,\n PUT: HttpMethod.Put,\n PATCH: HttpMethod.Patch,\n DELETE: HttpMethod.Delete,\n };\n return methodMap[upper] ?? HttpMethod.Get;\n}\n\n/**\n * Converts an AxiosError into a {@link ClassifiedError} with full context.\n */\nfunction classifyError(error: AxiosError): ClassifiedError {\n const response = error.response;\n const hasResponse = isValueDefined(response);\n\n const status = hasResponse ? response.status : NETWORK_ERROR_STATUS;\n const url = error.config?.url ?? 'unknown';\n const method = resolveHttpMethod(error.config?.method);\n const body = hasResponse ? response.data : undefined;\n\n const isTimeout = error.code === TIMEOUT_ERROR_CODE;\n const defaultMessage = isTimeout ? 'Request timed out' : 'Network error';\n const errorCode = extractErrorCode(body) ?? (isTimeout ? TIMEOUT_ERROR_CODE : undefined);\n const message = hasResponse ? extractErrorMessage(body, error.message) : defaultMessage;\n\n return {\n status,\n url,\n method,\n errorCode,\n message,\n body,\n originalError: error,\n timestamp: Date.now(),\n requestId: extractRequestId(response),\n };\n}\n\n/**\n * Handles response errors by classifying them and logging.\n */\nasync function handleResponseError(error: unknown, logger: BffLogger): Promise<never> {\n if (!isAxiosError(error)) {\n return Promise.reject(error);\n }\n\n const classified = classifyError(error);\n\n logger.warn(LOG_CONTEXT, `HTTP ${classified.method} ${classified.url} failed`, {\n status: classified.status,\n errorCode: classified.errorCode,\n message: classified.message,\n });\n\n return Promise.reject(error);\n}\n\n/**\n * Registers the error classifier interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerErrorClassifier(instance: AxiosInstance, logger: BffLogger): number {\n return instance.interceptors.response.use(\n (response) => response,\n (error: unknown) => handleResponseError(error, logger),\n );\n}\n\nexport { registerErrorClassifier, classifyError, handleResponseError };\n","/**\n * Request and response logging interceptor.\n *\n * Logs HTTP method, URL, status, and duration through the app-supplied\n * {@link BffLogger}. Only active in non-production environments. Request\n * bodies are NOT logged for security reasons.\n */\n\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';\n\nconst LOG_CONTEXT = 'http';\nconst REQUEST_START_HEADER = 'x-request-start-time';\n\nfunction isProduction(): boolean {\n return process.env.NODE_ENV === 'production';\n}\n\n/**\n * Logs the outgoing request and stamps the start time for duration tracking.\n */\nfunction logRequest(\n config: InternalAxiosRequestConfig,\n logger: BffLogger,\n): InternalAxiosRequestConfig {\n if (isProduction()) {\n return config;\n }\n\n const method = (config.method ?? 'GET').toUpperCase();\n const url = config.url ?? 'unknown';\n\n logger.debug(LOG_CONTEXT, `-> ${method} ${url}`);\n\n config.headers.set(REQUEST_START_HEADER, String(Date.now()));\n\n return config;\n}\n\nfunction calculateDurationMs(config: InternalAxiosRequestConfig): number | undefined {\n const startRaw = config.headers.get(REQUEST_START_HEADER);\n if (typeof startRaw !== 'string' || startRaw.length === 0) {\n return undefined;\n }\n\n const start = Number(startRaw);\n if (Number.isNaN(start)) {\n return undefined;\n }\n\n return Date.now() - start;\n}\n\n/**\n * Logs successful responses with status and duration.\n */\nfunction logResponse(response: AxiosResponse, logger: BffLogger): AxiosResponse {\n if (isProduction()) {\n return response;\n }\n\n const method = (response.config.method ?? 'GET').toUpperCase();\n const url = response.config.url ?? 'unknown';\n const status = response.status;\n const duration = calculateDurationMs(response.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.debug(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return response;\n}\n\ninterface AxiosLikeError {\n config: InternalAxiosRequestConfig;\n response?: AxiosResponse;\n message?: string;\n}\n\nfunction isAxiosLikeError(value: unknown): value is AxiosLikeError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'config' in value;\n}\n\n/**\n * Logs error responses with status and duration.\n */\nasync function logErrorResponse(error: unknown, logger: BffLogger): Promise<never> {\n if (isProduction()) {\n return Promise.reject(error);\n }\n if (!isAxiosLikeError(error)) {\n return Promise.reject(error);\n }\n\n const method = (error.config.method ?? 'GET').toUpperCase();\n const url = error.config.url ?? 'unknown';\n const status = error.response?.status ?? 0;\n const duration = calculateDurationMs(error.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.warn(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return Promise.reject(error);\n}\n\n/**\n * Registers request and response logging interceptors on an axios instance.\n * No-ops in production.\n * @returns An object with both interceptor IDs for potential ejection.\n */\nfunction registerLoggingInterceptor(\n instance: AxiosInstance,\n logger: BffLogger,\n): { request: number; response: number } {\n const requestId = instance.interceptors.request.use((config) => logRequest(config, logger));\n const responseId = instance.interceptors.response.use(\n (response) => logResponse(response, logger),\n (error: unknown) => logErrorResponse(error, logger),\n );\n return { request: requestId, response: responseId };\n}\n\nexport { registerLoggingInterceptor };\n","/**\n * Response interceptor: normalizes successful API responses.\n *\n * For successful mutation requests (POST/PUT/PATCH/DELETE), emits a toast via\n * the app-supplied {@link EmitToast} port so the UI can display a success\n * notification without coupling the package to a specific UI bus.\n */\n\nimport { ErrorSeverity } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger, EmitToast } from '../types';\nimport type { AxiosInstance, AxiosResponse } from 'axios';\n\nconst MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\nconst DEFAULT_SUCCESS_MESSAGE = 'Saved successfully.';\nconst LOG_CONTEXT = 'responseNormalizer';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction extractMessageFromBody(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const message = data.message;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n const detail = data.detail;\n if (typeof detail === 'string' && detail.length > 0) {\n return detail;\n }\n\n return undefined;\n}\n\nfunction isMutatingMethod(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return MUTATING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Handles a successful response by emitting a toast for mutations.\n *\n * Always returns the original `response` unchanged — an axios response\n * interceptor must pass the response through. The invariant return is the\n * required contract, not a code smell.\n */\n// eslint-disable-next-line sonarjs/no-invariant-returns\nfunction handleSuccessResponse(\n response: AxiosResponse,\n emitToast: EmitToast,\n logger: BffLogger,\n): AxiosResponse {\n try {\n const method = response.config.method;\n if (!isMutatingMethod(method)) {\n return response;\n }\n\n const message = extractMessageFromBody(response.data) ?? DEFAULT_SUCCESS_MESSAGE;\n emitToast(String(message), ErrorSeverity.Info);\n } catch (emitError) {\n logger.warn(LOG_CONTEXT, 'Failed to emit success notification', emitError);\n }\n\n return response;\n}\n\n/**\n * Registers the response normalizer interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerResponseNormalizer(\n instance: AxiosInstance,\n emitToast: EmitToast,\n logger: BffLogger,\n): number {\n return instance.interceptors.response.use((response) =>\n handleSuccessResponse(response, emitToast, logger),\n );\n}\n\nexport { registerResponseNormalizer };\n","/**\n * Response error interceptor: transparently retries transient warm-up failures.\n *\n * WHY (P1-06): on a resource-tight, scale-to-zero prod node the BFF (or its\n * upstream API) can be cold on the FIRST request after idle — the gateway answers\n * `502/503/504` for a few seconds while the upstream comes up, then settles to a\n * correct `401/200`. The unguarded `/bff/me` probe surfaced that transient as an\n * auth error / anonymous flash on first load. These statuses mean the request was\n * NOT processed (upstream unavailable), so retrying is safe for any method.\n *\n * This interceptor catches those statuses and re-issues the SAME request up to\n * `maxRetries` times with exponential backoff, BEFORE the error classifier turns\n * it into a surfaced error — so it must be registered FIRST in the response\n * chain. `4xx` (incl. 401) and `500` fall straight through: a 500 is a real\n * server error, not a warm-up, and must not be retried.\n *\n * Deterministic + timer-injectable: the backoff `sleep` is a port (defaults to\n * `setTimeout`) so unit tests resolve instantly and no jitter/`Math.random` is\n * used.\n */\n\nimport type { BffLogger, WarmupRetryConfig } from '../types';\nimport type { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';\n\nconst LOG_CONTEXT = 'warmupRetry';\n\n/** Gateway/upstream-not-ready statuses that mean \"request not processed → safe to retry\". */\nconst DEFAULT_RETRYABLE_STATUSES: readonly number[] = [502, 503, 504];\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_BASE_DELAY_MS = 400;\nconst BACKOFF_FACTOR = 2;\n\n/** Per-request attempt counter stashed on the axios config. */\ninterface RetryableConfig extends InternalAxiosRequestConfig {\n __warmupRetryCount?: number;\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isAxiosError(value: unknown): value is AxiosError {\n return typeof value === 'object' && value !== null && 'isAxiosError' in value;\n}\n\n/**\n * Registers the warm-up retry interceptor. Returns the interceptor ID for ejection.\n */\nfunction registerWarmupRetryInterceptor(\n instance: AxiosInstance,\n logger: BffLogger,\n config: WarmupRetryConfig = {},\n): number {\n const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n const baseDelayMs = config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n const retryableStatuses = config.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES;\n const sleep = config.sleep ?? defaultSleep;\n\n return instance.interceptors.response.use(\n (response) => response,\n async (error: unknown) => {\n if (!isAxiosError(error) || error.response === undefined) {\n return Promise.reject(error);\n }\n if (!retryableStatuses.includes(error.response.status)) {\n return Promise.reject(error);\n }\n\n const requestConfig = error.config as RetryableConfig | undefined;\n if (requestConfig === undefined) {\n return Promise.reject(error);\n }\n\n const attempt = (requestConfig.__warmupRetryCount ?? 0) + 1;\n if (attempt > maxRetries) {\n logger.warn(LOG_CONTEXT, `gave up after ${maxRetries} warm-up retries`, {\n url: requestConfig.url,\n status: error.response.status,\n });\n return Promise.reject(error);\n }\n\n requestConfig.__warmupRetryCount = attempt;\n const delayMs = baseDelayMs * BACKOFF_FACTOR ** (attempt - 1);\n logger.debug(LOG_CONTEXT, `upstream warming up — retry ${attempt}/${maxRetries} in ${delayMs}ms`, {\n url: requestConfig.url,\n status: error.response.status,\n });\n await sleep(delayMs);\n return instance.request(requestConfig);\n },\n );\n}\n\nexport {\n registerWarmupRetryInterceptor,\n DEFAULT_RETRYABLE_STATUSES,\n DEFAULT_MAX_RETRIES,\n DEFAULT_BASE_DELAY_MS,\n};\n","/**\n * Interceptor registration — BFF era.\n *\n * Wires the interceptor chain onto an axios instance in the correct order.\n *\n * Request interceptors run in REVERSE order of registration, so:\n * 1. logging (registered first, runs last = logs the FINAL config)\n * 2. csrf (registered second, runs first = attaches the CSRF header)\n *\n * Response interceptors run in ORDER of registration, so:\n * 1. warm-up retry (retries transient 502/503/504 upstream-cold failures)\n * 2. logging (logs response/error)\n * 3. normalizer (emits success toast)\n * 4. session expiry (handles 401 -> clear session, app-supplied port)\n * 5. error classifier (classifies remaining errors)\n *\n * The warm-up retry runs FIRST so a cold-start 502/503/504 is re-issued and\n * (usually) resolves before any downstream interceptor logs or surfaces it as\n * an error — the P1-06 \"auth-error/anonymous flash on first load\" fix.\n *\n * The package owns the logging, normalizer, and error-classifier bodies. The\n * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns\n * its CSRF strategy and its session store); `csrf` falls back to the package\n * default when omitted.\n */\n\nimport { registerDefaultCsrfInterceptor } from './interceptors/defaultCsrfInterceptor';\nimport { registerErrorClassifier } from './interceptors/errorClassifierInterceptor';\nimport { registerLoggingInterceptor } from './interceptors/loggingInterceptor';\nimport { registerResponseNormalizer } from './interceptors/responseNormalizer';\nimport { registerWarmupRetryInterceptor } from './interceptors/warmupRetryInterceptor';\n\nimport type { RegisterInterceptorsPorts } from './types';\nimport type { AxiosInstance } from 'axios';\n\n/**\n * Registers the full BFF interceptor chain on the provided axios instance.\n * Call this once during application bootstrap after creating the instance.\n */\nfunction registerInterceptors(instance: AxiosInstance, ports: RegisterInterceptorsPorts): void {\n const { logger, emitToast, csrf, onSessionExpiry, warmupRetry } = ports;\n\n // Request interceptors (registered order = reverse execution order)\n registerLoggingInterceptor(instance, logger);\n if (csrf) {\n csrf(instance);\n } else {\n registerDefaultCsrfInterceptor(instance);\n }\n\n // Response interceptors (registered order = execution order)\n // Warm-up retry runs FIRST so a cold-start 502/503/504 is retried before any\n // downstream interceptor surfaces it (P1-06). Pass `warmupRetry: false` to opt out.\n if (warmupRetry !== false) {\n registerWarmupRetryInterceptor(instance, logger, warmupRetry ?? {});\n }\n registerResponseNormalizer(instance, emitToast, logger);\n if (onSessionExpiry) {\n onSessionExpiry(instance);\n }\n registerErrorClassifier(instance, logger);\n}\n\nexport { registerInterceptors };\n"]}
package/dist/index.mjs CHANGED
@@ -258,15 +258,68 @@ function registerResponseNormalizer(instance, emitToast, logger) {
258
258
  );
259
259
  }
260
260
 
261
+ // src/interceptors/warmupRetryInterceptor.ts
262
+ var LOG_CONTEXT4 = "warmupRetry";
263
+ var DEFAULT_RETRYABLE_STATUSES = [502, 503, 504];
264
+ var DEFAULT_MAX_RETRIES = 3;
265
+ var DEFAULT_BASE_DELAY_MS = 400;
266
+ var BACKOFF_FACTOR = 2;
267
+ function defaultSleep(ms) {
268
+ return new Promise((resolve) => setTimeout(resolve, ms));
269
+ }
270
+ function isAxiosError2(value) {
271
+ return typeof value === "object" && value !== null && "isAxiosError" in value;
272
+ }
273
+ function registerWarmupRetryInterceptor(instance, logger, config = {}) {
274
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
275
+ const baseDelayMs = config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
276
+ const retryableStatuses = config.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES;
277
+ const sleep = config.sleep ?? defaultSleep;
278
+ return instance.interceptors.response.use(
279
+ (response) => response,
280
+ async (error) => {
281
+ if (!isAxiosError2(error) || error.response === void 0) {
282
+ return Promise.reject(error);
283
+ }
284
+ if (!retryableStatuses.includes(error.response.status)) {
285
+ return Promise.reject(error);
286
+ }
287
+ const requestConfig = error.config;
288
+ if (requestConfig === void 0) {
289
+ return Promise.reject(error);
290
+ }
291
+ const attempt = (requestConfig.__warmupRetryCount ?? 0) + 1;
292
+ if (attempt > maxRetries) {
293
+ logger.warn(LOG_CONTEXT4, `gave up after ${maxRetries} warm-up retries`, {
294
+ url: requestConfig.url,
295
+ status: error.response.status
296
+ });
297
+ return Promise.reject(error);
298
+ }
299
+ requestConfig.__warmupRetryCount = attempt;
300
+ const delayMs = baseDelayMs * BACKOFF_FACTOR ** (attempt - 1);
301
+ logger.debug(LOG_CONTEXT4, `upstream warming up \u2014 retry ${attempt}/${maxRetries} in ${delayMs}ms`, {
302
+ url: requestConfig.url,
303
+ status: error.response.status
304
+ });
305
+ await sleep(delayMs);
306
+ return instance.request(requestConfig);
307
+ }
308
+ );
309
+ }
310
+
261
311
  // src/registerInterceptors.ts
262
312
  function registerInterceptors(instance, ports) {
263
- const { logger, emitToast, csrf, onSessionExpiry } = ports;
313
+ const { logger, emitToast, csrf, onSessionExpiry, warmupRetry } = ports;
264
314
  registerLoggingInterceptor(instance, logger);
265
315
  if (csrf) {
266
316
  csrf(instance);
267
317
  } else {
268
318
  registerDefaultCsrfInterceptor(instance);
269
319
  }
320
+ if (warmupRetry !== false) {
321
+ registerWarmupRetryInterceptor(instance, logger, warmupRetry ?? {});
322
+ }
270
323
  registerResponseNormalizer(instance, emitToast, logger);
271
324
  if (onSessionExpiry) {
272
325
  onSessionExpiry(instance);
@@ -274,6 +327,6 @@ function registerInterceptors(instance, ports) {
274
327
  registerErrorClassifier(instance, logger);
275
328
  }
276
329
 
277
- export { attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer };
330
+ export { DEFAULT_BASE_DELAY_MS, DEFAULT_MAX_RETRIES, DEFAULT_RETRYABLE_STATUSES, attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer, registerWarmupRetryInterceptor };
278
331
  //# sourceMappingURL=index.mjs.map
279
332
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/createBffAxiosClient.ts","../src/interceptors/defaultCsrfInterceptor.ts","../src/interceptors/errorClassifierInterceptor.ts","../src/interceptors/loggingInterceptor.ts","../src/interceptors/responseNormalizer.ts","../src/registerInterceptors.ts"],"names":["LOG_CONTEXT","isValueDefined","isRecord"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Clean axios instance factory for the BFF era.\n *\n * Produces an axios instance with the BFF defaults (JSON, XHR marker,\n * credentialed) and no interceptors. Interceptors are wired separately via\n * {@link registerInterceptors} so the app controls the chain composition.\n */\n\nimport axios from 'axios';\n\nimport type { BffAxiosClientOptions } from './types';\nimport type { AxiosInstance } from 'axios';\n\nconst BFF_DEFAULT_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n};\n\n/**\n * Creates a credentialed axios instance with the shared BFF defaults. No\n * interceptors are registered here — call {@link registerInterceptors} once at\n * bootstrap to install the chain.\n */\nfunction createBffAxiosClient(options: BffAxiosClientOptions): AxiosInstance {\n return axios.create({\n timeout: options.timeoutMs,\n baseURL: options.baseURL,\n headers: { ...BFF_DEFAULT_HEADERS, ...(options.headers ?? {}) },\n withCredentials: true,\n });\n}\n\nexport { createBffAxiosClient };\n","/**\n * Default CSRF request interceptor.\n *\n * After a BFF cutover, an SPA authenticates via a cookie. Cookie auth\n * reintroduces CSRF risk, so the BFF's `Bff.AspNetCore` anti-forgery\n * middleware requires a custom header on every state-changing request — a\n * request a cross-site form POST cannot forge. This default attaches\n * `X-BFF-Csrf: 1` to all mutating methods.\n *\n * This is the **default implementation of the `csrf` port**. Apps that need a\n * different CSRF strategy supply their own registrar to\n * {@link registerInterceptors}; this body is what diverged per app and is kept\n * overridable on purpose.\n */\n\nimport type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';\n\n/** Header name + value the `Bff.AspNetCore` anti-forgery middleware checks. */\nconst CSRF_HEADER = 'X-BFF-Csrf';\nconst CSRF_HEADER_VALUE = '1';\n\n/** Methods the BFF anti-forgery middleware treats as state-changing. */\nconst STATE_CHANGING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\n\nfunction isStateChanging(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return STATE_CHANGING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Adds `X-BFF-Csrf` to every state-changing request. Safe (GET/HEAD) requests\n * are left untouched — the BFF only enforces the header on mutations.\n */\nfunction attachCsrfHeader(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {\n if (isStateChanging(config.method)) {\n config.headers.set(CSRF_HEADER, CSRF_HEADER_VALUE);\n }\n return config;\n}\n\n/**\n * Registers the default CSRF request interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerDefaultCsrfInterceptor(instance: AxiosInstance): number {\n return instance.interceptors.request.use(attachCsrfHeader);\n}\n\nexport { registerDefaultCsrfInterceptor, attachCsrfHeader };\n","/**\n * Response error interceptor: classifies axios errors.\n *\n * Converts raw AxiosError objects into {@link ClassifiedError} instances with\n * full context (status, url, method, errorCode, message, etc), logs them\n * through the app-supplied {@link BffLogger}, and re-rejects so callers and\n * downstream interceptors can react.\n */\n\nimport { HttpMethod } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { ClassifiedError } from '@dloizides/api-client-base';\nimport type { AxiosError, AxiosInstance, AxiosResponse } from 'axios';\n\nconst LOG_CONTEXT = 'errorClassifier';\nconst TIMEOUT_ERROR_CODE = 'ECONNABORTED';\nconst NETWORK_ERROR_STATUS = 0;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction isAxiosError(value: unknown): value is AxiosError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'isAxiosError' in value;\n}\n\nfunction extractErrorCode(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const code = data.errorCode ?? data.code ?? data.error;\n if (typeof code === 'string' && code.length > 0) {\n return code;\n }\n\n return undefined;\n}\n\nfunction extractErrorMessage(data: unknown, fallback: string): string {\n if (!isRecord(data)) {\n return fallback;\n }\n\n const message = data.message ?? data.detail ?? data.error ?? data.title;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n return fallback;\n}\n\nfunction extractStringHeader(headers: Record<string, unknown>, key: string): string | undefined {\n const value: unknown = headers[key];\n if (typeof value === 'string' && value.length > 0) {\n return value;\n }\n return undefined;\n}\n\nfunction extractRequestId(response: AxiosResponse | undefined): string | undefined {\n if (!isValueDefined(response)) {\n return undefined;\n }\n\n const headers = response.headers;\n if (!isRecord(headers)) {\n return undefined;\n }\n\n return extractStringHeader(headers, 'x-request-id') ?? extractStringHeader(headers, 'x-correlation-id');\n}\n\nfunction resolveHttpMethod(method: string | undefined): HttpMethod {\n if (typeof method !== 'string') {\n return HttpMethod.Get;\n }\n const upper = method.toUpperCase();\n const methodMap: Record<string, HttpMethod | undefined> = {\n GET: HttpMethod.Get,\n POST: HttpMethod.Post,\n PUT: HttpMethod.Put,\n PATCH: HttpMethod.Patch,\n DELETE: HttpMethod.Delete,\n };\n return methodMap[upper] ?? HttpMethod.Get;\n}\n\n/**\n * Converts an AxiosError into a {@link ClassifiedError} with full context.\n */\nfunction classifyError(error: AxiosError): ClassifiedError {\n const response = error.response;\n const hasResponse = isValueDefined(response);\n\n const status = hasResponse ? response.status : NETWORK_ERROR_STATUS;\n const url = error.config?.url ?? 'unknown';\n const method = resolveHttpMethod(error.config?.method);\n const body = hasResponse ? response.data : undefined;\n\n const isTimeout = error.code === TIMEOUT_ERROR_CODE;\n const defaultMessage = isTimeout ? 'Request timed out' : 'Network error';\n const errorCode = extractErrorCode(body) ?? (isTimeout ? TIMEOUT_ERROR_CODE : undefined);\n const message = hasResponse ? extractErrorMessage(body, error.message) : defaultMessage;\n\n return {\n status,\n url,\n method,\n errorCode,\n message,\n body,\n originalError: error,\n timestamp: Date.now(),\n requestId: extractRequestId(response),\n };\n}\n\n/**\n * Handles response errors by classifying them and logging.\n */\nasync function handleResponseError(error: unknown, logger: BffLogger): Promise<never> {\n if (!isAxiosError(error)) {\n return Promise.reject(error);\n }\n\n const classified = classifyError(error);\n\n logger.warn(LOG_CONTEXT, `HTTP ${classified.method} ${classified.url} failed`, {\n status: classified.status,\n errorCode: classified.errorCode,\n message: classified.message,\n });\n\n return Promise.reject(error);\n}\n\n/**\n * Registers the error classifier interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerErrorClassifier(instance: AxiosInstance, logger: BffLogger): number {\n return instance.interceptors.response.use(\n (response) => response,\n (error: unknown) => handleResponseError(error, logger),\n );\n}\n\nexport { registerErrorClassifier, classifyError, handleResponseError };\n","/**\n * Request and response logging interceptor.\n *\n * Logs HTTP method, URL, status, and duration through the app-supplied\n * {@link BffLogger}. Only active in non-production environments. Request\n * bodies are NOT logged for security reasons.\n */\n\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';\n\nconst LOG_CONTEXT = 'http';\nconst REQUEST_START_HEADER = 'x-request-start-time';\n\nfunction isProduction(): boolean {\n return process.env.NODE_ENV === 'production';\n}\n\n/**\n * Logs the outgoing request and stamps the start time for duration tracking.\n */\nfunction logRequest(\n config: InternalAxiosRequestConfig,\n logger: BffLogger,\n): InternalAxiosRequestConfig {\n if (isProduction()) {\n return config;\n }\n\n const method = (config.method ?? 'GET').toUpperCase();\n const url = config.url ?? 'unknown';\n\n logger.debug(LOG_CONTEXT, `-> ${method} ${url}`);\n\n config.headers.set(REQUEST_START_HEADER, String(Date.now()));\n\n return config;\n}\n\nfunction calculateDurationMs(config: InternalAxiosRequestConfig): number | undefined {\n const startRaw = config.headers.get(REQUEST_START_HEADER);\n if (typeof startRaw !== 'string' || startRaw.length === 0) {\n return undefined;\n }\n\n const start = Number(startRaw);\n if (Number.isNaN(start)) {\n return undefined;\n }\n\n return Date.now() - start;\n}\n\n/**\n * Logs successful responses with status and duration.\n */\nfunction logResponse(response: AxiosResponse, logger: BffLogger): AxiosResponse {\n if (isProduction()) {\n return response;\n }\n\n const method = (response.config.method ?? 'GET').toUpperCase();\n const url = response.config.url ?? 'unknown';\n const status = response.status;\n const duration = calculateDurationMs(response.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.debug(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return response;\n}\n\ninterface AxiosLikeError {\n config: InternalAxiosRequestConfig;\n response?: AxiosResponse;\n message?: string;\n}\n\nfunction isAxiosLikeError(value: unknown): value is AxiosLikeError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'config' in value;\n}\n\n/**\n * Logs error responses with status and duration.\n */\nasync function logErrorResponse(error: unknown, logger: BffLogger): Promise<never> {\n if (isProduction()) {\n return Promise.reject(error);\n }\n if (!isAxiosLikeError(error)) {\n return Promise.reject(error);\n }\n\n const method = (error.config.method ?? 'GET').toUpperCase();\n const url = error.config.url ?? 'unknown';\n const status = error.response?.status ?? 0;\n const duration = calculateDurationMs(error.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.warn(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return Promise.reject(error);\n}\n\n/**\n * Registers request and response logging interceptors on an axios instance.\n * No-ops in production.\n * @returns An object with both interceptor IDs for potential ejection.\n */\nfunction registerLoggingInterceptor(\n instance: AxiosInstance,\n logger: BffLogger,\n): { request: number; response: number } {\n const requestId = instance.interceptors.request.use((config) => logRequest(config, logger));\n const responseId = instance.interceptors.response.use(\n (response) => logResponse(response, logger),\n (error: unknown) => logErrorResponse(error, logger),\n );\n return { request: requestId, response: responseId };\n}\n\nexport { registerLoggingInterceptor };\n","/**\n * Response interceptor: normalizes successful API responses.\n *\n * For successful mutation requests (POST/PUT/PATCH/DELETE), emits a toast via\n * the app-supplied {@link EmitToast} port so the UI can display a success\n * notification without coupling the package to a specific UI bus.\n */\n\nimport { ErrorSeverity } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger, EmitToast } from '../types';\nimport type { AxiosInstance, AxiosResponse } from 'axios';\n\nconst MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\nconst DEFAULT_SUCCESS_MESSAGE = 'Saved successfully.';\nconst LOG_CONTEXT = 'responseNormalizer';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction extractMessageFromBody(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const message = data.message;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n const detail = data.detail;\n if (typeof detail === 'string' && detail.length > 0) {\n return detail;\n }\n\n return undefined;\n}\n\nfunction isMutatingMethod(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return MUTATING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Handles a successful response by emitting a toast for mutations.\n *\n * Always returns the original `response` unchanged — an axios response\n * interceptor must pass the response through. The invariant return is the\n * required contract, not a code smell.\n */\n// eslint-disable-next-line sonarjs/no-invariant-returns\nfunction handleSuccessResponse(\n response: AxiosResponse,\n emitToast: EmitToast,\n logger: BffLogger,\n): AxiosResponse {\n try {\n const method = response.config.method;\n if (!isMutatingMethod(method)) {\n return response;\n }\n\n const message = extractMessageFromBody(response.data) ?? DEFAULT_SUCCESS_MESSAGE;\n emitToast(String(message), ErrorSeverity.Info);\n } catch (emitError) {\n logger.warn(LOG_CONTEXT, 'Failed to emit success notification', emitError);\n }\n\n return response;\n}\n\n/**\n * Registers the response normalizer interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerResponseNormalizer(\n instance: AxiosInstance,\n emitToast: EmitToast,\n logger: BffLogger,\n): number {\n return instance.interceptors.response.use((response) =>\n handleSuccessResponse(response, emitToast, logger),\n );\n}\n\nexport { registerResponseNormalizer };\n","/**\n * Interceptor registration — BFF era.\n *\n * Wires the interceptor chain onto an axios instance in the correct order.\n *\n * Request interceptors run in REVERSE order of registration, so:\n * 1. logging (registered first, runs last = logs the FINAL config)\n * 2. csrf (registered second, runs first = attaches the CSRF header)\n *\n * Response interceptors run in ORDER of registration, so:\n * 1. logging (logs response/error first)\n * 2. normalizer (emits success toast)\n * 3. session expiry (handles 401 -> clear session, app-supplied port)\n * 4. error classifier (classifies remaining errors)\n *\n * The package owns the logging, normalizer, and error-classifier bodies. The\n * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns\n * its CSRF strategy and its session store); `csrf` falls back to the package\n * default when omitted.\n */\n\nimport { registerDefaultCsrfInterceptor } from './interceptors/defaultCsrfInterceptor';\nimport { registerErrorClassifier } from './interceptors/errorClassifierInterceptor';\nimport { registerLoggingInterceptor } from './interceptors/loggingInterceptor';\nimport { registerResponseNormalizer } from './interceptors/responseNormalizer';\n\nimport type { RegisterInterceptorsPorts } from './types';\nimport type { AxiosInstance } from 'axios';\n\n/**\n * Registers the full BFF interceptor chain on the provided axios instance.\n * Call this once during application bootstrap after creating the instance.\n */\nfunction registerInterceptors(instance: AxiosInstance, ports: RegisterInterceptorsPorts): void {\n const { logger, emitToast, csrf, onSessionExpiry } = ports;\n\n // Request interceptors (registered order = reverse execution order)\n registerLoggingInterceptor(instance, logger);\n if (csrf) {\n csrf(instance);\n } else {\n registerDefaultCsrfInterceptor(instance);\n }\n\n // Response interceptors (registered order = execution order)\n registerResponseNormalizer(instance, emitToast, logger);\n if (onSessionExpiry) {\n onSessionExpiry(instance);\n }\n registerErrorClassifier(instance, logger);\n}\n\nexport { registerInterceptors };\n"]}
1
+ {"version":3,"sources":["../src/createBffAxiosClient.ts","../src/interceptors/defaultCsrfInterceptor.ts","../src/interceptors/errorClassifierInterceptor.ts","../src/interceptors/loggingInterceptor.ts","../src/interceptors/responseNormalizer.ts","../src/interceptors/warmupRetryInterceptor.ts","../src/registerInterceptors.ts"],"names":["LOG_CONTEXT","isValueDefined","isRecord","isAxiosError"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Clean axios instance factory for the BFF era.\n *\n * Produces an axios instance with the BFF defaults (JSON, XHR marker,\n * credentialed) and no interceptors. Interceptors are wired separately via\n * {@link registerInterceptors} so the app controls the chain composition.\n */\n\nimport axios from 'axios';\n\nimport type { BffAxiosClientOptions } from './types';\nimport type { AxiosInstance } from 'axios';\n\nconst BFF_DEFAULT_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n};\n\n/**\n * Creates a credentialed axios instance with the shared BFF defaults. No\n * interceptors are registered here — call {@link registerInterceptors} once at\n * bootstrap to install the chain.\n */\nfunction createBffAxiosClient(options: BffAxiosClientOptions): AxiosInstance {\n return axios.create({\n timeout: options.timeoutMs,\n baseURL: options.baseURL,\n headers: { ...BFF_DEFAULT_HEADERS, ...(options.headers ?? {}) },\n withCredentials: true,\n });\n}\n\nexport { createBffAxiosClient };\n","/**\n * Default CSRF request interceptor.\n *\n * After a BFF cutover, an SPA authenticates via a cookie. Cookie auth\n * reintroduces CSRF risk, so the BFF's `Bff.AspNetCore` anti-forgery\n * middleware requires a custom header on every state-changing request — a\n * request a cross-site form POST cannot forge. This default attaches\n * `X-BFF-Csrf: 1` to all mutating methods.\n *\n * This is the **default implementation of the `csrf` port**. Apps that need a\n * different CSRF strategy supply their own registrar to\n * {@link registerInterceptors}; this body is what diverged per app and is kept\n * overridable on purpose.\n */\n\nimport type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';\n\n/** Header name + value the `Bff.AspNetCore` anti-forgery middleware checks. */\nconst CSRF_HEADER = 'X-BFF-Csrf';\nconst CSRF_HEADER_VALUE = '1';\n\n/** Methods the BFF anti-forgery middleware treats as state-changing. */\nconst STATE_CHANGING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\n\nfunction isStateChanging(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return STATE_CHANGING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Adds `X-BFF-Csrf` to every state-changing request. Safe (GET/HEAD) requests\n * are left untouched — the BFF only enforces the header on mutations.\n */\nfunction attachCsrfHeader(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {\n if (isStateChanging(config.method)) {\n config.headers.set(CSRF_HEADER, CSRF_HEADER_VALUE);\n }\n return config;\n}\n\n/**\n * Registers the default CSRF request interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerDefaultCsrfInterceptor(instance: AxiosInstance): number {\n return instance.interceptors.request.use(attachCsrfHeader);\n}\n\nexport { registerDefaultCsrfInterceptor, attachCsrfHeader };\n","/**\n * Response error interceptor: classifies axios errors.\n *\n * Converts raw AxiosError objects into {@link ClassifiedError} instances with\n * full context (status, url, method, errorCode, message, etc), logs them\n * through the app-supplied {@link BffLogger}, and re-rejects so callers and\n * downstream interceptors can react.\n */\n\nimport { HttpMethod } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { ClassifiedError } from '@dloizides/api-client-base';\nimport type { AxiosError, AxiosInstance, AxiosResponse } from 'axios';\n\nconst LOG_CONTEXT = 'errorClassifier';\nconst TIMEOUT_ERROR_CODE = 'ECONNABORTED';\nconst NETWORK_ERROR_STATUS = 0;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction isAxiosError(value: unknown): value is AxiosError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'isAxiosError' in value;\n}\n\nfunction extractErrorCode(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const code = data.errorCode ?? data.code ?? data.error;\n if (typeof code === 'string' && code.length > 0) {\n return code;\n }\n\n return undefined;\n}\n\nfunction extractErrorMessage(data: unknown, fallback: string): string {\n if (!isRecord(data)) {\n return fallback;\n }\n\n const message = data.message ?? data.detail ?? data.error ?? data.title;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n return fallback;\n}\n\nfunction extractStringHeader(headers: Record<string, unknown>, key: string): string | undefined {\n const value: unknown = headers[key];\n if (typeof value === 'string' && value.length > 0) {\n return value;\n }\n return undefined;\n}\n\nfunction extractRequestId(response: AxiosResponse | undefined): string | undefined {\n if (!isValueDefined(response)) {\n return undefined;\n }\n\n const headers = response.headers;\n if (!isRecord(headers)) {\n return undefined;\n }\n\n return extractStringHeader(headers, 'x-request-id') ?? extractStringHeader(headers, 'x-correlation-id');\n}\n\nfunction resolveHttpMethod(method: string | undefined): HttpMethod {\n if (typeof method !== 'string') {\n return HttpMethod.Get;\n }\n const upper = method.toUpperCase();\n const methodMap: Record<string, HttpMethod | undefined> = {\n GET: HttpMethod.Get,\n POST: HttpMethod.Post,\n PUT: HttpMethod.Put,\n PATCH: HttpMethod.Patch,\n DELETE: HttpMethod.Delete,\n };\n return methodMap[upper] ?? HttpMethod.Get;\n}\n\n/**\n * Converts an AxiosError into a {@link ClassifiedError} with full context.\n */\nfunction classifyError(error: AxiosError): ClassifiedError {\n const response = error.response;\n const hasResponse = isValueDefined(response);\n\n const status = hasResponse ? response.status : NETWORK_ERROR_STATUS;\n const url = error.config?.url ?? 'unknown';\n const method = resolveHttpMethod(error.config?.method);\n const body = hasResponse ? response.data : undefined;\n\n const isTimeout = error.code === TIMEOUT_ERROR_CODE;\n const defaultMessage = isTimeout ? 'Request timed out' : 'Network error';\n const errorCode = extractErrorCode(body) ?? (isTimeout ? TIMEOUT_ERROR_CODE : undefined);\n const message = hasResponse ? extractErrorMessage(body, error.message) : defaultMessage;\n\n return {\n status,\n url,\n method,\n errorCode,\n message,\n body,\n originalError: error,\n timestamp: Date.now(),\n requestId: extractRequestId(response),\n };\n}\n\n/**\n * Handles response errors by classifying them and logging.\n */\nasync function handleResponseError(error: unknown, logger: BffLogger): Promise<never> {\n if (!isAxiosError(error)) {\n return Promise.reject(error);\n }\n\n const classified = classifyError(error);\n\n logger.warn(LOG_CONTEXT, `HTTP ${classified.method} ${classified.url} failed`, {\n status: classified.status,\n errorCode: classified.errorCode,\n message: classified.message,\n });\n\n return Promise.reject(error);\n}\n\n/**\n * Registers the error classifier interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerErrorClassifier(instance: AxiosInstance, logger: BffLogger): number {\n return instance.interceptors.response.use(\n (response) => response,\n (error: unknown) => handleResponseError(error, logger),\n );\n}\n\nexport { registerErrorClassifier, classifyError, handleResponseError };\n","/**\n * Request and response logging interceptor.\n *\n * Logs HTTP method, URL, status, and duration through the app-supplied\n * {@link BffLogger}. Only active in non-production environments. Request\n * bodies are NOT logged for security reasons.\n */\n\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger } from '../types';\nimport type { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';\n\nconst LOG_CONTEXT = 'http';\nconst REQUEST_START_HEADER = 'x-request-start-time';\n\nfunction isProduction(): boolean {\n return process.env.NODE_ENV === 'production';\n}\n\n/**\n * Logs the outgoing request and stamps the start time for duration tracking.\n */\nfunction logRequest(\n config: InternalAxiosRequestConfig,\n logger: BffLogger,\n): InternalAxiosRequestConfig {\n if (isProduction()) {\n return config;\n }\n\n const method = (config.method ?? 'GET').toUpperCase();\n const url = config.url ?? 'unknown';\n\n logger.debug(LOG_CONTEXT, `-> ${method} ${url}`);\n\n config.headers.set(REQUEST_START_HEADER, String(Date.now()));\n\n return config;\n}\n\nfunction calculateDurationMs(config: InternalAxiosRequestConfig): number | undefined {\n const startRaw = config.headers.get(REQUEST_START_HEADER);\n if (typeof startRaw !== 'string' || startRaw.length === 0) {\n return undefined;\n }\n\n const start = Number(startRaw);\n if (Number.isNaN(start)) {\n return undefined;\n }\n\n return Date.now() - start;\n}\n\n/**\n * Logs successful responses with status and duration.\n */\nfunction logResponse(response: AxiosResponse, logger: BffLogger): AxiosResponse {\n if (isProduction()) {\n return response;\n }\n\n const method = (response.config.method ?? 'GET').toUpperCase();\n const url = response.config.url ?? 'unknown';\n const status = response.status;\n const duration = calculateDurationMs(response.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.debug(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return response;\n}\n\ninterface AxiosLikeError {\n config: InternalAxiosRequestConfig;\n response?: AxiosResponse;\n message?: string;\n}\n\nfunction isAxiosLikeError(value: unknown): value is AxiosLikeError {\n if (typeof value !== 'object') {\n return false;\n }\n if (!isValueDefined(value)) {\n return false;\n }\n return 'config' in value;\n}\n\n/**\n * Logs error responses with status and duration.\n */\nasync function logErrorResponse(error: unknown, logger: BffLogger): Promise<never> {\n if (isProduction()) {\n return Promise.reject(error);\n }\n if (!isAxiosLikeError(error)) {\n return Promise.reject(error);\n }\n\n const method = (error.config.method ?? 'GET').toUpperCase();\n const url = error.config.url ?? 'unknown';\n const status = error.response?.status ?? 0;\n const duration = calculateDurationMs(error.config);\n\n const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : '';\n logger.warn(LOG_CONTEXT, `<- ${method} ${url} ${status}${durationSuffix}`);\n\n return Promise.reject(error);\n}\n\n/**\n * Registers request and response logging interceptors on an axios instance.\n * No-ops in production.\n * @returns An object with both interceptor IDs for potential ejection.\n */\nfunction registerLoggingInterceptor(\n instance: AxiosInstance,\n logger: BffLogger,\n): { request: number; response: number } {\n const requestId = instance.interceptors.request.use((config) => logRequest(config, logger));\n const responseId = instance.interceptors.response.use(\n (response) => logResponse(response, logger),\n (error: unknown) => logErrorResponse(error, logger),\n );\n return { request: requestId, response: responseId };\n}\n\nexport { registerLoggingInterceptor };\n","/**\n * Response interceptor: normalizes successful API responses.\n *\n * For successful mutation requests (POST/PUT/PATCH/DELETE), emits a toast via\n * the app-supplied {@link EmitToast} port so the UI can display a success\n * notification without coupling the package to a specific UI bus.\n */\n\nimport { ErrorSeverity } from '@dloizides/api-client-base';\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { BffLogger, EmitToast } from '../types';\nimport type { AxiosInstance, AxiosResponse } from 'axios';\n\nconst MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];\nconst DEFAULT_SUCCESS_MESSAGE = 'Saved successfully.';\nconst LOG_CONTEXT = 'responseNormalizer';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction extractMessageFromBody(data: unknown): string | undefined {\n if (!isRecord(data)) {\n return undefined;\n }\n\n const message = data.message;\n if (typeof message === 'string' && message.length > 0) {\n return message;\n }\n\n const detail = data.detail;\n if (typeof detail === 'string' && detail.length > 0) {\n return detail;\n }\n\n return undefined;\n}\n\nfunction isMutatingMethod(method: string | undefined): boolean {\n if (typeof method !== 'string') {\n return false;\n }\n return MUTATING_METHODS.includes(method.toUpperCase());\n}\n\n/**\n * Handles a successful response by emitting a toast for mutations.\n *\n * Always returns the original `response` unchanged — an axios response\n * interceptor must pass the response through. The invariant return is the\n * required contract, not a code smell.\n */\n// eslint-disable-next-line sonarjs/no-invariant-returns\nfunction handleSuccessResponse(\n response: AxiosResponse,\n emitToast: EmitToast,\n logger: BffLogger,\n): AxiosResponse {\n try {\n const method = response.config.method;\n if (!isMutatingMethod(method)) {\n return response;\n }\n\n const message = extractMessageFromBody(response.data) ?? DEFAULT_SUCCESS_MESSAGE;\n emitToast(String(message), ErrorSeverity.Info);\n } catch (emitError) {\n logger.warn(LOG_CONTEXT, 'Failed to emit success notification', emitError);\n }\n\n return response;\n}\n\n/**\n * Registers the response normalizer interceptor on an axios instance.\n * @returns The interceptor ID for potential ejection.\n */\nfunction registerResponseNormalizer(\n instance: AxiosInstance,\n emitToast: EmitToast,\n logger: BffLogger,\n): number {\n return instance.interceptors.response.use((response) =>\n handleSuccessResponse(response, emitToast, logger),\n );\n}\n\nexport { registerResponseNormalizer };\n","/**\n * Response error interceptor: transparently retries transient warm-up failures.\n *\n * WHY (P1-06): on a resource-tight, scale-to-zero prod node the BFF (or its\n * upstream API) can be cold on the FIRST request after idle — the gateway answers\n * `502/503/504` for a few seconds while the upstream comes up, then settles to a\n * correct `401/200`. The unguarded `/bff/me` probe surfaced that transient as an\n * auth error / anonymous flash on first load. These statuses mean the request was\n * NOT processed (upstream unavailable), so retrying is safe for any method.\n *\n * This interceptor catches those statuses and re-issues the SAME request up to\n * `maxRetries` times with exponential backoff, BEFORE the error classifier turns\n * it into a surfaced error — so it must be registered FIRST in the response\n * chain. `4xx` (incl. 401) and `500` fall straight through: a 500 is a real\n * server error, not a warm-up, and must not be retried.\n *\n * Deterministic + timer-injectable: the backoff `sleep` is a port (defaults to\n * `setTimeout`) so unit tests resolve instantly and no jitter/`Math.random` is\n * used.\n */\n\nimport type { BffLogger, WarmupRetryConfig } from '../types';\nimport type { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';\n\nconst LOG_CONTEXT = 'warmupRetry';\n\n/** Gateway/upstream-not-ready statuses that mean \"request not processed → safe to retry\". */\nconst DEFAULT_RETRYABLE_STATUSES: readonly number[] = [502, 503, 504];\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_BASE_DELAY_MS = 400;\nconst BACKOFF_FACTOR = 2;\n\n/** Per-request attempt counter stashed on the axios config. */\ninterface RetryableConfig extends InternalAxiosRequestConfig {\n __warmupRetryCount?: number;\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isAxiosError(value: unknown): value is AxiosError {\n return typeof value === 'object' && value !== null && 'isAxiosError' in value;\n}\n\n/**\n * Registers the warm-up retry interceptor. Returns the interceptor ID for ejection.\n */\nfunction registerWarmupRetryInterceptor(\n instance: AxiosInstance,\n logger: BffLogger,\n config: WarmupRetryConfig = {},\n): number {\n const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n const baseDelayMs = config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n const retryableStatuses = config.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES;\n const sleep = config.sleep ?? defaultSleep;\n\n return instance.interceptors.response.use(\n (response) => response,\n async (error: unknown) => {\n if (!isAxiosError(error) || error.response === undefined) {\n return Promise.reject(error);\n }\n if (!retryableStatuses.includes(error.response.status)) {\n return Promise.reject(error);\n }\n\n const requestConfig = error.config as RetryableConfig | undefined;\n if (requestConfig === undefined) {\n return Promise.reject(error);\n }\n\n const attempt = (requestConfig.__warmupRetryCount ?? 0) + 1;\n if (attempt > maxRetries) {\n logger.warn(LOG_CONTEXT, `gave up after ${maxRetries} warm-up retries`, {\n url: requestConfig.url,\n status: error.response.status,\n });\n return Promise.reject(error);\n }\n\n requestConfig.__warmupRetryCount = attempt;\n const delayMs = baseDelayMs * BACKOFF_FACTOR ** (attempt - 1);\n logger.debug(LOG_CONTEXT, `upstream warming up — retry ${attempt}/${maxRetries} in ${delayMs}ms`, {\n url: requestConfig.url,\n status: error.response.status,\n });\n await sleep(delayMs);\n return instance.request(requestConfig);\n },\n );\n}\n\nexport {\n registerWarmupRetryInterceptor,\n DEFAULT_RETRYABLE_STATUSES,\n DEFAULT_MAX_RETRIES,\n DEFAULT_BASE_DELAY_MS,\n};\n","/**\n * Interceptor registration — BFF era.\n *\n * Wires the interceptor chain onto an axios instance in the correct order.\n *\n * Request interceptors run in REVERSE order of registration, so:\n * 1. logging (registered first, runs last = logs the FINAL config)\n * 2. csrf (registered second, runs first = attaches the CSRF header)\n *\n * Response interceptors run in ORDER of registration, so:\n * 1. warm-up retry (retries transient 502/503/504 upstream-cold failures)\n * 2. logging (logs response/error)\n * 3. normalizer (emits success toast)\n * 4. session expiry (handles 401 -> clear session, app-supplied port)\n * 5. error classifier (classifies remaining errors)\n *\n * The warm-up retry runs FIRST so a cold-start 502/503/504 is re-issued and\n * (usually) resolves before any downstream interceptor logs or surfaces it as\n * an error — the P1-06 \"auth-error/anonymous flash on first load\" fix.\n *\n * The package owns the logging, normalizer, and error-classifier bodies. The\n * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns\n * its CSRF strategy and its session store); `csrf` falls back to the package\n * default when omitted.\n */\n\nimport { registerDefaultCsrfInterceptor } from './interceptors/defaultCsrfInterceptor';\nimport { registerErrorClassifier } from './interceptors/errorClassifierInterceptor';\nimport { registerLoggingInterceptor } from './interceptors/loggingInterceptor';\nimport { registerResponseNormalizer } from './interceptors/responseNormalizer';\nimport { registerWarmupRetryInterceptor } from './interceptors/warmupRetryInterceptor';\n\nimport type { RegisterInterceptorsPorts } from './types';\nimport type { AxiosInstance } from 'axios';\n\n/**\n * Registers the full BFF interceptor chain on the provided axios instance.\n * Call this once during application bootstrap after creating the instance.\n */\nfunction registerInterceptors(instance: AxiosInstance, ports: RegisterInterceptorsPorts): void {\n const { logger, emitToast, csrf, onSessionExpiry, warmupRetry } = ports;\n\n // Request interceptors (registered order = reverse execution order)\n registerLoggingInterceptor(instance, logger);\n if (csrf) {\n csrf(instance);\n } else {\n registerDefaultCsrfInterceptor(instance);\n }\n\n // Response interceptors (registered order = execution order)\n // Warm-up retry runs FIRST so a cold-start 502/503/504 is retried before any\n // downstream interceptor surfaces it (P1-06). Pass `warmupRetry: false` to opt out.\n if (warmupRetry !== false) {\n registerWarmupRetryInterceptor(instance, logger, warmupRetry ?? {});\n }\n registerResponseNormalizer(instance, emitToast, logger);\n if (onSessionExpiry) {\n onSessionExpiry(instance);\n }\n registerErrorClassifier(instance, logger);\n}\n\nexport { registerInterceptors };\n"]}
package/package.json CHANGED
@@ -1,86 +1,86 @@
1
- {
2
- "name": "@dloizides/bff-web-client",
3
- "version": "1.0.1",
4
- "description": "Product-agnostic RN-web BFF HTTP layer for the dloizides.com portfolio: an axios instance factory plus an interceptor chain (logging, success-toast normalizer, error classifier) with app-supplied ports for the CSRF strategy, session-expiry handling, toast emitter, and logger. Pairs with @dloizides/api-client-base by composition, never imports a product.",
5
- "keywords": [
6
- "axios",
7
- "bff",
8
- "interceptors",
9
- "http",
10
- "csrf",
11
- "dloizides"
12
- ],
13
- "author": "dloizides",
14
- "license": "MIT",
15
- "repository": {
16
- "type": "git",
17
- "url": "https://github.com/openmindednewby/bff-web-client.git"
18
- },
19
- "homepage": "https://github.com/openmindednewby/bff-web-client#readme",
20
- "bugs": {
21
- "url": "https://github.com/openmindednewby/bff-web-client/issues"
22
- },
23
- "main": "./dist/index.js",
24
- "module": "./dist/index.mjs",
25
- "types": "./dist/index.d.ts",
26
- "exports": {
27
- ".": {
28
- "require": {
29
- "types": "./dist/index.d.ts",
30
- "default": "./dist/index.js"
31
- },
32
- "import": {
33
- "types": "./dist/index.d.mts",
34
- "default": "./dist/index.mjs"
35
- }
36
- }
37
- },
38
- "files": [
39
- "dist",
40
- "README.md",
41
- "CHANGELOG.md"
42
- ],
43
- "sideEffects": false,
44
- "engines": {
45
- "node": ">=18.0.0"
46
- },
47
- "scripts": {
48
- "build": "rimraf dist && tsup",
49
- "build:watch": "tsup --watch",
50
- "test": "jest",
51
- "test:watch": "jest --watch",
52
- "test:coverage": "jest --coverage",
53
- "lint": "eslint src --ext .ts",
54
- "lint:fix": "eslint src --ext .ts --fix",
55
- "typecheck": "tsc --noEmit",
56
- "clean": "rimraf dist",
57
- "security:audit": "npm audit --audit-level=high",
58
- "deps:outdated": "npm outdated || exit 0",
59
- "deps:unused": "npx depcheck --ignores \"@types/jest,@types/node,rimraf\"",
60
- "deps:licenses": "npx license-checker --onlyAllow \"MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;Unlicense;CC0-1.0\"",
61
- "deps:health": "npm run deps:outdated && npm run deps:unused",
62
- "prepublishOnly": "npm run clean && npm run build && npm run test"
63
- },
64
- "peerDependencies": {
65
- "@dloizides/api-client-base": "^1.0.0",
66
- "@dloizides/utils": "^1.0.0",
67
- "axios": ">=1.0.0"
68
- },
69
- "devDependencies": {
70
- "@dloizides/api-client-base": "^1.0.0",
71
- "@dloizides/utils": "^1.0.2",
72
- "@types/jest": "^29.5.0",
73
- "@types/node": "^20.19.32",
74
- "@typescript-eslint/eslint-plugin": "^7.0.0",
75
- "@typescript-eslint/parser": "^7.0.0",
76
- "axios": "^1.13.4",
77
- "eslint": "^8.57.0",
78
- "eslint-plugin-sonarjs": "^4.0.3",
79
- "jest": "^29.7.0",
80
- "jest-environment-jsdom": "^29.7.0",
81
- "rimraf": "^5.0.0",
82
- "ts-jest": "^29.1.0",
83
- "tsup": "^8.0.0",
84
- "typescript": "^5.4.0"
85
- }
86
- }
1
+ {
2
+ "name": "@dloizides/bff-web-client",
3
+ "version": "1.1.0",
4
+ "description": "Product-agnostic RN-web BFF HTTP layer for the dloizides.com portfolio: an axios instance factory plus an interceptor chain (logging, success-toast normalizer, error classifier) with app-supplied ports for the CSRF strategy, session-expiry handling, toast emitter, and logger. Pairs with @dloizides/api-client-base by composition, never imports a product.",
5
+ "keywords": [
6
+ "axios",
7
+ "bff",
8
+ "interceptors",
9
+ "http",
10
+ "csrf",
11
+ "dloizides"
12
+ ],
13
+ "author": "dloizides",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/openmindednewby/bff-web-client.git"
18
+ },
19
+ "homepage": "https://github.com/openmindednewby/bff-web-client#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/openmindednewby/bff-web-client/issues"
22
+ },
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.mjs",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "require": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "import": {
33
+ "types": "./dist/index.d.mts",
34
+ "default": "./dist/index.mjs"
35
+ }
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "CHANGELOG.md"
42
+ ],
43
+ "sideEffects": false,
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "scripts": {
48
+ "build": "rimraf dist && tsup",
49
+ "build:watch": "tsup --watch",
50
+ "test": "jest",
51
+ "test:watch": "jest --watch",
52
+ "test:coverage": "jest --coverage",
53
+ "lint": "eslint src --ext .ts",
54
+ "lint:fix": "eslint src --ext .ts --fix",
55
+ "typecheck": "tsc --noEmit",
56
+ "clean": "rimraf dist",
57
+ "security:audit": "npm audit --audit-level=high",
58
+ "deps:outdated": "npm outdated || exit 0",
59
+ "deps:unused": "npx depcheck --ignores \"@types/jest,@types/node,rimraf\"",
60
+ "deps:licenses": "npx license-checker --onlyAllow \"MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;Unlicense;CC0-1.0\"",
61
+ "deps:health": "npm run deps:outdated && npm run deps:unused",
62
+ "prepublishOnly": "npm run clean && npm run build && npm run test"
63
+ },
64
+ "peerDependencies": {
65
+ "@dloizides/api-client-base": "^1.0.0",
66
+ "@dloizides/utils": "^1.0.0",
67
+ "axios": ">=1.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "@dloizides/api-client-base": "^1.0.0",
71
+ "@dloizides/utils": "^1.0.2",
72
+ "@types/jest": "^29.5.0",
73
+ "@types/node": "^20.19.32",
74
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
75
+ "@typescript-eslint/parser": "^7.0.0",
76
+ "axios": "^1.13.4",
77
+ "eslint": "^8.57.0",
78
+ "eslint-plugin-sonarjs": "^4.0.3",
79
+ "jest": "^29.7.0",
80
+ "jest-environment-jsdom": "^29.7.0",
81
+ "rimraf": "^5.0.0",
82
+ "ts-jest": "^29.1.0",
83
+ "tsup": "^8.0.0",
84
+ "typescript": "^5.4.0"
85
+ }
86
+ }