@dloizides/bff-web-client 1.0.1

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 ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@dloizides/bff-web-client` are documented here.
4
+
5
+ ## [1.0.0] - 2026-06-14
6
+
7
+ ### Added
8
+ - Initial release. Extracted from the byte-identical BFF HTTP layer shared by
9
+ `erevna-web` and `katalogos-web`.
10
+ - `createBffAxiosClient(opts)` — credentialed axios instance factory with the
11
+ shared BFF defaults (JSON, `X-Requested-With`, `withCredentials`).
12
+ - `registerInterceptors(instance, ports)` — wires the BFF interceptor chain:
13
+ logging, success-toast normalizer, error classifier (package-owned), plus the
14
+ app-supplied `csrf` and `onSessionExpiry` ports.
15
+ - Individual interceptor registrars: `registerLoggingInterceptor`,
16
+ `registerResponseNormalizer`, `registerErrorClassifier`,
17
+ `registerDefaultCsrfInterceptor`.
18
+ - Ports/types: `BffLogger`, `EmitToast`, `InterceptorRegistrar`,
19
+ `BffAxiosClientOptions`, `RegisterInterceptorsPorts`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 dloizides
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @dloizides/bff-web-client
2
+
3
+ Product-agnostic RN-web **BFF HTTP layer** for the dloizides.com portfolio.
4
+
5
+ It owns the axios instance factory and the interceptor chain (logging, success
6
+ toast normalizer, error classifier) that `erevna-web` and `katalogos-web` shared
7
+ byte-for-byte. Every app-specific concern is a **port** the consuming app
8
+ supplies — its logger, its toast emitter, its CSRF strategy, and its
9
+ session-expiry handler. The package never imports a product, realm, or hardcoded
10
+ URL, and pairs with `@dloizides/api-client-base` by composition.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @dloizides/bff-web-client
16
+ ```
17
+
18
+ Peer deps: `axios`, `@dloizides/api-client-base`, `@dloizides/utils`.
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import {
24
+ createBffAxiosClient,
25
+ registerInterceptors,
26
+ } from '@dloizides/bff-web-client';
27
+
28
+ import { apiEventBus } from '@dloizides/api-client-base';
29
+ import { logger } from './my-logger';
30
+ import { registerCsrfInterceptor } from './csrfInterceptor'; // app-owned
31
+ import { registerSessionExpiryInterceptor } from './sessionExpiry'; // app-owned
32
+
33
+ export const apiClient = createBffAxiosClient({ timeoutMs: 30_000 });
34
+
35
+ registerInterceptors(apiClient, {
36
+ logger,
37
+ emitToast: (message, severity) =>
38
+ apiEventBus.emit({ type: 'toast', severity, message }),
39
+ csrf: registerCsrfInterceptor,
40
+ onSessionExpiry: registerSessionExpiryInterceptor,
41
+ });
42
+ ```
43
+
44
+ ## API
45
+
46
+ ### `createBffAxiosClient(options)`
47
+
48
+ | option | type | description |
49
+ | ----------- | -------------------------- | ---------------------------------------------------- |
50
+ | `timeoutMs` | `number` | Request timeout in milliseconds. |
51
+ | `baseURL` | `string` (optional) | Omit for same-origin (relative) BFF requests. |
52
+ | `headers` | `Record<string,string>` | Extra default headers merged over the BFF defaults. |
53
+
54
+ Returns a credentialed `AxiosInstance` with **no** interceptors registered.
55
+
56
+ ### `registerInterceptors(instance, ports)`
57
+
58
+ | port | type | required | description |
59
+ | ---------------- | -------------------------------------- | -------- | -------------------------------------------------------------------- |
60
+ | `logger` | `BffLogger` | yes | Structured logger the package logs through. |
61
+ | `emitToast` | `(message, severity) => void` | yes | Called when a mutating request succeeds. |
62
+ | `csrf` | `(instance) => void` | no | App CSRF registrar. Defaults to `X-BFF-Csrf: 1` on mutations. |
63
+ | `onSessionExpiry`| `(instance) => void` | no | App session-expiry registrar (app owns its session store). |
64
+
65
+ Chain order (matches the pre-extraction behaviour):
66
+
67
+ - **Request** (reverse execution): logging is registered first (runs last),
68
+ CSRF registered second (runs first).
69
+ - **Response** (execution order): logging → normalizer → session expiry →
70
+ error classifier.
71
+
72
+ ### Individual registrars
73
+
74
+ `registerLoggingInterceptor`, `registerResponseNormalizer`,
75
+ `registerErrorClassifier`, `registerDefaultCsrfInterceptor` — exported for
76
+ custom chains.
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,194 @@
1
+ import { ErrorSeverity, ClassifiedError } from '@dloizides/api-client-base';
2
+ import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
3
+
4
+ /**
5
+ * Shared types for `@dloizides/bff-web-client`.
6
+ *
7
+ * The package owns the BFF HTTP *logic* (the axios factory + the interceptor
8
+ * chain). Every app-specific concern is a **port** the consuming app supplies:
9
+ * its logger, its toast/UI bus, its CSRF strategy, and its session-expiry
10
+ * handler. The package never imports a product, realm, or hardcoded URL.
11
+ */
12
+
13
+ /**
14
+ * Minimal structured logger contract. The consuming app supplies an adapter
15
+ * over its own logging service so the package emits no `console` calls.
16
+ */
17
+ interface BffLogger {
18
+ debug: (context: string, message: string, data?: unknown) => void;
19
+ info: (context: string, message: string, data?: unknown) => void;
20
+ warn: (context: string, message: string, data?: unknown) => void;
21
+ error: (context: string, message: string, error?: unknown) => void;
22
+ }
23
+ /**
24
+ * Emits a UI toast. The app owns its UI event bus; the package only calls
25
+ * this port when a mutating request succeeds (the response normalizer).
26
+ */
27
+ type EmitToast = (message: string, severity: ErrorSeverity) => void;
28
+ /**
29
+ * Registers an interceptor on the given axios instance. Both the CSRF port and
30
+ * the session-expiry port have this shape: the app supplies a function that
31
+ * wires its own interceptor onto the instance. The package calls it during
32
+ * {@link registerInterceptors} in the correct chain position.
33
+ */
34
+ type InterceptorRegistrar = (instance: AxiosInstance) => void;
35
+ /**
36
+ * Options for {@link createBffAxiosClient}.
37
+ */
38
+ interface BffAxiosClientOptions {
39
+ /** Request timeout in milliseconds. */
40
+ timeoutMs: number;
41
+ /** Optional base URL; omit to use relative paths (the BFF same-origin case). */
42
+ baseURL?: string;
43
+ /** Extra default headers merged over the BFF defaults. */
44
+ headers?: Record<string, string>;
45
+ }
46
+ /**
47
+ * Ports for {@link registerInterceptors}. The 4 package-owned interceptors
48
+ * (logging, response normalizer, error classifier) run with the supplied
49
+ * {@link BffLogger} and {@link EmitToast}; `csrf` and `onSessionExpiry` are
50
+ * app-supplied registrars wired into the chain at the right position.
51
+ */
52
+ interface RegisterInterceptorsPorts {
53
+ /** Structured logger the package logs through. */
54
+ logger: BffLogger;
55
+ /** Toast emitter the response normalizer calls on a successful mutation. */
56
+ emitToast: EmitToast;
57
+ /**
58
+ * App-supplied CSRF request-interceptor registrar (the one body that
59
+ * diverges per app). If omitted, {@link registerDefaultCsrfInterceptor} is
60
+ * used, which attaches `X-BFF-Csrf: 1` to every state-changing request.
61
+ */
62
+ csrf?: InterceptorRegistrar;
63
+ /**
64
+ * App-supplied session-expiry response-interceptor registrar (the app owns
65
+ * its session store + its `/bff/me` probe). Optional: omit when an app does
66
+ * not handle 401 session death in the HTTP layer.
67
+ */
68
+ onSessionExpiry?: InterceptorRegistrar;
69
+ }
70
+
71
+ /**
72
+ * Clean axios instance factory for the BFF era.
73
+ *
74
+ * Produces an axios instance with the BFF defaults (JSON, XHR marker,
75
+ * credentialed) and no interceptors. Interceptors are wired separately via
76
+ * {@link registerInterceptors} so the app controls the chain composition.
77
+ */
78
+
79
+ /**
80
+ * Creates a credentialed axios instance with the shared BFF defaults. No
81
+ * interceptors are registered here — call {@link registerInterceptors} once at
82
+ * bootstrap to install the chain.
83
+ */
84
+ declare function createBffAxiosClient(options: BffAxiosClientOptions): AxiosInstance;
85
+
86
+ /**
87
+ * Interceptor registration — BFF era.
88
+ *
89
+ * Wires the interceptor chain onto an axios instance in the correct order.
90
+ *
91
+ * Request interceptors run in REVERSE order of registration, so:
92
+ * 1. logging (registered first, runs last = logs the FINAL config)
93
+ * 2. csrf (registered second, runs first = attaches the CSRF header)
94
+ *
95
+ * 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)
100
+ *
101
+ * The package owns the logging, normalizer, and error-classifier bodies. The
102
+ * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns
103
+ * its CSRF strategy and its session store); `csrf` falls back to the package
104
+ * default when omitted.
105
+ */
106
+
107
+ /**
108
+ * Registers the full BFF interceptor chain on the provided axios instance.
109
+ * Call this once during application bootstrap after creating the instance.
110
+ */
111
+ declare function registerInterceptors(instance: AxiosInstance, ports: RegisterInterceptorsPorts): void;
112
+
113
+ /**
114
+ * Request and response logging interceptor.
115
+ *
116
+ * Logs HTTP method, URL, status, and duration through the app-supplied
117
+ * {@link BffLogger}. Only active in non-production environments. Request
118
+ * bodies are NOT logged for security reasons.
119
+ */
120
+
121
+ /**
122
+ * Registers request and response logging interceptors on an axios instance.
123
+ * No-ops in production.
124
+ * @returns An object with both interceptor IDs for potential ejection.
125
+ */
126
+ declare function registerLoggingInterceptor(instance: AxiosInstance, logger: BffLogger): {
127
+ request: number;
128
+ response: number;
129
+ };
130
+
131
+ /**
132
+ * Response interceptor: normalizes successful API responses.
133
+ *
134
+ * For successful mutation requests (POST/PUT/PATCH/DELETE), emits a toast via
135
+ * the app-supplied {@link EmitToast} port so the UI can display a success
136
+ * notification without coupling the package to a specific UI bus.
137
+ */
138
+
139
+ /**
140
+ * Registers the response normalizer interceptor on an axios instance.
141
+ * @returns The interceptor ID for potential ejection.
142
+ */
143
+ declare function registerResponseNormalizer(instance: AxiosInstance, emitToast: EmitToast, logger: BffLogger): number;
144
+
145
+ /**
146
+ * Response error interceptor: classifies axios errors.
147
+ *
148
+ * Converts raw AxiosError objects into {@link ClassifiedError} instances with
149
+ * full context (status, url, method, errorCode, message, etc), logs them
150
+ * through the app-supplied {@link BffLogger}, and re-rejects so callers and
151
+ * downstream interceptors can react.
152
+ */
153
+
154
+ /**
155
+ * Converts an AxiosError into a {@link ClassifiedError} with full context.
156
+ */
157
+ declare function classifyError(error: AxiosError): ClassifiedError;
158
+ /**
159
+ * Handles response errors by classifying them and logging.
160
+ */
161
+ declare function handleResponseError(error: unknown, logger: BffLogger): Promise<never>;
162
+ /**
163
+ * Registers the error classifier interceptor on an axios instance.
164
+ * @returns The interceptor ID for potential ejection.
165
+ */
166
+ declare function registerErrorClassifier(instance: AxiosInstance, logger: BffLogger): number;
167
+
168
+ /**
169
+ * Default CSRF request interceptor.
170
+ *
171
+ * After a BFF cutover, an SPA authenticates via a cookie. Cookie auth
172
+ * reintroduces CSRF risk, so the BFF's `Bff.AspNetCore` anti-forgery
173
+ * middleware requires a custom header on every state-changing request — a
174
+ * request a cross-site form POST cannot forge. This default attaches
175
+ * `X-BFF-Csrf: 1` to all mutating methods.
176
+ *
177
+ * This is the **default implementation of the `csrf` port**. Apps that need a
178
+ * different CSRF strategy supply their own registrar to
179
+ * {@link registerInterceptors}; this body is what diverged per app and is kept
180
+ * overridable on purpose.
181
+ */
182
+
183
+ /**
184
+ * Adds `X-BFF-Csrf` to every state-changing request. Safe (GET/HEAD) requests
185
+ * are left untouched — the BFF only enforces the header on mutations.
186
+ */
187
+ declare function attachCsrfHeader(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig;
188
+ /**
189
+ * Registers the default CSRF request interceptor on an axios instance.
190
+ * @returns The interceptor ID for potential ejection.
191
+ */
192
+ declare function registerDefaultCsrfInterceptor(instance: AxiosInstance): number;
193
+
194
+ export { type BffAxiosClientOptions, type BffLogger, type EmitToast, type InterceptorRegistrar, type RegisterInterceptorsPorts, attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer };
@@ -0,0 +1,194 @@
1
+ import { ErrorSeverity, ClassifiedError } from '@dloizides/api-client-base';
2
+ import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
3
+
4
+ /**
5
+ * Shared types for `@dloizides/bff-web-client`.
6
+ *
7
+ * The package owns the BFF HTTP *logic* (the axios factory + the interceptor
8
+ * chain). Every app-specific concern is a **port** the consuming app supplies:
9
+ * its logger, its toast/UI bus, its CSRF strategy, and its session-expiry
10
+ * handler. The package never imports a product, realm, or hardcoded URL.
11
+ */
12
+
13
+ /**
14
+ * Minimal structured logger contract. The consuming app supplies an adapter
15
+ * over its own logging service so the package emits no `console` calls.
16
+ */
17
+ interface BffLogger {
18
+ debug: (context: string, message: string, data?: unknown) => void;
19
+ info: (context: string, message: string, data?: unknown) => void;
20
+ warn: (context: string, message: string, data?: unknown) => void;
21
+ error: (context: string, message: string, error?: unknown) => void;
22
+ }
23
+ /**
24
+ * Emits a UI toast. The app owns its UI event bus; the package only calls
25
+ * this port when a mutating request succeeds (the response normalizer).
26
+ */
27
+ type EmitToast = (message: string, severity: ErrorSeverity) => void;
28
+ /**
29
+ * Registers an interceptor on the given axios instance. Both the CSRF port and
30
+ * the session-expiry port have this shape: the app supplies a function that
31
+ * wires its own interceptor onto the instance. The package calls it during
32
+ * {@link registerInterceptors} in the correct chain position.
33
+ */
34
+ type InterceptorRegistrar = (instance: AxiosInstance) => void;
35
+ /**
36
+ * Options for {@link createBffAxiosClient}.
37
+ */
38
+ interface BffAxiosClientOptions {
39
+ /** Request timeout in milliseconds. */
40
+ timeoutMs: number;
41
+ /** Optional base URL; omit to use relative paths (the BFF same-origin case). */
42
+ baseURL?: string;
43
+ /** Extra default headers merged over the BFF defaults. */
44
+ headers?: Record<string, string>;
45
+ }
46
+ /**
47
+ * Ports for {@link registerInterceptors}. The 4 package-owned interceptors
48
+ * (logging, response normalizer, error classifier) run with the supplied
49
+ * {@link BffLogger} and {@link EmitToast}; `csrf` and `onSessionExpiry` are
50
+ * app-supplied registrars wired into the chain at the right position.
51
+ */
52
+ interface RegisterInterceptorsPorts {
53
+ /** Structured logger the package logs through. */
54
+ logger: BffLogger;
55
+ /** Toast emitter the response normalizer calls on a successful mutation. */
56
+ emitToast: EmitToast;
57
+ /**
58
+ * App-supplied CSRF request-interceptor registrar (the one body that
59
+ * diverges per app). If omitted, {@link registerDefaultCsrfInterceptor} is
60
+ * used, which attaches `X-BFF-Csrf: 1` to every state-changing request.
61
+ */
62
+ csrf?: InterceptorRegistrar;
63
+ /**
64
+ * App-supplied session-expiry response-interceptor registrar (the app owns
65
+ * its session store + its `/bff/me` probe). Optional: omit when an app does
66
+ * not handle 401 session death in the HTTP layer.
67
+ */
68
+ onSessionExpiry?: InterceptorRegistrar;
69
+ }
70
+
71
+ /**
72
+ * Clean axios instance factory for the BFF era.
73
+ *
74
+ * Produces an axios instance with the BFF defaults (JSON, XHR marker,
75
+ * credentialed) and no interceptors. Interceptors are wired separately via
76
+ * {@link registerInterceptors} so the app controls the chain composition.
77
+ */
78
+
79
+ /**
80
+ * Creates a credentialed axios instance with the shared BFF defaults. No
81
+ * interceptors are registered here — call {@link registerInterceptors} once at
82
+ * bootstrap to install the chain.
83
+ */
84
+ declare function createBffAxiosClient(options: BffAxiosClientOptions): AxiosInstance;
85
+
86
+ /**
87
+ * Interceptor registration — BFF era.
88
+ *
89
+ * Wires the interceptor chain onto an axios instance in the correct order.
90
+ *
91
+ * Request interceptors run in REVERSE order of registration, so:
92
+ * 1. logging (registered first, runs last = logs the FINAL config)
93
+ * 2. csrf (registered second, runs first = attaches the CSRF header)
94
+ *
95
+ * 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)
100
+ *
101
+ * The package owns the logging, normalizer, and error-classifier bodies. The
102
+ * `csrf` and `onSessionExpiry` registrars are app-supplied ports (the app owns
103
+ * its CSRF strategy and its session store); `csrf` falls back to the package
104
+ * default when omitted.
105
+ */
106
+
107
+ /**
108
+ * Registers the full BFF interceptor chain on the provided axios instance.
109
+ * Call this once during application bootstrap after creating the instance.
110
+ */
111
+ declare function registerInterceptors(instance: AxiosInstance, ports: RegisterInterceptorsPorts): void;
112
+
113
+ /**
114
+ * Request and response logging interceptor.
115
+ *
116
+ * Logs HTTP method, URL, status, and duration through the app-supplied
117
+ * {@link BffLogger}. Only active in non-production environments. Request
118
+ * bodies are NOT logged for security reasons.
119
+ */
120
+
121
+ /**
122
+ * Registers request and response logging interceptors on an axios instance.
123
+ * No-ops in production.
124
+ * @returns An object with both interceptor IDs for potential ejection.
125
+ */
126
+ declare function registerLoggingInterceptor(instance: AxiosInstance, logger: BffLogger): {
127
+ request: number;
128
+ response: number;
129
+ };
130
+
131
+ /**
132
+ * Response interceptor: normalizes successful API responses.
133
+ *
134
+ * For successful mutation requests (POST/PUT/PATCH/DELETE), emits a toast via
135
+ * the app-supplied {@link EmitToast} port so the UI can display a success
136
+ * notification without coupling the package to a specific UI bus.
137
+ */
138
+
139
+ /**
140
+ * Registers the response normalizer interceptor on an axios instance.
141
+ * @returns The interceptor ID for potential ejection.
142
+ */
143
+ declare function registerResponseNormalizer(instance: AxiosInstance, emitToast: EmitToast, logger: BffLogger): number;
144
+
145
+ /**
146
+ * Response error interceptor: classifies axios errors.
147
+ *
148
+ * Converts raw AxiosError objects into {@link ClassifiedError} instances with
149
+ * full context (status, url, method, errorCode, message, etc), logs them
150
+ * through the app-supplied {@link BffLogger}, and re-rejects so callers and
151
+ * downstream interceptors can react.
152
+ */
153
+
154
+ /**
155
+ * Converts an AxiosError into a {@link ClassifiedError} with full context.
156
+ */
157
+ declare function classifyError(error: AxiosError): ClassifiedError;
158
+ /**
159
+ * Handles response errors by classifying them and logging.
160
+ */
161
+ declare function handleResponseError(error: unknown, logger: BffLogger): Promise<never>;
162
+ /**
163
+ * Registers the error classifier interceptor on an axios instance.
164
+ * @returns The interceptor ID for potential ejection.
165
+ */
166
+ declare function registerErrorClassifier(instance: AxiosInstance, logger: BffLogger): number;
167
+
168
+ /**
169
+ * Default CSRF request interceptor.
170
+ *
171
+ * After a BFF cutover, an SPA authenticates via a cookie. Cookie auth
172
+ * reintroduces CSRF risk, so the BFF's `Bff.AspNetCore` anti-forgery
173
+ * middleware requires a custom header on every state-changing request — a
174
+ * request a cross-site form POST cannot forge. This default attaches
175
+ * `X-BFF-Csrf: 1` to all mutating methods.
176
+ *
177
+ * This is the **default implementation of the `csrf` port**. Apps that need a
178
+ * different CSRF strategy supply their own registrar to
179
+ * {@link registerInterceptors}; this body is what diverged per app and is kept
180
+ * overridable on purpose.
181
+ */
182
+
183
+ /**
184
+ * Adds `X-BFF-Csrf` to every state-changing request. Safe (GET/HEAD) requests
185
+ * are left untouched — the BFF only enforces the header on mutations.
186
+ */
187
+ declare function attachCsrfHeader(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig;
188
+ /**
189
+ * Registers the default CSRF request interceptor on an axios instance.
190
+ * @returns The interceptor ID for potential ejection.
191
+ */
192
+ declare function registerDefaultCsrfInterceptor(instance: AxiosInstance): number;
193
+
194
+ export { type BffAxiosClientOptions, type BffLogger, type EmitToast, type InterceptorRegistrar, type RegisterInterceptorsPorts, attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer };
package/dist/index.js ADDED
@@ -0,0 +1,293 @@
1
+ 'use strict';
2
+
3
+ var axios = require('axios');
4
+ var apiClientBase = require('@dloizides/api-client-base');
5
+ var utils = require('@dloizides/utils');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var axios__default = /*#__PURE__*/_interopDefault(axios);
10
+
11
+ // src/createBffAxiosClient.ts
12
+ var BFF_DEFAULT_HEADERS = {
13
+ "Content-Type": "application/json",
14
+ "Accept": "application/json",
15
+ "X-Requested-With": "XMLHttpRequest"
16
+ };
17
+ function createBffAxiosClient(options) {
18
+ return axios__default.default.create({
19
+ timeout: options.timeoutMs,
20
+ baseURL: options.baseURL,
21
+ headers: { ...BFF_DEFAULT_HEADERS, ...options.headers ?? {} },
22
+ withCredentials: true
23
+ });
24
+ }
25
+
26
+ // src/interceptors/defaultCsrfInterceptor.ts
27
+ var CSRF_HEADER = "X-BFF-Csrf";
28
+ var CSRF_HEADER_VALUE = "1";
29
+ var STATE_CHANGING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
30
+ function isStateChanging(method) {
31
+ if (typeof method !== "string") {
32
+ return false;
33
+ }
34
+ return STATE_CHANGING_METHODS.includes(method.toUpperCase());
35
+ }
36
+ function attachCsrfHeader(config) {
37
+ if (isStateChanging(config.method)) {
38
+ config.headers.set(CSRF_HEADER, CSRF_HEADER_VALUE);
39
+ }
40
+ return config;
41
+ }
42
+ function registerDefaultCsrfInterceptor(instance) {
43
+ return instance.interceptors.request.use(attachCsrfHeader);
44
+ }
45
+ var LOG_CONTEXT = "errorClassifier";
46
+ var TIMEOUT_ERROR_CODE = "ECONNABORTED";
47
+ var NETWORK_ERROR_STATUS = 0;
48
+ function isRecord(value) {
49
+ return utils.isValueDefined(value) && typeof value === "object";
50
+ }
51
+ function isAxiosError(value) {
52
+ if (typeof value !== "object") {
53
+ return false;
54
+ }
55
+ if (!utils.isValueDefined(value)) {
56
+ return false;
57
+ }
58
+ return "isAxiosError" in value;
59
+ }
60
+ function extractErrorCode(data) {
61
+ if (!isRecord(data)) {
62
+ return void 0;
63
+ }
64
+ const code = data.errorCode ?? data.code ?? data.error;
65
+ if (typeof code === "string" && code.length > 0) {
66
+ return code;
67
+ }
68
+ return void 0;
69
+ }
70
+ function extractErrorMessage(data, fallback) {
71
+ if (!isRecord(data)) {
72
+ return fallback;
73
+ }
74
+ const message = data.message ?? data.detail ?? data.error ?? data.title;
75
+ if (typeof message === "string" && message.length > 0) {
76
+ return message;
77
+ }
78
+ return fallback;
79
+ }
80
+ function extractStringHeader(headers, key) {
81
+ const value = headers[key];
82
+ if (typeof value === "string" && value.length > 0) {
83
+ return value;
84
+ }
85
+ return void 0;
86
+ }
87
+ function extractRequestId(response) {
88
+ if (!utils.isValueDefined(response)) {
89
+ return void 0;
90
+ }
91
+ const headers = response.headers;
92
+ if (!isRecord(headers)) {
93
+ return void 0;
94
+ }
95
+ return extractStringHeader(headers, "x-request-id") ?? extractStringHeader(headers, "x-correlation-id");
96
+ }
97
+ function resolveHttpMethod(method) {
98
+ if (typeof method !== "string") {
99
+ return apiClientBase.HttpMethod.Get;
100
+ }
101
+ const upper = method.toUpperCase();
102
+ const methodMap = {
103
+ GET: apiClientBase.HttpMethod.Get,
104
+ POST: apiClientBase.HttpMethod.Post,
105
+ PUT: apiClientBase.HttpMethod.Put,
106
+ PATCH: apiClientBase.HttpMethod.Patch,
107
+ DELETE: apiClientBase.HttpMethod.Delete
108
+ };
109
+ return methodMap[upper] ?? apiClientBase.HttpMethod.Get;
110
+ }
111
+ function classifyError(error) {
112
+ const response = error.response;
113
+ const hasResponse = utils.isValueDefined(response);
114
+ const status = hasResponse ? response.status : NETWORK_ERROR_STATUS;
115
+ const url = error.config?.url ?? "unknown";
116
+ const method = resolveHttpMethod(error.config?.method);
117
+ const body = hasResponse ? response.data : void 0;
118
+ const isTimeout = error.code === TIMEOUT_ERROR_CODE;
119
+ const defaultMessage = isTimeout ? "Request timed out" : "Network error";
120
+ const errorCode = extractErrorCode(body) ?? (isTimeout ? TIMEOUT_ERROR_CODE : void 0);
121
+ const message = hasResponse ? extractErrorMessage(body, error.message) : defaultMessage;
122
+ return {
123
+ status,
124
+ url,
125
+ method,
126
+ errorCode,
127
+ message,
128
+ body,
129
+ originalError: error,
130
+ timestamp: Date.now(),
131
+ requestId: extractRequestId(response)
132
+ };
133
+ }
134
+ async function handleResponseError(error, logger) {
135
+ if (!isAxiosError(error)) {
136
+ return Promise.reject(error);
137
+ }
138
+ const classified = classifyError(error);
139
+ logger.warn(LOG_CONTEXT, `HTTP ${classified.method} ${classified.url} failed`, {
140
+ status: classified.status,
141
+ errorCode: classified.errorCode,
142
+ message: classified.message
143
+ });
144
+ return Promise.reject(error);
145
+ }
146
+ function registerErrorClassifier(instance, logger) {
147
+ return instance.interceptors.response.use(
148
+ (response) => response,
149
+ (error) => handleResponseError(error, logger)
150
+ );
151
+ }
152
+ var LOG_CONTEXT2 = "http";
153
+ var REQUEST_START_HEADER = "x-request-start-time";
154
+ function isProduction() {
155
+ return process.env.NODE_ENV === "production";
156
+ }
157
+ function logRequest(config, logger) {
158
+ if (isProduction()) {
159
+ return config;
160
+ }
161
+ const method = (config.method ?? "GET").toUpperCase();
162
+ const url = config.url ?? "unknown";
163
+ logger.debug(LOG_CONTEXT2, `-> ${method} ${url}`);
164
+ config.headers.set(REQUEST_START_HEADER, String(Date.now()));
165
+ return config;
166
+ }
167
+ function calculateDurationMs(config) {
168
+ const startRaw = config.headers.get(REQUEST_START_HEADER);
169
+ if (typeof startRaw !== "string" || startRaw.length === 0) {
170
+ return void 0;
171
+ }
172
+ const start = Number(startRaw);
173
+ if (Number.isNaN(start)) {
174
+ return void 0;
175
+ }
176
+ return Date.now() - start;
177
+ }
178
+ function logResponse(response, logger) {
179
+ if (isProduction()) {
180
+ return response;
181
+ }
182
+ const method = (response.config.method ?? "GET").toUpperCase();
183
+ const url = response.config.url ?? "unknown";
184
+ const status = response.status;
185
+ const duration = calculateDurationMs(response.config);
186
+ const durationSuffix = utils.isValueDefined(duration) ? ` (${duration}ms)` : "";
187
+ logger.debug(LOG_CONTEXT2, `<- ${method} ${url} ${status}${durationSuffix}`);
188
+ return response;
189
+ }
190
+ function isAxiosLikeError(value) {
191
+ if (typeof value !== "object") {
192
+ return false;
193
+ }
194
+ if (!utils.isValueDefined(value)) {
195
+ return false;
196
+ }
197
+ return "config" in value;
198
+ }
199
+ async function logErrorResponse(error, logger) {
200
+ if (isProduction()) {
201
+ return Promise.reject(error);
202
+ }
203
+ if (!isAxiosLikeError(error)) {
204
+ return Promise.reject(error);
205
+ }
206
+ const method = (error.config.method ?? "GET").toUpperCase();
207
+ const url = error.config.url ?? "unknown";
208
+ const status = error.response?.status ?? 0;
209
+ const duration = calculateDurationMs(error.config);
210
+ const durationSuffix = utils.isValueDefined(duration) ? ` (${duration}ms)` : "";
211
+ logger.warn(LOG_CONTEXT2, `<- ${method} ${url} ${status}${durationSuffix}`);
212
+ return Promise.reject(error);
213
+ }
214
+ function registerLoggingInterceptor(instance, logger) {
215
+ const requestId = instance.interceptors.request.use((config) => logRequest(config, logger));
216
+ const responseId = instance.interceptors.response.use(
217
+ (response) => logResponse(response, logger),
218
+ (error) => logErrorResponse(error, logger)
219
+ );
220
+ return { request: requestId, response: responseId };
221
+ }
222
+ var MUTATING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
223
+ var DEFAULT_SUCCESS_MESSAGE = "Saved successfully.";
224
+ var LOG_CONTEXT3 = "responseNormalizer";
225
+ function isRecord2(value) {
226
+ return utils.isValueDefined(value) && typeof value === "object";
227
+ }
228
+ function extractMessageFromBody(data) {
229
+ if (!isRecord2(data)) {
230
+ return void 0;
231
+ }
232
+ const message = data.message;
233
+ if (typeof message === "string" && message.length > 0) {
234
+ return message;
235
+ }
236
+ const detail = data.detail;
237
+ if (typeof detail === "string" && detail.length > 0) {
238
+ return detail;
239
+ }
240
+ return void 0;
241
+ }
242
+ function isMutatingMethod(method) {
243
+ if (typeof method !== "string") {
244
+ return false;
245
+ }
246
+ return MUTATING_METHODS.includes(method.toUpperCase());
247
+ }
248
+ function handleSuccessResponse(response, emitToast, logger) {
249
+ try {
250
+ const method = response.config.method;
251
+ if (!isMutatingMethod(method)) {
252
+ return response;
253
+ }
254
+ const message = extractMessageFromBody(response.data) ?? DEFAULT_SUCCESS_MESSAGE;
255
+ emitToast(String(message), apiClientBase.ErrorSeverity.Info);
256
+ } catch (emitError) {
257
+ logger.warn(LOG_CONTEXT3, "Failed to emit success notification", emitError);
258
+ }
259
+ return response;
260
+ }
261
+ function registerResponseNormalizer(instance, emitToast, logger) {
262
+ return instance.interceptors.response.use(
263
+ (response) => handleSuccessResponse(response, emitToast, logger)
264
+ );
265
+ }
266
+
267
+ // src/registerInterceptors.ts
268
+ function registerInterceptors(instance, ports) {
269
+ const { logger, emitToast, csrf, onSessionExpiry } = ports;
270
+ registerLoggingInterceptor(instance, logger);
271
+ if (csrf) {
272
+ csrf(instance);
273
+ } else {
274
+ registerDefaultCsrfInterceptor(instance);
275
+ }
276
+ registerResponseNormalizer(instance, emitToast, logger);
277
+ if (onSessionExpiry) {
278
+ onSessionExpiry(instance);
279
+ }
280
+ registerErrorClassifier(instance, logger);
281
+ }
282
+
283
+ exports.attachCsrfHeader = attachCsrfHeader;
284
+ exports.classifyError = classifyError;
285
+ exports.createBffAxiosClient = createBffAxiosClient;
286
+ exports.handleResponseError = handleResponseError;
287
+ exports.registerDefaultCsrfInterceptor = registerDefaultCsrfInterceptor;
288
+ exports.registerErrorClassifier = registerErrorClassifier;
289
+ exports.registerInterceptors = registerInterceptors;
290
+ exports.registerLoggingInterceptor = registerLoggingInterceptor;
291
+ exports.registerResponseNormalizer = registerResponseNormalizer;
292
+ //# sourceMappingURL=index.js.map
293
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,279 @@
1
+ import axios from 'axios';
2
+ import { HttpMethod, ErrorSeverity } from '@dloizides/api-client-base';
3
+ import { isValueDefined } from '@dloizides/utils';
4
+
5
+ // src/createBffAxiosClient.ts
6
+ var BFF_DEFAULT_HEADERS = {
7
+ "Content-Type": "application/json",
8
+ "Accept": "application/json",
9
+ "X-Requested-With": "XMLHttpRequest"
10
+ };
11
+ function createBffAxiosClient(options) {
12
+ return axios.create({
13
+ timeout: options.timeoutMs,
14
+ baseURL: options.baseURL,
15
+ headers: { ...BFF_DEFAULT_HEADERS, ...options.headers ?? {} },
16
+ withCredentials: true
17
+ });
18
+ }
19
+
20
+ // src/interceptors/defaultCsrfInterceptor.ts
21
+ var CSRF_HEADER = "X-BFF-Csrf";
22
+ var CSRF_HEADER_VALUE = "1";
23
+ var STATE_CHANGING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
24
+ function isStateChanging(method) {
25
+ if (typeof method !== "string") {
26
+ return false;
27
+ }
28
+ return STATE_CHANGING_METHODS.includes(method.toUpperCase());
29
+ }
30
+ function attachCsrfHeader(config) {
31
+ if (isStateChanging(config.method)) {
32
+ config.headers.set(CSRF_HEADER, CSRF_HEADER_VALUE);
33
+ }
34
+ return config;
35
+ }
36
+ function registerDefaultCsrfInterceptor(instance) {
37
+ return instance.interceptors.request.use(attachCsrfHeader);
38
+ }
39
+ var LOG_CONTEXT = "errorClassifier";
40
+ var TIMEOUT_ERROR_CODE = "ECONNABORTED";
41
+ var NETWORK_ERROR_STATUS = 0;
42
+ function isRecord(value) {
43
+ return isValueDefined(value) && typeof value === "object";
44
+ }
45
+ function isAxiosError(value) {
46
+ if (typeof value !== "object") {
47
+ return false;
48
+ }
49
+ if (!isValueDefined(value)) {
50
+ return false;
51
+ }
52
+ return "isAxiosError" in value;
53
+ }
54
+ function extractErrorCode(data) {
55
+ if (!isRecord(data)) {
56
+ return void 0;
57
+ }
58
+ const code = data.errorCode ?? data.code ?? data.error;
59
+ if (typeof code === "string" && code.length > 0) {
60
+ return code;
61
+ }
62
+ return void 0;
63
+ }
64
+ function extractErrorMessage(data, fallback) {
65
+ if (!isRecord(data)) {
66
+ return fallback;
67
+ }
68
+ const message = data.message ?? data.detail ?? data.error ?? data.title;
69
+ if (typeof message === "string" && message.length > 0) {
70
+ return message;
71
+ }
72
+ return fallback;
73
+ }
74
+ function extractStringHeader(headers, key) {
75
+ const value = headers[key];
76
+ if (typeof value === "string" && value.length > 0) {
77
+ return value;
78
+ }
79
+ return void 0;
80
+ }
81
+ function extractRequestId(response) {
82
+ if (!isValueDefined(response)) {
83
+ return void 0;
84
+ }
85
+ const headers = response.headers;
86
+ if (!isRecord(headers)) {
87
+ return void 0;
88
+ }
89
+ return extractStringHeader(headers, "x-request-id") ?? extractStringHeader(headers, "x-correlation-id");
90
+ }
91
+ function resolveHttpMethod(method) {
92
+ if (typeof method !== "string") {
93
+ return HttpMethod.Get;
94
+ }
95
+ const upper = method.toUpperCase();
96
+ const methodMap = {
97
+ GET: HttpMethod.Get,
98
+ POST: HttpMethod.Post,
99
+ PUT: HttpMethod.Put,
100
+ PATCH: HttpMethod.Patch,
101
+ DELETE: HttpMethod.Delete
102
+ };
103
+ return methodMap[upper] ?? HttpMethod.Get;
104
+ }
105
+ function classifyError(error) {
106
+ const response = error.response;
107
+ const hasResponse = isValueDefined(response);
108
+ const status = hasResponse ? response.status : NETWORK_ERROR_STATUS;
109
+ const url = error.config?.url ?? "unknown";
110
+ const method = resolveHttpMethod(error.config?.method);
111
+ const body = hasResponse ? response.data : void 0;
112
+ const isTimeout = error.code === TIMEOUT_ERROR_CODE;
113
+ const defaultMessage = isTimeout ? "Request timed out" : "Network error";
114
+ const errorCode = extractErrorCode(body) ?? (isTimeout ? TIMEOUT_ERROR_CODE : void 0);
115
+ const message = hasResponse ? extractErrorMessage(body, error.message) : defaultMessage;
116
+ return {
117
+ status,
118
+ url,
119
+ method,
120
+ errorCode,
121
+ message,
122
+ body,
123
+ originalError: error,
124
+ timestamp: Date.now(),
125
+ requestId: extractRequestId(response)
126
+ };
127
+ }
128
+ async function handleResponseError(error, logger) {
129
+ if (!isAxiosError(error)) {
130
+ return Promise.reject(error);
131
+ }
132
+ const classified = classifyError(error);
133
+ logger.warn(LOG_CONTEXT, `HTTP ${classified.method} ${classified.url} failed`, {
134
+ status: classified.status,
135
+ errorCode: classified.errorCode,
136
+ message: classified.message
137
+ });
138
+ return Promise.reject(error);
139
+ }
140
+ function registerErrorClassifier(instance, logger) {
141
+ return instance.interceptors.response.use(
142
+ (response) => response,
143
+ (error) => handleResponseError(error, logger)
144
+ );
145
+ }
146
+ var LOG_CONTEXT2 = "http";
147
+ var REQUEST_START_HEADER = "x-request-start-time";
148
+ function isProduction() {
149
+ return process.env.NODE_ENV === "production";
150
+ }
151
+ function logRequest(config, logger) {
152
+ if (isProduction()) {
153
+ return config;
154
+ }
155
+ const method = (config.method ?? "GET").toUpperCase();
156
+ const url = config.url ?? "unknown";
157
+ logger.debug(LOG_CONTEXT2, `-> ${method} ${url}`);
158
+ config.headers.set(REQUEST_START_HEADER, String(Date.now()));
159
+ return config;
160
+ }
161
+ function calculateDurationMs(config) {
162
+ const startRaw = config.headers.get(REQUEST_START_HEADER);
163
+ if (typeof startRaw !== "string" || startRaw.length === 0) {
164
+ return void 0;
165
+ }
166
+ const start = Number(startRaw);
167
+ if (Number.isNaN(start)) {
168
+ return void 0;
169
+ }
170
+ return Date.now() - start;
171
+ }
172
+ function logResponse(response, logger) {
173
+ if (isProduction()) {
174
+ return response;
175
+ }
176
+ const method = (response.config.method ?? "GET").toUpperCase();
177
+ const url = response.config.url ?? "unknown";
178
+ const status = response.status;
179
+ const duration = calculateDurationMs(response.config);
180
+ const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : "";
181
+ logger.debug(LOG_CONTEXT2, `<- ${method} ${url} ${status}${durationSuffix}`);
182
+ return response;
183
+ }
184
+ function isAxiosLikeError(value) {
185
+ if (typeof value !== "object") {
186
+ return false;
187
+ }
188
+ if (!isValueDefined(value)) {
189
+ return false;
190
+ }
191
+ return "config" in value;
192
+ }
193
+ async function logErrorResponse(error, logger) {
194
+ if (isProduction()) {
195
+ return Promise.reject(error);
196
+ }
197
+ if (!isAxiosLikeError(error)) {
198
+ return Promise.reject(error);
199
+ }
200
+ const method = (error.config.method ?? "GET").toUpperCase();
201
+ const url = error.config.url ?? "unknown";
202
+ const status = error.response?.status ?? 0;
203
+ const duration = calculateDurationMs(error.config);
204
+ const durationSuffix = isValueDefined(duration) ? ` (${duration}ms)` : "";
205
+ logger.warn(LOG_CONTEXT2, `<- ${method} ${url} ${status}${durationSuffix}`);
206
+ return Promise.reject(error);
207
+ }
208
+ function registerLoggingInterceptor(instance, logger) {
209
+ const requestId = instance.interceptors.request.use((config) => logRequest(config, logger));
210
+ const responseId = instance.interceptors.response.use(
211
+ (response) => logResponse(response, logger),
212
+ (error) => logErrorResponse(error, logger)
213
+ );
214
+ return { request: requestId, response: responseId };
215
+ }
216
+ var MUTATING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
217
+ var DEFAULT_SUCCESS_MESSAGE = "Saved successfully.";
218
+ var LOG_CONTEXT3 = "responseNormalizer";
219
+ function isRecord2(value) {
220
+ return isValueDefined(value) && typeof value === "object";
221
+ }
222
+ function extractMessageFromBody(data) {
223
+ if (!isRecord2(data)) {
224
+ return void 0;
225
+ }
226
+ const message = data.message;
227
+ if (typeof message === "string" && message.length > 0) {
228
+ return message;
229
+ }
230
+ const detail = data.detail;
231
+ if (typeof detail === "string" && detail.length > 0) {
232
+ return detail;
233
+ }
234
+ return void 0;
235
+ }
236
+ function isMutatingMethod(method) {
237
+ if (typeof method !== "string") {
238
+ return false;
239
+ }
240
+ return MUTATING_METHODS.includes(method.toUpperCase());
241
+ }
242
+ function handleSuccessResponse(response, emitToast, logger) {
243
+ try {
244
+ const method = response.config.method;
245
+ if (!isMutatingMethod(method)) {
246
+ return response;
247
+ }
248
+ const message = extractMessageFromBody(response.data) ?? DEFAULT_SUCCESS_MESSAGE;
249
+ emitToast(String(message), ErrorSeverity.Info);
250
+ } catch (emitError) {
251
+ logger.warn(LOG_CONTEXT3, "Failed to emit success notification", emitError);
252
+ }
253
+ return response;
254
+ }
255
+ function registerResponseNormalizer(instance, emitToast, logger) {
256
+ return instance.interceptors.response.use(
257
+ (response) => handleSuccessResponse(response, emitToast, logger)
258
+ );
259
+ }
260
+
261
+ // src/registerInterceptors.ts
262
+ function registerInterceptors(instance, ports) {
263
+ const { logger, emitToast, csrf, onSessionExpiry } = ports;
264
+ registerLoggingInterceptor(instance, logger);
265
+ if (csrf) {
266
+ csrf(instance);
267
+ } else {
268
+ registerDefaultCsrfInterceptor(instance);
269
+ }
270
+ registerResponseNormalizer(instance, emitToast, logger);
271
+ if (onSessionExpiry) {
272
+ onSessionExpiry(instance);
273
+ }
274
+ registerErrorClassifier(instance, logger);
275
+ }
276
+
277
+ export { attachCsrfHeader, classifyError, createBffAxiosClient, handleResponseError, registerDefaultCsrfInterceptor, registerErrorClassifier, registerInterceptors, registerLoggingInterceptor, registerResponseNormalizer };
278
+ //# sourceMappingURL=index.mjs.map
279
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +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
+ }