@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 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
- // Factory to create transport for a specific service URL
283
- function createServiceTransport(baseUrl) {
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 };
@@ -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":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/client.ts"],"names":[],"mappings":"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"}
@@ -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;AAMxD,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"}
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;AAkED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAwE3E"}
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 { Blocks23Provider, useAuth, useSearch, useRagBlock } from '@23blocks/react';
576
+ import { Provider, useAuth, useClient } from '@23blocks/react';
550
577
 
551
578
  function App() {
552
579
  return (
553
- <Blocks23Provider config={{
554
- apiKey: 'your-api-key',
555
- urls: { authentication: '...', search: '...' },
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
- </Blocks23Provider>
586
+ </Provider>
559
587
  );
560
588
  }
561
589
 
562
590
  function MyComponent() {
563
- const auth = useAuth(); // AuthenticationBlock
564
- const search = useSearch(); // SearchBlock
565
- // Blocks are memoized - stable references across re-renders
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
- // Angular services delegate to block sub-services via typed getters
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.auth.signIn() wraps in Observable with token management
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
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@23blocks/sdk",
3
- "version": "13.4.0",
3
+ "version": "13.5.1",
4
4
  "description": "23blocks SDK - unified meta-package re-exporting all blocks and utilities",
5
5
  "license": "MIT",
6
6
  "author": "23blocks <hello@23blocks.com>",