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 +205 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +205 -27
- 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,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
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
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
|
-
|
|
417
|
-
const
|
|
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 ??
|
|
421
|
-
userName: user.name || user.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
|
-
|
|
532
|
+
this.scheduleRefresh();
|
|
427
533
|
}
|
|
428
|
-
|
|
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:
|
|
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
|
-
() =>
|
|
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
|
}
|