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 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 (original implementation) ---
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
- const token = body[preset.auth.tokenField];
388
- const user = body[preset.auth.userField] ?? body;
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 ?? data.email,
421
- userName: user.name || user.email || data.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
- return user;
522
+ this.scheduleRefresh();
427
523
  }
428
- generateToken(user) {
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: Date.now() + 24 * 60 * 60 * 1e3
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
- () => memoryAuthState,
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
  }