@23blocks/sdk 13.4.0 → 13.5.1
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/CHANGELOG.md +30 -0
- package/dist/index.esm.js +311 -4
- package/dist/src/lib/client.d.ts +27 -0
- package/dist/src/lib/client.d.ts.map +1 -1
- package/dist/src/lib/sdk.d.ts +2 -1
- package/dist/src/lib/sdk.d.ts.map +1 -1
- package/dist/src/lib/token-lifecycle.d.ts +95 -0
- package/dist/src/lib/token-lifecycle.d.ts.map +1 -0
- package/dist/src/lib/token-manager.d.ts +6 -0
- package/dist/src/lib/token-manager.d.ts.map +1 -1
- package/llms.txt +60 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## 13.5.1 (2026-03-14)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- **@23blocks/sdk:** fix token lifecycle security and correctness issues ([0886883](https://github.com/23blocks-OS/frontend-sdk/commit/0886883))
|
|
6
|
+
|
|
7
|
+
### 📖 Documentation
|
|
8
|
+
|
|
9
|
+
- update llms.txt and JSDoc for token lifecycle across all packages ([74d8319](https://github.com/23blocks-OS/frontend-sdk/commit/74d8319))
|
|
10
|
+
|
|
11
|
+
### ❤️ Thank You
|
|
12
|
+
|
|
13
|
+
- Claude Opus 4.6
|
|
14
|
+
- Juan Pelaez
|
|
15
|
+
|
|
16
|
+
## 13.5.0 (2026-03-14)
|
|
17
|
+
|
|
18
|
+
### 🚀 Features
|
|
19
|
+
|
|
20
|
+
- **@23blocks/sdk:** add token lifecycle management with auto-refresh and 401 retry ([5426358](https://github.com/23blocks-OS/frontend-sdk/commit/5426358))
|
|
21
|
+
|
|
22
|
+
### 📖 Documentation
|
|
23
|
+
|
|
24
|
+
- add token lifecycle to llms.txt ([f42d730](https://github.com/23blocks-OS/frontend-sdk/commit/f42d730))
|
|
25
|
+
|
|
26
|
+
### ❤️ Thank You
|
|
27
|
+
|
|
28
|
+
- Claude Opus 4.6
|
|
29
|
+
- Juan Pelaez
|
|
30
|
+
|
|
1
31
|
## 13.4.0 (2026-03-09)
|
|
2
32
|
|
|
3
33
|
### 🚀 Features
|
package/dist/index.esm.js
CHANGED
|
@@ -55,6 +55,7 @@ export { blockOnboarding as onboarding };
|
|
|
55
55
|
import { createUniversityBlock } from '@23blocks/block-university';
|
|
56
56
|
import * as blockUniversity from '@23blocks/block-university';
|
|
57
57
|
export { blockUniversity as university };
|
|
58
|
+
import { BlockErrorException } from '@23blocks/contracts';
|
|
58
59
|
export * from '@23blocks/contracts';
|
|
59
60
|
export * from '@23blocks/jsonapi-codec';
|
|
60
61
|
import * as blockRag from '@23blocks/block-rag';
|
|
@@ -204,6 +205,258 @@ export { blockRag as rag };
|
|
|
204
205
|
};
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
209
|
+
// JWT Decode Utility
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* Decode the `exp` claim from a JWT without external dependencies.
|
|
213
|
+
* Returns the expiry as a Unix timestamp (seconds), or null if unavailable.
|
|
214
|
+
*/ function decodeJwtExp(token) {
|
|
215
|
+
try {
|
|
216
|
+
const parts = token.split('.');
|
|
217
|
+
if (parts.length !== 3) return null;
|
|
218
|
+
// Base64url → Base64
|
|
219
|
+
let payload = parts[1];
|
|
220
|
+
payload = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
221
|
+
// Pad to multiple of 4
|
|
222
|
+
const pad = payload.length % 4;
|
|
223
|
+
if (pad) {
|
|
224
|
+
payload += '='.repeat(4 - pad);
|
|
225
|
+
}
|
|
226
|
+
// Decode — works in browser (atob) and Node 16+ (Buffer)
|
|
227
|
+
let decoded;
|
|
228
|
+
if (typeof atob === 'function') {
|
|
229
|
+
decoded = atob(payload);
|
|
230
|
+
} else if (typeof Buffer !== 'undefined') {
|
|
231
|
+
decoded = Buffer.from(payload, 'base64').toString('utf-8');
|
|
232
|
+
} else {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const parsed = JSON.parse(decoded);
|
|
236
|
+
if (typeof parsed.exp === 'number') {
|
|
237
|
+
return parsed.exp;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
|
+
// Lifecycle Manager Factory
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
247
|
+
/**
|
|
248
|
+
* Create a token lifecycle manager that automatically refreshes tokens,
|
|
249
|
+
* handles tab visibility, and notifies listeners of auth state changes.
|
|
250
|
+
*
|
|
251
|
+
* @param tokenManager - Token storage (read/write tokens)
|
|
252
|
+
* @param refreshFn - Function to call the backend refresh endpoint
|
|
253
|
+
* @param config - Lifecycle configuration
|
|
254
|
+
*/ function createTokenLifecycleManager(tokenManager, refreshFn, config = {}) {
|
|
255
|
+
const { refreshBufferSeconds = 120, enableVisibilityRefresh = true, enableProactiveRefresh = true } = config;
|
|
256
|
+
const listeners = new Set();
|
|
257
|
+
let refreshTimer = null;
|
|
258
|
+
let refreshPromise = null;
|
|
259
|
+
let visibilityHandler = null;
|
|
260
|
+
let destroyed = false;
|
|
261
|
+
let running = false;
|
|
262
|
+
function notify(event) {
|
|
263
|
+
listeners.forEach((listener)=>{
|
|
264
|
+
try {
|
|
265
|
+
listener(event);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// Listener errors should not break the lifecycle
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function clearTimer() {
|
|
272
|
+
if (refreshTimer !== null) {
|
|
273
|
+
clearTimeout(refreshTimer);
|
|
274
|
+
refreshTimer = null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function scheduleRefresh() {
|
|
278
|
+
if (!enableProactiveRefresh || destroyed || !running) return;
|
|
279
|
+
clearTimer();
|
|
280
|
+
const accessToken = tokenManager.getAccessToken();
|
|
281
|
+
if (!accessToken) return;
|
|
282
|
+
const exp = decodeJwtExp(accessToken);
|
|
283
|
+
if (!exp) return;
|
|
284
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
285
|
+
const secondsUntilExpiry = exp - nowSeconds;
|
|
286
|
+
const refreshInSeconds = secondsUntilExpiry - refreshBufferSeconds;
|
|
287
|
+
if (refreshInSeconds <= 0) {
|
|
288
|
+
// Token is already expired or within buffer — refresh immediately
|
|
289
|
+
refreshNow().catch(()=>{
|
|
290
|
+
// Error handled inside refreshNow
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
refreshTimer = setTimeout(()=>{
|
|
295
|
+
if (!destroyed && running) {
|
|
296
|
+
refreshNow().catch(()=>{
|
|
297
|
+
// Error handled inside refreshNow
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}, refreshInSeconds * 1000);
|
|
301
|
+
}
|
|
302
|
+
function handleVisibilityChange() {
|
|
303
|
+
if (destroyed || !running) return;
|
|
304
|
+
if (typeof document === 'undefined') return;
|
|
305
|
+
if (document.visibilityState === 'visible') {
|
|
306
|
+
const accessToken = tokenManager.getAccessToken();
|
|
307
|
+
if (!accessToken) return;
|
|
308
|
+
const exp = decodeJwtExp(accessToken);
|
|
309
|
+
if (!exp) {
|
|
310
|
+
// No exp claim — refresh to be safe
|
|
311
|
+
refreshNow().catch(()=>{});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
315
|
+
const secondsUntilExpiry = exp - nowSeconds;
|
|
316
|
+
// Refresh if expired or within buffer
|
|
317
|
+
if (secondsUntilExpiry <= refreshBufferSeconds) {
|
|
318
|
+
refreshNow().catch(()=>{});
|
|
319
|
+
} else {
|
|
320
|
+
// Reschedule proactive refresh (timer may have drifted during sleep)
|
|
321
|
+
scheduleRefresh();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function registerVisibilityListener() {
|
|
326
|
+
if (!enableVisibilityRefresh || !isBrowser$1() || visibilityHandler) return;
|
|
327
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
328
|
+
visibilityHandler = handleVisibilityChange;
|
|
329
|
+
document.addEventListener('visibilitychange', visibilityHandler);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function removeVisibilityListener() {
|
|
333
|
+
if (visibilityHandler && typeof document !== 'undefined') {
|
|
334
|
+
document.removeEventListener('visibilitychange', visibilityHandler);
|
|
335
|
+
visibilityHandler = null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function refreshNow() {
|
|
339
|
+
if (destroyed) {
|
|
340
|
+
throw new Error('[23blocks] Token lifecycle manager has been destroyed');
|
|
341
|
+
}
|
|
342
|
+
// Concurrency lock — all callers share the same in-flight promise
|
|
343
|
+
if (refreshPromise) {
|
|
344
|
+
return refreshPromise;
|
|
345
|
+
}
|
|
346
|
+
refreshPromise = (async ()=>{
|
|
347
|
+
try {
|
|
348
|
+
const refreshToken = tokenManager.getRefreshToken();
|
|
349
|
+
if (!refreshToken) {
|
|
350
|
+
throw new Error('No refresh token available');
|
|
351
|
+
}
|
|
352
|
+
const result = await refreshFn(refreshToken);
|
|
353
|
+
// Guard: don't store tokens if lifecycle was stopped/destroyed during the async call
|
|
354
|
+
if (!running || destroyed) {
|
|
355
|
+
return result.accessToken;
|
|
356
|
+
}
|
|
357
|
+
// Store new tokens
|
|
358
|
+
tokenManager.setTokens(result.accessToken, result.refreshToken);
|
|
359
|
+
// Reschedule proactive refresh
|
|
360
|
+
scheduleRefresh();
|
|
361
|
+
notify('TOKEN_REFRESHED');
|
|
362
|
+
return result.accessToken;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
// Refresh failed — session is dead
|
|
365
|
+
clearTimer();
|
|
366
|
+
tokenManager.clearTokens();
|
|
367
|
+
running = false;
|
|
368
|
+
notify('SESSION_EXPIRED');
|
|
369
|
+
throw error;
|
|
370
|
+
} finally{
|
|
371
|
+
refreshPromise = null;
|
|
372
|
+
}
|
|
373
|
+
})();
|
|
374
|
+
return refreshPromise;
|
|
375
|
+
}
|
|
376
|
+
function start() {
|
|
377
|
+
if (destroyed) return;
|
|
378
|
+
running = true;
|
|
379
|
+
scheduleRefresh();
|
|
380
|
+
registerVisibilityListener();
|
|
381
|
+
notify('SIGNED_IN');
|
|
382
|
+
}
|
|
383
|
+
function stop() {
|
|
384
|
+
running = false;
|
|
385
|
+
clearTimer();
|
|
386
|
+
refreshPromise = null;
|
|
387
|
+
notify('SIGNED_OUT');
|
|
388
|
+
}
|
|
389
|
+
function destroy() {
|
|
390
|
+
destroyed = true;
|
|
391
|
+
stop();
|
|
392
|
+
removeVisibilityListener();
|
|
393
|
+
listeners.clear();
|
|
394
|
+
}
|
|
395
|
+
function onAuthStateChanged(listener) {
|
|
396
|
+
listeners.add(listener);
|
|
397
|
+
return ()=>{
|
|
398
|
+
listeners.delete(listener);
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
start,
|
|
403
|
+
stop,
|
|
404
|
+
onAuthStateChanged,
|
|
405
|
+
refreshNow,
|
|
406
|
+
destroy
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
410
|
+
// Retrying Transport Wrapper
|
|
411
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
412
|
+
/**
|
|
413
|
+
* Wrap a transport with automatic 401 retry via token refresh.
|
|
414
|
+
*
|
|
415
|
+
* On a 401 BlockErrorException, the wrapper calls `getLifecycle().refreshNow()`
|
|
416
|
+
* to obtain a fresh token, then retries the request once.
|
|
417
|
+
*
|
|
418
|
+
* @param baseTransport - The underlying HTTP transport
|
|
419
|
+
* @param getLifecycle - Lazy getter for the lifecycle manager (supports React refs and late init)
|
|
420
|
+
*/ function createRetryingTransport(baseTransport, getLifecycle) {
|
|
421
|
+
async function withRetry(fn) {
|
|
422
|
+
try {
|
|
423
|
+
return await fn();
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (error instanceof BlockErrorException && error.status === 401) {
|
|
426
|
+
const lifecycle = getLifecycle();
|
|
427
|
+
if (lifecycle) {
|
|
428
|
+
try {
|
|
429
|
+
await lifecycle.refreshNow();
|
|
430
|
+
// Retry once — transport reads fresh token from tokenManager on next call
|
|
431
|
+
return await fn();
|
|
432
|
+
} catch (e) {
|
|
433
|
+
// Refresh failed — throw original 401
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
get (path, options) {
|
|
443
|
+
return withRetry(()=>baseTransport.get(path, options));
|
|
444
|
+
},
|
|
445
|
+
post (path, body, options) {
|
|
446
|
+
return withRetry(()=>baseTransport.post(path, body, options));
|
|
447
|
+
},
|
|
448
|
+
patch (path, body, options) {
|
|
449
|
+
return withRetry(()=>baseTransport.patch(path, body, options));
|
|
450
|
+
},
|
|
451
|
+
put (path, body, options) {
|
|
452
|
+
return withRetry(()=>baseTransport.put(path, body, options));
|
|
453
|
+
},
|
|
454
|
+
delete (path, options) {
|
|
455
|
+
return withRetry(()=>baseTransport.delete(path, options));
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
207
460
|
/**
|
|
208
461
|
* Detect browser environment.
|
|
209
462
|
* Uses try/catch to handle edge runtimes (Lambda@Edge, Cloudflare Workers)
|
|
@@ -269,7 +522,7 @@ export { blockRag as rag };
|
|
|
269
522
|
* });
|
|
270
523
|
* ```
|
|
271
524
|
*/ function create23BlocksClient(config) {
|
|
272
|
-
const { urls, apiKey, tenantId, authMode = 'token', storage = isBrowser() ? 'localStorage' : 'memory', headers: staticHeaders = {}, timeout } = config;
|
|
525
|
+
const { urls, apiKey, tenantId, authMode = 'token', storage = isBrowser() ? 'localStorage' : 'memory', headers: staticHeaders = {}, timeout, tokenLifecycle: lifecycleConfig = {} } = config;
|
|
273
526
|
// Create token manager for token mode
|
|
274
527
|
let tokenManager = null;
|
|
275
528
|
if (authMode === 'token') {
|
|
@@ -279,8 +532,11 @@ export { blockRag as rag };
|
|
|
279
532
|
storage
|
|
280
533
|
});
|
|
281
534
|
}
|
|
282
|
-
//
|
|
283
|
-
|
|
535
|
+
// Token lifecycle manager (created lazily after auth block exists)
|
|
536
|
+
let lifecycle = null;
|
|
537
|
+
const lifecycleEnabled = authMode === 'token' && lifecycleConfig !== false;
|
|
538
|
+
// Factory to create base transport for a specific service URL
|
|
539
|
+
function createBaseTransport(baseUrl) {
|
|
284
540
|
return createHttpTransport({
|
|
285
541
|
baseUrl,
|
|
286
542
|
timeout,
|
|
@@ -303,6 +559,14 @@ export { blockRag as rag };
|
|
|
303
559
|
}
|
|
304
560
|
});
|
|
305
561
|
}
|
|
562
|
+
// Factory to create transport with optional 401 retry
|
|
563
|
+
function createServiceTransport(baseUrl) {
|
|
564
|
+
const base = createBaseTransport(baseUrl);
|
|
565
|
+
if (lifecycleEnabled) {
|
|
566
|
+
return createRetryingTransport(base, ()=>lifecycle);
|
|
567
|
+
}
|
|
568
|
+
return base;
|
|
569
|
+
}
|
|
306
570
|
// Helper to create a proxy that throws when accessing unconfigured service
|
|
307
571
|
function createUnconfiguredServiceProxy(serviceName, urlKey) {
|
|
308
572
|
return new Proxy({}, {
|
|
@@ -335,12 +599,33 @@ export { blockRag as rag };
|
|
|
335
599
|
const jarvisBlock = urls.jarvis ? createJarvisBlock(createServiceTransport(urls.jarvis), blockConfig) : null;
|
|
336
600
|
const onboardingBlock = urls.onboarding ? createOnboardingBlock(createServiceTransport(urls.onboarding), blockConfig) : null;
|
|
337
601
|
const universityBlock = urls.university ? createUniversityBlock(createServiceTransport(urls.university), blockConfig) : null;
|
|
602
|
+
// Create lifecycle manager if enabled and auth block is available
|
|
603
|
+
if (lifecycleEnabled && tokenManager && urls.authentication) {
|
|
604
|
+
// Dedicated transport for refresh calls — NOT wrapped with retry to avoid circular 401 handling
|
|
605
|
+
const refreshAuthBlock = createAuthenticationBlock(createBaseTransport(urls.authentication), blockConfig);
|
|
606
|
+
const lifecycleRefreshFn = async (refreshToken)=>{
|
|
607
|
+
const response = await refreshAuthBlock.auth.refreshToken({
|
|
608
|
+
refreshToken
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
accessToken: response.accessToken,
|
|
612
|
+
refreshToken: response.refreshToken,
|
|
613
|
+
expiresIn: response.expiresIn
|
|
614
|
+
};
|
|
615
|
+
};
|
|
616
|
+
lifecycle = createTokenLifecycleManager(tokenManager, lifecycleRefreshFn, typeof lifecycleConfig === 'object' ? lifecycleConfig : {});
|
|
617
|
+
// Auto-start if tokens already exist (page reload scenario)
|
|
618
|
+
if (tokenManager.getAccessToken() && tokenManager.getRefreshToken()) {
|
|
619
|
+
lifecycle.start();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
338
622
|
// Create managed auth service with automatic token handling (only if auth URL configured)
|
|
339
623
|
const managedAuth = authenticationBlock ? {
|
|
340
624
|
async signIn (request) {
|
|
341
625
|
const response = await authenticationBlock.auth.signIn(request);
|
|
342
626
|
if (authMode === 'token' && tokenManager && response.accessToken) {
|
|
343
627
|
tokenManager.setTokens(response.accessToken, response.refreshToken);
|
|
628
|
+
lifecycle == null ? void 0 : lifecycle.start();
|
|
344
629
|
}
|
|
345
630
|
return response;
|
|
346
631
|
},
|
|
@@ -348,10 +633,12 @@ export { blockRag as rag };
|
|
|
348
633
|
const response = await authenticationBlock.auth.signUp(request);
|
|
349
634
|
if (authMode === 'token' && tokenManager && response.accessToken) {
|
|
350
635
|
tokenManager.setTokens(response.accessToken);
|
|
636
|
+
lifecycle == null ? void 0 : lifecycle.start();
|
|
351
637
|
}
|
|
352
638
|
return response;
|
|
353
639
|
},
|
|
354
640
|
async signOut () {
|
|
641
|
+
lifecycle == null ? void 0 : lifecycle.stop();
|
|
355
642
|
await authenticationBlock.auth.signOut();
|
|
356
643
|
if (authMode === 'token' && tokenManager) {
|
|
357
644
|
tokenManager.clearTokens();
|
|
@@ -361,6 +648,7 @@ export { blockRag as rag };
|
|
|
361
648
|
const response = await authenticationBlock.auth.verifyMagicLink(request);
|
|
362
649
|
if (authMode === 'token' && tokenManager && response.accessToken) {
|
|
363
650
|
tokenManager.setTokens(response.accessToken, response.refreshToken);
|
|
651
|
+
lifecycle == null ? void 0 : lifecycle.start();
|
|
364
652
|
}
|
|
365
653
|
return response;
|
|
366
654
|
},
|
|
@@ -368,6 +656,7 @@ export { blockRag as rag };
|
|
|
368
656
|
const response = await authenticationBlock.auth.acceptInvitation(request);
|
|
369
657
|
if (authMode === 'token' && tokenManager && response.accessToken) {
|
|
370
658
|
tokenManager.setTokens(response.accessToken, response.refreshToken);
|
|
659
|
+
lifecycle == null ? void 0 : lifecycle.start();
|
|
371
660
|
}
|
|
372
661
|
return response;
|
|
373
662
|
},
|
|
@@ -375,6 +664,7 @@ export { blockRag as rag };
|
|
|
375
664
|
const response = await authenticationBlock.auth.verifyPasswordOtp(request);
|
|
376
665
|
if (authMode === 'token' && tokenManager && response.accessToken) {
|
|
377
666
|
tokenManager.setTokens(response.accessToken, response.refreshToken);
|
|
667
|
+
lifecycle == null ? void 0 : lifecycle.start();
|
|
378
668
|
}
|
|
379
669
|
return response;
|
|
380
670
|
},
|
|
@@ -445,8 +735,25 @@ export { blockRag as rag };
|
|
|
445
735
|
return null;
|
|
446
736
|
}
|
|
447
737
|
return tokenManager ? !!tokenManager.getAccessToken() : false;
|
|
738
|
+
},
|
|
739
|
+
onAuthStateChanged (listener) {
|
|
740
|
+
if (lifecycle) {
|
|
741
|
+
return lifecycle.onAuthStateChanged(listener);
|
|
742
|
+
}
|
|
743
|
+
// No lifecycle — return no-op unsubscribe
|
|
744
|
+
return ()=>{};
|
|
745
|
+
},
|
|
746
|
+
async refreshSession () {
|
|
747
|
+
if (!lifecycle) {
|
|
748
|
+
throw new Error('[23blocks] Token lifecycle is not available. ' + 'Ensure authMode is "token" and tokenLifecycle is not disabled.');
|
|
749
|
+
}
|
|
750
|
+
return lifecycle.refreshNow();
|
|
751
|
+
},
|
|
752
|
+
destroy () {
|
|
753
|
+
lifecycle == null ? void 0 : lifecycle.destroy();
|
|
754
|
+
lifecycle = null;
|
|
448
755
|
}
|
|
449
756
|
};
|
|
450
757
|
}
|
|
451
758
|
|
|
452
|
-
export { create23BlocksClient, createTokenManager };
|
|
759
|
+
export { create23BlocksClient, createRetryingTransport, createTokenLifecycleManager, createTokenManager, isBrowser$1 as isBrowser };
|
package/dist/src/lib/client.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { type JarvisBlock } from '@23blocks/block-jarvis';
|
|
|
17
17
|
import { type OnboardingBlock } from '@23blocks/block-onboarding';
|
|
18
18
|
import { type UniversityBlock } from '@23blocks/block-university';
|
|
19
19
|
import { type StorageType } from './token-manager.js';
|
|
20
|
+
import { type TokenLifecycleConfig, type AuthStateListener } from './token-lifecycle.js';
|
|
20
21
|
/**
|
|
21
22
|
* Authentication mode
|
|
22
23
|
* - 'token': Store tokens in browser storage, attach Authorization header
|
|
@@ -114,6 +115,16 @@ export interface ClientConfig {
|
|
|
114
115
|
* @default 30000
|
|
115
116
|
*/
|
|
116
117
|
timeout?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Token lifecycle configuration for automatic refresh and 401 retry.
|
|
120
|
+
* - Pass an object to customize (e.g., `{ refreshBufferSeconds: 60 }`)
|
|
121
|
+
* - Pass `false` to disable entirely
|
|
122
|
+
* - Omit or pass `{}` to use defaults (enabled with 120s buffer)
|
|
123
|
+
*
|
|
124
|
+
* Only applies in token mode. Ignored in cookie mode.
|
|
125
|
+
* @default {} (enabled with defaults)
|
|
126
|
+
*/
|
|
127
|
+
tokenLifecycle?: TokenLifecycleConfig | false;
|
|
117
128
|
}
|
|
118
129
|
/**
|
|
119
130
|
* Auth service wrapper with automatic token management
|
|
@@ -286,6 +297,22 @@ export interface Blocks23Client {
|
|
|
286
297
|
* In cookie mode: always returns null (check with validateToken instead)
|
|
287
298
|
*/
|
|
288
299
|
isAuthenticated(): boolean | null;
|
|
300
|
+
/**
|
|
301
|
+
* Subscribe to auth state changes (token refreshed, session expired, etc.).
|
|
302
|
+
* Returns an unsubscribe function.
|
|
303
|
+
* Only active when tokenLifecycle is enabled (token mode).
|
|
304
|
+
*/
|
|
305
|
+
onAuthStateChanged(listener: AuthStateListener): () => void;
|
|
306
|
+
/**
|
|
307
|
+
* Force an immediate token refresh.
|
|
308
|
+
* Returns the new access token. Throws if no lifecycle or refresh fails.
|
|
309
|
+
*/
|
|
310
|
+
refreshSession(): Promise<string>;
|
|
311
|
+
/**
|
|
312
|
+
* Destroy the client — stops lifecycle timers and cleans up listeners.
|
|
313
|
+
* Call when the client is no longer needed (e.g., component unmount).
|
|
314
|
+
*/
|
|
315
|
+
destroy(): void;
|
|
289
316
|
}
|
|
290
317
|
/**
|
|
291
318
|
* Create a 23blocks client instance
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/client.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC9B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC5F,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAClG,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACzF,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAEzF,OAAO,EAAsB,KAAK,WAAW,EAAqB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/client.ts"],"names":[],"mappings":"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"}
|
package/dist/src/lib/sdk.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { create23BlocksClient, type AuthMode, type ClientConfig, type ServiceUrls, type Blocks23Client, type ManagedAuthService, type StorageType, type TokenManager, } from './client.js';
|
|
2
|
-
export { createTokenManager } from './token-manager.js';
|
|
2
|
+
export { createTokenManager, isBrowser } from './token-manager.js';
|
|
3
|
+
export { createTokenLifecycleManager, createRetryingTransport, type AuthStateEvent, type AuthStateListener, type RefreshTokenFn, type TokenLifecycleConfig, type TokenLifecycleManager, } from './token-lifecycle.js';
|
|
3
4
|
export * from '@23blocks/contracts';
|
|
4
5
|
export * from '@23blocks/jsonapi-codec';
|
|
5
6
|
export * from '@23blocks/transport-http';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../../src/lib/sdk.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,oBAAoB,EACpB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../../src/lib/sdk.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,oBAAoB,EACpB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAC;AAM9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AAqBzC,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,0BAA0B,CAAC;AACrD,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAC3C,OAAO,KAAK,OAAO,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,WAAW,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,aAAa,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,KAAK,MAAM,uBAAuB,CAAC;AAC/C,OAAO,KAAK,KAAK,MAAM,uBAAuB,CAAC;AAC/C,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,SAAS,MAAM,2BAA2B,CAAC;AACvD,OAAO,KAAK,OAAO,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,OAAO,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,KAAK,MAAM,uBAAuB,CAAC;AAC/C,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,UAAU,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,UAAU,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Transport } from '@23blocks/contracts';
|
|
2
|
+
import type { TokenManager } from './token-manager.js';
|
|
3
|
+
/**
|
|
4
|
+
* Auth state change events emitted by the lifecycle manager.
|
|
5
|
+
*
|
|
6
|
+
* - `SIGNED_IN` — User signed in and lifecycle started
|
|
7
|
+
* - `SIGNED_OUT` — User signed out and lifecycle stopped
|
|
8
|
+
* - `TOKEN_REFRESHED` — Access token was silently refreshed
|
|
9
|
+
* - `SESSION_EXPIRED` — Refresh failed, tokens cleared, user must re-authenticate
|
|
10
|
+
*/
|
|
11
|
+
export type AuthStateEvent = 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'SESSION_EXPIRED';
|
|
12
|
+
/**
|
|
13
|
+
* Callback invoked when auth state changes
|
|
14
|
+
*/
|
|
15
|
+
export type AuthStateListener = (event: AuthStateEvent) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Function that performs the actual token refresh against the backend.
|
|
18
|
+
* Accepts the current refresh token and returns new credentials.
|
|
19
|
+
*/
|
|
20
|
+
export type RefreshTokenFn = (refreshToken: string) => Promise<{
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken?: string;
|
|
23
|
+
expiresIn?: number;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Configuration for automatic token lifecycle management
|
|
27
|
+
*/
|
|
28
|
+
export interface TokenLifecycleConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Seconds before token expiry to trigger a proactive refresh.
|
|
31
|
+
* @default 120
|
|
32
|
+
*/
|
|
33
|
+
refreshBufferSeconds?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Refresh token when tab becomes visible (handles laptop sleep/wake).
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
enableVisibilityRefresh?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Schedule proactive refresh based on JWT `exp` claim.
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
enableProactiveRefresh?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Token lifecycle manager interface.
|
|
47
|
+
* Manages automatic token refresh, tab visibility sync, and auth state notifications.
|
|
48
|
+
*/
|
|
49
|
+
export interface TokenLifecycleManager {
|
|
50
|
+
/**
|
|
51
|
+
* Start the lifecycle — schedule proactive refresh and register visibility listener.
|
|
52
|
+
* Call after successful sign-in.
|
|
53
|
+
*/
|
|
54
|
+
start(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Stop the lifecycle — clear timers and listeners but keep the manager reusable.
|
|
57
|
+
* Call on sign-out.
|
|
58
|
+
*/
|
|
59
|
+
stop(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to auth state changes.
|
|
62
|
+
* Returns an unsubscribe function.
|
|
63
|
+
*/
|
|
64
|
+
onAuthStateChanged(listener: AuthStateListener): () => void;
|
|
65
|
+
/**
|
|
66
|
+
* Force an immediate token refresh.
|
|
67
|
+
* Multiple concurrent calls share the same in-flight promise (concurrency lock).
|
|
68
|
+
* Returns the new access token on success.
|
|
69
|
+
*/
|
|
70
|
+
refreshNow(): Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Permanently destroy the manager — clears everything and prevents reuse.
|
|
73
|
+
*/
|
|
74
|
+
destroy(): void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a token lifecycle manager that automatically refreshes tokens,
|
|
78
|
+
* handles tab visibility, and notifies listeners of auth state changes.
|
|
79
|
+
*
|
|
80
|
+
* @param tokenManager - Token storage (read/write tokens)
|
|
81
|
+
* @param refreshFn - Function to call the backend refresh endpoint
|
|
82
|
+
* @param config - Lifecycle configuration
|
|
83
|
+
*/
|
|
84
|
+
export declare function createTokenLifecycleManager(tokenManager: TokenManager, refreshFn: RefreshTokenFn, config?: TokenLifecycleConfig): TokenLifecycleManager;
|
|
85
|
+
/**
|
|
86
|
+
* Wrap a transport with automatic 401 retry via token refresh.
|
|
87
|
+
*
|
|
88
|
+
* On a 401 BlockErrorException, the wrapper calls `getLifecycle().refreshNow()`
|
|
89
|
+
* to obtain a fresh token, then retries the request once.
|
|
90
|
+
*
|
|
91
|
+
* @param baseTransport - The underlying HTTP transport
|
|
92
|
+
* @param getLifecycle - Lazy getter for the lifecycle manager (supports React refs and late init)
|
|
93
|
+
*/
|
|
94
|
+
export declare function createRetryingTransport(baseTransport: Transport, getLifecycle: () => TokenLifecycleManager | null): Transport;
|
|
95
|
+
//# sourceMappingURL=token-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-lifecycle.d.ts","sourceRoot":"","sources":["../../../src/lib/token-lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,qBAAqB,CAAC;AAErE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAOvD;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEhG;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;AAEhE;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,IAAI,IAAI,CAAC;IAEd;;;OAGG;IACH,IAAI,IAAI,IAAI,CAAC;IAEb;;;OAGG;IACH,kBAAkB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAAC;IAE5D;;;;OAIG;IACH,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9B;;OAEG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAiDD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,cAAc,EACzB,MAAM,GAAE,oBAAyB,GAChC,qBAAqB,CA8LvB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,SAAS,EACxB,YAAY,EAAE,MAAM,qBAAqB,GAAG,IAAI,GAC/C,SAAS,CAuCX"}
|
|
@@ -47,6 +47,12 @@ export interface TokenManager {
|
|
|
47
47
|
*/
|
|
48
48
|
onStorageChange(callback: () => void): () => void;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Detect if we're running in a browser environment.
|
|
52
|
+
* Uses try/catch to handle edge runtimes (Lambda@Edge, Cloudflare Workers)
|
|
53
|
+
* that may throw on property access.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isBrowser(): boolean;
|
|
50
56
|
/**
|
|
51
57
|
* Create a token manager instance
|
|
52
58
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../../src/lib/token-manager.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI,CAAC;IAEhC;;OAEG;IACH,eAAe,IAAI,MAAM,GAAG,IAAI,CAAC;IAEjC;;OAEG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5D;;OAEG;IACH,WAAW,IAAI,IAAI,CAAC;IAEpB;;;OAGG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CACnD;
|
|
1
|
+
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../../src/lib/token-manager.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI,CAAC;IAEhC;;OAEG;IACH,eAAe,IAAI,MAAM,GAAG,IAAI,CAAC;IAEjC;;OAEG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5D;;OAEG;IACH,WAAW,IAAI,IAAI,CAAC;IAEpB;;;OAGG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CACnD;AA6BD;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CASnC;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAwE3E"}
|
package/llms.txt
CHANGED
|
@@ -294,6 +294,33 @@ The client provides:
|
|
|
294
294
|
- `client.authentication` - Full authentication block
|
|
295
295
|
- `client.{blockName}` - All other blocks
|
|
296
296
|
- `client.getAccessToken()`, `client.setTokens()`, `client.clearSession()` - Token utilities
|
|
297
|
+
- `client.onAuthStateChanged(listener)` - Subscribe to auth state changes (TOKEN_REFRESHED, SESSION_EXPIRED)
|
|
298
|
+
- `client.refreshSession()` - Force immediate token refresh
|
|
299
|
+
- `client.destroy()` - Cleanup lifecycle timers and listeners
|
|
300
|
+
|
|
301
|
+
### Token Lifecycle (Auto-Refresh & 401 Retry)
|
|
302
|
+
|
|
303
|
+
Enabled by default in token mode. Handles:
|
|
304
|
+
- **Proactive refresh** - Decodes JWT `exp`, schedules refresh 120s before expiry
|
|
305
|
+
- **401 retry** - On 401 errors, refreshes token and retries the request once
|
|
306
|
+
- **Tab visibility** - Refreshes stale tokens when tab becomes visible (laptop sleep/wake)
|
|
307
|
+
- **Concurrency lock** - Multiple simultaneous 401s share a single refresh call
|
|
308
|
+
- **Session expiry** - Clears tokens and emits SESSION_EXPIRED if refresh fails
|
|
309
|
+
|
|
310
|
+
Auth events: `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`, `SESSION_EXPIRED`
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// Disable lifecycle
|
|
314
|
+
const client = create23BlocksClient({ ..., tokenLifecycle: false });
|
|
315
|
+
|
|
316
|
+
// Custom config
|
|
317
|
+
const client = create23BlocksClient({ ..., tokenLifecycle: { refreshBufferSeconds: 60 } });
|
|
318
|
+
|
|
319
|
+
// Listen for events
|
|
320
|
+
const unsub = client.onAuthStateChanged((event) => {
|
|
321
|
+
if (event === 'SESSION_EXPIRED') redirectToLogin();
|
|
322
|
+
});
|
|
323
|
+
```
|
|
297
324
|
|
|
298
325
|
## Health Check
|
|
299
326
|
|
|
@@ -546,23 +573,36 @@ const [authHealth, crmHealth] = await Promise.all([
|
|
|
546
573
|
## React Integration
|
|
547
574
|
|
|
548
575
|
```typescript
|
|
549
|
-
import {
|
|
576
|
+
import { Provider, useAuth, useClient } from '@23blocks/react';
|
|
550
577
|
|
|
551
578
|
function App() {
|
|
552
579
|
return (
|
|
553
|
-
<
|
|
554
|
-
apiKey
|
|
555
|
-
urls
|
|
556
|
-
|
|
580
|
+
<Provider
|
|
581
|
+
apiKey="your-api-key"
|
|
582
|
+
urls={{ authentication: '...', crm: '...', search: '...' }}
|
|
583
|
+
// tokenLifecycle enabled by default in token mode
|
|
584
|
+
>
|
|
557
585
|
<MyComponent />
|
|
558
|
-
</
|
|
586
|
+
</Provider>
|
|
559
587
|
);
|
|
560
588
|
}
|
|
561
589
|
|
|
562
590
|
function MyComponent() {
|
|
563
|
-
const
|
|
564
|
-
const search =
|
|
565
|
-
|
|
591
|
+
const { signIn, signOut, isAuthenticated, onAuthStateChanged, refreshSession } = useAuth();
|
|
592
|
+
const { crm, search } = useClient();
|
|
593
|
+
|
|
594
|
+
// Token lifecycle is automatic:
|
|
595
|
+
// - Proactive refresh before JWT expiry
|
|
596
|
+
// - 401 retry with token refresh
|
|
597
|
+
// - Tab visibility refresh (laptop sleep/wake)
|
|
598
|
+
// - SESSION_EXPIRED event when refresh fails
|
|
599
|
+
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
const unsub = onAuthStateChanged((event) => {
|
|
602
|
+
if (event === 'SESSION_EXPIRED') router.push('/login');
|
|
603
|
+
});
|
|
604
|
+
return unsub;
|
|
605
|
+
}, [onAuthStateChanged]);
|
|
566
606
|
}
|
|
567
607
|
```
|
|
568
608
|
|
|
@@ -577,6 +617,7 @@ export const appConfig = {
|
|
|
577
617
|
provideBlocks23({
|
|
578
618
|
apiKey: 'your-api-key',
|
|
579
619
|
urls: { authentication: '...', crm: '...' },
|
|
620
|
+
// tokenLifecycle enabled by default in token mode
|
|
580
621
|
}),
|
|
581
622
|
],
|
|
582
623
|
};
|
|
@@ -589,8 +630,16 @@ export class MyComponent {
|
|
|
589
630
|
private auth = inject(AuthenticationService);
|
|
590
631
|
private crm = inject(CrmService);
|
|
591
632
|
|
|
592
|
-
//
|
|
633
|
+
// Auth-flow methods return Observables with automatic token + lifecycle management:
|
|
634
|
+
// auth.signIn({ email, password }).subscribe(...)
|
|
635
|
+
// auth.signOut().subscribe(...)
|
|
636
|
+
|
|
637
|
+
// Sub-services are Promise-based getters:
|
|
593
638
|
// auth.users.list() returns Promise<PageResult<User>>
|
|
594
|
-
// auth.
|
|
639
|
+
// auth.roles.list() returns Promise<PageResult<Role>>
|
|
640
|
+
|
|
641
|
+
// Token lifecycle:
|
|
642
|
+
// auth.onAuthStateChanged(event => { ... }) — subscribe to SIGNED_IN, TOKEN_REFRESHED, SESSION_EXPIRED
|
|
643
|
+
// auth.refreshSession() — force immediate token refresh
|
|
595
644
|
}
|
|
596
645
|
```
|