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