|
1 | 1 | import type { TokenResource } from '@clerk/types';
|
2 | 2 |
|
3 | 3 | import { debugLogger } from '@/utils/debug';
|
4 |
| -import { buildTokenId } from '@/utils/tokenId'; |
| 4 | +import { TokenId } from '@/utils/tokenId'; |
5 | 5 |
|
6 | 6 | import { Token } from './resources/internal';
|
7 | 7 |
|
@@ -30,30 +30,6 @@ interface TokenCacheEntry extends TokenCacheKeyJSON {
|
30 | 30 | tokenResolver: Promise<TokenResource>;
|
31 | 31 | }
|
32 | 32 |
|
33 |
| -/** |
34 |
| - * Metadata broadcast to other tabs when a token is fetched or refreshed. |
35 |
| - * Contains session and organization context along with the raw token value. |
36 |
| - */ |
37 |
| -export interface TokenBroadcastMetadata { |
38 |
| - /** |
39 |
| - * Organization ID associated with the token, if any. |
40 |
| - * Nullable for tokens not scoped to an organization. |
41 |
| - */ |
42 |
| - organizationId?: string | null; |
43 |
| - /** |
44 |
| - * Session ID for which the token was issued. |
45 |
| - */ |
46 |
| - sessionId: string; |
47 |
| - /** |
48 |
| - * JWT template name used to generate the token, if applicable. |
49 |
| - */ |
50 |
| - template?: string; |
51 |
| - /** |
52 |
| - * Raw JWT token string. |
53 |
| - */ |
54 |
| - tokenRaw: string; |
55 |
| -} |
56 |
| - |
57 | 33 | type Seconds = number;
|
58 | 34 |
|
59 | 35 | /**
|
@@ -90,13 +66,12 @@ export interface TokenCache {
|
90 | 66 | get(cacheKeyJSON: TokenCacheKeyJSON, leeway?: number): TokenCacheEntry | undefined;
|
91 | 67 |
|
92 | 68 | /**
|
93 |
| - * Stores a token entry in the cache and optionally broadcasts to other tabs. |
| 69 | + * Stores a token entry in the cache and broadcasts to other tabs when the token resolves. |
94 | 70 | *
|
95 | 71 | * @param entry - TokenCacheEntry containing tokenId, tokenResolver, and optional audience
|
96 |
| - * @param broadcast - Optional metadata to broadcast this token update to other tabs via BroadcastChannel |
97 |
| - * Side effects: Schedules automatic expiration cleanup, broadcasts to other tabs if metadata provided |
| 72 | + * Side effects: Schedules automatic expiration cleanup, broadcasts to other tabs when token resolves |
98 | 73 | */
|
99 |
| - set(entry: TokenCacheEntry, broadcast?: TokenBroadcastMetadata): void; |
| 74 | + set(entry: TokenCacheEntry): void; |
100 | 75 |
|
101 | 76 | /**
|
102 | 77 | * Returns the current number of cached entries.
|
@@ -224,7 +199,7 @@ const MemoryTokenCache = (prefix = KEY_PREFIX): TokenCache => {
|
224 | 199 | * Validates token ID, parses JWT, and updates cache if token is newer than existing entry.
|
225 | 200 | */
|
226 | 201 | const handleBroadcastMessage = async ({ data }: MessageEvent<SessionTokenEvent>) => {
|
227 |
| - const expectedTokenId = buildTokenId(data.sessionId, data.template, data.organizationId); |
| 202 | + const expectedTokenId = TokenId.build(data.sessionId, data.template, data.organizationId); |
228 | 203 | if (data.tokenId !== expectedTokenId) {
|
229 | 204 | debugLogger.warn(
|
230 | 205 | 'Ignoring token broadcast with mismatched tokenId',
|
@@ -304,49 +279,16 @@ const MemoryTokenCache = (prefix = KEY_PREFIX): TokenCache => {
|
304 | 279 | });
|
305 | 280 | };
|
306 | 281 |
|
307 |
| - const set = (entry: TokenCacheEntry, broadcast?: TokenBroadcastMetadata) => { |
308 |
| - // Ensure BroadcastChannel is initialized so this tab can receive broadcasts |
309 |
| - const channel = ensureBroadcastChannel(); |
| 282 | + const set = (entry: TokenCacheEntry) => { |
| 283 | + ensureBroadcastChannel(); |
310 | 284 |
|
311 | 285 | setInternal(entry);
|
312 |
| - |
313 |
| - if (broadcast && channel) { |
314 |
| - const expectedTokenId = buildTokenId(broadcast.sessionId, broadcast.template, broadcast.organizationId); |
315 |
| - if (entry.tokenId !== expectedTokenId) { |
316 |
| - return; |
317 |
| - } |
318 |
| - |
319 |
| - // Generate a unique trace ID for this broadcast |
320 |
| - const traceId = `bc_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; |
321 |
| - |
322 |
| - debugLogger.info( |
323 |
| - 'Broadcasting token update to other tabs', |
324 |
| - { |
325 |
| - organizationId: broadcast.organizationId, |
326 |
| - sessionId: broadcast.sessionId, |
327 |
| - template: broadcast.template, |
328 |
| - tokenId: entry.tokenId, |
329 |
| - traceId, |
330 |
| - }, |
331 |
| - 'tokenCache', |
332 |
| - ); |
333 |
| - |
334 |
| - const message: SessionTokenEvent = { |
335 |
| - organizationId: broadcast.organizationId, |
336 |
| - sessionId: broadcast.sessionId, |
337 |
| - template: broadcast.template, |
338 |
| - tokenId: entry.tokenId, |
339 |
| - tokenRaw: broadcast.tokenRaw, |
340 |
| - traceId, |
341 |
| - }; |
342 |
| - |
343 |
| - channel.postMessage(message); |
344 |
| - } |
345 | 286 | };
|
346 | 287 |
|
347 | 288 | /**
|
348 | 289 | * Internal cache setter that stores an entry and schedules expiration cleanup.
|
349 | 290 | * Resolves the token promise to extract expiration claims and set a deletion timeout.
|
| 291 | + * Automatically broadcasts to other tabs when the token resolves. |
350 | 292 | */
|
351 | 293 | const setInternal = (entry: TokenCacheEntry) => {
|
352 | 294 | const cacheKey = new TokenCacheKey(prefix, {
|
@@ -395,6 +337,44 @@ const MemoryTokenCache = (prefix = KEY_PREFIX): TokenCache => {
|
395 | 337 | if (typeof (timeoutId as any).unref === 'function') {
|
396 | 338 | (timeoutId as any).unref();
|
397 | 339 | }
|
| 340 | + |
| 341 | + const channel = broadcastChannel; |
| 342 | + if (channel) { |
| 343 | + const tokenRaw = newToken.getRawString(); |
| 344 | + if (tokenRaw && claims.sid) { |
| 345 | + const sessionId = claims.sid; |
| 346 | + const organizationId = claims.org_id || (claims.o as any)?.id; |
| 347 | + const template = TokenId.extractTemplate(entry.tokenId, sessionId, organizationId); |
| 348 | + |
| 349 | + const expectedTokenId = TokenId.build(sessionId, template, organizationId); |
| 350 | + if (entry.tokenId === expectedTokenId) { |
| 351 | + const traceId = `bc_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; |
| 352 | + |
| 353 | + debugLogger.info( |
| 354 | + 'Broadcasting token update to other tabs', |
| 355 | + { |
| 356 | + organizationId, |
| 357 | + sessionId, |
| 358 | + template, |
| 359 | + tokenId: entry.tokenId, |
| 360 | + traceId, |
| 361 | + }, |
| 362 | + 'tokenCache', |
| 363 | + ); |
| 364 | + |
| 365 | + const message: SessionTokenEvent = { |
| 366 | + organizationId, |
| 367 | + sessionId, |
| 368 | + template, |
| 369 | + tokenId: entry.tokenId, |
| 370 | + tokenRaw, |
| 371 | + traceId, |
| 372 | + }; |
| 373 | + |
| 374 | + channel.postMessage(message); |
| 375 | + } |
| 376 | + } |
| 377 | + } |
398 | 378 | })
|
399 | 379 | .catch(() => {
|
400 | 380 | deleteKey();
|
|
0 commit comments