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.d.cts CHANGED
@@ -264,8 +264,11 @@ declare class HttpDriver implements Driver {
264
264
  private defaultHeaders;
265
265
  private endpoints;
266
266
  private authProvider;
267
+ private onUnauthorized;
267
268
  constructor(config: HttpDriverOptions);
268
269
  setAuthProvider(provider: AuthProvider$1): void;
270
+ /** @internal — set callback to refresh token on 401 */
271
+ setOnUnauthorized(handler: () => Promise<boolean>): void;
269
272
  registerEndpoint(resource: string, endpoint: string): void;
270
273
  private getEndpoint;
271
274
  private buildUrl;
@@ -308,6 +311,8 @@ interface AuthState {
308
311
  userName?: string;
309
312
  role?: string;
310
313
  token: string;
314
+ refreshToken?: string;
315
+ expiresAt?: number;
311
316
  }
312
317
  interface AuthContext {
313
318
  userId: string;
@@ -318,6 +323,8 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
318
323
  private saveState;
319
324
  private httpDriver;
320
325
  private authChangeListeners;
326
+ private refreshTimer;
327
+ private _isRefreshing;
321
328
  /** @internal — called by createClient to wire persistence */
322
329
  _initAuth(loadState: () => AuthState | null, saveState: (state: AuthState | null) => void): void;
323
330
  /** @internal — called by createClient when using HttpDriver */
@@ -325,16 +332,33 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
325
332
  login(credentials: LoginCredentials): Promise<T>;
326
333
  register(data: Partial<T>): Promise<T>;
327
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>;
328
342
  get currentUser(): T | null;
329
343
  get isLoggedIn(): boolean;
330
344
  get token(): string | null;
345
+ get refreshToken(): string | null;
346
+ get expiresAt(): number | null;
347
+ get isExpired(): boolean;
331
348
  hasRole(role: string): boolean;
332
349
  getAuthContext(): AuthContext | null;
333
350
  private localLogin;
334
351
  private localRegister;
352
+ private localRefresh;
335
353
  private httpLogin;
336
354
  private httpRegister;
355
+ private httpRefresh;
356
+ private unwrapBody;
357
+ private setAuthFromResponse;
337
358
  private generateToken;
359
+ private generateRefreshToken;
360
+ private scheduleRefresh;
361
+ private clearRefreshTimer;
338
362
  /** @internal — called by createClient to listen for auth state changes */
339
363
  _onAuthChange(listener: () => void): void;
340
364
  private persistState;
@@ -454,7 +478,10 @@ interface Preset {
454
478
  loginUrl: string;
455
479
  registerUrl: string;
456
480
  logoutUrl?: string;
481
+ refreshUrl?: string;
457
482
  tokenField: string;
483
+ refreshTokenField?: string;
484
+ expiresInField?: string;
458
485
  userField: string;
459
486
  headerFormat: string;
460
487
  };
package/dist/index.d.ts CHANGED
@@ -264,8 +264,11 @@ declare class HttpDriver implements Driver {
264
264
  private defaultHeaders;
265
265
  private endpoints;
266
266
  private authProvider;
267
+ private onUnauthorized;
267
268
  constructor(config: HttpDriverOptions);
268
269
  setAuthProvider(provider: AuthProvider$1): void;
270
+ /** @internal — set callback to refresh token on 401 */
271
+ setOnUnauthorized(handler: () => Promise<boolean>): void;
269
272
  registerEndpoint(resource: string, endpoint: string): void;
270
273
  private getEndpoint;
271
274
  private buildUrl;
@@ -308,6 +311,8 @@ interface AuthState {
308
311
  userName?: string;
309
312
  role?: string;
310
313
  token: string;
314
+ refreshToken?: string;
315
+ expiresAt?: number;
311
316
  }
312
317
  interface AuthContext {
313
318
  userId: string;
@@ -318,6 +323,8 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
318
323
  private saveState;
319
324
  private httpDriver;
320
325
  private authChangeListeners;
326
+ private refreshTimer;
327
+ private _isRefreshing;
321
328
  /** @internal — called by createClient to wire persistence */
322
329
  _initAuth(loadState: () => AuthState | null, saveState: (state: AuthState | null) => void): void;
323
330
  /** @internal — called by createClient when using HttpDriver */
@@ -325,16 +332,33 @@ declare abstract class AuthService<T extends Entity> extends Service<T> {
325
332
  login(credentials: LoginCredentials): Promise<T>;
326
333
  register(data: Partial<T>): Promise<T>;
327
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>;
328
342
  get currentUser(): T | null;
329
343
  get isLoggedIn(): boolean;
330
344
  get token(): string | null;
345
+ get refreshToken(): string | null;
346
+ get expiresAt(): number | null;
347
+ get isExpired(): boolean;
331
348
  hasRole(role: string): boolean;
332
349
  getAuthContext(): AuthContext | null;
333
350
  private localLogin;
334
351
  private localRegister;
352
+ private localRefresh;
335
353
  private httpLogin;
336
354
  private httpRegister;
355
+ private httpRefresh;
356
+ private unwrapBody;
357
+ private setAuthFromResponse;
337
358
  private generateToken;
359
+ private generateRefreshToken;
360
+ private scheduleRefresh;
361
+ private clearRefreshTimer;
338
362
  /** @internal — called by createClient to listen for auth state changes */
339
363
  _onAuthChange(listener: () => void): void;
340
364
  private persistState;
@@ -454,7 +478,10 @@ interface Preset {
454
478
  loginUrl: string;
455
479
  registerUrl: string;
456
480
  logoutUrl?: string;
481
+ refreshUrl?: string;
457
482
  tokenField: string;
483
+ refreshTokenField?: string;
484
+ expiresInField?: string;
458
485
  userField: string;
459
486
  headerFormat: string;
460
487
  };
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 (original implementation) ---
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,9 @@ var AuthService = class extends Service {
382
447
  throw new ForbiddenError(body2.message ?? "Login failed");
383
448
  }
384
449
  const body = await response.json();
385
- const token = body[preset.auth.tokenField];
386
- const user = body[preset.auth.userField] ?? body;
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
+ const unwrapped = this.unwrapBody(body);
452
+ return unwrapped[preset.auth.userField] ?? unwrapped;
396
453
  }
397
454
  async httpRegister(data) {
398
455
  const preset = this.httpDriver.preset;
@@ -411,27 +468,103 @@ var AuthService = class extends Service {
411
468
  throw new ForbiddenError(body2.message ?? "Registration failed");
412
469
  }
413
470
  const body = await response.json();
414
- const token = body[preset.auth.tokenField];
415
- const user = body[preset.auth.userField] ?? body;
471
+ this.setAuthFromResponse(body, preset, data.email);
472
+ const unwrapped = this.unwrapBody(body);
473
+ return unwrapped[preset.auth.userField] ?? unwrapped;
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 unwrapped = this.unwrapBody(body);
493
+ const token = unwrapped[preset.auth.tokenField];
494
+ const refreshToken = preset.auth.refreshTokenField ? unwrapped[preset.auth.refreshTokenField] : this.authState.refreshToken;
495
+ const expiresIn = preset.auth.expiresInField ? unwrapped[preset.auth.expiresInField] : null;
496
+ const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
497
+ this.authState = {
498
+ ...this.authState,
499
+ token,
500
+ refreshToken,
501
+ expiresAt
502
+ };
503
+ this.persistState();
504
+ this.scheduleRefresh();
505
+ return token;
506
+ }
507
+ unwrapBody(body) {
508
+ if (body.data && typeof body.data === "object" && !Array.isArray(body.data)) {
509
+ return body.data;
510
+ }
511
+ return body;
512
+ }
513
+ setAuthFromResponse(body, preset, fallbackEmail) {
514
+ const unwrapped = this.unwrapBody(body);
515
+ const token = unwrapped[preset.auth.tokenField];
516
+ const user = unwrapped[preset.auth.userField] ?? unwrapped;
517
+ const refreshToken = preset.auth.refreshTokenField ? unwrapped[preset.auth.refreshTokenField] : void 0;
518
+ const expiresIn = preset.auth.expiresInField ? unwrapped[preset.auth.expiresInField] : null;
519
+ const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
416
520
  this.authState = {
417
521
  userId: user.id,
418
- email: user.email ?? data.email,
419
- userName: user.name || user.email || data.email,
522
+ email: user.email ?? fallbackEmail,
523
+ userName: user.name || user.email || fallbackEmail,
420
524
  role: user.role,
421
- token
525
+ token,
526
+ refreshToken,
527
+ expiresAt
422
528
  };
423
529
  this.persistState();
424
- return user;
530
+ this.scheduleRefresh();
425
531
  }
426
- generateToken(user) {
532
+ // --- Token generation (local mode) ---
533
+ generateToken(user, expiresAt) {
427
534
  return btoa(JSON.stringify({
428
- userId: user.id,
535
+ userId: user.id ?? user.userId,
429
536
  email: user.email,
430
537
  role: user.role,
431
538
  iat: Date.now(),
432
- exp: Date.now() + 24 * 60 * 60 * 1e3
539
+ exp: expiresAt
433
540
  }));
434
541
  }
542
+ generateRefreshToken(user) {
543
+ return btoa(JSON.stringify({
544
+ userId: user.id ?? user.userId,
545
+ email: user.email,
546
+ role: user.role,
547
+ type: "refresh",
548
+ iat: Date.now()
549
+ }));
550
+ }
551
+ // --- Refresh scheduling ---
552
+ scheduleRefresh() {
553
+ this.clearRefreshTimer();
554
+ if (!this.authState?.expiresAt) return;
555
+ const delay = this.authState.expiresAt - Date.now() - 60 * 1e3;
556
+ if (delay <= 0) return;
557
+ this.refreshTimer = setTimeout(() => {
558
+ this.refresh().catch(() => {
559
+ });
560
+ }, delay);
561
+ }
562
+ clearRefreshTimer() {
563
+ if (this.refreshTimer) {
564
+ clearTimeout(this.refreshTimer);
565
+ this.refreshTimer = null;
566
+ }
567
+ }
435
568
  /** @internal — called by createClient to listen for auth state changes */
436
569
  _onAuthChange(listener) {
437
570
  this.authChangeListeners.push(listener);
@@ -1003,7 +1136,7 @@ var defaultPreset = definePreset({
1003
1136
  var springBootPreset = definePreset({
1004
1137
  name: "spring-boot",
1005
1138
  response: {
1006
- single: (raw) => ({ data: raw }),
1139
+ single: (raw) => ({ data: raw.data ?? raw }),
1007
1140
  list: (raw) => ({
1008
1141
  items: raw.content ?? [],
1009
1142
  meta: {
@@ -1035,7 +1168,10 @@ var springBootPreset = definePreset({
1035
1168
  auth: {
1036
1169
  loginUrl: "/api/auth/login",
1037
1170
  registerUrl: "/api/auth/register",
1171
+ refreshUrl: "/api/auth/refresh",
1038
1172
  tokenField: "token",
1173
+ refreshTokenField: "refreshToken",
1174
+ expiresInField: "expiresIn",
1039
1175
  userField: "user",
1040
1176
  headerFormat: "Bearer {token}"
1041
1177
  }
@@ -1086,7 +1222,7 @@ var laravelPreset = definePreset({
1086
1222
  var djangoPreset = definePreset({
1087
1223
  name: "django",
1088
1224
  response: {
1089
- single: (raw) => ({ data: raw }),
1225
+ single: (raw) => ({ data: raw.data ?? raw }),
1090
1226
  list: (raw) => ({
1091
1227
  items: raw.results ?? [],
1092
1228
  meta: {
@@ -1275,6 +1411,7 @@ var HttpDriver = class {
1275
1411
  defaultHeaders;
1276
1412
  endpoints = /* @__PURE__ */ new Map();
1277
1413
  authProvider = null;
1414
+ onUnauthorized = null;
1278
1415
  constructor(config) {
1279
1416
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
1280
1417
  this.preset = typeof config.preset === "string" ? getPreset(config.preset ?? "default") : config.preset ?? getPreset("default");
@@ -1286,6 +1423,10 @@ var HttpDriver = class {
1286
1423
  setAuthProvider(provider) {
1287
1424
  this.authProvider = provider;
1288
1425
  }
1426
+ /** @internal — set callback to refresh token on 401 */
1427
+ setOnUnauthorized(handler) {
1428
+ this.onUnauthorized = handler;
1429
+ }
1289
1430
  registerEndpoint(resource, endpoint) {
1290
1431
  this.endpoints.set(resource, endpoint);
1291
1432
  }
@@ -1319,6 +1460,12 @@ var HttpDriver = class {
1319
1460
  });
1320
1461
  clearTimeout(timer);
1321
1462
  if (!response.ok) {
1463
+ if (response.status === 401 && retryCount === 0 && this.onUnauthorized) {
1464
+ const refreshed = await this.onUnauthorized();
1465
+ if (refreshed) {
1466
+ return this._fetch(url, options, 1);
1467
+ }
1468
+ }
1322
1469
  if (response.status >= 500 && retryCount < this.maxRetries) {
1323
1470
  const delay = this.baseDelay * Math.pow(2, retryCount);
1324
1471
  await new Promise((r) => setTimeout(r, delay));
@@ -1442,7 +1589,7 @@ var HttpDriver = class {
1442
1589
  url += `?${params.toString()}`;
1443
1590
  }
1444
1591
  return this._fetch(url, {
1445
- method: options?.method ?? "POST",
1592
+ method: options?.method ?? (options?.body !== void 0 ? "POST" : "GET"),
1446
1593
  body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
1447
1594
  });
1448
1595
  }
@@ -1682,10 +1829,25 @@ function createClient(config) {
1682
1829
  } else if (defaultDriver instanceof HttpDriver) {
1683
1830
  authInstance._init(defaultDriver, resourceName);
1684
1831
  defaultDriver.registerEndpoint(resourceName, authInstance.endpoint);
1832
+ const hasLocalStorage = typeof localStorage !== "undefined";
1833
+ const LS_AUTH_KEY = "fauxbase:auth";
1685
1834
  let memoryAuthState = null;
1686
1835
  authInstance._initAuth(
1687
- () => memoryAuthState,
1836
+ () => {
1837
+ if (hasLocalStorage) {
1838
+ const raw = localStorage.getItem(LS_AUTH_KEY);
1839
+ return raw ? JSON.parse(raw) : null;
1840
+ }
1841
+ return memoryAuthState;
1842
+ },
1688
1843
  (state) => {
1844
+ if (hasLocalStorage) {
1845
+ if (state) {
1846
+ localStorage.setItem(LS_AUTH_KEY, JSON.stringify(state));
1847
+ } else {
1848
+ localStorage.removeItem(LS_AUTH_KEY);
1849
+ }
1850
+ }
1689
1851
  memoryAuthState = state;
1690
1852
  }
1691
1853
  );
@@ -1694,6 +1856,14 @@ function createClient(config) {
1694
1856
  const token = authInstance.token;
1695
1857
  return token ? { token } : null;
1696
1858
  });
1859
+ defaultDriver.setOnUnauthorized(async () => {
1860
+ try {
1861
+ await authInstance.refresh();
1862
+ return true;
1863
+ } catch {
1864
+ return false;
1865
+ }
1866
+ });
1697
1867
  }
1698
1868
  client.auth = authInstance;
1699
1869
  for (const driver of overrideDrivers.values()) {
@@ -1702,6 +1872,14 @@ function createClient(config) {
1702
1872
  const token = client.auth?.token;
1703
1873
  return token ? { token } : null;
1704
1874
  });
1875
+ driver.setOnUnauthorized(async () => {
1876
+ try {
1877
+ await client.auth.refresh();
1878
+ return true;
1879
+ } catch {
1880
+ return false;
1881
+ }
1882
+ });
1705
1883
  }
1706
1884
  }
1707
1885
  }