@nauth-toolkit/client 0.1.3
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/LICENSE +90 -0
- package/README.md +92 -0
- package/dist/angular/index.cjs +1958 -0
- package/dist/angular/index.cjs.map +1 -0
- package/dist/angular/index.d.mts +623 -0
- package/dist/angular/index.d.ts +623 -0
- package/dist/angular/index.mjs +1926 -0
- package/dist/angular/index.mjs.map +1 -0
- package/dist/index.cjs +1473 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +638 -0
- package/dist/index.d.ts +638 -0
- package/dist/index.mjs +1433 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,1926 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
6
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
7
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
8
|
+
if (decorator = decorators[i])
|
|
9
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
10
|
+
if (kind && result) __defProp(target, key, result);
|
|
11
|
+
return result;
|
|
12
|
+
};
|
|
13
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
14
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
15
|
+
|
|
16
|
+
// src/angular/tokens.ts
|
|
17
|
+
import { InjectionToken } from "@angular/core";
|
|
18
|
+
var NAUTH_CLIENT_CONFIG = new InjectionToken("NAUTH_CLIENT_CONFIG");
|
|
19
|
+
|
|
20
|
+
// src/angular/auth.service.ts
|
|
21
|
+
import { Inject, Injectable as Injectable2, Optional, inject as inject2 } from "@angular/core";
|
|
22
|
+
import { BehaviorSubject, from, Subject } from "rxjs";
|
|
23
|
+
import { filter } from "rxjs/operators";
|
|
24
|
+
|
|
25
|
+
// src/angular/http-adapter.ts
|
|
26
|
+
import { Injectable, inject } from "@angular/core";
|
|
27
|
+
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
|
|
28
|
+
import { firstValueFrom } from "rxjs";
|
|
29
|
+
|
|
30
|
+
// src/core/errors.ts
|
|
31
|
+
var _NAuthClientError = class _NAuthClientError extends Error {
|
|
32
|
+
/**
|
|
33
|
+
* Create a new client error.
|
|
34
|
+
*
|
|
35
|
+
* @param code - Error code from NAuthErrorCode enum
|
|
36
|
+
* @param message - Human-readable error message
|
|
37
|
+
* @param options - Optional metadata including details, statusCode, timestamp, and network error flag
|
|
38
|
+
*/
|
|
39
|
+
constructor(code, message, options) {
|
|
40
|
+
super(message);
|
|
41
|
+
__publicField(this, "code");
|
|
42
|
+
__publicField(this, "details");
|
|
43
|
+
__publicField(this, "statusCode");
|
|
44
|
+
__publicField(this, "timestamp");
|
|
45
|
+
__publicField(this, "isNetworkError");
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.details = options?.details;
|
|
48
|
+
this.statusCode = options?.statusCode;
|
|
49
|
+
this.timestamp = options?.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
50
|
+
this.isNetworkError = options?.isNetworkError ?? false;
|
|
51
|
+
this.name = "NAuthClientError";
|
|
52
|
+
Object.setPrototypeOf(this, _NAuthClientError.prototype);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if error matches a specific error code.
|
|
56
|
+
*
|
|
57
|
+
* @param code - Error code to check against
|
|
58
|
+
* @returns True if the error code matches
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* if (error.isCode(NAuthErrorCode.RATE_LIMIT_SMS)) {
|
|
63
|
+
* // Handle SMS rate limit
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
isCode(code) {
|
|
68
|
+
return this.code === code;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get error details/metadata.
|
|
72
|
+
*
|
|
73
|
+
* @returns Error details object or undefined
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const details = error.getDetails();
|
|
78
|
+
* if (details?.retryAfter) {
|
|
79
|
+
* console.log(`Retry after ${details.retryAfter} seconds`);
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
getDetails() {
|
|
84
|
+
return this.details;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the error code.
|
|
88
|
+
*
|
|
89
|
+
* @returns The error code enum value
|
|
90
|
+
*/
|
|
91
|
+
getCode() {
|
|
92
|
+
return this.code;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Serialize error to JSON object.
|
|
96
|
+
*
|
|
97
|
+
* @returns Plain object representation
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const errorJson = error.toJSON();
|
|
102
|
+
* // { code: 'AUTH_INVALID_CREDENTIALS', message: '...', timestamp: '...', details: {...} }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
toJSON() {
|
|
106
|
+
return {
|
|
107
|
+
code: this.code,
|
|
108
|
+
message: this.message,
|
|
109
|
+
timestamp: this.timestamp,
|
|
110
|
+
details: this.details,
|
|
111
|
+
statusCode: this.statusCode
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
__name(_NAuthClientError, "NAuthClientError");
|
|
116
|
+
var NAuthClientError = _NAuthClientError;
|
|
117
|
+
|
|
118
|
+
// src/angular/http-adapter.ts
|
|
119
|
+
var AngularHttpAdapter = class {
|
|
120
|
+
constructor() {
|
|
121
|
+
__publicField(this, "http", inject(HttpClient));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Execute HTTP request using Angular's HttpClient.
|
|
125
|
+
*
|
|
126
|
+
* @param config - Request configuration
|
|
127
|
+
* @returns Response with parsed data
|
|
128
|
+
* @throws NAuthClientError if request fails
|
|
129
|
+
*/
|
|
130
|
+
async request(config) {
|
|
131
|
+
try {
|
|
132
|
+
const data = await firstValueFrom(
|
|
133
|
+
this.http.request(config.method, config.url, {
|
|
134
|
+
body: config.body,
|
|
135
|
+
headers: config.headers,
|
|
136
|
+
withCredentials: config.credentials === "include",
|
|
137
|
+
observe: "body"
|
|
138
|
+
// Only return body data
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
data,
|
|
143
|
+
status: 200,
|
|
144
|
+
// HttpClient only returns data on success
|
|
145
|
+
headers: {}
|
|
146
|
+
// Can extract from observe: 'response' if needed
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof HttpErrorResponse) {
|
|
150
|
+
const errorData = error.error || {};
|
|
151
|
+
const code = typeof errorData["code"] === "string" ? errorData.code : "INTERNAL_ERROR" /* INTERNAL_ERROR */;
|
|
152
|
+
const message = typeof errorData["message"] === "string" ? errorData.message : error.message || `Request failed with status ${error.status}`;
|
|
153
|
+
const timestamp = typeof errorData["timestamp"] === "string" ? errorData.timestamp : void 0;
|
|
154
|
+
const details = errorData["details"];
|
|
155
|
+
throw new NAuthClientError(code, message, {
|
|
156
|
+
statusCode: error.status,
|
|
157
|
+
timestamp,
|
|
158
|
+
details,
|
|
159
|
+
isNetworkError: error.status === 0
|
|
160
|
+
// Network error (no response from server)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
__name(AngularHttpAdapter, "AngularHttpAdapter");
|
|
168
|
+
AngularHttpAdapter = __decorateClass([
|
|
169
|
+
Injectable({ providedIn: "root" })
|
|
170
|
+
], AngularHttpAdapter);
|
|
171
|
+
|
|
172
|
+
// src/core/config.ts
|
|
173
|
+
var defaultEndpoints = {
|
|
174
|
+
login: "/login",
|
|
175
|
+
signup: "/signup",
|
|
176
|
+
logout: "/logout",
|
|
177
|
+
logoutAll: "/logout/all",
|
|
178
|
+
refresh: "/refresh",
|
|
179
|
+
respondChallenge: "/respond-challenge",
|
|
180
|
+
resendCode: "/challenge/resend",
|
|
181
|
+
getSetupData: "/challenge/setup-data",
|
|
182
|
+
getChallengeData: "/challenge/challenge-data",
|
|
183
|
+
profile: "/profile",
|
|
184
|
+
changePassword: "/change-password",
|
|
185
|
+
requestPasswordChange: "/request-password-change",
|
|
186
|
+
mfaStatus: "/mfa/status",
|
|
187
|
+
mfaDevices: "/mfa/devices",
|
|
188
|
+
mfaSetupData: "/mfa/setup-data",
|
|
189
|
+
mfaVerifySetup: "/mfa/verify-setup",
|
|
190
|
+
mfaRemove: "/mfa/method",
|
|
191
|
+
mfaPreferred: "/mfa/preferred-method",
|
|
192
|
+
mfaBackupCodes: "/mfa/backup-codes/generate",
|
|
193
|
+
mfaExemption: "/mfa/exemption",
|
|
194
|
+
socialAuthUrl: "/social/auth-url",
|
|
195
|
+
socialCallback: "/social/callback",
|
|
196
|
+
socialLinked: "/social/linked",
|
|
197
|
+
socialLink: "/social/link",
|
|
198
|
+
socialUnlink: "/social/unlink",
|
|
199
|
+
socialVerify: "/social/:provider/verify",
|
|
200
|
+
trustDevice: "/trust-device",
|
|
201
|
+
isTrustedDevice: "/is-trusted-device",
|
|
202
|
+
auditHistory: "/audit/history",
|
|
203
|
+
updateProfile: "/profile"
|
|
204
|
+
};
|
|
205
|
+
var resolveConfig = /* @__PURE__ */ __name((config, defaultAdapter) => {
|
|
206
|
+
const resolvedEndpoints = {
|
|
207
|
+
...defaultEndpoints,
|
|
208
|
+
...config.endpoints ?? {}
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
...config,
|
|
212
|
+
csrf: {
|
|
213
|
+
cookieName: config.csrf?.cookieName ?? "nauth_csrf_token",
|
|
214
|
+
headerName: config.csrf?.headerName ?? "x-csrf-token"
|
|
215
|
+
},
|
|
216
|
+
deviceTrust: {
|
|
217
|
+
headerName: config.deviceTrust?.headerName ?? "X-Device-Token",
|
|
218
|
+
storageKey: config.deviceTrust?.storageKey ?? "nauth_device_token"
|
|
219
|
+
},
|
|
220
|
+
headers: config.headers ?? {},
|
|
221
|
+
timeout: config.timeout ?? 3e4,
|
|
222
|
+
endpoints: resolvedEndpoints,
|
|
223
|
+
storage: config.storage,
|
|
224
|
+
httpAdapter: config.httpAdapter ?? defaultAdapter
|
|
225
|
+
};
|
|
226
|
+
}, "resolveConfig");
|
|
227
|
+
|
|
228
|
+
// src/core/refresh.ts
|
|
229
|
+
var ACCESS_TOKEN_KEY = "nauth_access_token";
|
|
230
|
+
var REFRESH_TOKEN_KEY = "nauth_refresh_token";
|
|
231
|
+
var ACCESS_EXPIRES_AT_KEY = "nauth_access_token_expires_at";
|
|
232
|
+
var REFRESH_EXPIRES_AT_KEY = "nauth_refresh_token_expires_at";
|
|
233
|
+
var USER_KEY = "nauth_user";
|
|
234
|
+
var CHALLENGE_KEY = "nauth_challenge_session";
|
|
235
|
+
var _TokenManager = class _TokenManager {
|
|
236
|
+
/**
|
|
237
|
+
* @param storage - storage adapter
|
|
238
|
+
*/
|
|
239
|
+
constructor(storage) {
|
|
240
|
+
__publicField(this, "storage");
|
|
241
|
+
__publicField(this, "refreshPromise", null);
|
|
242
|
+
__publicField(this, "isBrowser", typeof window !== "undefined");
|
|
243
|
+
this.storage = storage;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Load tokens from storage.
|
|
247
|
+
*/
|
|
248
|
+
async getTokens() {
|
|
249
|
+
const [accessToken, refreshToken, accessExpRaw, refreshExpRaw] = await Promise.all([
|
|
250
|
+
this.storage.getItem(ACCESS_TOKEN_KEY),
|
|
251
|
+
this.storage.getItem(REFRESH_TOKEN_KEY),
|
|
252
|
+
this.storage.getItem(ACCESS_EXPIRES_AT_KEY),
|
|
253
|
+
this.storage.getItem(REFRESH_EXPIRES_AT_KEY)
|
|
254
|
+
]);
|
|
255
|
+
return {
|
|
256
|
+
accessToken,
|
|
257
|
+
refreshToken,
|
|
258
|
+
accessTokenExpiresAt: accessExpRaw ? Number(accessExpRaw) : null,
|
|
259
|
+
refreshTokenExpiresAt: refreshExpRaw ? Number(refreshExpRaw) : null
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Persist tokens.
|
|
264
|
+
*
|
|
265
|
+
* @param tokens - new token pair
|
|
266
|
+
*/
|
|
267
|
+
async setTokens(tokens) {
|
|
268
|
+
await Promise.all([
|
|
269
|
+
this.storage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken),
|
|
270
|
+
this.storage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken),
|
|
271
|
+
this.storage.setItem(ACCESS_EXPIRES_AT_KEY, tokens.accessTokenExpiresAt.toString()),
|
|
272
|
+
this.storage.setItem(REFRESH_EXPIRES_AT_KEY, tokens.refreshTokenExpiresAt.toString())
|
|
273
|
+
]);
|
|
274
|
+
this.broadcastStorage();
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Clear tokens and related auth state.
|
|
278
|
+
*/
|
|
279
|
+
async clearTokens() {
|
|
280
|
+
await Promise.all([
|
|
281
|
+
this.storage.removeItem(ACCESS_TOKEN_KEY),
|
|
282
|
+
this.storage.removeItem(REFRESH_TOKEN_KEY),
|
|
283
|
+
this.storage.removeItem(ACCESS_EXPIRES_AT_KEY),
|
|
284
|
+
this.storage.removeItem(REFRESH_EXPIRES_AT_KEY),
|
|
285
|
+
this.storage.removeItem(USER_KEY),
|
|
286
|
+
this.storage.removeItem(CHALLENGE_KEY)
|
|
287
|
+
]);
|
|
288
|
+
this.broadcastStorage();
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Ensure only one refresh in flight.
|
|
292
|
+
*
|
|
293
|
+
* @param refreshFn - function performing refresh request
|
|
294
|
+
*/
|
|
295
|
+
async refreshOnce(refreshFn) {
|
|
296
|
+
if (!this.refreshPromise) {
|
|
297
|
+
this.refreshPromise = refreshFn().then(async (tokens) => {
|
|
298
|
+
await this.setTokens(tokens);
|
|
299
|
+
return tokens;
|
|
300
|
+
}).catch((error) => {
|
|
301
|
+
throw error;
|
|
302
|
+
}).finally(() => {
|
|
303
|
+
this.refreshPromise = null;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return this.refreshPromise;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Validate that a refresh token exists before attempting refresh.
|
|
310
|
+
*/
|
|
311
|
+
async assertHasRefreshToken() {
|
|
312
|
+
const state = await this.getTokens();
|
|
313
|
+
if (!state.refreshToken) {
|
|
314
|
+
throw new NAuthClientError("AUTH_SESSION_NOT_FOUND" /* AUTH_SESSION_NOT_FOUND */, "No refresh token available");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Broadcast a no-op write to trigger storage listeners in other tabs.
|
|
319
|
+
*/
|
|
320
|
+
broadcastStorage() {
|
|
321
|
+
if (!this.isBrowser) return;
|
|
322
|
+
try {
|
|
323
|
+
window.localStorage.setItem("nauth_sync", Date.now().toString());
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
__name(_TokenManager, "TokenManager");
|
|
329
|
+
var TokenManager = _TokenManager;
|
|
330
|
+
|
|
331
|
+
// src/storage/browser.ts
|
|
332
|
+
var _BrowserStorage = class _BrowserStorage {
|
|
333
|
+
/**
|
|
334
|
+
* Create a browser storage adapter.
|
|
335
|
+
*
|
|
336
|
+
* @param storage - Storage implementation (localStorage by default)
|
|
337
|
+
*/
|
|
338
|
+
constructor(storage = window.localStorage) {
|
|
339
|
+
__publicField(this, "storage");
|
|
340
|
+
this.storage = storage;
|
|
341
|
+
}
|
|
342
|
+
async getItem(key) {
|
|
343
|
+
return this.storage.getItem(key);
|
|
344
|
+
}
|
|
345
|
+
async setItem(key, value) {
|
|
346
|
+
this.storage.setItem(key, value);
|
|
347
|
+
}
|
|
348
|
+
async removeItem(key) {
|
|
349
|
+
this.storage.removeItem(key);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
__name(_BrowserStorage, "BrowserStorage");
|
|
353
|
+
var BrowserStorage = _BrowserStorage;
|
|
354
|
+
|
|
355
|
+
// src/storage/memory.ts
|
|
356
|
+
var _InMemoryStorage = class _InMemoryStorage {
|
|
357
|
+
constructor() {
|
|
358
|
+
__publicField(this, "store", /* @__PURE__ */ new Map());
|
|
359
|
+
}
|
|
360
|
+
async getItem(key) {
|
|
361
|
+
return this.store.has(key) ? this.store.get(key) : null;
|
|
362
|
+
}
|
|
363
|
+
async setItem(key, value) {
|
|
364
|
+
this.store.set(key, value);
|
|
365
|
+
}
|
|
366
|
+
async removeItem(key) {
|
|
367
|
+
this.store.delete(key);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
__name(_InMemoryStorage, "InMemoryStorage");
|
|
371
|
+
var InMemoryStorage = _InMemoryStorage;
|
|
372
|
+
|
|
373
|
+
// src/core/events.ts
|
|
374
|
+
var _EventEmitter = class _EventEmitter {
|
|
375
|
+
constructor() {
|
|
376
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Subscribe to an authentication event
|
|
380
|
+
*
|
|
381
|
+
* @param event - Event type or '*' for all events
|
|
382
|
+
* @param listener - Callback function
|
|
383
|
+
* @returns Unsubscribe function
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```typescript
|
|
387
|
+
* const unsubscribe = emitter.on('auth:success', (event) => {
|
|
388
|
+
* console.log('User logged in:', event.data);
|
|
389
|
+
* });
|
|
390
|
+
*
|
|
391
|
+
* // Later
|
|
392
|
+
* unsubscribe();
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
on(event, listener) {
|
|
396
|
+
if (!this.listeners.has(event)) {
|
|
397
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
398
|
+
}
|
|
399
|
+
this.listeners.get(event).add(listener);
|
|
400
|
+
return () => this.off(event, listener);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Unsubscribe from an authentication event
|
|
404
|
+
*
|
|
405
|
+
* @param event - Event type or '*'
|
|
406
|
+
* @param listener - Callback function to remove
|
|
407
|
+
*/
|
|
408
|
+
off(event, listener) {
|
|
409
|
+
this.listeners.get(event)?.delete(listener);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Emit an authentication event
|
|
413
|
+
*
|
|
414
|
+
* Notifies all listeners for the specific event type and wildcard listeners.
|
|
415
|
+
*
|
|
416
|
+
* @param event - Event to emit
|
|
417
|
+
* @internal
|
|
418
|
+
*/
|
|
419
|
+
emit(event) {
|
|
420
|
+
const specificListeners = this.listeners.get(event.type);
|
|
421
|
+
const wildcardListeners = this.listeners.get("*");
|
|
422
|
+
specificListeners?.forEach((listener) => {
|
|
423
|
+
try {
|
|
424
|
+
listener(event);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error(`Error in ${event.type} event listener:`, error);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
wildcardListeners?.forEach((listener) => {
|
|
430
|
+
try {
|
|
431
|
+
listener(event);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error(`Error in wildcard event listener:`, error);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Remove all listeners
|
|
439
|
+
*
|
|
440
|
+
* @internal
|
|
441
|
+
*/
|
|
442
|
+
clear() {
|
|
443
|
+
this.listeners.clear();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
__name(_EventEmitter, "EventEmitter");
|
|
447
|
+
var EventEmitter = _EventEmitter;
|
|
448
|
+
|
|
449
|
+
// src/adapters/fetch-adapter.ts
|
|
450
|
+
var _FetchAdapter = class _FetchAdapter {
|
|
451
|
+
/**
|
|
452
|
+
* Execute HTTP request using native fetch.
|
|
453
|
+
*
|
|
454
|
+
* @param config - Request configuration
|
|
455
|
+
* @returns Response with parsed data
|
|
456
|
+
* @throws NAuthClientError if request fails
|
|
457
|
+
*/
|
|
458
|
+
async request(config) {
|
|
459
|
+
const fetchOptions = {
|
|
460
|
+
method: config.method,
|
|
461
|
+
headers: config.headers,
|
|
462
|
+
signal: config.signal,
|
|
463
|
+
credentials: config.credentials
|
|
464
|
+
};
|
|
465
|
+
if (config.body !== void 0) {
|
|
466
|
+
fetchOptions.body = JSON.stringify(config.body);
|
|
467
|
+
}
|
|
468
|
+
let response;
|
|
469
|
+
try {
|
|
470
|
+
response = await fetch(config.url, fetchOptions);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw new NAuthClientError("INTERNAL_ERROR" /* INTERNAL_ERROR */, "Network request failed", {
|
|
473
|
+
isNetworkError: true,
|
|
474
|
+
details: { url: config.url, message: error.message }
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
const status = response.status;
|
|
478
|
+
let data = null;
|
|
479
|
+
const text = await response.text();
|
|
480
|
+
if (text) {
|
|
481
|
+
try {
|
|
482
|
+
data = JSON.parse(text);
|
|
483
|
+
} catch {
|
|
484
|
+
data = text;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const headers = {};
|
|
488
|
+
response.headers.forEach((value, key) => {
|
|
489
|
+
headers[key] = value;
|
|
490
|
+
});
|
|
491
|
+
if (!response.ok) {
|
|
492
|
+
const errorData = typeof data === "object" && data !== null ? data : {};
|
|
493
|
+
const code = typeof errorData["code"] === "string" ? errorData.code : "INTERNAL_ERROR" /* INTERNAL_ERROR */;
|
|
494
|
+
const message = typeof errorData["message"] === "string" ? errorData.message : `Request failed with status ${status}`;
|
|
495
|
+
const timestamp = typeof errorData["timestamp"] === "string" ? errorData.timestamp : void 0;
|
|
496
|
+
const details = errorData["details"];
|
|
497
|
+
throw new NAuthClientError(code, message, {
|
|
498
|
+
statusCode: status,
|
|
499
|
+
timestamp,
|
|
500
|
+
details
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return { data, status, headers };
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
__name(_FetchAdapter, "FetchAdapter");
|
|
507
|
+
var FetchAdapter = _FetchAdapter;
|
|
508
|
+
|
|
509
|
+
// src/core/client.ts
|
|
510
|
+
var USER_KEY2 = "nauth_user";
|
|
511
|
+
var CHALLENGE_KEY2 = "nauth_challenge_session";
|
|
512
|
+
var hasWindow = /* @__PURE__ */ __name(() => typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined", "hasWindow");
|
|
513
|
+
var defaultStorage = /* @__PURE__ */ __name(() => {
|
|
514
|
+
if (hasWindow() && typeof window.localStorage !== "undefined") {
|
|
515
|
+
return new BrowserStorage();
|
|
516
|
+
}
|
|
517
|
+
return new InMemoryStorage();
|
|
518
|
+
}, "defaultStorage");
|
|
519
|
+
var _NAuthClient = class _NAuthClient {
|
|
520
|
+
/**
|
|
521
|
+
* Create a new client instance.
|
|
522
|
+
*
|
|
523
|
+
* @param userConfig - Client configuration
|
|
524
|
+
*/
|
|
525
|
+
constructor(userConfig) {
|
|
526
|
+
__publicField(this, "config");
|
|
527
|
+
__publicField(this, "tokenManager");
|
|
528
|
+
__publicField(this, "eventEmitter");
|
|
529
|
+
__publicField(this, "currentUser", null);
|
|
530
|
+
/**
|
|
531
|
+
* Handle cross-tab storage updates.
|
|
532
|
+
*/
|
|
533
|
+
__publicField(this, "handleStorageEvent", /* @__PURE__ */ __name((event) => {
|
|
534
|
+
if (event.key === "nauth_sync") {
|
|
535
|
+
this.config.storage.getItem(USER_KEY2).then((value) => value ? JSON.parse(value) : null).then((user) => {
|
|
536
|
+
this.currentUser = user;
|
|
537
|
+
this.config.onAuthStateChange?.(user);
|
|
538
|
+
}).catch(() => {
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}, "handleStorageEvent"));
|
|
542
|
+
const storage = userConfig.storage ?? defaultStorage();
|
|
543
|
+
const defaultAdapter = userConfig.httpAdapter ?? new FetchAdapter();
|
|
544
|
+
this.config = resolveConfig({ ...userConfig, storage }, defaultAdapter);
|
|
545
|
+
this.tokenManager = new TokenManager(storage);
|
|
546
|
+
this.eventEmitter = new EventEmitter();
|
|
547
|
+
if (hasWindow()) {
|
|
548
|
+
window.addEventListener("storage", this.handleStorageEvent);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Clean up resources.
|
|
553
|
+
*/
|
|
554
|
+
dispose() {
|
|
555
|
+
if (hasWindow()) {
|
|
556
|
+
window.removeEventListener("storage", this.handleStorageEvent);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Login with identifier and password.
|
|
561
|
+
*/
|
|
562
|
+
async login(identifier, password) {
|
|
563
|
+
const loginEvent = { type: "auth:login", data: { identifier }, timestamp: Date.now() };
|
|
564
|
+
this.eventEmitter.emit(loginEvent);
|
|
565
|
+
try {
|
|
566
|
+
const body = { identifier, password };
|
|
567
|
+
const response = await this.post(this.config.endpoints.login, body);
|
|
568
|
+
await this.handleAuthResponse(response);
|
|
569
|
+
if (response.challengeName) {
|
|
570
|
+
const challengeEvent = { type: "auth:challenge", data: response, timestamp: Date.now() };
|
|
571
|
+
this.eventEmitter.emit(challengeEvent);
|
|
572
|
+
} else {
|
|
573
|
+
const successEvent = { type: "auth:success", data: response, timestamp: Date.now() };
|
|
574
|
+
this.eventEmitter.emit(successEvent);
|
|
575
|
+
}
|
|
576
|
+
return response;
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const authError = error instanceof NAuthClientError ? error : new NAuthClientError("AUTH_INVALID_CREDENTIALS" /* AUTH_INVALID_CREDENTIALS */, error.message || "Login failed");
|
|
579
|
+
const errorEvent = { type: "auth:error", data: authError, timestamp: Date.now() };
|
|
580
|
+
this.eventEmitter.emit(errorEvent);
|
|
581
|
+
throw authError;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Signup with credentials.
|
|
586
|
+
*/
|
|
587
|
+
async signup(payload) {
|
|
588
|
+
this.eventEmitter.emit({ type: "auth:signup", data: { email: payload.email }, timestamp: Date.now() });
|
|
589
|
+
try {
|
|
590
|
+
const response = await this.post(this.config.endpoints.signup, payload);
|
|
591
|
+
await this.handleAuthResponse(response);
|
|
592
|
+
if (response.challengeName) {
|
|
593
|
+
this.eventEmitter.emit({ type: "auth:challenge", data: response, timestamp: Date.now() });
|
|
594
|
+
} else {
|
|
595
|
+
this.eventEmitter.emit({ type: "auth:success", data: response, timestamp: Date.now() });
|
|
596
|
+
}
|
|
597
|
+
return response;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
const authError = error instanceof NAuthClientError ? error : new NAuthClientError("AUTH_INVALID_CREDENTIALS" /* AUTH_INVALID_CREDENTIALS */, error.message || "Signup failed");
|
|
600
|
+
this.eventEmitter.emit({ type: "auth:error", data: authError, timestamp: Date.now() });
|
|
601
|
+
throw authError;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Refresh tokens manually.
|
|
606
|
+
*/
|
|
607
|
+
async refreshTokens() {
|
|
608
|
+
const tokenDelivery = this.getTokenDeliveryMode();
|
|
609
|
+
if (tokenDelivery === "json") {
|
|
610
|
+
await this.tokenManager.assertHasRefreshToken();
|
|
611
|
+
}
|
|
612
|
+
const body = tokenDelivery === "json" ? { refreshToken: (await this.tokenManager.getTokens()).refreshToken } : { refreshToken: "" };
|
|
613
|
+
const refreshFn = /* @__PURE__ */ __name(async () => {
|
|
614
|
+
return this.post(this.config.endpoints.refresh, body, false);
|
|
615
|
+
}, "refreshFn");
|
|
616
|
+
const tokens = await this.tokenManager.refreshOnce(refreshFn);
|
|
617
|
+
this.config.onTokenRefresh?.();
|
|
618
|
+
this.eventEmitter.emit({ type: "auth:refresh", data: { success: true }, timestamp: Date.now() });
|
|
619
|
+
return tokens;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Logout current session.
|
|
623
|
+
*
|
|
624
|
+
* Uses GET request to avoid CSRF token issues.
|
|
625
|
+
*
|
|
626
|
+
* @param forgetDevice - If true, also untrust the device (require MFA on next login)
|
|
627
|
+
*/
|
|
628
|
+
async logout(forgetDevice) {
|
|
629
|
+
const queryParams = forgetDevice ? "?forgetMe=true" : "";
|
|
630
|
+
try {
|
|
631
|
+
await this.get(this.config.endpoints.logout + queryParams, true);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
console.warn("[nauth] Logout request failed (session may already be invalid):", error);
|
|
634
|
+
} finally {
|
|
635
|
+
await this.clearAuthState(forgetDevice);
|
|
636
|
+
this.eventEmitter.emit({
|
|
637
|
+
type: "auth:logout",
|
|
638
|
+
data: { forgetDevice: !!forgetDevice, global: false },
|
|
639
|
+
timestamp: Date.now()
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Logout all sessions.
|
|
645
|
+
*
|
|
646
|
+
* Revokes all active sessions for the current user across all devices.
|
|
647
|
+
* Optionally revokes all trusted devices if forgetDevices is true.
|
|
648
|
+
*
|
|
649
|
+
* @param forgetDevices - If true, also revokes all trusted devices (default: false)
|
|
650
|
+
* @returns Number of sessions revoked
|
|
651
|
+
*/
|
|
652
|
+
async logoutAll(forgetDevices) {
|
|
653
|
+
try {
|
|
654
|
+
const payload = {
|
|
655
|
+
forgetDevices: forgetDevices ?? false
|
|
656
|
+
};
|
|
657
|
+
const result = await this.post(
|
|
658
|
+
this.config.endpoints.logoutAll,
|
|
659
|
+
payload,
|
|
660
|
+
true
|
|
661
|
+
);
|
|
662
|
+
await this.clearAuthState(forgetDevices);
|
|
663
|
+
this.eventEmitter.emit({
|
|
664
|
+
type: "auth:logout",
|
|
665
|
+
data: { forgetDevice: !!forgetDevices, global: true },
|
|
666
|
+
timestamp: Date.now()
|
|
667
|
+
});
|
|
668
|
+
return { revokedCount: result.revokedCount };
|
|
669
|
+
} catch (error) {
|
|
670
|
+
await this.clearAuthState(forgetDevices);
|
|
671
|
+
this.eventEmitter.emit({
|
|
672
|
+
type: "auth:logout",
|
|
673
|
+
data: { forgetDevice: !!forgetDevices, global: true },
|
|
674
|
+
timestamp: Date.now()
|
|
675
|
+
});
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Respond to a challenge.
|
|
681
|
+
*
|
|
682
|
+
* Validates challenge response data before sending to backend.
|
|
683
|
+
* Provides helpful error messages for common validation issues.
|
|
684
|
+
*
|
|
685
|
+
* @param response - Challenge response data
|
|
686
|
+
* @returns Auth response from backend
|
|
687
|
+
* @throws {NAuthClientError} If validation fails
|
|
688
|
+
*/
|
|
689
|
+
async respondToChallenge(response) {
|
|
690
|
+
if (response.type === "MFA_SETUP_REQUIRED" /* MFA_SETUP_REQUIRED */ && response.method === "totp") {
|
|
691
|
+
const setupData = response.setupData;
|
|
692
|
+
if (!setupData || typeof setupData !== "object") {
|
|
693
|
+
throw new NAuthClientError(
|
|
694
|
+
"VALIDATION_FAILED" /* VALIDATION_FAILED */,
|
|
695
|
+
"TOTP setup requires setupData with both secret and code",
|
|
696
|
+
{ details: { field: "setupData" } }
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const secret = setupData["secret"];
|
|
700
|
+
const code = setupData["code"];
|
|
701
|
+
if (!secret || typeof secret !== "string") {
|
|
702
|
+
throw new NAuthClientError(
|
|
703
|
+
"VALIDATION_FAILED" /* VALIDATION_FAILED */,
|
|
704
|
+
"TOTP setup requires secret in setupData. Make sure to include the secret from getSetupData() response.",
|
|
705
|
+
{ details: { field: "secret" } }
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (!code || typeof code !== "string") {
|
|
709
|
+
throw new NAuthClientError(
|
|
710
|
+
"VALIDATION_FAILED" /* VALIDATION_FAILED */,
|
|
711
|
+
"TOTP setup requires code in setupData. Please enter the verification code from your authenticator app.",
|
|
712
|
+
{ details: { field: "code" } }
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
const result = await this.post(this.config.endpoints.respondChallenge, response);
|
|
718
|
+
await this.handleAuthResponse(result);
|
|
719
|
+
if (result.challengeName) {
|
|
720
|
+
const challengeEvent = { type: "auth:challenge", data: result, timestamp: Date.now() };
|
|
721
|
+
this.eventEmitter.emit(challengeEvent);
|
|
722
|
+
} else {
|
|
723
|
+
const successEvent = { type: "auth:success", data: result, timestamp: Date.now() };
|
|
724
|
+
this.eventEmitter.emit(successEvent);
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
const authError = error instanceof NAuthClientError ? error : new NAuthClientError(
|
|
729
|
+
"CHALLENGE_INVALID" /* CHALLENGE_INVALID */,
|
|
730
|
+
error.message || "Challenge response failed"
|
|
731
|
+
);
|
|
732
|
+
const errorEvent = { type: "auth:error", data: authError, timestamp: Date.now() };
|
|
733
|
+
this.eventEmitter.emit(errorEvent);
|
|
734
|
+
throw authError;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Resend a challenge code.
|
|
739
|
+
*/
|
|
740
|
+
async resendCode(session) {
|
|
741
|
+
const payload = { session };
|
|
742
|
+
return this.post(this.config.endpoints.resendCode, payload);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Get setup data for MFA.
|
|
746
|
+
*
|
|
747
|
+
* Returns method-specific setup information:
|
|
748
|
+
* - TOTP: { secret, qrCode, manualEntryKey }
|
|
749
|
+
* - SMS: { maskedPhone }
|
|
750
|
+
* - Email: { maskedEmail }
|
|
751
|
+
* - Passkey: WebAuthn registration options
|
|
752
|
+
*
|
|
753
|
+
* @param session - Challenge session token
|
|
754
|
+
* @param method - MFA method to set up
|
|
755
|
+
* @returns Setup data wrapped in GetSetupDataResponse
|
|
756
|
+
*/
|
|
757
|
+
async getSetupData(session, method) {
|
|
758
|
+
const payload = { session, method };
|
|
759
|
+
return this.post(this.config.endpoints.getSetupData, payload);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Get challenge data (e.g., WebAuthn options).
|
|
763
|
+
*
|
|
764
|
+
* Returns challenge-specific data for verification flows.
|
|
765
|
+
*
|
|
766
|
+
* @param session - Challenge session token
|
|
767
|
+
* @param method - Challenge method to get data for
|
|
768
|
+
* @returns Challenge data wrapped in GetChallengeDataResponse
|
|
769
|
+
*/
|
|
770
|
+
async getChallengeData(session, method) {
|
|
771
|
+
const payload = { session, method };
|
|
772
|
+
return this.post(this.config.endpoints.getChallengeData, payload);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Get current user profile.
|
|
776
|
+
*/
|
|
777
|
+
async getProfile() {
|
|
778
|
+
const profile = await this.get(this.config.endpoints.profile, true);
|
|
779
|
+
await this.setUser(profile);
|
|
780
|
+
return profile;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Update user profile.
|
|
784
|
+
*/
|
|
785
|
+
async updateProfile(updates) {
|
|
786
|
+
const updated = await this.put(this.config.endpoints.updateProfile, updates, true);
|
|
787
|
+
await this.setUser(updated);
|
|
788
|
+
return updated;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Change user password.
|
|
792
|
+
*/
|
|
793
|
+
async changePassword(oldPassword, newPassword) {
|
|
794
|
+
const payload = { currentPassword: oldPassword, newPassword };
|
|
795
|
+
await this.post(this.config.endpoints.changePassword, payload, true);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Request password change (must change on next login).
|
|
799
|
+
*/
|
|
800
|
+
async requestPasswordChange() {
|
|
801
|
+
await this.post(this.config.endpoints.requestPasswordChange, {}, true);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Get MFA status.
|
|
805
|
+
*/
|
|
806
|
+
async getMfaStatus() {
|
|
807
|
+
return this.get(this.config.endpoints.mfaStatus, true);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Get MFA devices.
|
|
811
|
+
*/
|
|
812
|
+
async getMfaDevices() {
|
|
813
|
+
return this.get(this.config.endpoints.mfaDevices, true);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Setup MFA device (authenticated user).
|
|
817
|
+
*/
|
|
818
|
+
async setupMfaDevice(method) {
|
|
819
|
+
return this.post(this.config.endpoints.mfaSetupData, { method }, true);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Verify MFA setup (authenticated user).
|
|
823
|
+
*/
|
|
824
|
+
async verifyMfaSetup(method, setupData, deviceName) {
|
|
825
|
+
return this.post(
|
|
826
|
+
this.config.endpoints.mfaVerifySetup,
|
|
827
|
+
{ method, setupData, deviceName },
|
|
828
|
+
true
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Remove MFA method.
|
|
833
|
+
*/
|
|
834
|
+
async removeMfaDevice(method) {
|
|
835
|
+
const path = `${this.config.endpoints.mfaRemove}/${method}`;
|
|
836
|
+
return this.delete(path, true);
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Set preferred MFA method.
|
|
840
|
+
*
|
|
841
|
+
* @param method - Device method to set as preferred ('totp', 'sms', 'email', or 'passkey'). Cannot be 'backup'.
|
|
842
|
+
* @returns Success message
|
|
843
|
+
*/
|
|
844
|
+
async setPreferredMfaMethod(method) {
|
|
845
|
+
return this.post(this.config.endpoints.mfaPreferred, { method }, true);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Generate backup codes.
|
|
849
|
+
*/
|
|
850
|
+
async generateBackupCodes() {
|
|
851
|
+
const result = await this.post(this.config.endpoints.mfaBackupCodes, {}, true);
|
|
852
|
+
return result.codes;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Set MFA exemption (admin/test scenarios).
|
|
856
|
+
*/
|
|
857
|
+
async setMfaExemption(exempt, reason) {
|
|
858
|
+
await this.post(this.config.endpoints.mfaExemption, { exempt, reason }, true);
|
|
859
|
+
}
|
|
860
|
+
// ============================================================================
|
|
861
|
+
// Event System
|
|
862
|
+
// ============================================================================
|
|
863
|
+
/**
|
|
864
|
+
* Subscribe to authentication events.
|
|
865
|
+
*
|
|
866
|
+
* Emits events throughout the auth lifecycle for custom logic, analytics, or UI updates.
|
|
867
|
+
*
|
|
868
|
+
* @param event - Event type to listen for, or '*' for all events
|
|
869
|
+
* @param listener - Callback function to handle the event
|
|
870
|
+
* @returns Unsubscribe function
|
|
871
|
+
*
|
|
872
|
+
* @example
|
|
873
|
+
* ```typescript
|
|
874
|
+
* // Listen to successful authentication
|
|
875
|
+
* const unsubscribe = client.on('auth:success', (event) => {
|
|
876
|
+
* console.log('User logged in:', event.data.user);
|
|
877
|
+
* analytics.track('login_success');
|
|
878
|
+
* });
|
|
879
|
+
*
|
|
880
|
+
* // Listen to all events
|
|
881
|
+
* client.on('*', (event) => {
|
|
882
|
+
* console.log('Auth event:', event.type, event.data);
|
|
883
|
+
* });
|
|
884
|
+
*
|
|
885
|
+
* // Unsubscribe when done
|
|
886
|
+
* unsubscribe();
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
889
|
+
on(event, listener) {
|
|
890
|
+
return this.eventEmitter.on(event, listener);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Unsubscribe from authentication events.
|
|
894
|
+
*
|
|
895
|
+
* @param event - Event type
|
|
896
|
+
* @param listener - Callback function to remove
|
|
897
|
+
*/
|
|
898
|
+
off(event, listener) {
|
|
899
|
+
this.eventEmitter.off(event, listener);
|
|
900
|
+
}
|
|
901
|
+
// ============================================================================
|
|
902
|
+
// Social Authentication
|
|
903
|
+
// ============================================================================
|
|
904
|
+
/**
|
|
905
|
+
* Start social OAuth flow with automatic state management.
|
|
906
|
+
*
|
|
907
|
+
* Generates a secure state token, stores OAuth context, and redirects to the OAuth provider.
|
|
908
|
+
* After OAuth callback, use `handleOAuthCallback()` to complete authentication.
|
|
909
|
+
*
|
|
910
|
+
* @param provider - OAuth provider ('google', 'apple', 'facebook')
|
|
911
|
+
* @param options - Optional configuration
|
|
912
|
+
*
|
|
913
|
+
* @example
|
|
914
|
+
* ```typescript
|
|
915
|
+
* // Simple usage
|
|
916
|
+
* await client.loginWithSocial('google');
|
|
917
|
+
*
|
|
918
|
+
* // With custom redirect URI
|
|
919
|
+
* await client.loginWithSocial('apple', {
|
|
920
|
+
* redirectUri: 'https://example.com/auth/callback'
|
|
921
|
+
* });
|
|
922
|
+
* ```
|
|
923
|
+
*/
|
|
924
|
+
async loginWithSocial(provider, _options) {
|
|
925
|
+
this.eventEmitter.emit({ type: "oauth:started", data: { provider }, timestamp: Date.now() });
|
|
926
|
+
const { url } = await this.getSocialAuthUrl({ provider });
|
|
927
|
+
if (hasWindow()) {
|
|
928
|
+
window.location.href = url;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Auto-detect and handle OAuth callback.
|
|
933
|
+
*
|
|
934
|
+
* Call this on app initialization or in callback route.
|
|
935
|
+
* Returns null if not an OAuth callback (no provider/code params).
|
|
936
|
+
*
|
|
937
|
+
* The SDK validates the state token, completes authentication via backend,
|
|
938
|
+
* and emits appropriate events.
|
|
939
|
+
*
|
|
940
|
+
* @param urlOrParams - Optional URL string or URLSearchParams (auto-detects from window.location if not provided)
|
|
941
|
+
* @returns AuthResponse if OAuth callback detected, null otherwise
|
|
942
|
+
*
|
|
943
|
+
* @example
|
|
944
|
+
* ```typescript
|
|
945
|
+
* // Auto-detect on app init
|
|
946
|
+
* const response = await client.handleOAuthCallback();
|
|
947
|
+
* if (response) {
|
|
948
|
+
* if (response.challengeName) {
|
|
949
|
+
* router.navigate(['/challenge', response.challengeName]);
|
|
950
|
+
* } else {
|
|
951
|
+
* router.navigate(['/']); // Navigate to your app's home route
|
|
952
|
+
* }
|
|
953
|
+
* }
|
|
954
|
+
*
|
|
955
|
+
* // In callback route
|
|
956
|
+
* const response = await client.handleOAuthCallback(window.location.search);
|
|
957
|
+
* ```
|
|
958
|
+
*/
|
|
959
|
+
async handleOAuthCallback(urlOrParams) {
|
|
960
|
+
let params;
|
|
961
|
+
if (urlOrParams instanceof URLSearchParams) {
|
|
962
|
+
params = urlOrParams;
|
|
963
|
+
} else if (typeof urlOrParams === "string") {
|
|
964
|
+
params = new URLSearchParams(urlOrParams);
|
|
965
|
+
} else if (hasWindow()) {
|
|
966
|
+
params = new URLSearchParams(window.location.search);
|
|
967
|
+
} else {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const provider = params.get("provider");
|
|
971
|
+
const code = params.get("code");
|
|
972
|
+
const state = params.get("state");
|
|
973
|
+
const error = params.get("error");
|
|
974
|
+
if (!provider || !code && !error) {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
this.eventEmitter.emit({ type: "oauth:callback", data: { provider }, timestamp: Date.now() });
|
|
978
|
+
try {
|
|
979
|
+
if (error) {
|
|
980
|
+
const authError = new NAuthClientError(
|
|
981
|
+
"SOCIAL_TOKEN_INVALID" /* SOCIAL_TOKEN_INVALID */,
|
|
982
|
+
params.get("error_description") || error,
|
|
983
|
+
{ details: { error, provider } }
|
|
984
|
+
);
|
|
985
|
+
this.eventEmitter.emit({ type: "oauth:error", data: authError, timestamp: Date.now() });
|
|
986
|
+
throw authError;
|
|
987
|
+
}
|
|
988
|
+
if (!state) {
|
|
989
|
+
throw new NAuthClientError("CHALLENGE_INVALID" /* CHALLENGE_INVALID */, "Missing OAuth state parameter");
|
|
990
|
+
}
|
|
991
|
+
const response = await this.handleSocialCallback({
|
|
992
|
+
provider,
|
|
993
|
+
code,
|
|
994
|
+
state
|
|
995
|
+
});
|
|
996
|
+
if (response.challengeName) {
|
|
997
|
+
this.eventEmitter.emit({ type: "auth:challenge", data: response, timestamp: Date.now() });
|
|
998
|
+
} else {
|
|
999
|
+
this.eventEmitter.emit({ type: "auth:success", data: response, timestamp: Date.now() });
|
|
1000
|
+
}
|
|
1001
|
+
this.eventEmitter.emit({ type: "oauth:completed", data: response, timestamp: Date.now() });
|
|
1002
|
+
return response;
|
|
1003
|
+
} catch (error2) {
|
|
1004
|
+
const authError = error2 instanceof NAuthClientError ? error2 : new NAuthClientError(
|
|
1005
|
+
"SOCIAL_TOKEN_INVALID" /* SOCIAL_TOKEN_INVALID */,
|
|
1006
|
+
error2.message || "OAuth callback failed"
|
|
1007
|
+
);
|
|
1008
|
+
this.eventEmitter.emit({ type: "oauth:error", data: authError, timestamp: Date.now() });
|
|
1009
|
+
throw authError;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Get social auth URL (low-level API).
|
|
1014
|
+
*
|
|
1015
|
+
* For most cases, use `loginWithSocial()` which handles state management automatically.
|
|
1016
|
+
*/
|
|
1017
|
+
async getSocialAuthUrl(request) {
|
|
1018
|
+
return this.post(this.config.endpoints.socialAuthUrl, request);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Handle social callback.
|
|
1022
|
+
*/
|
|
1023
|
+
async handleSocialCallback(request) {
|
|
1024
|
+
const result = await this.post(this.config.endpoints.socialCallback, request);
|
|
1025
|
+
await this.handleAuthResponse(result);
|
|
1026
|
+
return result;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Verify native social token (mobile).
|
|
1030
|
+
*/
|
|
1031
|
+
async verifyNativeSocial(request) {
|
|
1032
|
+
try {
|
|
1033
|
+
const path = this.config.endpoints.socialVerify.replace(":provider", request.provider);
|
|
1034
|
+
const result = await this.post(path, request);
|
|
1035
|
+
await this.handleAuthResponse(result);
|
|
1036
|
+
if (result.challengeName) {
|
|
1037
|
+
const challengeEvent = { type: "auth:challenge", data: result, timestamp: Date.now() };
|
|
1038
|
+
this.eventEmitter.emit(challengeEvent);
|
|
1039
|
+
} else {
|
|
1040
|
+
const successEvent = { type: "auth:success", data: result, timestamp: Date.now() };
|
|
1041
|
+
this.eventEmitter.emit(successEvent);
|
|
1042
|
+
}
|
|
1043
|
+
return result;
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
const authError = error instanceof NAuthClientError ? error : new NAuthClientError(
|
|
1046
|
+
"SOCIAL_TOKEN_INVALID" /* SOCIAL_TOKEN_INVALID */,
|
|
1047
|
+
error.message || "Social verification failed"
|
|
1048
|
+
);
|
|
1049
|
+
const errorEvent = { type: "auth:error", data: authError, timestamp: Date.now() };
|
|
1050
|
+
this.eventEmitter.emit(errorEvent);
|
|
1051
|
+
throw authError;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Get linked accounts.
|
|
1056
|
+
*/
|
|
1057
|
+
async getLinkedAccounts() {
|
|
1058
|
+
return this.get(this.config.endpoints.socialLinked, true);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Link social account.
|
|
1062
|
+
*/
|
|
1063
|
+
async linkSocialAccount(provider, code, state) {
|
|
1064
|
+
return this.post(this.config.endpoints.socialLink, { provider, code, state }, true);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Unlink social account.
|
|
1068
|
+
*/
|
|
1069
|
+
async unlinkSocialAccount(provider) {
|
|
1070
|
+
return this.post(this.config.endpoints.socialUnlink, { provider }, true);
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Trust current device.
|
|
1074
|
+
*/
|
|
1075
|
+
async trustDevice() {
|
|
1076
|
+
const result = await this.post(this.config.endpoints.trustDevice, {}, true);
|
|
1077
|
+
await this.setDeviceToken(result.deviceToken);
|
|
1078
|
+
return result;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Check if the current device is trusted.
|
|
1082
|
+
*
|
|
1083
|
+
* Returns whether the current device is trusted based on the device token
|
|
1084
|
+
* (from cookie in cookies mode, or header in JSON mode).
|
|
1085
|
+
*
|
|
1086
|
+
* This performs a server-side validation of the device token and checks:
|
|
1087
|
+
* - Device token exists and is valid
|
|
1088
|
+
* - Device token matches a trusted device record in the database
|
|
1089
|
+
* - Trust has not expired
|
|
1090
|
+
*
|
|
1091
|
+
* @returns Object with trusted status
|
|
1092
|
+
*
|
|
1093
|
+
* @example
|
|
1094
|
+
* ```typescript
|
|
1095
|
+
* const result = await client.isTrustedDevice();
|
|
1096
|
+
* if (result.trusted) {
|
|
1097
|
+
* console.log('This device is trusted');
|
|
1098
|
+
* }
|
|
1099
|
+
* ```
|
|
1100
|
+
*/
|
|
1101
|
+
async isTrustedDevice() {
|
|
1102
|
+
return this.get(this.config.endpoints.isTrustedDevice, true);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Get paginated audit history for the current user.
|
|
1106
|
+
*
|
|
1107
|
+
* Returns authentication and security events with full audit details including:
|
|
1108
|
+
* - Event type (login, logout, MFA, etc.)
|
|
1109
|
+
* - Event status (success, failure, suspicious)
|
|
1110
|
+
* - Device information, location, risk factors
|
|
1111
|
+
*
|
|
1112
|
+
* @param params - Query parameters for filtering and pagination
|
|
1113
|
+
* @returns Paginated audit history response
|
|
1114
|
+
*
|
|
1115
|
+
* @example
|
|
1116
|
+
* ```typescript
|
|
1117
|
+
* const history = await client.getAuditHistory({
|
|
1118
|
+
* page: 1,
|
|
1119
|
+
* limit: 20,
|
|
1120
|
+
* eventType: 'LOGIN_SUCCESS'
|
|
1121
|
+
* });
|
|
1122
|
+
* ```
|
|
1123
|
+
*/
|
|
1124
|
+
async getAuditHistory(params) {
|
|
1125
|
+
const entries = Object.entries(params ?? {}).map(([k, v]) => [k, String(v)]);
|
|
1126
|
+
const query = entries.length > 0 ? `?${new URLSearchParams(entries).toString()}` : "";
|
|
1127
|
+
const path = `${this.config.endpoints.auditHistory}${query}`;
|
|
1128
|
+
return this.get(path, true);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Initialize client by hydrating state from storage.
|
|
1132
|
+
* Call this on app startup to restore auth state.
|
|
1133
|
+
*/
|
|
1134
|
+
async initialize() {
|
|
1135
|
+
const userJson = await this.config.storage.getItem(USER_KEY2);
|
|
1136
|
+
if (userJson) {
|
|
1137
|
+
try {
|
|
1138
|
+
this.currentUser = JSON.parse(userJson);
|
|
1139
|
+
this.config.onAuthStateChange?.(this.currentUser);
|
|
1140
|
+
} catch {
|
|
1141
|
+
await this.config.storage.removeItem(USER_KEY2);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Determine if user is authenticated (async - checks tokens).
|
|
1147
|
+
*/
|
|
1148
|
+
async isAuthenticated() {
|
|
1149
|
+
const tokens = await this.tokenManager.getTokens();
|
|
1150
|
+
return Boolean(tokens.accessToken);
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Determine if user is authenticated (sync - checks cached user).
|
|
1154
|
+
* Use this for guards and sync checks. Use `isAuthenticated()` for definitive check.
|
|
1155
|
+
*/
|
|
1156
|
+
isAuthenticatedSync() {
|
|
1157
|
+
return this.currentUser !== null;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Get current access token (may be null).
|
|
1161
|
+
*/
|
|
1162
|
+
async getAccessToken() {
|
|
1163
|
+
const tokens = await this.tokenManager.getTokens();
|
|
1164
|
+
return tokens.accessToken ?? null;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Get current user (cached, sync).
|
|
1168
|
+
*/
|
|
1169
|
+
getCurrentUser() {
|
|
1170
|
+
return this.currentUser;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Get stored challenge session (for resuming challenge flows).
|
|
1174
|
+
*/
|
|
1175
|
+
async getStoredChallenge() {
|
|
1176
|
+
const raw = await this.config.storage.getItem(CHALLENGE_KEY2);
|
|
1177
|
+
if (!raw) return null;
|
|
1178
|
+
try {
|
|
1179
|
+
return JSON.parse(raw);
|
|
1180
|
+
} catch {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Clear stored challenge session.
|
|
1186
|
+
*/
|
|
1187
|
+
async clearStoredChallenge() {
|
|
1188
|
+
await this.config.storage.removeItem(CHALLENGE_KEY2);
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Internal: handle auth response (tokens or challenge).
|
|
1192
|
+
*
|
|
1193
|
+
* In cookies mode: Tokens are set as httpOnly cookies by backend, not stored in client storage.
|
|
1194
|
+
* In JSON mode: Tokens are stored in tokenManager for Authorization header.
|
|
1195
|
+
*/
|
|
1196
|
+
async handleAuthResponse(response) {
|
|
1197
|
+
if (response.challengeName) {
|
|
1198
|
+
await this.persistChallenge(response);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (this.config.tokenDelivery === "json" && response.accessToken && response.refreshToken) {
|
|
1202
|
+
await this.tokenManager.setTokens({
|
|
1203
|
+
accessToken: response.accessToken,
|
|
1204
|
+
refreshToken: response.refreshToken,
|
|
1205
|
+
accessTokenExpiresAt: response.accessTokenExpiresAt ?? 0,
|
|
1206
|
+
refreshTokenExpiresAt: response.refreshTokenExpiresAt ?? 0
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (this.config.tokenDelivery === "json" && response.deviceToken) {
|
|
1210
|
+
await this.setDeviceToken(response.deviceToken);
|
|
1211
|
+
}
|
|
1212
|
+
if (response.user) {
|
|
1213
|
+
await this.setUser(response.user);
|
|
1214
|
+
}
|
|
1215
|
+
await this.clearChallenge();
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Persist challenge state.
|
|
1219
|
+
*/
|
|
1220
|
+
async persistChallenge(challenge) {
|
|
1221
|
+
await this.config.storage.setItem(CHALLENGE_KEY2, JSON.stringify(challenge));
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Clear challenge state.
|
|
1225
|
+
*/
|
|
1226
|
+
async clearChallenge() {
|
|
1227
|
+
await this.config.storage.removeItem(CHALLENGE_KEY2);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Persist user.
|
|
1231
|
+
*/
|
|
1232
|
+
async setUser(user) {
|
|
1233
|
+
this.currentUser = user;
|
|
1234
|
+
await this.config.storage.setItem(USER_KEY2, JSON.stringify(user));
|
|
1235
|
+
this.config.onAuthStateChange?.(user);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Clear all auth state.
|
|
1239
|
+
*
|
|
1240
|
+
* @param forgetDevice - If true, also clear device token (for JSON mode)
|
|
1241
|
+
*/
|
|
1242
|
+
async clearAuthState(forgetDevice) {
|
|
1243
|
+
this.currentUser = null;
|
|
1244
|
+
await this.tokenManager.clearTokens();
|
|
1245
|
+
await this.config.storage.removeItem(USER_KEY2);
|
|
1246
|
+
if (forgetDevice && this.config.tokenDelivery === "json") {
|
|
1247
|
+
await this.config.storage.removeItem(this.config.deviceTrust.storageKey);
|
|
1248
|
+
}
|
|
1249
|
+
this.config.onAuthStateChange?.(null);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Persist device token (json mode mobile).
|
|
1253
|
+
*/
|
|
1254
|
+
async setDeviceToken(token) {
|
|
1255
|
+
await this.config.storage.setItem(this.config.deviceTrust.storageKey, token);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Determine token delivery mode for this environment.
|
|
1259
|
+
*/
|
|
1260
|
+
getTokenDeliveryMode() {
|
|
1261
|
+
return this.config.tokenDelivery;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Build request URL by combining baseUrl with path.
|
|
1265
|
+
* @private
|
|
1266
|
+
*/
|
|
1267
|
+
buildUrl(path) {
|
|
1268
|
+
return `${this.config.baseUrl}${path}`;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Build request headers for authentication.
|
|
1272
|
+
* @private
|
|
1273
|
+
*/
|
|
1274
|
+
async buildHeaders(auth) {
|
|
1275
|
+
const headers = {
|
|
1276
|
+
"Content-Type": "application/json",
|
|
1277
|
+
...this.config.headers
|
|
1278
|
+
};
|
|
1279
|
+
if (auth && this.config.tokenDelivery === "json") {
|
|
1280
|
+
const accessToken = (await this.tokenManager.getTokens()).accessToken;
|
|
1281
|
+
if (accessToken) {
|
|
1282
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (this.config.tokenDelivery === "cookies" && hasWindow()) {
|
|
1286
|
+
const csrfToken = this.getCsrfToken();
|
|
1287
|
+
if (csrfToken) {
|
|
1288
|
+
headers[this.config.csrf.headerName] = csrfToken;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return headers;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Get CSRF token from cookie (browser only).
|
|
1295
|
+
* @private
|
|
1296
|
+
*/
|
|
1297
|
+
getCsrfToken() {
|
|
1298
|
+
if (!hasWindow() || typeof document === "undefined") return null;
|
|
1299
|
+
const match = document.cookie.match(new RegExp(`(^| )${this.config.csrf.cookieName}=([^;]+)`));
|
|
1300
|
+
return match ? decodeURIComponent(match[2]) : null;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Execute GET request.
|
|
1304
|
+
* Note: 401 refresh is handled by framework interceptors (Angular) or manually.
|
|
1305
|
+
*/
|
|
1306
|
+
async get(path, auth = false) {
|
|
1307
|
+
const url = this.buildUrl(path);
|
|
1308
|
+
const headers = await this.buildHeaders(auth);
|
|
1309
|
+
const credentials = this.config.tokenDelivery === "cookies" ? "include" : "omit";
|
|
1310
|
+
const response = await this.config.httpAdapter.request({
|
|
1311
|
+
method: "GET",
|
|
1312
|
+
url,
|
|
1313
|
+
headers,
|
|
1314
|
+
credentials
|
|
1315
|
+
});
|
|
1316
|
+
return response.data;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Execute POST request.
|
|
1320
|
+
* Note: 401 refresh is handled by framework interceptors (Angular) or manually.
|
|
1321
|
+
*/
|
|
1322
|
+
async post(path, body, auth = false) {
|
|
1323
|
+
const url = this.buildUrl(path);
|
|
1324
|
+
const headers = await this.buildHeaders(auth);
|
|
1325
|
+
const credentials = this.config.tokenDelivery === "cookies" ? "include" : "omit";
|
|
1326
|
+
const response = await this.config.httpAdapter.request({
|
|
1327
|
+
method: "POST",
|
|
1328
|
+
url,
|
|
1329
|
+
headers,
|
|
1330
|
+
body,
|
|
1331
|
+
credentials
|
|
1332
|
+
});
|
|
1333
|
+
return response.data;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Execute PUT request.
|
|
1337
|
+
* Note: 401 refresh is handled by framework interceptors (Angular) or manually.
|
|
1338
|
+
*/
|
|
1339
|
+
async put(path, body, auth = false) {
|
|
1340
|
+
const url = this.buildUrl(path);
|
|
1341
|
+
const headers = await this.buildHeaders(auth);
|
|
1342
|
+
const credentials = this.config.tokenDelivery === "cookies" ? "include" : "omit";
|
|
1343
|
+
const response = await this.config.httpAdapter.request({
|
|
1344
|
+
method: "PUT",
|
|
1345
|
+
url,
|
|
1346
|
+
headers,
|
|
1347
|
+
body,
|
|
1348
|
+
credentials
|
|
1349
|
+
});
|
|
1350
|
+
return response.data;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Execute DELETE request.
|
|
1354
|
+
* Note: 401 refresh is handled by framework interceptors (Angular) or manually.
|
|
1355
|
+
*/
|
|
1356
|
+
async delete(path, auth = false) {
|
|
1357
|
+
const url = this.buildUrl(path);
|
|
1358
|
+
const headers = await this.buildHeaders(auth);
|
|
1359
|
+
const credentials = this.config.tokenDelivery === "cookies" ? "include" : "omit";
|
|
1360
|
+
const response = await this.config.httpAdapter.request({
|
|
1361
|
+
method: "DELETE",
|
|
1362
|
+
url,
|
|
1363
|
+
headers,
|
|
1364
|
+
credentials
|
|
1365
|
+
});
|
|
1366
|
+
return response.data;
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
__name(_NAuthClient, "NAuthClient");
|
|
1370
|
+
var NAuthClient = _NAuthClient;
|
|
1371
|
+
|
|
1372
|
+
// src/angular/auth.service.ts
|
|
1373
|
+
var AuthService = class {
|
|
1374
|
+
/**
|
|
1375
|
+
* @param config - Injected client configuration
|
|
1376
|
+
*
|
|
1377
|
+
* Note: AngularHttpAdapter is automatically injected via Angular DI.
|
|
1378
|
+
* This ensures all requests go through Angular's HttpClient and interceptors.
|
|
1379
|
+
*/
|
|
1380
|
+
constructor(config) {
|
|
1381
|
+
__publicField(this, "client");
|
|
1382
|
+
__publicField(this, "config");
|
|
1383
|
+
__publicField(this, "currentUserSubject", new BehaviorSubject(null));
|
|
1384
|
+
__publicField(this, "isAuthenticatedSubject", new BehaviorSubject(false));
|
|
1385
|
+
__publicField(this, "challengeSubject", new BehaviorSubject(null));
|
|
1386
|
+
__publicField(this, "authEventsSubject", new Subject());
|
|
1387
|
+
__publicField(this, "initialized", false);
|
|
1388
|
+
if (!config) {
|
|
1389
|
+
throw new Error("NAUTH_CLIENT_CONFIG is required to initialize AuthService");
|
|
1390
|
+
}
|
|
1391
|
+
this.config = config;
|
|
1392
|
+
const httpAdapter = config.httpAdapter ?? inject2(AngularHttpAdapter);
|
|
1393
|
+
this.client = new NAuthClient({
|
|
1394
|
+
...config,
|
|
1395
|
+
httpAdapter,
|
|
1396
|
+
// Automatically use Angular's HttpClient
|
|
1397
|
+
onAuthStateChange: /* @__PURE__ */ __name((user) => {
|
|
1398
|
+
this.currentUserSubject.next(user);
|
|
1399
|
+
this.isAuthenticatedSubject.next(Boolean(user));
|
|
1400
|
+
config.onAuthStateChange?.(user);
|
|
1401
|
+
}, "onAuthStateChange")
|
|
1402
|
+
});
|
|
1403
|
+
this.client.on("*", (event) => {
|
|
1404
|
+
this.authEventsSubject.next(event);
|
|
1405
|
+
});
|
|
1406
|
+
this.initialize();
|
|
1407
|
+
}
|
|
1408
|
+
// ============================================================================
|
|
1409
|
+
// Reactive State Observables
|
|
1410
|
+
// ============================================================================
|
|
1411
|
+
/**
|
|
1412
|
+
* Current user observable.
|
|
1413
|
+
*/
|
|
1414
|
+
get currentUser$() {
|
|
1415
|
+
return this.currentUserSubject.asObservable();
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Authenticated state observable.
|
|
1419
|
+
*/
|
|
1420
|
+
get isAuthenticated$() {
|
|
1421
|
+
return this.isAuthenticatedSubject.asObservable();
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Current challenge observable (for reactive challenge navigation).
|
|
1425
|
+
*/
|
|
1426
|
+
get challenge$() {
|
|
1427
|
+
return this.challengeSubject.asObservable();
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Authentication events stream.
|
|
1431
|
+
* Emits all auth lifecycle events for custom logic, analytics, or UI updates.
|
|
1432
|
+
*/
|
|
1433
|
+
get authEvents$() {
|
|
1434
|
+
return this.authEventsSubject.asObservable();
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Successful authentication events stream.
|
|
1438
|
+
* Emits when user successfully authenticates (login, signup, social auth).
|
|
1439
|
+
*/
|
|
1440
|
+
get authSuccess$() {
|
|
1441
|
+
return this.authEventsSubject.pipe(filter((e) => e.type === "auth:success"));
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Authentication error events stream.
|
|
1445
|
+
* Emits when authentication fails (login error, OAuth error, etc.).
|
|
1446
|
+
*/
|
|
1447
|
+
get authError$() {
|
|
1448
|
+
return this.authEventsSubject.pipe(filter((e) => e.type === "auth:error" || e.type === "oauth:error"));
|
|
1449
|
+
}
|
|
1450
|
+
// ============================================================================
|
|
1451
|
+
// Sync State Accessors (for guards, templates)
|
|
1452
|
+
// ============================================================================
|
|
1453
|
+
/**
|
|
1454
|
+
* Check if authenticated (sync, uses cached state).
|
|
1455
|
+
*/
|
|
1456
|
+
isAuthenticated() {
|
|
1457
|
+
return this.client.isAuthenticatedSync();
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Get current user (sync, uses cached state).
|
|
1461
|
+
*/
|
|
1462
|
+
getCurrentUser() {
|
|
1463
|
+
return this.client.getCurrentUser();
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Get current challenge (sync).
|
|
1467
|
+
*/
|
|
1468
|
+
getCurrentChallenge() {
|
|
1469
|
+
return this.challengeSubject.value;
|
|
1470
|
+
}
|
|
1471
|
+
// ============================================================================
|
|
1472
|
+
// Core Auth Methods (Observable wrappers)
|
|
1473
|
+
// ============================================================================
|
|
1474
|
+
/**
|
|
1475
|
+
* Login with identifier and password.
|
|
1476
|
+
*/
|
|
1477
|
+
login(identifier, password) {
|
|
1478
|
+
return from(this.client.login(identifier, password).then((res) => this.updateChallengeState(res)));
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Signup with credentials.
|
|
1482
|
+
*/
|
|
1483
|
+
signup(payload) {
|
|
1484
|
+
return from(this.client.signup(payload).then((res) => this.updateChallengeState(res)));
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Logout current session.
|
|
1488
|
+
*/
|
|
1489
|
+
logout(forgetDevice) {
|
|
1490
|
+
return from(
|
|
1491
|
+
this.client.logout(forgetDevice).then(() => {
|
|
1492
|
+
this.challengeSubject.next(null);
|
|
1493
|
+
this.currentUserSubject.next(null);
|
|
1494
|
+
this.isAuthenticatedSubject.next(false);
|
|
1495
|
+
if (this.config.tokenDelivery === "cookies" && typeof document !== "undefined") {
|
|
1496
|
+
const csrfCookieName = this.config.csrf?.cookieName ?? "nauth_csrf_token";
|
|
1497
|
+
try {
|
|
1498
|
+
const url = new URL(this.config.baseUrl);
|
|
1499
|
+
document.cookie = `${csrfCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${url.hostname}`;
|
|
1500
|
+
document.cookie = `${csrfCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
|
|
1501
|
+
} catch {
|
|
1502
|
+
document.cookie = `${csrfCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
})
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Logout all sessions.
|
|
1510
|
+
*
|
|
1511
|
+
* Revokes all active sessions for the current user across all devices.
|
|
1512
|
+
* Optionally revokes all trusted devices if forgetDevices is true.
|
|
1513
|
+
*
|
|
1514
|
+
* @param forgetDevices - If true, also revokes all trusted devices (default: false)
|
|
1515
|
+
* @returns Observable with number of sessions revoked
|
|
1516
|
+
*/
|
|
1517
|
+
logoutAll(forgetDevices) {
|
|
1518
|
+
return from(
|
|
1519
|
+
this.client.logoutAll(forgetDevices).then((res) => {
|
|
1520
|
+
this.challengeSubject.next(null);
|
|
1521
|
+
this.currentUserSubject.next(null);
|
|
1522
|
+
this.isAuthenticatedSubject.next(false);
|
|
1523
|
+
return res;
|
|
1524
|
+
})
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Refresh tokens.
|
|
1529
|
+
*/
|
|
1530
|
+
refresh() {
|
|
1531
|
+
return from(this.client.refreshTokens());
|
|
1532
|
+
}
|
|
1533
|
+
// ============================================================================
|
|
1534
|
+
// Challenge Flow Methods (Essential for any auth flow)
|
|
1535
|
+
// ============================================================================
|
|
1536
|
+
/**
|
|
1537
|
+
* Respond to a challenge (VERIFY_EMAIL, VERIFY_PHONE, MFA_REQUIRED, etc.).
|
|
1538
|
+
*/
|
|
1539
|
+
respondToChallenge(response) {
|
|
1540
|
+
return from(this.client.respondToChallenge(response).then((res) => this.updateChallengeState(res)));
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Resend challenge code.
|
|
1544
|
+
*/
|
|
1545
|
+
resendCode(session) {
|
|
1546
|
+
return from(this.client.resendCode(session));
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Get MFA setup data (for MFA_SETUP_REQUIRED challenge).
|
|
1550
|
+
*
|
|
1551
|
+
* Returns method-specific setup information:
|
|
1552
|
+
* - TOTP: { secret, qrCode, manualEntryKey }
|
|
1553
|
+
* - SMS: { maskedPhone }
|
|
1554
|
+
* - Email: { maskedEmail }
|
|
1555
|
+
* - Passkey: WebAuthn registration options
|
|
1556
|
+
*
|
|
1557
|
+
* @param session - Challenge session token
|
|
1558
|
+
* @param method - MFA method to set up
|
|
1559
|
+
* @returns Observable of setup data response
|
|
1560
|
+
*/
|
|
1561
|
+
getSetupData(session, method) {
|
|
1562
|
+
return from(this.client.getSetupData(session, method));
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Get MFA challenge data (for MFA_REQUIRED challenge - e.g., passkey options).
|
|
1566
|
+
*
|
|
1567
|
+
* @param session - Challenge session token
|
|
1568
|
+
* @param method - Challenge method
|
|
1569
|
+
* @returns Observable of challenge data response
|
|
1570
|
+
*/
|
|
1571
|
+
getChallengeData(session, method) {
|
|
1572
|
+
return from(this.client.getChallengeData(session, method));
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Clear stored challenge (when navigating away from challenge flow).
|
|
1576
|
+
*/
|
|
1577
|
+
clearChallenge() {
|
|
1578
|
+
return from(
|
|
1579
|
+
this.client.clearStoredChallenge().then(() => {
|
|
1580
|
+
this.challengeSubject.next(null);
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
// ============================================================================
|
|
1585
|
+
// Social Authentication
|
|
1586
|
+
// ============================================================================
|
|
1587
|
+
/**
|
|
1588
|
+
* Initiate social OAuth login flow.
|
|
1589
|
+
* Redirects to OAuth provider with automatic state management.
|
|
1590
|
+
*/
|
|
1591
|
+
loginWithSocial(provider, options) {
|
|
1592
|
+
return this.client.loginWithSocial(provider, options);
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Get social auth URL to redirect user for OAuth (low-level API).
|
|
1596
|
+
*/
|
|
1597
|
+
getSocialAuthUrl(provider, redirectUri) {
|
|
1598
|
+
return from(
|
|
1599
|
+
this.client.getSocialAuthUrl({ provider, redirectUri })
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Handle social auth callback (low-level API).
|
|
1604
|
+
*/
|
|
1605
|
+
handleSocialCallback(provider, code, state) {
|
|
1606
|
+
return from(
|
|
1607
|
+
this.client.handleSocialCallback({ provider, code, state }).then((res) => this.updateChallengeState(res))
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
// ============================================================================
|
|
1611
|
+
// Escape Hatch
|
|
1612
|
+
// ============================================================================
|
|
1613
|
+
/**
|
|
1614
|
+
* Expose underlying NAuthClient for advanced scenarios.
|
|
1615
|
+
*
|
|
1616
|
+
* Use this for operations not directly exposed by this service:
|
|
1617
|
+
* - Profile management (getProfile, updateProfile)
|
|
1618
|
+
* - MFA management (getMfaStatus, setupMfaDevice, etc.)
|
|
1619
|
+
* - Social account linking (linkSocialAccount, unlinkSocialAccount)
|
|
1620
|
+
* - Audit history (getAuditHistory)
|
|
1621
|
+
* - Device trust (trustDevice)
|
|
1622
|
+
*
|
|
1623
|
+
* @example
|
|
1624
|
+
* ```typescript
|
|
1625
|
+
* // Get MFA status
|
|
1626
|
+
* const status = await this.auth.getClient().getMfaStatus();
|
|
1627
|
+
*
|
|
1628
|
+
* // Update profile
|
|
1629
|
+
* const user = await this.auth.getClient().updateProfile({ firstName: 'John' });
|
|
1630
|
+
* ```
|
|
1631
|
+
*/
|
|
1632
|
+
getClient() {
|
|
1633
|
+
return this.client;
|
|
1634
|
+
}
|
|
1635
|
+
// ============================================================================
|
|
1636
|
+
// Internal Methods
|
|
1637
|
+
// ============================================================================
|
|
1638
|
+
/**
|
|
1639
|
+
* Initialize by hydrating state from storage.
|
|
1640
|
+
* Called automatically on construction.
|
|
1641
|
+
*/
|
|
1642
|
+
async initialize() {
|
|
1643
|
+
if (this.initialized) return;
|
|
1644
|
+
this.initialized = true;
|
|
1645
|
+
await this.client.initialize();
|
|
1646
|
+
const storedChallenge = await this.client.getStoredChallenge();
|
|
1647
|
+
if (storedChallenge) {
|
|
1648
|
+
this.challengeSubject.next(storedChallenge);
|
|
1649
|
+
}
|
|
1650
|
+
const user = this.client.getCurrentUser();
|
|
1651
|
+
if (user) {
|
|
1652
|
+
this.currentUserSubject.next(user);
|
|
1653
|
+
this.isAuthenticatedSubject.next(true);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Update challenge state after auth response.
|
|
1658
|
+
*/
|
|
1659
|
+
updateChallengeState(response) {
|
|
1660
|
+
if (response.challengeName) {
|
|
1661
|
+
this.challengeSubject.next(response);
|
|
1662
|
+
} else {
|
|
1663
|
+
this.challengeSubject.next(null);
|
|
1664
|
+
}
|
|
1665
|
+
return response;
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
__name(AuthService, "AuthService");
|
|
1669
|
+
AuthService = __decorateClass([
|
|
1670
|
+
Injectable2({
|
|
1671
|
+
providedIn: "root"
|
|
1672
|
+
}),
|
|
1673
|
+
__decorateParam(0, Optional()),
|
|
1674
|
+
__decorateParam(0, Inject(NAUTH_CLIENT_CONFIG))
|
|
1675
|
+
], AuthService);
|
|
1676
|
+
|
|
1677
|
+
// src/angular/auth.interceptor.ts
|
|
1678
|
+
import { inject as inject3, PLATFORM_ID } from "@angular/core";
|
|
1679
|
+
import { isPlatformBrowser } from "@angular/common";
|
|
1680
|
+
import { HttpClient as HttpClient2, HttpErrorResponse as HttpErrorResponse2 } from "@angular/common/http";
|
|
1681
|
+
import { Router } from "@angular/router";
|
|
1682
|
+
import { catchError, switchMap, throwError, filter as filter2, take, BehaviorSubject as BehaviorSubject2, from as from2 } from "rxjs";
|
|
1683
|
+
var isRefreshing = false;
|
|
1684
|
+
var refreshTokenSubject = new BehaviorSubject2(null);
|
|
1685
|
+
var retriedRequests = /* @__PURE__ */ new WeakSet();
|
|
1686
|
+
function getCsrfToken(cookieName) {
|
|
1687
|
+
if (typeof document === "undefined") return null;
|
|
1688
|
+
const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
|
|
1689
|
+
return match ? decodeURIComponent(match[2]) : null;
|
|
1690
|
+
}
|
|
1691
|
+
__name(getCsrfToken, "getCsrfToken");
|
|
1692
|
+
var authInterceptor = /* @__PURE__ */ __name((req, next) => {
|
|
1693
|
+
const config = inject3(NAUTH_CLIENT_CONFIG);
|
|
1694
|
+
const http = inject3(HttpClient2);
|
|
1695
|
+
const authService = inject3(AuthService);
|
|
1696
|
+
const platformId = inject3(PLATFORM_ID);
|
|
1697
|
+
const router = inject3(Router);
|
|
1698
|
+
const isBrowser = isPlatformBrowser(platformId);
|
|
1699
|
+
if (!isBrowser) {
|
|
1700
|
+
return next(req);
|
|
1701
|
+
}
|
|
1702
|
+
const tokenDelivery = config.tokenDelivery;
|
|
1703
|
+
const baseUrl = config.baseUrl;
|
|
1704
|
+
const endpoints = config.endpoints ?? {};
|
|
1705
|
+
const refreshPath = endpoints.refresh ?? "/refresh";
|
|
1706
|
+
const loginPath = endpoints.login ?? "/login";
|
|
1707
|
+
const signupPath = endpoints.signup ?? "/signup";
|
|
1708
|
+
const socialAuthUrlPath = endpoints.socialAuthUrl ?? "/social/auth-url";
|
|
1709
|
+
const socialCallbackPath = endpoints.socialCallback ?? "/social/callback";
|
|
1710
|
+
const refreshUrl = `${baseUrl}${refreshPath}`;
|
|
1711
|
+
const isAuthApiRequest = req.url.includes(baseUrl);
|
|
1712
|
+
const isRefreshEndpoint = req.url.includes(refreshPath);
|
|
1713
|
+
const isPublicEndpoint = req.url.includes(loginPath) || req.url.includes(signupPath) || req.url.includes(socialAuthUrlPath) || req.url.includes(socialCallbackPath);
|
|
1714
|
+
let authReq = req;
|
|
1715
|
+
if (tokenDelivery === "cookies") {
|
|
1716
|
+
authReq = authReq.clone({ withCredentials: true });
|
|
1717
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(req.method)) {
|
|
1718
|
+
const csrfCookieName = config.csrf?.cookieName ?? "nauth_csrf_token";
|
|
1719
|
+
const csrfHeaderName = config.csrf?.headerName ?? "x-csrf-token";
|
|
1720
|
+
const csrfToken = getCsrfToken(csrfCookieName);
|
|
1721
|
+
if (csrfToken) {
|
|
1722
|
+
authReq = authReq.clone({ setHeaders: { [csrfHeaderName]: csrfToken } });
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return next(authReq).pipe(
|
|
1727
|
+
catchError((error) => {
|
|
1728
|
+
const shouldHandle = error instanceof HttpErrorResponse2 && error.status === 401 && isAuthApiRequest && !isRefreshEndpoint && !isPublicEndpoint && !retriedRequests.has(req);
|
|
1729
|
+
if (!shouldHandle) {
|
|
1730
|
+
return throwError(() => error);
|
|
1731
|
+
}
|
|
1732
|
+
if (config.debug) {
|
|
1733
|
+
console.warn("[nauth-interceptor] 401 detected:", req.url);
|
|
1734
|
+
}
|
|
1735
|
+
if (!isRefreshing) {
|
|
1736
|
+
isRefreshing = true;
|
|
1737
|
+
refreshTokenSubject.next(null);
|
|
1738
|
+
if (config.debug) {
|
|
1739
|
+
console.warn("[nauth-interceptor] Starting refresh...");
|
|
1740
|
+
}
|
|
1741
|
+
const refresh$ = tokenDelivery === "cookies" ? http.post(refreshUrl, {}, { withCredentials: true }) : from2(authService.getClient().refreshTokens());
|
|
1742
|
+
return refresh$.pipe(
|
|
1743
|
+
switchMap((response) => {
|
|
1744
|
+
if (config.debug) {
|
|
1745
|
+
console.warn("[nauth-interceptor] Refresh successful");
|
|
1746
|
+
}
|
|
1747
|
+
isRefreshing = false;
|
|
1748
|
+
const newToken = "accessToken" in response ? response.accessToken : "success";
|
|
1749
|
+
refreshTokenSubject.next(newToken ?? "success");
|
|
1750
|
+
const retryReq = buildRetryRequest(authReq, tokenDelivery, newToken);
|
|
1751
|
+
retriedRequests.add(retryReq);
|
|
1752
|
+
if (config.debug) {
|
|
1753
|
+
console.warn("[nauth-interceptor] Retrying:", req.url);
|
|
1754
|
+
}
|
|
1755
|
+
return next(retryReq);
|
|
1756
|
+
}),
|
|
1757
|
+
catchError((err) => {
|
|
1758
|
+
if (config.debug) {
|
|
1759
|
+
console.error("[nauth-interceptor] Refresh failed:", err);
|
|
1760
|
+
}
|
|
1761
|
+
isRefreshing = false;
|
|
1762
|
+
refreshTokenSubject.next(null);
|
|
1763
|
+
if (config.redirects?.sessionExpired) {
|
|
1764
|
+
router.navigateByUrl(config.redirects.sessionExpired).catch((navError) => {
|
|
1765
|
+
if (config.debug) {
|
|
1766
|
+
console.error("[nauth-interceptor] Navigation failed:", navError);
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
return throwError(() => err);
|
|
1771
|
+
})
|
|
1772
|
+
);
|
|
1773
|
+
} else {
|
|
1774
|
+
if (config.debug) {
|
|
1775
|
+
console.warn("[nauth-interceptor] Waiting for refresh...");
|
|
1776
|
+
}
|
|
1777
|
+
return refreshTokenSubject.pipe(
|
|
1778
|
+
filter2((token) => token !== null),
|
|
1779
|
+
take(1),
|
|
1780
|
+
switchMap((token) => {
|
|
1781
|
+
if (config.debug) {
|
|
1782
|
+
console.warn("[nauth-interceptor] Refresh done, retrying:", req.url);
|
|
1783
|
+
}
|
|
1784
|
+
const retryReq = buildRetryRequest(authReq, tokenDelivery, token);
|
|
1785
|
+
retriedRequests.add(retryReq);
|
|
1786
|
+
return next(retryReq);
|
|
1787
|
+
})
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
})
|
|
1791
|
+
);
|
|
1792
|
+
}, "authInterceptor");
|
|
1793
|
+
function buildRetryRequest(originalReq, tokenDelivery, newToken) {
|
|
1794
|
+
if (tokenDelivery === "json" && newToken && newToken !== "success") {
|
|
1795
|
+
return originalReq.clone({
|
|
1796
|
+
setHeaders: { Authorization: `Bearer ${newToken}` }
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
return originalReq.clone();
|
|
1800
|
+
}
|
|
1801
|
+
__name(buildRetryRequest, "buildRetryRequest");
|
|
1802
|
+
var _AuthInterceptor = class _AuthInterceptor {
|
|
1803
|
+
intercept(req, next) {
|
|
1804
|
+
return authInterceptor(req, next);
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
__name(_AuthInterceptor, "AuthInterceptor");
|
|
1808
|
+
var AuthInterceptor = _AuthInterceptor;
|
|
1809
|
+
|
|
1810
|
+
// src/angular/auth.guard.ts
|
|
1811
|
+
import { inject as inject4 } from "@angular/core";
|
|
1812
|
+
import { Router as Router2 } from "@angular/router";
|
|
1813
|
+
function authGuard(redirectTo = "/login") {
|
|
1814
|
+
return () => {
|
|
1815
|
+
const auth = inject4(AuthService);
|
|
1816
|
+
const router = inject4(Router2);
|
|
1817
|
+
if (auth.isAuthenticated()) {
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1820
|
+
return router.createUrlTree([redirectTo]);
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
__name(authGuard, "authGuard");
|
|
1824
|
+
var _AuthGuard = class _AuthGuard {
|
|
1825
|
+
/**
|
|
1826
|
+
* @param auth - Authentication service
|
|
1827
|
+
* @param router - Angular router
|
|
1828
|
+
*/
|
|
1829
|
+
constructor(auth, router) {
|
|
1830
|
+
this.auth = auth;
|
|
1831
|
+
this.router = router;
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Check if route can be activated.
|
|
1835
|
+
*
|
|
1836
|
+
* @returns True if authenticated, otherwise redirects to login
|
|
1837
|
+
*/
|
|
1838
|
+
canActivate() {
|
|
1839
|
+
if (this.auth.isAuthenticated()) {
|
|
1840
|
+
return true;
|
|
1841
|
+
}
|
|
1842
|
+
return this.router.createUrlTree(["/login"]);
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
__name(_AuthGuard, "AuthGuard");
|
|
1846
|
+
var AuthGuard = _AuthGuard;
|
|
1847
|
+
|
|
1848
|
+
// src/angular/oauth-callback.guard.ts
|
|
1849
|
+
import { inject as inject5, PLATFORM_ID as PLATFORM_ID2 } from "@angular/core";
|
|
1850
|
+
import { isPlatformBrowser as isPlatformBrowser2 } from "@angular/common";
|
|
1851
|
+
var oauthCallbackGuard = /* @__PURE__ */ __name(async () => {
|
|
1852
|
+
const auth = inject5(AuthService);
|
|
1853
|
+
const config = inject5(NAUTH_CLIENT_CONFIG);
|
|
1854
|
+
const platformId = inject5(PLATFORM_ID2);
|
|
1855
|
+
const isBrowser = isPlatformBrowser2(platformId);
|
|
1856
|
+
if (!isBrowser) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
try {
|
|
1860
|
+
const response = await auth.getClient().handleOAuthCallback();
|
|
1861
|
+
if (!response) {
|
|
1862
|
+
const homeUrl = config.redirects?.success || "/";
|
|
1863
|
+
window.location.replace(homeUrl);
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
if (response.challengeName) {
|
|
1867
|
+
const challengeBase = config.redirects?.challengeBase || "/auth/challenge";
|
|
1868
|
+
const challengeRoute = response.challengeName.toLowerCase().replace(/_/g, "-");
|
|
1869
|
+
const challengePath = `${challengeBase}/${challengeRoute}`;
|
|
1870
|
+
if (config.debug) {
|
|
1871
|
+
console.warn("[oauth-callback-guard] Redirecting to challenge:", challengePath);
|
|
1872
|
+
}
|
|
1873
|
+
window.location.replace(challengePath);
|
|
1874
|
+
} else {
|
|
1875
|
+
const successUrl = config.redirects?.success || "/";
|
|
1876
|
+
if (config.debug) {
|
|
1877
|
+
console.warn("[oauth-callback-guard] Redirecting to success URL:", successUrl);
|
|
1878
|
+
}
|
|
1879
|
+
window.location.replace(successUrl);
|
|
1880
|
+
}
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
console.error("[oauth-callback-guard] OAuth callback failed:", error);
|
|
1883
|
+
const errorUrl = config.redirects?.oauthError || "/login";
|
|
1884
|
+
if (config.debug) {
|
|
1885
|
+
console.warn("[oauth-callback-guard] Redirecting to error URL:", errorUrl);
|
|
1886
|
+
}
|
|
1887
|
+
window.location.replace(errorUrl);
|
|
1888
|
+
}
|
|
1889
|
+
return false;
|
|
1890
|
+
}, "oauthCallbackGuard");
|
|
1891
|
+
|
|
1892
|
+
// src/angular/auth.module.ts
|
|
1893
|
+
import { NgModule } from "@angular/core";
|
|
1894
|
+
import { HTTP_INTERCEPTORS } from "@angular/common/http";
|
|
1895
|
+
var NAuthModule = class {
|
|
1896
|
+
/**
|
|
1897
|
+
* Configure the module with client settings.
|
|
1898
|
+
*
|
|
1899
|
+
* @param config - Client configuration
|
|
1900
|
+
*/
|
|
1901
|
+
static forRoot(config) {
|
|
1902
|
+
return {
|
|
1903
|
+
ngModule: NAuthModule,
|
|
1904
|
+
providers: [
|
|
1905
|
+
{ provide: NAUTH_CLIENT_CONFIG, useValue: config },
|
|
1906
|
+
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
|
|
1907
|
+
]
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
__name(NAuthModule, "NAuthModule");
|
|
1912
|
+
NAuthModule = __decorateClass([
|
|
1913
|
+
NgModule({})
|
|
1914
|
+
], NAuthModule);
|
|
1915
|
+
export {
|
|
1916
|
+
AngularHttpAdapter,
|
|
1917
|
+
AuthGuard,
|
|
1918
|
+
AuthInterceptor,
|
|
1919
|
+
AuthService,
|
|
1920
|
+
NAUTH_CLIENT_CONFIG,
|
|
1921
|
+
NAuthModule,
|
|
1922
|
+
authGuard,
|
|
1923
|
+
authInterceptor,
|
|
1924
|
+
oauthCallbackGuard
|
|
1925
|
+
};
|
|
1926
|
+
//# sourceMappingURL=index.mjs.map
|