@opensourcekd/ng-common-libs 2.0.10 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/index.cjs +369 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +215 -2
- package/dist/index.mjs +369 -10
- package/dist/index.mjs.map +1 -1
- package/dist/styles/tokens.css +311 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
- **AuthService**: Complete Auth0 authentication with token management (Pure TypeScript - no Angular dependency)
|
|
25
25
|
- **EventBus**: Framework-agnostic event bus using RxJS
|
|
26
26
|
- **Logger**: OpenTelemetry-compliant structured logging with observable log streams
|
|
27
|
+
- **BearerTokenInterceptor**: Singleton fetch interceptor that attaches bearer tokens to API requests
|
|
28
|
+
- **CSS Tokens**: Shared design tokens (colors, typography, spacing, shadows, and more) as CSS custom properties
|
|
27
29
|
- **Works with any framework**: Angular, React, Vue, Svelte, vanilla JS
|
|
28
30
|
- **Module Federation Support**: Singleton pattern works across shell + MFEs
|
|
29
31
|
- **Zero Framework Lock-in**: Core services are pure TypeScript classes
|
|
@@ -92,6 +94,53 @@ All documentation is in the `/docs` folder:
|
|
|
92
94
|
- **[MICROAPP_SETUP_REQUIRED.md](./docs/MICROAPP_SETUP_REQUIRED.md)** - Microapp setup
|
|
93
95
|
- **[COPILOT_EFFICIENCY.md](./docs/COPILOT_EFFICIENCY.md)** - ⚡ How to use GitHub Copilot efficiently
|
|
94
96
|
|
|
97
|
+
## 🎨 CSS Design Tokens
|
|
98
|
+
|
|
99
|
+
Shared CSS custom properties for consistent styling across all micro-frontends.
|
|
100
|
+
|
|
101
|
+
### Usage
|
|
102
|
+
|
|
103
|
+
In your global stylesheet (`styles.css` / `styles.scss`):
|
|
104
|
+
|
|
105
|
+
```css
|
|
106
|
+
@import '@opensourcekd/ng-common-libs/styles/tokens.css';
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Available Token Categories
|
|
110
|
+
|
|
111
|
+
| Category | Example Tokens |
|
|
112
|
+
|---|---|
|
|
113
|
+
| **Colors** | `--color-primary-500`, `--color-success-600`, `--color-error-500` |
|
|
114
|
+
| **Semantic Colors** | `--color-brand`, `--color-text-primary`, `--color-bg-surface` |
|
|
115
|
+
| **Typography** | `--font-size-md`, `--font-weight-bold`, `--line-height-normal` |
|
|
116
|
+
| **Spacing** | `--space-4` (16px), `--space-8` (32px), `--space-16` (64px) |
|
|
117
|
+
| **Border Radius** | `--radius-sm`, `--radius-lg`, `--radius-full` |
|
|
118
|
+
| **Shadows** | `--shadow-sm`, `--shadow-md`, `--shadow-xl`, `--shadow-focus` |
|
|
119
|
+
| **Z-index** | `--z-index-modal`, `--z-index-toast`, `--z-index-tooltip` |
|
|
120
|
+
| **Transitions** | `--transition-fast`, `--transition-normal`, `--duration-fast` |
|
|
121
|
+
| **Opacity** | `--opacity-50`, `--opacity-75`, `--opacity-100` |
|
|
122
|
+
| **Breakpoints** | `--breakpoint-sm` (640px), `--breakpoint-lg` (1024px) |
|
|
123
|
+
|
|
124
|
+
### Example
|
|
125
|
+
|
|
126
|
+
```css
|
|
127
|
+
.my-button {
|
|
128
|
+
background-color: var(--color-brand);
|
|
129
|
+
color: var(--color-text-on-brand);
|
|
130
|
+
padding: var(--space-2) var(--space-4);
|
|
131
|
+
border-radius: var(--radius-md);
|
|
132
|
+
font-size: var(--font-size-sm);
|
|
133
|
+
font-weight: var(--font-weight-semibold);
|
|
134
|
+
box-shadow: var(--shadow-sm);
|
|
135
|
+
transition: var(--transition-normal);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.my-button:hover {
|
|
139
|
+
background-color: var(--color-brand-hover);
|
|
140
|
+
box-shadow: var(--shadow-md);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
95
144
|
## 📝 What's Exported
|
|
96
145
|
|
|
97
146
|
Everything you need from one import:
|
|
@@ -102,6 +151,7 @@ import {
|
|
|
102
151
|
AuthService,
|
|
103
152
|
EventBus,
|
|
104
153
|
Logger,
|
|
154
|
+
BearerTokenInterceptor,
|
|
105
155
|
|
|
106
156
|
// Helper Functions
|
|
107
157
|
configureAuth0,
|
|
@@ -134,6 +184,7 @@ import {
|
|
|
134
184
|
LogAttributes,
|
|
135
185
|
LogSeverity,
|
|
136
186
|
LoggerOptions,
|
|
187
|
+
BearerTokenInterceptorConfig,
|
|
137
188
|
} from '@opensourcekd/ng-common-libs';
|
|
138
189
|
```
|
|
139
190
|
|
package/dist/index.cjs
CHANGED
|
@@ -448,6 +448,7 @@ function e(e,t){var n={};for(var o in e)Object.prototype.hasOwnProperty.call(e,o
|
|
|
448
448
|
class AuthService {
|
|
449
449
|
auth0Client = null;
|
|
450
450
|
initializationPromise = null;
|
|
451
|
+
/** Remains false if callback processing fails, allowing the callback to be retried. */
|
|
451
452
|
callbackHandled = false;
|
|
452
453
|
callbackPromise = null;
|
|
453
454
|
userSubject;
|
|
@@ -482,6 +483,14 @@ class AuthService {
|
|
|
482
483
|
const existingUserInfo = this.getUserInfoFromStorage();
|
|
483
484
|
this.userSubject = new rxjs.BehaviorSubject(existingUserInfo);
|
|
484
485
|
this.user$ = this.userSubject.asObservable();
|
|
486
|
+
// Eagerly begin Auth0 client initialization so logs are visible immediately.
|
|
487
|
+
// The .catch() only suppresses an unhandled-rejection warning; the rejected
|
|
488
|
+
// promise is stored in initializationPromise and re-throws when awaited by
|
|
489
|
+
// ensureInitialized() or ensureAuth0Client(), surfacing the error to callers.
|
|
490
|
+
this.initializationPromise = this.initializeAuth0();
|
|
491
|
+
this.initializationPromise.catch(error => {
|
|
492
|
+
console.error('[AuthService] Failed to initialize Auth0 client:', error);
|
|
493
|
+
});
|
|
485
494
|
}
|
|
486
495
|
/**
|
|
487
496
|
* Get the identifier of this AuthService instance
|
|
@@ -521,6 +530,25 @@ class AuthService {
|
|
|
521
530
|
console.log('[AuthService] Auth0 client initialized successfully');
|
|
522
531
|
this.emitAuthEvent('init', null);
|
|
523
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Ensure the Auth0 client instance is created, without triggering callback detection.
|
|
535
|
+
* Used internally by {@link handleCallback} to avoid a circular async dependency:
|
|
536
|
+
* `ensureInitialized` → `checkAndHandleCallback` → `handleCallback` → `ensureInitialized`.
|
|
537
|
+
* @throws {Error} When the Auth0 client fails to initialize
|
|
538
|
+
*/
|
|
539
|
+
async ensureAuth0Client() {
|
|
540
|
+
if (this.auth0Client)
|
|
541
|
+
return;
|
|
542
|
+
if (this.initializationPromise) {
|
|
543
|
+
await this.initializationPromise;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
this.initializationPromise = this.initializeAuth0();
|
|
547
|
+
await this.initializationPromise;
|
|
548
|
+
if (!this.auth0Client) {
|
|
549
|
+
throw new Error('[AuthService] Auth0 client failed to initialize');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
524
552
|
/**
|
|
525
553
|
* Ensure the Auth0 client is initialized before use
|
|
526
554
|
* Lazy-initializes on the first call and auto-handles OAuth callbacks when detected
|
|
@@ -549,6 +577,10 @@ class AuthService {
|
|
|
549
577
|
* The Auth0 SDK's `handleRedirectCallback` validates the `state` parameter
|
|
550
578
|
* to prevent CSRF attacks. This method only detects presence of callback
|
|
551
579
|
* params before delegating securely to the SDK.
|
|
580
|
+
*
|
|
581
|
+
* Uses an async IIFE to store the in-flight promise for concurrency deduplication
|
|
582
|
+
* (concurrent callers await the same promise) while still using async/await
|
|
583
|
+
* internally instead of `.then()/.catch()` chains.
|
|
552
584
|
*/
|
|
553
585
|
async checkAndHandleCallback() {
|
|
554
586
|
if (this.callbackHandled || typeof window === 'undefined')
|
|
@@ -558,15 +590,22 @@ class AuthService {
|
|
|
558
590
|
return;
|
|
559
591
|
}
|
|
560
592
|
const urlParams = new URLSearchParams(window.location.search);
|
|
561
|
-
if (urlParams.has('code')
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
593
|
+
if (!urlParams.has('code') || !urlParams.has('state'))
|
|
594
|
+
return;
|
|
595
|
+
console.log('[AuthService] Auth0 callback detected in URL, processing...');
|
|
596
|
+
this.emitAuthEvent('callback_detected', null);
|
|
597
|
+
// Store the promise before the first await so concurrent callers see it
|
|
598
|
+
// and deduplicate by awaiting the same in-flight promise.
|
|
599
|
+
this.callbackPromise = (async () => {
|
|
600
|
+
await this.handleCallback();
|
|
601
|
+
this.callbackHandled = true;
|
|
602
|
+
})();
|
|
603
|
+
try {
|
|
568
604
|
await this.callbackPromise;
|
|
569
605
|
}
|
|
606
|
+
finally {
|
|
607
|
+
this.callbackPromise = null;
|
|
608
|
+
}
|
|
570
609
|
}
|
|
571
610
|
/**
|
|
572
611
|
* Redirect the user to Auth0 Universal Login
|
|
@@ -603,11 +642,15 @@ class AuthService {
|
|
|
603
642
|
/**
|
|
604
643
|
* Handle the OAuth2 redirect callback after successful authorization
|
|
605
644
|
* Stores the user info and access token, then cleans up the callback URL
|
|
645
|
+
*
|
|
646
|
+
* Uses {@link ensureAuth0Client} (not {@link ensureInitialized}) to avoid a circular
|
|
647
|
+
* async dependency: `ensureInitialized` → `checkAndHandleCallback` → `handleCallback`
|
|
648
|
+
* → `ensureInitialized`. Only the Auth0 client instance is needed here.
|
|
606
649
|
* @returns {@link CallbackResult} with `success` flag and optional `appState`
|
|
607
650
|
*/
|
|
608
651
|
async handleCallback() {
|
|
609
652
|
try {
|
|
610
|
-
await this.
|
|
653
|
+
await this.ensureAuth0Client();
|
|
611
654
|
console.log('[AuthService] Processing Auth0 redirect callback...');
|
|
612
655
|
const result = await this.auth0Client.handleRedirectCallback();
|
|
613
656
|
const user = await this.auth0Client.getUser();
|
|
@@ -631,6 +674,11 @@ class AuthService {
|
|
|
631
674
|
}
|
|
632
675
|
/**
|
|
633
676
|
* Log the user out, clear all stored auth data, and redirect to the logout URI
|
|
677
|
+
*
|
|
678
|
+
* Uses {@link ensureAuth0Client} (not {@link ensureInitialized}) to avoid triggering
|
|
679
|
+
* callback detection during logout. Calling `ensureInitialized` here would invoke
|
|
680
|
+
* `checkAndHandleCallback`, which re-authenticates the user if callback URL params
|
|
681
|
+
* are present, causing them to be sent back to Auth0 instead of the logout URI.
|
|
634
682
|
*/
|
|
635
683
|
async logout() {
|
|
636
684
|
console.log('[AuthService] User logging out...');
|
|
@@ -640,7 +688,7 @@ class AuthService {
|
|
|
640
688
|
this.userSubject.next(null);
|
|
641
689
|
this.emitAuthEvent('logout', null);
|
|
642
690
|
try {
|
|
643
|
-
await this.
|
|
691
|
+
await this.ensureAuth0Client();
|
|
644
692
|
await this.auth0Client.logout({
|
|
645
693
|
logoutParams: { returnTo: this.config.logoutUri }
|
|
646
694
|
});
|
|
@@ -1025,9 +1073,321 @@ class Logger {
|
|
|
1025
1073
|
}
|
|
1026
1074
|
}
|
|
1027
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* BearerTokenInterceptor — HTTP interceptor for bearer token injection.
|
|
1078
|
+
* Patches both `window.fetch` and `XMLHttpRequest` (for Angular's default
|
|
1079
|
+
* HttpClient backend), pure TypeScript, framework-agnostic, truly singleton
|
|
1080
|
+
* per identifier.
|
|
1081
|
+
*/
|
|
1082
|
+
/**
|
|
1083
|
+
* BearerTokenInterceptor
|
|
1084
|
+
*
|
|
1085
|
+
* A per-identifier singleton that patches the global `window.fetch` to
|
|
1086
|
+
* automatically attach an `Authorization: Bearer <token>` header to every
|
|
1087
|
+
* request whose URL begins with the configured API base URL.
|
|
1088
|
+
*
|
|
1089
|
+
* **Singleton behaviour**: the first call to {@link BearerTokenInterceptor.getInstance}
|
|
1090
|
+
* for a given `id` creates the instance; subsequent calls with the same `id`
|
|
1091
|
+
* return the previously created instance, regardless of the `config` argument.
|
|
1092
|
+
*
|
|
1093
|
+
* Designed for Module Federation micro-frontend environments where both the
|
|
1094
|
+
* fetch wrapper and the XHR prototype must be shared across the shell and all
|
|
1095
|
+
* remote applications.
|
|
1096
|
+
*
|
|
1097
|
+
* @example
|
|
1098
|
+
* ```typescript
|
|
1099
|
+
* import { BearerTokenInterceptor, APP_CONFIG } from '@opensourcekd/ng-common-libs';
|
|
1100
|
+
*
|
|
1101
|
+
* // In the shell app — creates the instance
|
|
1102
|
+
* const interceptor = BearerTokenInterceptor.getInstance('shell', {
|
|
1103
|
+
* apiUrl: APP_CONFIG.apiUrl,
|
|
1104
|
+
* getToken: () => authService.getTokenSync(),
|
|
1105
|
+
* });
|
|
1106
|
+
* interceptor.activate();
|
|
1107
|
+
*
|
|
1108
|
+
* // In any MFE — same instance is returned
|
|
1109
|
+
* const same = BearerTokenInterceptor.getInstance('shell', { getToken: () => null });
|
|
1110
|
+
* console.log(same === interceptor); // true
|
|
1111
|
+
* ```
|
|
1112
|
+
*/
|
|
1113
|
+
class BearerTokenInterceptor {
|
|
1114
|
+
static instances = new Map();
|
|
1115
|
+
static originalFetch = null;
|
|
1116
|
+
static originalXhrOpen = null;
|
|
1117
|
+
static originalXhrSend = null;
|
|
1118
|
+
id;
|
|
1119
|
+
apiUrl;
|
|
1120
|
+
tokenFn;
|
|
1121
|
+
active = false;
|
|
1122
|
+
constructor(id, config) {
|
|
1123
|
+
this.id = id;
|
|
1124
|
+
this.apiUrl = config.apiUrl ?? APP_CONFIG.apiUrl;
|
|
1125
|
+
this.tokenFn = config.getToken;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Get or create the singleton interceptor for the given identifier.
|
|
1129
|
+
*
|
|
1130
|
+
* The first invocation with a given `id` creates the instance using the
|
|
1131
|
+
* supplied `config`. Subsequent calls with the same `id` return the existing
|
|
1132
|
+
* instance — the `config` argument is ignored on subsequent calls.
|
|
1133
|
+
*
|
|
1134
|
+
* @param id - Unique identifier for this interceptor (e.g. `'shell'`, `'mfe-orders'`)
|
|
1135
|
+
* @param config - Configuration used only when creating a new instance
|
|
1136
|
+
* @returns The singleton {@link BearerTokenInterceptor} for the given `id`
|
|
1137
|
+
*
|
|
1138
|
+
* @example
|
|
1139
|
+
* ```typescript
|
|
1140
|
+
* const interceptor = BearerTokenInterceptor.getInstance('shell', {
|
|
1141
|
+
* apiUrl: 'https://api.example.com',
|
|
1142
|
+
* getToken: () => authService.getTokenSync(),
|
|
1143
|
+
* });
|
|
1144
|
+
* ```
|
|
1145
|
+
*/
|
|
1146
|
+
static getInstance(id, config) {
|
|
1147
|
+
if (BearerTokenInterceptor.instances.has(id)) {
|
|
1148
|
+
return BearerTokenInterceptor.instances.get(id);
|
|
1149
|
+
}
|
|
1150
|
+
const instance = new BearerTokenInterceptor(id, config);
|
|
1151
|
+
BearerTokenInterceptor.instances.set(id, instance);
|
|
1152
|
+
return instance;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Get the identifier of this interceptor instance.
|
|
1156
|
+
* @returns The `id` string supplied when the instance was created
|
|
1157
|
+
*/
|
|
1158
|
+
getId() {
|
|
1159
|
+
return this.id;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Get the API base URL that this interceptor matches against.
|
|
1163
|
+
* @returns The configured API URL string
|
|
1164
|
+
*/
|
|
1165
|
+
getApiUrl() {
|
|
1166
|
+
return this.apiUrl;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Check whether this interceptor is currently active.
|
|
1170
|
+
* @returns `true` if the interceptor has been activated and not yet deactivated
|
|
1171
|
+
*/
|
|
1172
|
+
isActive() {
|
|
1173
|
+
return this.active;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Activate the interceptor.
|
|
1177
|
+
*
|
|
1178
|
+
* Patches `window.fetch` and `XMLHttpRequest` once (on the first active
|
|
1179
|
+
* interceptor) so that matching requests receive the bearer token.
|
|
1180
|
+
* Subsequent calls are no-ops. No-op in non-browser environments.
|
|
1181
|
+
*/
|
|
1182
|
+
activate() {
|
|
1183
|
+
if (this.active || typeof window === 'undefined')
|
|
1184
|
+
return;
|
|
1185
|
+
this.active = true;
|
|
1186
|
+
BearerTokenInterceptor.patchFetch();
|
|
1187
|
+
BearerTokenInterceptor.patchXhr();
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Deactivate the interceptor.
|
|
1191
|
+
*
|
|
1192
|
+
* Stops attaching the bearer token for this interceptor's URL pattern.
|
|
1193
|
+
* Both the global fetch wrapper and the XHR prototype patches are removed
|
|
1194
|
+
* automatically once all registered interceptors have been deactivated.
|
|
1195
|
+
* No-op when already inactive.
|
|
1196
|
+
*/
|
|
1197
|
+
deactivate() {
|
|
1198
|
+
if (!this.active)
|
|
1199
|
+
return;
|
|
1200
|
+
this.active = false;
|
|
1201
|
+
if (typeof window !== 'undefined') {
|
|
1202
|
+
BearerTokenInterceptor.maybeRestoreFetch();
|
|
1203
|
+
BearerTokenInterceptor.maybeRestoreXhr();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Return the effective bearer token for a given request URL, or `null` if
|
|
1208
|
+
* this interceptor should not modify the request.
|
|
1209
|
+
*
|
|
1210
|
+
* Returns `null` when the interceptor is inactive, the URL does not begin
|
|
1211
|
+
* with the configured API base URL, or the token getter returns `null`.
|
|
1212
|
+
*
|
|
1213
|
+
* @param url - The absolute URL of the outgoing request
|
|
1214
|
+
* @returns Bearer token string or `null`
|
|
1215
|
+
*/
|
|
1216
|
+
getEffectiveToken(url) {
|
|
1217
|
+
if (!this.active || !url.startsWith(this.apiUrl))
|
|
1218
|
+
return null;
|
|
1219
|
+
return this.tokenFn();
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Extract headers from a `RequestInit` object into a plain key-value map.
|
|
1223
|
+
* Handles `Headers` instances, `[key, value]` arrays, and plain objects.
|
|
1224
|
+
*
|
|
1225
|
+
* @param init - Optional `RequestInit` whose headers to extract
|
|
1226
|
+
* @returns A plain `Record<string, string>` copy of the headers
|
|
1227
|
+
*/
|
|
1228
|
+
static extractHeaders(init) {
|
|
1229
|
+
if (!init?.headers)
|
|
1230
|
+
return {};
|
|
1231
|
+
if (init.headers instanceof Headers) {
|
|
1232
|
+
const h = {};
|
|
1233
|
+
init.headers.forEach((v, k) => { h[k] = v; });
|
|
1234
|
+
return h;
|
|
1235
|
+
}
|
|
1236
|
+
if (Array.isArray(init.headers)) {
|
|
1237
|
+
return Object.fromEntries(init.headers);
|
|
1238
|
+
}
|
|
1239
|
+
return { ...init.headers };
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Find the bearer token for a given URL by consulting all active interceptors.
|
|
1243
|
+
* Uses first-match semantics to avoid ambiguity when multiple interceptors
|
|
1244
|
+
* share the same API URL prefix.
|
|
1245
|
+
*
|
|
1246
|
+
* @param url - Absolute URL of the outgoing request
|
|
1247
|
+
* @returns The first matching bearer token string, or `null`
|
|
1248
|
+
*/
|
|
1249
|
+
static findToken(url) {
|
|
1250
|
+
return ([...BearerTokenInterceptor.instances.values()]
|
|
1251
|
+
.map((i) => i.getEffectiveToken(url))
|
|
1252
|
+
.find((t) => t !== null) ?? null);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Apply the `Authorization: Bearer` header from the first active interceptor
|
|
1256
|
+
* that matches the given URL. Uses first-match semantics to avoid ambiguity
|
|
1257
|
+
* when multiple interceptors share the same API URL prefix.
|
|
1258
|
+
*
|
|
1259
|
+
* @param url - Absolute URL of the outgoing fetch request
|
|
1260
|
+
* @param init - Original `RequestInit` options
|
|
1261
|
+
* @returns Modified `RequestInit` with the `Authorization` header added when applicable
|
|
1262
|
+
*/
|
|
1263
|
+
static applyBearerToken(url, init) {
|
|
1264
|
+
const headers = BearerTokenInterceptor.extractHeaders(init);
|
|
1265
|
+
const token = BearerTokenInterceptor.findToken(url);
|
|
1266
|
+
if (token) {
|
|
1267
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
1268
|
+
}
|
|
1269
|
+
return { ...init, headers };
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Install the global fetch wrapper exactly once.
|
|
1273
|
+
* The wrapper delegates to {@link applyBearerToken} for every outgoing request.
|
|
1274
|
+
* No-op when already patched or in a non-browser environment.
|
|
1275
|
+
*/
|
|
1276
|
+
static patchFetch() {
|
|
1277
|
+
if (BearerTokenInterceptor.originalFetch !== null || typeof window === 'undefined')
|
|
1278
|
+
return;
|
|
1279
|
+
// Store without bind so the original reference is preserved for identity checks on restore.
|
|
1280
|
+
BearerTokenInterceptor.originalFetch = window.fetch;
|
|
1281
|
+
window.fetch = (input, init) => {
|
|
1282
|
+
// Extract the URL string from all three possible input types:
|
|
1283
|
+
// • Request-like objects (e.g. the Fetch API Request) have a `.url` string property.
|
|
1284
|
+
// • URL objects do NOT have `.url`; String(urlObj) yields the full href (e.g. "https://…").
|
|
1285
|
+
// • Plain strings pass through String() unchanged.
|
|
1286
|
+
const hasUrl = typeof input.url === 'string';
|
|
1287
|
+
const url = hasUrl ? input.url : String(input);
|
|
1288
|
+
return BearerTokenInterceptor.originalFetch(input, BearerTokenInterceptor.applyBearerToken(url, init));
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Restore the original `window.fetch` if no interceptors remain active.
|
|
1293
|
+
* No-op when the fetch has not been patched or some interceptors are still active.
|
|
1294
|
+
*/
|
|
1295
|
+
static maybeRestoreFetch() {
|
|
1296
|
+
if (BearerTokenInterceptor.originalFetch === null)
|
|
1297
|
+
return;
|
|
1298
|
+
const anyActive = [...BearerTokenInterceptor.instances.values()].some((i) => i.active);
|
|
1299
|
+
if (anyActive)
|
|
1300
|
+
return;
|
|
1301
|
+
window.fetch = BearerTokenInterceptor.originalFetch;
|
|
1302
|
+
BearerTokenInterceptor.originalFetch = null;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Install a global `XMLHttpRequest` prototype patch exactly once.
|
|
1306
|
+
*
|
|
1307
|
+
* Overrides `XMLHttpRequest.prototype.open` to capture the request URL on
|
|
1308
|
+
* the XHR instance, and `XMLHttpRequest.prototype.send` to inject the
|
|
1309
|
+
* `Authorization: Bearer` header (via `setRequestHeader`) before dispatching
|
|
1310
|
+
* the request. Supports Angular's default XHR-based `HttpClient` backend.
|
|
1311
|
+
*
|
|
1312
|
+
* No-op when already patched or in a non-browser environment.
|
|
1313
|
+
*/
|
|
1314
|
+
static patchXhr() {
|
|
1315
|
+
if (BearerTokenInterceptor.originalXhrOpen !== null ||
|
|
1316
|
+
typeof window === 'undefined' ||
|
|
1317
|
+
typeof XMLHttpRequest === 'undefined')
|
|
1318
|
+
return;
|
|
1319
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
1320
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
1321
|
+
BearerTokenInterceptor.originalXhrOpen = originalOpen;
|
|
1322
|
+
BearerTokenInterceptor.originalXhrSend = originalSend;
|
|
1323
|
+
// Capture the request URL so send() can match it against registered interceptors.
|
|
1324
|
+
// The `async` parameter is made optional to faithfully match both XHR `open()` overloads.
|
|
1325
|
+
XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
|
|
1326
|
+
const urlStr = url instanceof URL ? url.href : url;
|
|
1327
|
+
try {
|
|
1328
|
+
this._interceptorUrl = new URL(urlStr, window.location.href).href;
|
|
1329
|
+
}
|
|
1330
|
+
catch {
|
|
1331
|
+
this._interceptorUrl = urlStr;
|
|
1332
|
+
}
|
|
1333
|
+
// `async` defaults to true per the XHR spec when not supplied by the caller.
|
|
1334
|
+
originalOpen.call(this, method, url, async ?? true, username, password);
|
|
1335
|
+
};
|
|
1336
|
+
// Inject the Authorization header before dispatching the request.
|
|
1337
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
1338
|
+
const url = this._interceptorUrl;
|
|
1339
|
+
if (url) {
|
|
1340
|
+
const token = BearerTokenInterceptor.findToken(url);
|
|
1341
|
+
if (token) {
|
|
1342
|
+
this.setRequestHeader('Authorization', `Bearer ${token}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
originalSend.call(this, body);
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Restore `XMLHttpRequest.prototype.open` and `.send` to their original
|
|
1350
|
+
* values if no interceptors remain active.
|
|
1351
|
+
* No-op when XHR has not been patched or some interceptors are still active.
|
|
1352
|
+
*/
|
|
1353
|
+
static maybeRestoreXhr() {
|
|
1354
|
+
if (BearerTokenInterceptor.originalXhrOpen === null)
|
|
1355
|
+
return;
|
|
1356
|
+
const anyActive = [...BearerTokenInterceptor.instances.values()].some((i) => i.active);
|
|
1357
|
+
if (anyActive)
|
|
1358
|
+
return;
|
|
1359
|
+
XMLHttpRequest.prototype.open = BearerTokenInterceptor.originalXhrOpen;
|
|
1360
|
+
XMLHttpRequest.prototype.send = BearerTokenInterceptor.originalXhrSend;
|
|
1361
|
+
BearerTokenInterceptor.originalXhrOpen = null;
|
|
1362
|
+
BearerTokenInterceptor.originalXhrSend = null;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Remove all registered instances and restore `window.fetch` and
|
|
1366
|
+
* `XMLHttpRequest` prototype methods to their original values.
|
|
1367
|
+
* Intended for use in test teardown only.
|
|
1368
|
+
* @internal
|
|
1369
|
+
*/
|
|
1370
|
+
static _reset() {
|
|
1371
|
+
if (typeof window !== 'undefined') {
|
|
1372
|
+
if (BearerTokenInterceptor.originalFetch !== null) {
|
|
1373
|
+
window.fetch = BearerTokenInterceptor.originalFetch;
|
|
1374
|
+
}
|
|
1375
|
+
if (BearerTokenInterceptor.originalXhrOpen !== null && typeof XMLHttpRequest !== 'undefined') {
|
|
1376
|
+
XMLHttpRequest.prototype.open = BearerTokenInterceptor.originalXhrOpen;
|
|
1377
|
+
XMLHttpRequest.prototype.send = BearerTokenInterceptor.originalXhrSend;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
BearerTokenInterceptor.originalFetch = null;
|
|
1381
|
+
BearerTokenInterceptor.originalXhrOpen = null;
|
|
1382
|
+
BearerTokenInterceptor.originalXhrSend = null;
|
|
1383
|
+
BearerTokenInterceptor.instances.clear();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1028
1387
|
exports.APP_CONFIG = APP_CONFIG;
|
|
1029
1388
|
exports.AUTH0_CONFIG = AUTH0_CONFIG;
|
|
1030
1389
|
exports.AuthService = AuthService;
|
|
1390
|
+
exports.BearerTokenInterceptor = BearerTokenInterceptor;
|
|
1031
1391
|
exports.EventBus = EventBus;
|
|
1032
1392
|
exports.Logger = Logger;
|
|
1033
1393
|
exports.STANDARD_JWT_CLAIMS = STANDARD_JWT_CLAIMS;
|