fauxbase 0.5.6 → 0.5.8

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,9 @@ 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
+ const unwrapped = this.unwrapBody(body);
454
+ return unwrapped[preset.auth.userField] ?? unwrapped;
398
455
  }
399
456
  async httpRegister(data) {
400
457
  const preset = this.httpDriver.preset;
@@ -413,27 +470,103 @@ var AuthService = class extends Service {
413
470
  throw new ForbiddenError(body2.message ?? "Registration failed");
414
471
  }
415
472
  const body = await response.json();
416
- const token = body[preset.auth.tokenField];
417
- const user = body[preset.auth.userField] ?? body;
473
+ this.setAuthFromResponse(body, preset, data.email);
474
+ const unwrapped = this.unwrapBody(body);
475
+ return unwrapped[preset.auth.userField] ?? unwrapped;
476
+ }
477
+ async httpRefresh() {
478
+ const preset = this.httpDriver.preset;
479
+ const baseUrl = this.httpDriver.baseUrl;
480
+ const refreshUrl = preset.auth.refreshUrl;
481
+ if (!refreshUrl) {
482
+ throw new ForbiddenError("Refresh URL not configured in preset");
483
+ }
484
+ const response = await fetch(`${baseUrl}${refreshUrl}`, {
485
+ method: "POST",
486
+ headers: { "Content-Type": "application/json" },
487
+ body: JSON.stringify({ refreshToken: this.authState.refreshToken })
488
+ });
489
+ if (!response.ok) {
490
+ this.logout();
491
+ throw new ForbiddenError("Session expired. Please log in again.");
492
+ }
493
+ const body = await response.json();
494
+ const unwrapped = this.unwrapBody(body);
495
+ const token = unwrapped[preset.auth.tokenField];
496
+ const refreshToken = preset.auth.refreshTokenField ? unwrapped[preset.auth.refreshTokenField] : this.authState.refreshToken;
497
+ const expiresIn = preset.auth.expiresInField ? unwrapped[preset.auth.expiresInField] : null;
498
+ const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
499
+ this.authState = {
500
+ ...this.authState,
501
+ token,
502
+ refreshToken,
503
+ expiresAt
504
+ };
505
+ this.persistState();
506
+ this.scheduleRefresh();
507
+ return token;
508
+ }
509
+ unwrapBody(body) {
510
+ if (body.data && typeof body.data === "object" && !Array.isArray(body.data)) {
511
+ return body.data;
512
+ }
513
+ return body;
514
+ }
515
+ setAuthFromResponse(body, preset, fallbackEmail) {
516
+ const unwrapped = this.unwrapBody(body);
517
+ const token = unwrapped[preset.auth.tokenField];
518
+ const user = unwrapped[preset.auth.userField] ?? unwrapped;
519
+ const refreshToken = preset.auth.refreshTokenField ? unwrapped[preset.auth.refreshTokenField] : void 0;
520
+ const expiresIn = preset.auth.expiresInField ? unwrapped[preset.auth.expiresInField] : null;
521
+ const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
418
522
  this.authState = {
419
523
  userId: user.id,
420
- email: user.email ?? data.email,
421
- userName: user.name || user.email || data.email,
524
+ email: user.email ?? fallbackEmail,
525
+ userName: user.name || user.email || fallbackEmail,
422
526
  role: user.role,
423
- token
527
+ token,
528
+ refreshToken,
529
+ expiresAt
424
530
  };
425
531
  this.persistState();
426
- return user;
532
+ this.scheduleRefresh();
427
533
  }
