fauxbase 0.5.5 → 0.5.7
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/dist/index.cjs +221 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +221 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -50,6 +50,11 @@ interface LocalDriverConfig {
|
|
|
50
50
|
type: 'local';
|
|
51
51
|
persist?: 'memory' | 'localStorage' | 'indexeddb';
|
|
52
52
|
dbName?: string;
|
|
53
|
+
latency?: number | {
|
|
54
|
+
min: number;
|
|
55
|
+
max: number;
|
|
56
|
+
};
|
|
57
|
+
errorRate?: number;
|
|
53
58
|
}
|
|
54
59
|
interface HttpDriverConfig {
|
|
55
60
|
type: 'http';
|
|
@@ -259,8 +264,11 @@ declare class HttpDriver implements Driver {
|
|
|
259
264
|
private defaultHeaders;
|
|
260
265
|
private endpoints;
|
|
261
266
|
private authProvider;
|
|
267
|
+
private onUnauthorized;
|
|
262
268
|
constructor(config: HttpDriverOptions);
|
|
263
269
|
setAuthProvider(provider: AuthProvider$1): void;
|
|
270
|
+
/** @internal — set callback to refresh token on 401 */
|
|
271
|
+
setOnUnauthorized(handler: () => Promise<boolean>): void;
|
|
264
272
|
registerEndpoint(resource: string, endpoint: string): void;
|
|
265
273
|
private getEndpoint;
|
|
266
274
|
private buildUrl;
|
|
@@ -303,6 +311,8 @@ interface AuthState {
|
|
|
303
311
|
userName?: string;
|
|
304
312
|
role?: string;
|
|
305
313
|
token: string;
|
|
314
|
+
refreshToken?: string;
|
|
315
|
+
expiresAt?: number;
|
|
306
316
|
}
|
|
307
317
|
interface AuthContext {
|
|
308
318
|
userId: string;
|
|
@@ -313,6 +323,8 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
|
|
|
313
323
|
private saveState;
|
|
314
324
|
private httpDriver;
|
|
315
325
|
private authChangeListeners;
|
|
326
|
+
private refreshTimer;
|
|
327
|
+
private _isRefreshing;
|
|
316
328
|
/** @internal — called by createClient to wire persistence */
|
|
317
329
|
_initAuth(loadState: () => AuthState | null, saveState: (state: AuthState | null) => void): void;
|
|
318
330
|
/** @internal — called by createClient when using HttpDriver */
|
|
@@ -320,16 +332,32 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
|
|
|
320
332
|
login(credentials: LoginCredentials): Promise<T>;
|
|
321
333
|
register(data: Partial<T>): Promise<T>;
|
|
322
334
|
logout(): void;
|
|
335
|
+
/** Manually refresh the token. Returns the new token. */
|
|
336
|
+
refresh(): Promise<string>;
|
|
337
|
+
/**
|
|
338
|
+
* Ensure the token is valid before making a request.
|
|
339
|
+
* If expired, auto-refreshes. Safe to call concurrently.
|
|
340
|
+
*/
|
|
341
|
+
ensureValidToken(): Promise<void>;
|
|
323
342
|
get currentUser(): T | null;
|
|
324
343
|
get isLoggedIn(): boolean;
|
|
325
344
|
get token(): string | null;
|
|
345
|
+
get refreshToken(): string | null;
|
|
346
|
+
get expiresAt(): number | null;
|
|
347
|
+
get isExpired(): boolean;
|
|
326
348
|
hasRole(role: string): boolean;
|
|
327
349
|
getAuthContext(): AuthContext | null;
|
|
328
350
|
private localLogin;
|
|
329
351
|
private localRegister;
|
|
352
|
+
private localRefresh;
|
|
330
353
|
private httpLogin;
|
|
331
354
|
private httpRegister;
|
|
355
|
+
private httpRefresh;
|
|
356
|
+
private setAuthFromResponse;
|
|
332
357
|
private generateToken;
|
|
358
|
+
private generateRefreshToken;
|
|
359
|
+
private scheduleRefresh;
|
|
360
|
+
private clearRefreshTimer;
|
|
333
361
|
/** @internal — called by createClient to listen for auth state changes */
|
|
334
362
|
_onAuthChange(listener: () => void): void;
|
|
335
363
|
private persistState;
|
|
@@ -378,7 +406,11 @@ declare class LocalDriver implements Driver {
|
|
|
378
406
|
private authProvider;
|
|
379
407
|
private _ready;
|
|
380
408
|
private _isReady;
|
|
409
|
+
private latencyMs;
|
|
410
|
+
private errorRate;
|
|
381
411
|
constructor(config: LocalDriverConfig);
|
|
412
|
+
private simulate;
|
|
413
|
+
private getLatency;
|
|
382
414
|
get ready(): Promise<void>;
|
|
383
415
|
get isReady(): boolean;
|
|
384
416
|
setAuthProvider(provider: AuthProvider): void;
|
|
@@ -445,7 +477,10 @@ interface Preset {
|
|
|
445
477
|
loginUrl: string;
|
|
446
478
|
registerUrl: string;
|
|
447
479
|
logoutUrl?: string;
|
|
480
|
+
refreshUrl?: string;
|
|
448
481
|
tokenField: string;
|
|
482
|
+
refreshTokenField?: string;
|
|
483
|
+
expiresInField?: string;
|
|
449
484
|
userField: string;
|
|
450
485
|
headerFormat: string;
|
|
451
486
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,11 @@ interface LocalDriverConfig {
|
|
|
50
50
|
type: 'local';
|
|
51
51
|
persist?: 'memory' | 'localStorage' | 'indexeddb';
|
|
52
52
|
dbName?: string;
|
|
53
|
+
latency?: number | {
|
|
54
|
+
min: number;
|
|
55
|
+
max: number;
|
|
56
|
+
};
|
|
57
|
+
errorRate?: number;
|
|
53
58
|
}
|
|
54
59
|
interface HttpDriverConfig {
|
|
55
60
|
type: 'http';
|
|
@@ -259,8 +264,11 @@ declare class HttpDriver implements Driver {
|
|
|
259
264
|
private defaultHeaders;
|
|
260
265
|
private endpoints;
|
|
261
266
|
private authProvider;
|
|
267
|
+
private onUnauthorized;
|
|
262
268
|
constructor(config: HttpDriverOptions);
|
|
263
269
|
setAuthProvider(provider: AuthProvider$1): void;
|
|
270
|
+
/** @internal — set callback to refresh token on 401 */
|
|
271
|
+
setOnUnauthorized(handler: () => Promise<boolean>): void;
|
|
264
272
|
registerEndpoint(resource: string, endpoint: string): void;
|
|
265
273
|
private getEndpoint;
|
|
266
274
|
private buildUrl;
|
|
@@ -303,6 +311,8 @@ interface AuthState {
|
|
|
303
311
|
userName?: string;
|
|
304
312
|
role?: string;
|
|
305
313
|
token: string;
|
|
314
|
+
refreshToken?: string;
|
|
315
|
+
expiresAt?: number;
|
|
306
316
|
}
|
|
307
317
|
interface AuthContext {
|
|
308
318
|
userId: string;
|
|
@@ -313,6 +323,8 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
|
|
|
313
323
|
private saveState;
|
|
314
324
|
private httpDriver;
|
|
315
325
|
private authChangeListeners;
|
|
326
|
+
private refreshTimer;
|
|
327
|
+
private _isRefreshing;
|
|
316
328
|
/** @internal — called by createClient to wire persistence */
|
|
317
329
|
_initAuth(loadState: () => AuthState | null, saveState: (state: AuthState | null) => void): void;
|
|
318
330
|
/** @internal — called by createClient when using HttpDriver */
|
|
@@ -320,16 +332,32 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
|
|
|
320
332
|
login(credentials: LoginCredentials): Promise<T>;
|
|
321
333
|
register(data: Partial<T>): Promise<T>;
|
|
322
334
|
logout(): void;
|
|
335
|
+
/** Manually refresh the token. Returns the new token. */
|
|
336
|
+
refresh(): Promise<string>;
|
|
337
|
+
/**
|
|
338
|
+
* Ensure the token is valid before making a request.
|
|
339
|
+
* If expired, auto-refreshes. Safe to call concurrently.
|
|
340
|
+
*/
|
|
341
|
+
ensureValidToken(): Promise<void>;
|
|
323
342
|
get currentUser(): T | null;
|
|
324
343
|
get isLoggedIn(): boolean;
|
|
325
344
|
get token(): string | null;
|
|
345
|
+
get refreshToken(): string | null;
|
|
346
|
+
get expiresAt(): number | null;
|
|
347
|
+
get isExpired(): boolean;
|
|
326
348
|
hasRole(role: string): boolean;
|
|
327
349
|
getAuthContext(): AuthContext | null;
|
|
328
350
|
private localLogin;
|
|
329
351
|
private localRegister;
|
|
352
|
+
private localRefresh;
|
|
330
353
|
private httpLogin;
|
|
331
354
|
private httpRegister;
|
|
355
|
+
private httpRefresh;
|
|
356
|
+
private setAuthFromResponse;
|
|
332
357
|
private generateToken;
|
|
358
|
+
private generateRefreshToken;
|
|
359
|
+
private scheduleRefresh;
|
|
360
|
+
private clearRefreshTimer;
|
|
333
361
|
/** @internal — called by createClient to listen for auth state changes */
|
|
334
362
|
_onAuthChange(listener: () => void): void;
|
|
335
363
|
private persistState;
|
|
@@ -378,7 +406,11 @@ declare class LocalDriver implements Driver {
|
|
|
378
406
|
private authProvider;
|
|
379
407
|
private _ready;
|
|
380
408
|
private _isReady;
|
|
409
|
+
private latencyMs;
|
|
410
|
+
private errorRate;
|
|
381
411
|
constructor(config: LocalDriverConfig);
|
|
412
|
+
private simulate;
|
|
413
|
+
private getLatency;
|
|
382
414
|
get ready(): Promise<void>;
|
|
383
415
|
get isReady(): boolean;
|
|
384
416
|
setAuthProvider(provider: AuthProvider): void;
|
|
@@ -445,7 +477,10 @@ interface Preset {
|
|
|
445
477
|
loginUrl: string;
|
|
446
478
|
registerUrl: string;
|
|
447
479
|
logoutUrl?: string;
|
|
480
|
+
refreshUrl?: string;
|
|
448
481
|
tokenField: string;
|
|
482
|
+
refreshTokenField?: string;
|
|
483
|
+
expiresInField?: string;
|
|
449
484
|
userField: string;
|
|
450
485
|
headerFormat: string;
|
|
451
486
|
};
|
package/dist/index.js
CHANGED
|
@@ -277,10 +277,15 @@ var AuthService = class extends Service {
|
|
|
277
277
|
saveState = null;
|
|
278
278
|
httpDriver = null;
|
|
279
279
|
authChangeListeners = [];
|
|
280
|
+
refreshTimer = null;
|
|
281
|
+
_isRefreshing = null;
|
|
280
282
|
/** @internal — called by createClient to wire persistence */
|
|
281
283
|
_initAuth(loadState, saveState) {
|
|
282
284
|
this.saveState = saveState;
|
|
283
285
|
this.authState = loadState();
|
|
286
|
+
if (this.authState?.expiresAt) {
|
|
287
|
+
this.scheduleRefresh();
|
|
288
|
+
}
|
|
284
289
|
}
|
|
285
290
|
/** @internal — called by createClient when using HttpDriver */
|
|
286
291
|
_setHttpMode(driver) {
|
|
@@ -299,9 +304,38 @@ var AuthService = class extends Service {
|
|
|
299
304
|
return this.localRegister(data);
|
|
300
305
|
}
|
|
301
306
|
logout() {
|
|
307
|
+
this.clearRefreshTimer();
|
|
302
308
|
this.authState = null;
|
|
303
309
|
this.persistState();
|
|
304
310
|
}
|
|
311
|
+
/** Manually refresh the token. Returns the new token. */
|
|
312
|
+
async refresh() {
|
|
313
|
+
if (!this.authState?.refreshToken) {
|
|
314
|
+
throw new ForbiddenError("No refresh token available");
|
|
315
|
+
}
|
|
316
|
+
if (this.httpDriver) {
|
|
317
|
+
return this.httpRefresh();
|
|
318
|
+
}
|
|
319
|
+
return this.localRefresh();
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Ensure the token is valid before making a request.
|
|
323
|
+
* If expired, auto-refreshes. Safe to call concurrently.
|
|
324
|
+
*/
|
|
325
|
+
async ensureValidToken() {
|
|
326
|
+
if (!this.authState) return;
|
|
327
|
+
if (!this.authState.expiresAt) return;
|
|
328
|
+
const buffer = 30 * 1e3;
|
|
329
|
+
if (Date.now() + buffer >= this.authState.expiresAt) {
|
|
330
|
+
if (!this._isRefreshing) {
|
|
331
|
+
this._isRefreshing = this.refresh().then(() => {
|
|
332
|
+
}).finally(() => {
|
|
333
|
+
this._isRefreshing = null;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
await this._isRefreshing;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
305
339
|
get currentUser() {
|
|
306
340
|
return this.authState ? { id: this.authState.userId, email: this.authState.email } : null;
|
|
307
341
|
}
|
|
@@ -311,6 +345,16 @@ var AuthService = class extends Service {
|
|
|
311
345
|
get token() {
|
|
312
346
|
return this.authState?.token ?? null;
|
|
313
347
|
}
|
|
348
|
+
get refreshToken() {
|
|
349
|
+
return this.authState?.refreshToken ?? null;
|
|
350
|
+
}
|
|
351
|
+
get expiresAt() {
|
|
352
|
+
return this.authState?.expiresAt ?? null;
|
|
353
|
+
}
|
|
354
|
+
get isExpired() {
|
|
355
|
+
if (!this.authState?.expiresAt) return false;
|
|
356
|
+
return Date.now() >= this.authState.expiresAt;
|
|
357
|
+
}
|
|
314
358
|
hasRole(role) {
|
|
315
359
|
return this.authState?.role === role;
|
|
316
360
|
}
|
|
@@ -321,7 +365,7 @@ var AuthService = class extends Service {
|
|
|
321
365
|
userName: this.authState.userName
|
|
322
366
|
};
|
|
323
367
|
}
|
|
324
|
-
// --- Local mode
|
|
368
|
+
// --- Local mode ---
|
|
325
369
|
async localLogin(credentials) {
|
|
326
370
|
const { items } = await this.list({ filter: { email: credentials.email } });
|
|
327
371
|
if (items.length === 0) {
|
|
@@ -331,14 +375,18 @@ var AuthService = class extends Service {
|
|
|
331
375
|
if (user.password !== credentials.password) {
|
|
332
376
|
throw new ForbiddenError("Invalid email or password");
|
|
333
377
|
}
|
|
378
|
+
const expiresAt = Date.now() + 60 * 60 * 1e3;
|
|
334
379
|
this.authState = {
|
|
335
380
|
userId: user.id,
|
|
336
381
|
email: user.email,
|
|
337
382
|
userName: user.name || user.email,
|
|
338
383
|
role: user.role,
|
|
339
|
-
token: this.generateToken(user)
|
|
384
|
+
token: this.generateToken(user, expiresAt),
|
|
385
|
+
refreshToken: this.generateRefreshToken(user),
|
|
386
|
+
expiresAt
|
|
340
387
|
};
|
|
341
388
|
this.persistState();
|
|
389
|
+
this.scheduleRefresh();
|
|
342
390
|
return user;
|
|
343
391
|
}
|
|
344
392
|
async localRegister(data) {
|
|
@@ -351,16 +399,33 @@ var AuthService = class extends Service {
|
|
|
351
399
|
}
|
|
352
400
|
const { data: user } = await this.create(data);
|
|
353
401
|
const u = user;
|
|
402
|
+
const expiresAt = Date.now() + 60 * 60 * 1e3;
|
|
354
403
|
this.authState = {
|
|
355
404
|
userId: u.id,
|
|
356
405
|
email: u.email,
|
|
357
406
|
userName: u.name || u.email,
|
|
358
407
|
role: u.role,
|
|
359
|
-
token: this.generateToken(u)
|
|
408
|
+
token: this.generateToken(u, expiresAt),
|
|
409
|
+
refreshToken: this.generateRefreshToken(u),
|
|
410
|
+
expiresAt
|
|
360
411
|
};
|
|
361
412
|
this.persistState();
|
|
413
|
+
this.scheduleRefresh();
|
|
362
414
|
return user;
|
|
363
415
|
}
|
|
416
|
+
async localRefresh() {
|
|
417
|
+
const payload = JSON.parse(atob(this.authState.refreshToken));
|
|
418
|
+
const expiresAt = Date.now() + 60 * 60 * 1e3;
|
|
419
|
+
this.authState = {
|
|
420
|
+
...this.authState,
|
|
421
|
+
token: this.generateToken(payload, expiresAt),
|
|
422
|
+
refreshToken: this.generateRefreshToken(payload),
|
|
423
|
+
expiresAt
|
|
424
|
+
};
|
|
425
|
+
this.persistState();
|
|
426
|
+
this.scheduleRefresh();
|
|
427
|
+
return this.authState.token;
|
|
428
|
+
}
|
|
364
429
|
// --- HTTP mode ---
|
|
365
430
|
async httpLogin(credentials) {
|
|
366
431
|
const preset = this.httpDriver.preset;
|
|
@@ -382,17 +447,8 @@ var AuthService = class extends Service {
|
|
|
382
447
|
throw new ForbiddenError(body2.message ?? "Login failed");
|
|
383
448
|
}
|
|
384
449
|
const body = await response.json();
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
this.authState = {
|
|
388
|
-
userId: user.id,
|
|
389
|
-
email: user.email ?? credentials.email,
|
|
390
|
-
userName: user.name || user.email || credentials.email,
|
|
391
|
-
role: user.role,
|
|
392
|
-
token
|
|
393
|
-
};
|
|
394
|
-
this.persistState();
|
|
395
|
-
return user;
|
|
450
|
+
this.setAuthFromResponse(body, preset, credentials.email);
|
|
451
|
+
return body[preset.auth.userField] ?? body;
|
|
396
452
|
}
|
|
397
453
|
async httpRegister(data) {
|
|
398
454
|
const preset = this.httpDriver.preset;
|
|
@@ -411,27 +467,94 @@ var AuthService = class extends Service {
|
|
|
411
467
|
throw new ForbiddenError(body2.message ?? "Registration failed");
|
|
412
468
|
}
|
|
413
469
|
const body = await response.json();
|
|
470
|
+
this.setAuthFromResponse(body, preset, data.email);
|
|
471
|
+
return body[preset.auth.userField] ?? body;
|
|
472
|
+
}
|
|
473
|
+
async httpRefresh() {
|
|
474
|
+
const preset = this.httpDriver.preset;
|
|
475
|
+
const baseUrl = this.httpDriver.baseUrl;
|
|
476
|
+
const refreshUrl = preset.auth.refreshUrl;
|
|
477
|
+
if (!refreshUrl) {
|
|
478
|
+
throw new ForbiddenError("Refresh URL not configured in preset");
|
|
479
|
+
}
|
|
480
|
+
const response = await fetch(`${baseUrl}${refreshUrl}`, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: { "Content-Type": "application/json" },
|
|
483
|
+
body: JSON.stringify({ refreshToken: this.authState.refreshToken })
|
|
484
|
+
});
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
this.logout();
|
|
487
|
+
throw new ForbiddenError("Session expired. Please log in again.");
|
|
488
|
+
}
|
|
489
|
+
const body = await response.json();
|
|
490
|
+
const token = body[preset.auth.tokenField];
|
|
491
|
+
const refreshToken = preset.auth.refreshTokenField ? body[preset.auth.refreshTokenField] : this.authState.refreshToken;
|
|
492
|
+
const expiresIn = preset.auth.expiresInField ? body[preset.auth.expiresInField] : null;
|
|
493
|
+
const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
|
|
494
|
+
this.authState = {
|
|
495
|
+
...this.authState,
|
|
496
|
+
token,
|
|
497
|
+
refreshToken,
|
|
498
|
+
expiresAt
|
|
499
|
+
};
|
|
500
|
+
this.persistState();
|
|
501
|
+
this.scheduleRefresh();
|
|
502
|
+
return token;
|
|
503
|
+
}
|
|
504
|
+
setAuthFromResponse(body, preset, fallbackEmail) {
|
|
414
505
|
const token = body[preset.auth.tokenField];
|
|
415
506
|
const user = body[preset.auth.userField] ?? body;
|
|
507
|
+
const refreshToken = preset.auth.refreshTokenField ? body[preset.auth.refreshTokenField] : void 0;
|
|
508
|
+
const expiresIn = preset.auth.expiresInField ? body[preset.auth.expiresInField] : null;
|
|
509
|
+
const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
|
|
416
510
|
this.authState = {
|
|
417
511
|
userId: user.id,
|
|
418
|
-
email: user.email ??
|
|
419
|
-
userName: user.name || user.email ||
|
|
512
|
+
email: user.email ?? fallbackEmail,
|
|
513
|
+
userName: user.name || user.email || fallbackEmail,
|
|
420
514
|
role: user.role,
|
|
421
|
-
token
|
|
515
|
+
token,
|
|
516
|
+
refreshToken,
|
|
517
|
+
expiresAt
|
|
422
518
|
};
|
|
423
519
|
this.persistState();
|
|
424
|
-
|
|
520
|
+
this.scheduleRefresh();
|
|
425
521
|
}
|
|
426
|
-
|
|
522
|
+
// --- Token generation (local mode) ---
|
|
523
|
+
generateToken(user, expiresAt) {
|
|
427
524
|
return btoa(JSON.stringify({
|
|
428
|
-
userId: user.id,
|
|
525
|
+
userId: user.id ?? user.userId,
|
|
429
526
|
email: user.email,
|
|
430
527
|
role: user.role,
|
|
431
528
|
iat: Date.now(),
|
|
432
|
-
exp:
|
|
529
|
+
exp: expiresAt
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
generateRefreshToken(user) {
|
|
533
|
+
return btoa(JSON.stringify({
|
|
534
|
+
userId: user.id ?? user.userId,
|
|
535
|
+
email: user.email,
|
|
536
|
+
role: user.role,
|
|
537
|
+
type: "refresh",
|
|
538
|
+
iat: Date.now()
|
|
433
539
|
}));
|
|
434
540
|
}
|
|
541
|
+
// --- Refresh scheduling ---
|
|
542
|
+
scheduleRefresh() {
|
|
543
|
+
this.clearRefreshTimer();
|
|
544
|
+
if (!this.authState?.expiresAt) return;
|
|
545
|
+
const delay = this.authState.expiresAt - Date.now() - 60 * 1e3;
|
|
546
|
+
if (delay <= 0) return;
|
|
547
|
+
this.refreshTimer = setTimeout(() => {
|
|
548
|
+
this.refresh().catch(() => {
|
|
549
|
+
});
|
|
550
|
+
}, delay);
|
|
551
|
+
}
|
|
552
|
+
clearRefreshTimer() {
|
|
553
|
+
if (this.refreshTimer) {
|
|
554
|
+
clearTimeout(this.refreshTimer);
|
|
555
|
+
this.refreshTimer = null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
435
558
|
/** @internal — called by createClient to listen for auth state changes */
|
|
436
559
|
_onAuthChange(listener) {
|
|
437
560
|
this.authChangeListeners.push(listener);
|
|
@@ -746,7 +869,11 @@ var LocalDriver = class {
|
|
|
746
869
|
authProvider = null;
|
|
747
870
|
_ready;
|
|
748
871
|
_isReady;
|
|
872
|
+
latencyMs;
|
|
873
|
+
errorRate;
|
|
749
874
|
constructor(config) {
|
|
875
|
+
this.latencyMs = config.latency ?? 0;
|
|
876
|
+
this.errorRate = config.errorRate ?? 0;
|
|
750
877
|
if (config.persist === "indexeddb") {
|
|
751
878
|
const backend = new IndexedDBBackend(config.dbName ?? "fauxbase");
|
|
752
879
|
this.storage = backend;
|
|
@@ -760,6 +887,27 @@ var LocalDriver = class {
|
|
|
760
887
|
this._ready = Promise.resolve();
|
|
761
888
|
}
|
|
762
889
|
}
|
|
890
|
+
async simulate() {
|
|
891
|
+
if (this.errorRate > 0 && Math.random() < this.errorRate) {
|
|
892
|
+
const errors = [
|
|
893
|
+
() => new NetworkError("Simulated network failure"),
|
|
894
|
+
() => new TimeoutError("Simulated request timeout"),
|
|
895
|
+
() => new NetworkError("Simulated connection refused")
|
|
896
|
+
];
|
|
897
|
+
const delay2 = this.getLatency();
|
|
898
|
+
if (delay2 > 0) await new Promise((r) => setTimeout(r, delay2 / 2));
|
|
899
|
+
throw errors[Math.floor(Math.random() * errors.length)]();
|
|
900
|
+
}
|
|
901
|
+
const delay = this.getLatency();
|
|
902
|
+
if (delay > 0) {
|
|
903
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
getLatency() {
|
|
907
|
+
if (typeof this.latencyMs === "number") return this.latencyMs;
|
|
908
|
+
const { min, max } = this.latencyMs;
|
|
909
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
910
|
+
}
|
|
763
911
|
get ready() {
|
|
764
912
|
return this._ready;
|
|
765
913
|
}
|
|
@@ -776,12 +924,14 @@ var LocalDriver = class {
|
|
|
776
924
|
this.entityClasses.set(resource, entityClass);
|
|
777
925
|
}
|
|
778
926
|
async list(resource, query) {
|
|
927
|
+
await this.simulate();
|
|
779
928
|
const items = this.storage.getAll(resource);
|
|
780
929
|
const entityClass = this.entityClasses.get(resource);
|
|
781
930
|
const processed = entityClass ? items.map((item) => applyComputedFields(item, entityClass)) : items;
|
|
782
931
|
return executeQuery(processed, query);
|
|
783
932
|
}
|
|
784
933
|
async get(resource, id) {
|
|
934
|
+
await this.simulate();
|
|
785
935
|
const item = this.storage.getById(resource, id);
|
|
786
936
|
if (!item || item.deletedAt) {
|
|
787
937
|
throw new NotFoundError(`${resource} with id "${id}" not found`);
|
|
@@ -791,6 +941,7 @@ var LocalDriver = class {
|
|
|
791
941
|
return { data };
|
|
792
942
|
}
|
|
793
943
|
async create(resource, data) {
|
|
944
|
+
await this.simulate();
|
|
794
945
|
const entityClass = this.entityClasses.get(resource);
|
|
795
946
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
796
947
|
const authContext = this.authProvider?.();
|
|
@@ -817,6 +968,7 @@ var LocalDriver = class {
|
|
|
817
968
|
return { data: result };
|
|
818
969
|
}
|
|
819
970
|
async update(resource, id, data) {
|
|
971
|
+
await this.simulate();
|
|
820
972
|
const existing = this.storage.getById(resource, id);
|
|
821
973
|
if (!existing || existing.deletedAt) {
|
|
822
974
|
throw new NotFoundError(`${resource} with id "${id}" not found`);
|
|
@@ -843,6 +995,7 @@ var LocalDriver = class {
|
|
|
843
995
|
return { data: result };
|
|
844
996
|
}
|
|
845
997
|
async delete(resource, id) {
|
|
998
|
+
await this.simulate();
|
|
846
999
|
const existing = this.storage.getById(resource, id);
|
|
847
1000
|
if (!existing || existing.deletedAt) {
|
|
848
1001
|
throw new NotFoundError(`${resource} with id "${id}" not found`);
|
|
@@ -865,6 +1018,7 @@ var LocalDriver = class {
|
|
|
865
1018
|
return { data: record };
|
|
866
1019
|
}
|
|
867
1020
|
async count(resource, filter) {
|
|
1021
|
+
await this.simulate();
|
|
868
1022
|
let items = this.storage.getAll(resource).filter((item) => !item.deletedAt);
|
|
869
1023
|
if (filter) {
|
|
870
1024
|
items = applyFilters(items, filter);
|
|
@@ -1004,7 +1158,10 @@ var springBootPreset = definePreset({
|
|
|
1004
1158
|
auth: {
|
|
1005
1159
|
loginUrl: "/api/auth/login",
|
|
1006
1160
|
registerUrl: "/api/auth/register",
|
|
1161
|
+
refreshUrl: "/api/auth/refresh",
|
|
1007
1162
|
tokenField: "token",
|
|
1163
|
+
refreshTokenField: "refreshToken",
|
|
1164
|
+
expiresInField: "expiresIn",
|
|
1008
1165
|
userField: "user",
|
|
1009
1166
|
headerFormat: "Bearer {token}"
|
|
1010
1167
|
}
|
|
@@ -1244,6 +1401,7 @@ var HttpDriver = class {
|
|
|
1244
1401
|
defaultHeaders;
|
|
1245
1402
|
endpoints = /* @__PURE__ */ new Map();
|
|
1246
1403
|
authProvider = null;
|
|
1404
|
+
onUnauthorized = null;
|
|
1247
1405
|
constructor(config) {
|
|
1248
1406
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
1249
1407
|
this.preset = typeof config.preset === "string" ? getPreset(config.preset ?? "default") : config.preset ?? getPreset("default");
|
|
@@ -1255,6 +1413,10 @@ var HttpDriver = class {
|
|
|
1255
1413
|
setAuthProvider(provider) {
|
|
1256
1414
|
this.authProvider = provider;
|
|
1257
1415
|
}
|
|
1416
|
+
/** @internal — set callback to refresh token on 401 */
|
|
1417
|
+
setOnUnauthorized(handler) {
|
|
1418
|
+
this.onUnauthorized = handler;
|
|
1419
|
+
}
|
|
1258
1420
|
registerEndpoint(resource, endpoint) {
|
|
1259
1421
|
this.endpoints.set(resource, endpoint);
|
|
1260
1422
|
}
|
|
@@ -1288,6 +1450,12 @@ var HttpDriver = class {
|
|
|
1288
1450
|
});
|
|
1289
1451
|
clearTimeout(timer);
|
|
1290
1452
|
if (!response.ok) {
|
|
1453
|
+
if (response.status === 401 && retryCount === 0 && this.onUnauthorized) {
|
|
1454
|
+
const refreshed = await this.onUnauthorized();
|
|
1455
|
+
if (refreshed) {
|
|
1456
|
+
return this._fetch(url, options, 1);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1291
1459
|
if (response.status >= 500 && retryCount < this.maxRetries) {
|
|
1292
1460
|
const delay = this.baseDelay * Math.pow(2, retryCount);
|
|
1293
1461
|
await new Promise((r) => setTimeout(r, delay));
|
|
@@ -1651,10 +1819,25 @@ function createClient(config) {
|
|
|
1651
1819
|
} else if (defaultDriver instanceof HttpDriver) {
|
|
1652
1820
|
authInstance._init(defaultDriver, resourceName);
|
|
1653
1821
|
defaultDriver.registerEndpoint(resourceName, authInstance.endpoint);
|
|
1822
|
+
const hasLocalStorage = typeof localStorage !== "undefined";
|
|
1823
|
+
const LS_AUTH_KEY = "fauxbase:auth";
|
|
1654
1824
|
let memoryAuthState = null;
|
|
1655
1825
|
authInstance._initAuth(
|
|
1656
|
-
() =>
|
|
1826
|
+
() => {
|
|
1827
|
+
if (hasLocalStorage) {
|
|
1828
|
+
const raw = localStorage.getItem(LS_AUTH_KEY);
|
|
1829
|
+
return raw ? JSON.parse(raw) : null;
|
|
1830
|
+
}
|
|
1831
|
+
return memoryAuthState;
|
|
1832
|
+
},
|
|
1657
1833
|
(state) => {
|
|
1834
|
+
if (hasLocalStorage) {
|
|
1835
|
+
if (state) {
|
|
1836
|
+
localStorage.setItem(LS_AUTH_KEY, JSON.stringify(state));
|
|
1837
|
+
} else {
|
|
1838
|
+
localStorage.removeItem(LS_AUTH_KEY);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1658
1841
|
memoryAuthState = state;
|
|
1659
1842
|
}
|
|
1660
1843
|
);
|
|
@@ -1663,6 +1846,14 @@ function createClient(config) {
|
|
|
1663
1846
|
const token = authInstance.token;
|
|
1664
1847
|
return token ? { token } : null;
|
|
1665
1848
|
});
|
|
1849
|
+
defaultDriver.setOnUnauthorized(async () => {
|
|
1850
|
+
try {
|
|
1851
|
+
await authInstance.refresh();
|
|
1852
|
+
return true;
|
|
1853
|
+
} catch {
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1666
1857
|
}
|
|
1667
1858
|
client.auth = authInstance;
|
|
1668
1859
|
for (const driver of overrideDrivers.values()) {
|
|
@@ -1671,6 +1862,14 @@ function createClient(config) {
|
|
|
1671
1862
|
const token = client.auth?.token;
|
|
1672
1863
|
return token ? { token } : null;
|
|
1673
1864
|
});
|
|
1865
|
+
driver.setOnUnauthorized(async () => {
|
|
1866
|
+
try {
|
|
1867
|
+
await client.auth.refresh();
|
|
1868
|
+
return true;
|
|
1869
|
+
} catch {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1674
1873
|
}
|
|
1675
1874
|
}
|
|
1676
1875
|
}
|