@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 +19 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/index.d.mts +194 -0
- package/dist/index.d.ts +194 -0
- package/dist/index.js +293 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +279 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +86 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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":";;;;;;;;;;;AAaA,IAAM,mBAAA,GAA8C;AAAA,EAClD,cAAA,EAAgB,kBAAA;AAAA,EAChB,QAAA,EAAU,kBAAA;AAAA,EACV,kBAAA,EAAoB;AACtB,CAAA;AAOA,SAAS,qBAAqB,OAAA,EAA+C;AAC3E,EAAA,OAAOA,uBAAM,MAAA,CAAO;AAAA,IAClB,SAAS,OAAA,CAAQ,SAAA;AAAA,IACjB,SAAS,OAAA,CAAQ,OAAA;AAAA,IACjB,OAAA,EAAS,EAAE,GAAG,mBAAA,EAAqB,GAAI,OAAA,CAAQ,OAAA,IAAW,EAAC,EAAG;AAAA,IAC9D,eAAA,EAAiB;AAAA,GAClB,CAAA;AACH;;;ACbA,IAAM,WAAA,GAAc,YAAA;AACpB,IAAM,iBAAA,GAAoB,GAAA;AAG1B,IAAM,sBAAA,GAAyB,CAAC,MAAA,EAAQ,KAAA,EAAO,SAAS,QAAQ,CAAA;AAEhE,SAAS,gBAAgB,MAAA,EAAqC;AAC5D,EAAA,IAAI,OAAO,WAAW,QAAA,EAAU;AAC9B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,sBAAA,CAAuB,QAAA,CAAS,MAAA,CAAO,WAAA,EAAa,CAAA;AAC7D;AAMA,SAAS,iBAAiB,MAAA,EAAgE;AACxF,EAAA,IAAI,eAAA,CAAgB,MAAA,CAAO,MAAM,CAAA,EAAG;AAClC,IAAA,MAAA,CAAO,OAAA,CAAQ,GAAA,CAAI,WAAA,EAAa,iBAAiB,CAAA;AAAA,EACnD;AACA,EAAA,OAAO,MAAA;AACT;AAMA,SAAS,+BAA+B,QAAA,EAAiC;AACvE,EAAA,OAAO,QAAA,CAAS,YAAA,CAAa,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAA;AAC3D;AChCA,IAAM,WAAA,GAAc,iBAAA;AACpB,IAAM,kBAAA,GAAqB,cAAA;AAC3B,IAAM,oBAAA,GAAuB,CAAA;AAE7B,SAAS,SAAS,KAAA,EAAkD;AAClE,EAAA,OAAOC,oBAAA,CAAe,KAAK,CAAA,IAAK,OAAO,KAAA,KAAU,QAAA;AACnD;AAEA,SAAS,aAAa,KAAA,EAAqC;AACzD,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,CAACA,oBAAA,CAAe,KAAK,CAAA,EAAG;AAC1B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,cAAA,IAAkB,KAAA;AAC3B;AAEA,SAAS,iBAAiB,IAAA,EAAmC;AAC3D,EAAA,IAAI,CAAC,QAAA,CAAS,IAAI,CAAA,EAAG;AACnB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,IAAa,IAAA,CAAK,QAAQ,IAAA,CAAK,KAAA;AACjD,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,SAAS,CAAA,EAAG;AAC/C,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,mBAAA,CAAoB,MAAe,QAAA,EAA0B;AACpE,EAAA,IAAI,CAAC,QAAA,CAAS,IAAI,CAAA,EAAG;AACnB,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,MAAM,UAAU,IAAA,CAAK,OAAA,IAAW,KAAK,MAAA,IAAU,IAAA,CAAK,SAAS,IAAA,CAAK,KAAA;AAClE,EAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,CAAQ,SAAS,CAAA,EAAG;AACrD,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,OAAO,QAAA;AACT;AAEA,SAAS,mBAAA,CAAoB,SAAkC,GAAA,EAAiC;AAC9F,EAAA,MAAM,KAAA,GAAiB,QAAQ,GAAG,CAAA;AAClC,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,SAAS,CAAA,EAAG;AACjD,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,iBAAiB,QAAA,EAAyD;AACjF,EAAA,IAAI,CAACA,oBAAA,CAAe,QAAQ,CAAA,EAAG;AAC7B,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,UAAU,QAAA,CAAS,OAAA;AACzB,EAAA,IAAI,CAAC,QAAA,CAAS,OAAO,CAAA,EAAG;AACtB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,oBAAoB,OAAA,EAAS,cAAc,CAAA,IAAK,mBAAA,CAAoB,SAAS,kBAAkB,CAAA;AACxG;AAEA,SAAS,kBAAkB,MAAA,EAAwC;AACjE,EAAA,IAAI,OAAO,WAAW,QAAA,EAAU;AAC9B,IAAA,OAAOC,wBAAA,CAAW,GAAA;AAAA,EACpB;AACA,EAAA,MAAM,KAAA,GAAQ,OAAO,WAAA,EAAY;AACjC,EAAA,MAAM,SAAA,GAAoD;AAAA,IACxD,KAAKA,wBAAA,CAAW,GAAA;AAAA,IAChB,MAAMA,wBAAA,CAAW,IAAA;AAAA,IACjB,KAAKA,wBAAA,CAAW,GAAA;AAAA,IAChB,OAAOA,wBAAA,CAAW,KAAA;AAAA,IAClB,QAAQA,wBAAA,CAAW;AAAA,GACrB;AACA,EAAA,OAAO,SAAA,CAAU,KAAK,CAAA,IAAKA,wBAAA,CAAW,GAAA;AACxC;AAKA,SAAS,cAAc,KAAA,EAAoC;AACzD,EAAA,MAAM,WAAW,KAAA,CAAM,QAAA;AACvB,EAAA,MAAM,WAAA,GAAcD,qBAAe,QAAQ,CAAA;AAE3C,EAAA,MAAM,MAAA,GAAS,WAAA,GAAc,QAAA,CAAS,MAAA,GAAS,oBAAA;AAC/C,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,MAAA,EAAQ,GAAA,IAAO,SAAA;AACjC,EAAA,MAAM,MAAA,GAAS,iBAAA,CAAkB,KAAA,CAAM,MAAA,EAAQ,MAAM,CAAA;AACrD,EAAA,MAAM,IAAA,GAAO,WAAA,GAAc,QAAA,CAAS,IAAA,GAAO,MAAA;AAE3C,EAAA,MAAM,SAAA,GAAY,MAAM,IAAA,KAAS,kBAAA;AACjC,EAAA,MAAM,cAAA,GAAiB,YAAY,mBAAA,GAAsB,eAAA;AACzD,EAAA,MAAM,SAAA,GAAY,gBAAA,CAAiB,IAAI,CAAA,KAAM,YAAY,kBAAA,GAAqB,MAAA,CAAA;AAC9E,EAAA,MAAM,UAAU,WAAA,GAAc,mBAAA,CAAoB,IAAA,EAAM,KAAA,CAAM,OAAO,CAAA,GAAI,cAAA;AAEzE,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,GAAA;AAAA,IACA,MAAA;AAAA,IACA,SAAA;AAAA,IACA,OAAA;AAAA,IACA,IAAA;AAAA,IACA,aAAA,EAAe,KAAA;AAAA,IACf,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,IACpB,SAAA,EAAW,iBAAiB,QAAQ;AAAA,GACtC;AACF;AAKA,eAAe,mBAAA,CAAoB,OAAgB,MAAA,EAAmC;AACpF,EAAA,IAAI,CAAC,YAAA,CAAa,KAAK,CAAA,EAAG;AACxB,IAAA,OAAO,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA,EAC7B;AAEA,EAAA,MAAM,UAAA,GAAa,cAAc,KAAK,CAAA;AAEtC,EAAA,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA,KAAA,EAAQ,UAAA,CAAW,MAAM,CAAA,CAAA,EAAI,UAAA,CAAW,GAAG,CAAA,OAAA,CAAA,EAAW;AAAA,IAC7E,QAAQ,UAAA,CAAW,MAAA;AAAA,IACnB,WAAW,UAAA,CAAW,SAAA;AAAA,IACtB,SAAS,UAAA,CAAW;AAAA,GACrB,CAAA;AAED,EAAA,OAAO,OAAA,CAAQ,OAAO,KAAK,CAAA;AAC7B;AAMA,SAAS,uBAAA,CAAwB,UAAyB,MAAA,EAA2B;AACnF,EAAA,OAAO,QAAA,CAAS,aAAa,QAAA,CAAS,GAAA;AAAA,IACpC,CAAC,QAAA,KAAa,QAAA;AAAA,IACd,CAAC,KAAA,KAAmB,mBAAA,CAAoB,KAAA,EAAO,MAAM;AAAA,GACvD;AACF;AC7IA,IAAME,YAAAA,GAAc,MAAA;AACpB,IAAM,oBAAA,GAAuB,sBAAA;AAE7B,SAAS,YAAA,GAAwB;AAC/B,EAAA,OAAO,OAAA,CAAQ,IAAI,QAAA,KAAa,YAAA;AAClC;AAKA,SAAS,UAAA,CACP,QACA,MAAA,EAC4B;AAC5B,EAAA,IAAI,cAAa,EAAG;AAClB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,MAAA,GAAA,CAAU,MAAA,CAAO,MAAA,IAAU,KAAA,EAAO,WAAA,EAAY;AACpD,EAAA,MAAM,GAAA,GAAM,OAAO,GAAA,IAAO,SAAA;AAE1B,EAAA,MAAA,CAAO,MAAMA,YAAAA,EAAa,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAA;AAE/C,EAAA,MAAA,CAAO,QAAQ,GAAA,CAAI,oBAAA,EAAsB,OAAO,IAAA,CAAK,GAAA,EAAK,CAAC,CAAA;AAE3D,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,oBAAoB,MAAA,EAAwD;AACnF,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,OAAA,CAAQ,GAAA,CAAI,oBAAoB,CAAA;AACxD,EAAA,IAAI,OAAO,QAAA,KAAa,QAAA,IAAY,QAAA,CAAS,WAAW,CAAA,EAAG;AACzD,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAC7B,EAAA,IAAI,MAAA,CAAO,KAAA,CAAM,KAAK,CAAA,EAAG;AACvB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,IAAA,CAAK,KAAI,GAAI,KAAA;AACtB;AAKA,SAAS,WAAA,CAAY,UAAyB,MAAA,EAAkC;AAC9E,EAAA,IAAI,cAAa,EAAG;AAClB,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,MAAM,MAAA,GAAA,CAAU,QAAA,CAAS,MAAA,CAAO,MAAA,IAAU,OAAO,WAAA,EAAY;AAC7D,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,MAAA,CAAO,GAAA,IAAO,SAAA;AACnC,EAAA,MAAM,SAAS,QAAA,CAAS,MAAA;AACxB,EAAA,MAAM,QAAA,GAAW,mBAAA,CAAoB,QAAA,CAAS,MAAM,CAAA;AAEpD,EAAA,MAAM,iBAAiBF,oBAAAA,CAAe,QAAQ,CAAA,GAAI,CAAA,EAAA,EAAK,QAAQ,CAAA,GAAA,CAAA,GAAQ,EAAA;AACvE,EAAA,MAAA,CAAO,KAAA,CAAME,YAAAA,EAAa,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,EAAI,MAAM,CAAA,EAAG,cAAc,CAAA,CAAE,CAAA;AAE1E,EAAA,OAAO,QAAA;AACT;AAQA,SAAS,iBAAiB,KAAA,EAAyC;AACjE,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,CAACF,oBAAAA,CAAe,KAAK,CAAA,EAAG;AAC1B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,QAAA,IAAY,KAAA;AACrB;AAKA,eAAe,gBAAA,CAAiB,OAAgB,MAAA,EAAmC;AACjF,EAAA,IAAI,cAAa,EAAG;AAClB,IAAA,OAAO,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA,EAC7B;AACA,EAAA,IAAI,CAAC,gBAAA,CAAiB,KAAK,CAAA,EAAG;AAC5B,IAAA,OAAO,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA,EAC7B;AAEA,EAAA,MAAM,MAAA,GAAA,CAAU,KAAA,CAAM,MAAA,CAAO,MAAA,IAAU,OAAO,WAAA,EAAY;AAC1D,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,MAAA,CAAO,GAAA,IAAO,SAAA;AAChC,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,QAAA,EAAU,MAAA,IAAU,CAAA;AACzC,EAAA,MAAM,QAAA,GAAW,mBAAA,CAAoB,KAAA,CAAM,MAAM,CAAA;AAEjD,EAAA,MAAM,iBAAiBA,oBAAAA,CAAe,QAAQ,CAAA,GAAI,CAAA,EAAA,EAAK,QAAQ,CAAA,GAAA,CAAA,GAAQ,EAAA;AACvE,EAAA,MAAA,CAAO,IAAA,CAAKE,YAAAA,EAAa,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,EAAI,MAAM,CAAA,EAAG,cAAc,CAAA,CAAE,CAAA;AAEzE,EAAA,OAAO,OAAA,CAAQ,OAAO,KAAK,CAAA;AAC7B;AAOA,SAAS,0BAAA,CACP,UACA,MAAA,EACuC;AACvC,EAAA,MAAM,SAAA,GAAY,QAAA,CAAS,YAAA,CAAa,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,KAAW,UAAA,CAAW,MAAA,EAAQ,MAAM,CAAC,CAAA;AAC1F,EAAA,MAAM,UAAA,GAAa,QAAA,CAAS,YAAA,CAAa,QAAA,CAAS,GAAA;AAAA,IAChD,CAAC,QAAA,KAAa,WAAA,CAAY,QAAA,EAAU,MAAM,CAAA;AAAA,IAC1C,CAAC,KAAA,KAAmB,gBAAA,CAAiB,KAAA,EAAO,MAAM;AAAA,GACpD;AACA,EAAA,OAAO,EAAE,OAAA,EAAS,SAAA,EAAW,QAAA,EAAU,UAAA,EAAW;AACpD;ACjHA,IAAM,gBAAA,GAAmB,CAAC,MAAA,EAAQ,KAAA,EAAO,SAAS,QAAQ,CAAA;AAC1D,IAAM,uBAAA,GAA0B,qBAAA;AAChC,IAAMA,YAAAA,GAAc,oBAAA;AAEpB,SAASC,UAAS,KAAA,EAAkD;AAClE,EAAA,OAAOH,oBAAAA,CAAe,KAAK,CAAA,IAAK,OAAO,KAAA,KAAU,QAAA;AACnD;AAEA,SAAS,uBAAuB,IAAA,EAAmC;AACjE,EAAA,IAAI,CAACG,SAAAA,CAAS,IAAI,CAAA,EAAG;AACnB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,UAAU,IAAA,CAAK,OAAA;AACrB,EAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,CAAQ,SAAS,CAAA,EAAG;AACrD,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,MAAM,SAAS,IAAA,CAAK,MAAA;AACpB,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,SAAS,CAAA,EAAG;AACnD,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,iBAAiB,MAAA,EAAqC;AAC7D,EAAA,IAAI,OAAO,WAAW,QAAA,EAAU;AAC9B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,gBAAA,CAAiB,QAAA,CAAS,MAAA,CAAO,WAAA,EAAa,CAAA;AACvD;AAUA,SAAS,qBAAA,CACP,QAAA,EACA,SAAA,EACA,MAAA,EACe;AACf,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,SAAS,MAAA,CAAO,MAAA;AAC/B,IAAA,IAAI,CAAC,gBAAA,CAAiB,MAAM,CAAA,EAAG;AAC7B,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,MAAM,OAAA,GAAU,sBAAA,CAAuB,QAAA,CAAS,IAAI,CAAA,IAAK,uBAAA;AACzD,IAAA,SAAA,CAAU,MAAA,CAAO,OAAO,CAAA,EAAGC,2BAAA,CAAc,IAAI,CAAA;AAAA,EAC/C,SAAS,SAAA,EAAW;AAClB,IAAA,MAAA,CAAO,IAAA,CAAKF,YAAAA,EAAa,qCAAA,EAAuC,SAAS,CAAA;AAAA,EAC3E;AAEA,EAAA,OAAO,QAAA;AACT;AAMA,SAAS,0BAAA,CACP,QAAA,EACA,SAAA,EACA,MAAA,EACQ;AACR,EAAA,OAAO,QAAA,CAAS,aAAa,QAAA,CAAS,GAAA;AAAA,IAAI,CAAC,QAAA,KACzC,qBAAA,CAAsB,QAAA,EAAU,WAAW,MAAM;AAAA,GACnD;AACF;;;ACtDA,SAAS,oBAAA,CAAqB,UAAyB,KAAA,EAAwC;AAC7F,EAAA,MAAM,EAAE,MAAA,EAAQ,SAAA,EAAW,IAAA,EAAM,iBAAgB,GAAI,KAAA;AAGrD,EAAA,0BAAA,CAA2B,UAAU,MAAM,CAAA;AAC3C,EAAA,IAAI,IAAA,EAAM;AACR,IAAA,IAAA,CAAK,QAAQ,CAAA;AAAA,EACf,CAAA,MAAO;AACL,IAAA,8BAAA,CAA+B,QAAQ,CAAA;AAAA,EACzC;AAGA,EAAA,0BAAA,CAA2B,QAAA,EAAU,WAAW,MAAM,CAAA;AACtD,EAAA,IAAI,eAAA,EAAiB;AACnB,IAAA,eAAA,CAAgB,QAAQ,CAAA;AAAA,EAC1B;AACA,EAAA,uBAAA,CAAwB,UAAU,MAAM,CAAA;AAC1C","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
|
+
}
|