428
- generateToken(user) {
534
+ // --- Token generation (local mode) ---
535
+ generateToken(user, expiresAt) {
429
536
  return btoa(JSON.stringify({
430
- userId: user.id,
537
+ userId: user.id ?? user.userId,
431
538
  email: user.email,
432
539
  role: user.role,
433
540
  iat: Date.now(),
434
- exp: Date.now() + 24 * 60 * 60 * 1e3
541
+ exp: expiresAt
435
542
  }));
436
543
  }
544
+ generateRefreshToken(user) {
545
+ return btoa(JSON.stringify({
546
+ userId: user.id ?? user.userId,
547
+ email: user.email,
548
+ role: user.role,
549
+ type: "refresh",
550
+ iat: Date.now()
551
+ }));
552
+ }
553
+ // --- Refresh scheduling ---
554
+ scheduleRefresh() {
555
+ this.clearRefreshTimer();
556
+ if (!this.authState?.expiresAt) return;
557
+ const delay = this.authState.expiresAt - Date.now() - 60 * 1e3;
558
+ if (delay <= 0) return;
559
+ this.refreshTimer = setTimeout(() => {
560
+ this.refresh().catch(() => {
561
+ });
562
+ }, delay);
563
+ }
564
+ clearRefreshTimer() {
565
+ if (this.refreshTimer) {
566
+ clearTimeout(this.refreshTimer);
567
+ this.refreshTimer = null;
568
+ }
569
+ }
437
570
  /** @internal — called by createClient to listen for auth state changes */
438
571
  _onAuthChange(listener) {
439
572
  this.authChangeListeners.push(listener);
@@ -1005,7 +1138,7 @@ var defaultPreset = definePreset({
1005
1138
  var springBootPreset = definePreset({
1006
1139
  name: "spring-boot",
1007
1140
  response: {
1008
- single: (raw) => ({ data: raw }),
1141
+ single: (raw) => ({ data: raw.data ?? raw }),
1009
1142
  list: (raw) => ({
1010
1143
  items: raw.content ?? [],
1011
1144
  meta: {
@@ -1037,7 +1170,10 @@ var springBootPreset = definePreset({
1037
1170
  auth: {
1038
1171
  loginUrl: "/api/auth/login",
1039
1172
  registerUrl: "/api/auth/register",
1173
+ refreshUrl: "/api/auth/refresh",
1040
1174
  tokenField: "token",
1175
+ refreshTokenField: "refreshToken",
1176
+ expiresInField: "expiresIn",
1041
1177
  userField: "user",
1042
1178
  headerFormat: "Bearer {token}"
1043
1179
  }
@@ -1088,7 +1224,7 @@ var laravelPreset = definePreset({
1088
1224
  var djangoPreset = definePreset({
1089
1225
  name: "django",
1090
1226
  response: {
1091
- single: (raw) => ({ data: raw }),
1227
+ single: (raw) => ({ data: raw.data ?? raw }),
1092
1228
  list: (raw) => ({
1093
1229
  items: raw.results ?? [],
1094
1230
  meta: {
@@ -1277,6 +1413,7 @@ var HttpDriver = class {
1277
1413
  defaultHeaders;
1278
1414
  endpoints = /* @__PURE__ */ new Map();
1279
1415
  authProvider = null;
1416
+ onUnauthorized = null;
1280
1417
  constructor(config) {
1281
1418
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
1282
1419
  this.preset = typeof config.preset === "string" ? getPreset(config.preset ?? "default") : config.preset ?? getPreset("default");
@@ -1288,6 +1425,10 @@ var HttpDriver = class {
1288
1425
  setAuthProvider(provider) {
1289
1426
  this.authProvider = provider;
1290
1427
  }
1428
+ /** @internal — set callback to refresh token on 401 */
1429
+ setOnUnauthorized(handler) {
1430
+ this.onUnauthorized = handler;
1431
+ }
1291
1432
  registerEndpoint(resource, endpoint) {
1292
1433
  this.endpoints.set(resource, endpoint);
1293
1434
  }
@@ -1321,6 +1462,12 @@ var HttpDriver = class {
1321
1462
  });
1322
1463
  clearTimeout(timer);
1323
1464
  if (!response.ok) {
1465
+ if (response.status === 401 && retryCount === 0 && this.onUnauthorized) {
1466
+ const refreshed = await this.onUnauthorized();
1467
+ if (refreshed) {
1468
+ return this._fetch(url, options, 1);
1469
+ }
1470
+ }
1324
1471
  if (response.status >= 500 && retryCount < this.maxRetries) {
1325
1472
  const delay = this.baseDelay * Math.pow(2, retryCount);
1326
1473
  await new Promise((r) => setTimeout(r, delay));
@@ -1444,7 +1591,7 @@ var HttpDriver = class {
1444
1591
  url += `?${params.toString()}`;
1445
1592
  }
1446
1593
  return this._fetch(url, {
1447
- method: options?.method ?? "POST",
1594
+ method: options?.method ?? (options?.body !== void 0 ? "POST" : "GET"),
1448
1595
  body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
1449
1596
  });
1450
1597
  }
@@ -1684,10 +1831,25 @@ function createClient(config) {
1684
1831
  } else if (defaultDriver instanceof HttpDriver) {
1685
1832
  authInstance._init(defaultDriver, resourceName);
1686
1833
  defaultDriver.registerEndpoint(resourceName, authInstance.endpoint);
1834
+ const hasLocalStorage = typeof localStorage !== "undefined";
1835
+ const LS_AUTH_KEY = "fauxbase:auth";
1687
1836
  let memoryAuthState = null;
1688
1837
  authInstance._initAuth(
1689
- () => memoryAuthState,
1838
+ () => {
1839
+ if (hasLocalStorage) {
1840
+ const raw = localStorage.getItem(LS_AUTH_KEY);
1841
+ return raw ? JSON.parse(raw) : null;
1842
+ }
1843
+ return memoryAuthState;
1844
+ },
1690
1845
  (state) => {
1846
+ if (hasLocalStorage) {
1847
+ if (state) {
1848
+ localStorage.setItem(LS_AUTH_KEY, JSON.stringify(state));
1849
+ } else {
1850
+ localStorage.removeItem(LS_AUTH_KEY);
1851
+ }
1852
+ }
1691
1853
  memoryAuthState = state;
1692
1854
  }
1693
1855
  );
@@ -1696,6 +1858,14 @@ function createClient(config) {
1696
1858
  const token = authInstance.token;
1697
1859
  return token ? { token } : null;
1698
1860
  });
1861
+ defaultDriver.setOnUnauthorized(async () => {
1862
+ try {
1863
+ await authInstance.refresh();
1864
+ return true;
1865
+ } catch {
1866
+ return false;
1867
+ }
1868
+ });
1699
1869
  }
1700
1870
  client.auth = authInstance;
1701
1871
  for (const driver of overrideDrivers.values()) {
@@ -1704,6 +1874,14 @@ function createClient(config) {
1704
1874
  const token = client.auth?.token;
1705
1875
  return token ? { token } : null;
1706
1876
  });
1877
+ driver.setOnUnauthorized(async () => {
1878
+ try {
1879
+ await client.auth.refresh();
1880
+ return true;
1881
+ } catch {
1882
+ return false;
1883
+ }
1884
+ });
1707
1885
  }
1708
1886
  }
1709
1887
  }