@payloadcms/ui 4.0.0-canary.20 → 4.0.0-canary.21
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/elements/CopyToClipboard/index.d.ts +1 -0
- package/dist/elements/CopyToClipboard/index.d.ts.map +1 -1
- package/dist/elements/CopyToClipboard/index.js +10 -8
- package/dist/elements/CopyToClipboard/index.js.map +1 -1
- package/dist/elements/FileManager/FileToolbar/index.d.ts.map +1 -1
- package/dist/elements/FileManager/FileToolbar/index.js +3 -0
- package/dist/elements/FileManager/FileToolbar/index.js.map +1 -1
- package/dist/exports/client/index.d.ts +2 -1
- package/dist/exports/client/index.d.ts.map +1 -1
- package/dist/exports/client/index.js +1 -1
- package/dist/exports/client/index.js.map +3 -3
- package/dist/forms/Form/index.d.ts.map +1 -1
- package/dist/forms/Form/index.js +4 -1
- package/dist/forms/Form/index.js.map +1 -1
- package/dist/providers/Auth/AuthSessionDebug/index.css +48 -0
- package/dist/providers/Auth/AuthSessionDebug/index.d.ts +13 -0
- package/dist/providers/Auth/AuthSessionDebug/index.d.ts.map +1 -0
- package/dist/providers/Auth/AuthSessionDebug/index.js +93 -0
- package/dist/providers/Auth/AuthSessionDebug/index.js.map +1 -0
- package/dist/providers/Auth/AuthSessionDebug/utilities.d.ts +21 -0
- package/dist/providers/Auth/AuthSessionDebug/utilities.d.ts.map +1 -0
- package/dist/providers/Auth/AuthSessionDebug/utilities.js +53 -0
- package/dist/providers/Auth/AuthSessionDebug/utilities.js.map +1 -0
- package/dist/providers/Auth/authSessionRequests.d.ts +27 -0
- package/dist/providers/Auth/authSessionRequests.d.ts.map +1 -0
- package/dist/providers/Auth/authSessionRequests.js +119 -0
- package/dist/providers/Auth/authSessionRequests.js.map +1 -0
- package/dist/providers/Auth/authSessionRequests.spec.js +121 -0
- package/dist/providers/Auth/authSessionRequests.spec.js.map +1 -0
- package/dist/providers/Auth/index.d.ts +2 -56
- package/dist/providers/Auth/index.d.ts.map +1 -1
- package/dist/providers/Auth/index.js +283 -135
- package/dist/providers/Auth/index.js.map +1 -1
- package/dist/providers/Auth/sessionActivity.d.ts +23 -0
- package/dist/providers/Auth/sessionActivity.d.ts.map +1 -0
- package/dist/providers/Auth/sessionActivity.js +49 -0
- package/dist/providers/Auth/sessionActivity.js.map +1 -0
- package/dist/providers/Auth/sessionActivity.spec.js +44 -0
- package/dist/providers/Auth/sessionActivity.spec.js.map +1 -0
- package/dist/providers/Auth/sessionTiming.d.ts +11 -0
- package/dist/providers/Auth/sessionTiming.d.ts.map +1 -0
- package/dist/providers/Auth/sessionTiming.js +18 -0
- package/dist/providers/Auth/sessionTiming.js.map +1 -0
- package/dist/providers/Auth/tabSessionSync/index.d.ts +17 -0
- package/dist/providers/Auth/tabSessionSync/index.d.ts.map +1 -0
- package/dist/providers/Auth/tabSessionSync/index.js +263 -0
- package/dist/providers/Auth/tabSessionSync/index.js.map +1 -0
- package/dist/providers/Auth/tabSessionSync/index.spec.js +463 -0
- package/dist/providers/Auth/tabSessionSync/index.spec.js.map +1 -0
- package/dist/providers/Auth/tabSessionSync/types.d.ts +107 -0
- package/dist/providers/Auth/tabSessionSync/types.d.ts.map +1 -0
- package/dist/providers/Auth/tabSessionSync/types.js +6 -0
- package/dist/providers/Auth/tabSessionSync/types.js.map +1 -0
- package/dist/providers/Auth/tabSessionSync/useTabSessionSync.d.ts +11 -0
- package/dist/providers/Auth/tabSessionSync/useTabSessionSync.d.ts.map +1 -0
- package/dist/providers/Auth/tabSessionSync/useTabSessionSync.js +90 -0
- package/dist/providers/Auth/tabSessionSync/useTabSessionSync.js.map +1 -0
- package/dist/providers/Auth/tabSessionSync/utilities.d.ts +26 -0
- package/dist/providers/Auth/tabSessionSync/utilities.d.ts.map +1 -0
- package/dist/providers/Auth/tabSessionSync/utilities.js +159 -0
- package/dist/providers/Auth/tabSessionSync/utilities.js.map +1 -0
- package/dist/providers/Auth/types.d.ts +66 -0
- package/dist/providers/Auth/types.d.ts.map +1 -0
- package/dist/providers/Auth/types.js +2 -0
- package/dist/providers/Auth/types.js.map +1 -0
- package/dist/providers/Auth/useAuthSessionTimers.d.ts +26 -0
- package/dist/providers/Auth/useAuthSessionTimers.d.ts.map +1 -0
- package/dist/providers/Auth/useAuthSessionTimers.js +203 -0
- package/dist/providers/Auth/useAuthSessionTimers.js.map +1 -0
- package/dist/providers/Auth/useAuthSessionTimers.spec.js +311 -0
- package/dist/providers/Auth/useAuthSessionTimers.spec.js.map +1 -0
- package/dist/providers/FormErrorHandler/index.d.ts +5 -1
- package/dist/providers/FormErrorHandler/index.d.ts.map +1 -1
- package/dist/providers/FormErrorHandler/index.js.map +1 -1
- package/dist/providers/Root/index.js +4 -4
- package/dist/providers/Root/index.js.map +1 -1
- package/dist/providers/RouteTransition/ProgressBar/index.js +4 -1
- package/dist/providers/RouteTransition/ProgressBar/index.js.map +1 -1
- package/dist/providers/RouteTransition/ProgressBar/index.spec.js +44 -0
- package/dist/providers/RouteTransition/ProgressBar/index.spec.js.map +1 -0
- package/dist/providers/RouteTransition/index.d.ts +2 -0
- package/dist/providers/RouteTransition/index.d.ts.map +1 -1
- package/dist/providers/RouteTransition/index.js +42 -14
- package/dist/providers/RouteTransition/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { TAB_SESSION_EVENT_TYPES, createTabSessionSync } from './index.js';
|
|
4
|
+
const tabSessionSyncCleanups = [];
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
resetMockBroadcastChannels();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
for (const cleanup of tabSessionSyncCleanups.splice(0)) {
|
|
10
|
+
cleanup();
|
|
11
|
+
}
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
vi.unstubAllGlobals();
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
describe('createTabSessionSync', () => {
|
|
17
|
+
it('should broadcast refreshed sessions with source-tab and timing metadata', () => {
|
|
18
|
+
const session = createSession({
|
|
19
|
+
expirationMs: 20_000,
|
|
20
|
+
token: 'refreshed-token'
|
|
21
|
+
});
|
|
22
|
+
const tabSessionSync = createTestTabSessionSync({
|
|
23
|
+
now: () => 123
|
|
24
|
+
});
|
|
25
|
+
tabSessionSync.broadcast({
|
|
26
|
+
refreshStartedAt: 100,
|
|
27
|
+
session,
|
|
28
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
29
|
+
});
|
|
30
|
+
expect(getBroadcastChannel().postMessage).toHaveBeenCalledWith({
|
|
31
|
+
refreshStartedAt: 100,
|
|
32
|
+
session,
|
|
33
|
+
sourceTabID: 'local-tab',
|
|
34
|
+
sentAt: 123,
|
|
35
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('should apply a newer refresh when the provider shortens the token lifetime', () => {
|
|
39
|
+
const onSessionRefreshed = vi.fn();
|
|
40
|
+
const session = createSession({
|
|
41
|
+
expirationMs: 30_000
|
|
42
|
+
});
|
|
43
|
+
createTestTabSessionSync({
|
|
44
|
+
localExpirationMs: 40_000,
|
|
45
|
+
onSessionRefreshed
|
|
46
|
+
});
|
|
47
|
+
const channel = getBroadcastChannel();
|
|
48
|
+
channel.emit(createTabSessionMessage({
|
|
49
|
+
sentAt: 100,
|
|
50
|
+
session,
|
|
51
|
+
sourceTabID: 'refresh-tab',
|
|
52
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
53
|
+
}));
|
|
54
|
+
expect(onSessionRefreshed).toHaveBeenCalledWith(session);
|
|
55
|
+
});
|
|
56
|
+
it('should ignore expiration events for older sessions', () => {
|
|
57
|
+
const onSessionExpired = vi.fn();
|
|
58
|
+
createTestTabSessionSync({
|
|
59
|
+
localExpirationMs: 40_000,
|
|
60
|
+
onSessionExpired
|
|
61
|
+
});
|
|
62
|
+
const channel = getBroadcastChannel();
|
|
63
|
+
channel.emit(createTabSessionMessage({
|
|
64
|
+
expiredTokenAt: 30_000,
|
|
65
|
+
sourceTabID: 'stale-expiration-tab',
|
|
66
|
+
type: TAB_SESSION_EVENT_TYPES.EXPIRED
|
|
67
|
+
}));
|
|
68
|
+
expect(onSessionExpired).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it('should converge on logout when equal-time events arrive in opposite orders', () => {
|
|
71
|
+
let firstState = 'initial';
|
|
72
|
+
let secondState = 'initial';
|
|
73
|
+
const refreshMessage = createTabSessionMessage({
|
|
74
|
+
sentAt: 500,
|
|
75
|
+
session: createSession({
|
|
76
|
+
expirationMs: 30_000,
|
|
77
|
+
token: 'refreshed-token'
|
|
78
|
+
}),
|
|
79
|
+
sourceTabID: 'refresh-tab',
|
|
80
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
81
|
+
});
|
|
82
|
+
const logoutMessage = createTabSessionMessage({
|
|
83
|
+
sentAt: 500,
|
|
84
|
+
sourceTabID: 'logout-tab',
|
|
85
|
+
type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT
|
|
86
|
+
});
|
|
87
|
+
createTestTabSessionSync({
|
|
88
|
+
onSessionLoggedOut: () => {
|
|
89
|
+
firstState = 'logged-out';
|
|
90
|
+
},
|
|
91
|
+
onSessionRefreshed: () => {
|
|
92
|
+
firstState = 'refreshed';
|
|
93
|
+
},
|
|
94
|
+
sourceTabID: 'first-local-tab'
|
|
95
|
+
});
|
|
96
|
+
const firstChannel = getBroadcastChannel();
|
|
97
|
+
createTestTabSessionSync({
|
|
98
|
+
onSessionLoggedOut: () => {
|
|
99
|
+
secondState = 'logged-out';
|
|
100
|
+
},
|
|
101
|
+
onSessionRefreshed: () => {
|
|
102
|
+
secondState = 'refreshed';
|
|
103
|
+
},
|
|
104
|
+
sourceTabID: 'second-local-tab'
|
|
105
|
+
});
|
|
106
|
+
const secondChannel = getBroadcastChannel();
|
|
107
|
+
firstChannel.emit(refreshMessage);
|
|
108
|
+
firstChannel.emit(logoutMessage);
|
|
109
|
+
secondChannel.emit(logoutMessage);
|
|
110
|
+
secondChannel.emit(refreshMessage);
|
|
111
|
+
expect(firstState).toBe('logged-out');
|
|
112
|
+
expect(secondState).toBe('logged-out');
|
|
113
|
+
});
|
|
114
|
+
it('should keep logout ahead of a refresh that started earlier and broadcast later', () => {
|
|
115
|
+
const onSessionLoggedOut = vi.fn();
|
|
116
|
+
const onSessionRefreshed = vi.fn();
|
|
117
|
+
const refreshMessage = createTabSessionMessage({
|
|
118
|
+
refreshStartedAt: 100,
|
|
119
|
+
sentAt: 300,
|
|
120
|
+
session: createSession({
|
|
121
|
+
expirationMs: 30_000,
|
|
122
|
+
token: 'late-refreshed-token'
|
|
123
|
+
}),
|
|
124
|
+
sourceTabID: 'refresh-tab',
|
|
125
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
126
|
+
});
|
|
127
|
+
const logoutMessage = createTabSessionMessage({
|
|
128
|
+
sentAt: 200,
|
|
129
|
+
sourceTabID: 'logout-tab',
|
|
130
|
+
type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT
|
|
131
|
+
});
|
|
132
|
+
createTestTabSessionSync({
|
|
133
|
+
onSessionLoggedOut,
|
|
134
|
+
onSessionRefreshed,
|
|
135
|
+
sourceTabID: 'receiving-tab'
|
|
136
|
+
});
|
|
137
|
+
const channel = getBroadcastChannel();
|
|
138
|
+
channel.emit(logoutMessage);
|
|
139
|
+
channel.emit(refreshMessage);
|
|
140
|
+
expect(onSessionLoggedOut).toHaveBeenCalledOnce();
|
|
141
|
+
expect(onSessionRefreshed).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
it('should apply refreshes in publication order instead of request-start ordering for refresh-vs-refresh', () => {
|
|
144
|
+
let appliedSession;
|
|
145
|
+
const onSessionRefreshed = vi.fn(session => {
|
|
146
|
+
appliedSession = session;
|
|
147
|
+
});
|
|
148
|
+
const refreshB = createTabSessionMessage({
|
|
149
|
+
refreshStartedAt: 200,
|
|
150
|
+
sentAt: 100,
|
|
151
|
+
session: createSession({
|
|
152
|
+
expirationMs: 30_000,
|
|
153
|
+
token: 'refresh-b'
|
|
154
|
+
}),
|
|
155
|
+
sourceTabID: 'refresh-b-tab',
|
|
156
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
157
|
+
});
|
|
158
|
+
const refreshA = createTabSessionMessage({
|
|
159
|
+
refreshStartedAt: 100,
|
|
160
|
+
sentAt: 200,
|
|
161
|
+
session: createSession({
|
|
162
|
+
expirationMs: 40_000,
|
|
163
|
+
token: 'refresh-a'
|
|
164
|
+
}),
|
|
165
|
+
sourceTabID: 'refresh-a-tab',
|
|
166
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
167
|
+
});
|
|
168
|
+
createTestTabSessionSync({
|
|
169
|
+
onSessionRefreshed,
|
|
170
|
+
sourceTabID: 'receiving-tab'
|
|
171
|
+
});
|
|
172
|
+
const channel = getBroadcastChannel();
|
|
173
|
+
channel.emit(refreshB);
|
|
174
|
+
channel.emit(refreshA);
|
|
175
|
+
expect(onSessionRefreshed).toHaveBeenCalledTimes(2);
|
|
176
|
+
expect(onSessionRefreshed).toHaveBeenLastCalledWith(refreshA.session);
|
|
177
|
+
expect(appliedSession).toBe(refreshA.session);
|
|
178
|
+
});
|
|
179
|
+
it('should ignore storage notifications while BroadcastChannel is healthy', async () => {
|
|
180
|
+
const reconcileSession = vi.fn().mockResolvedValue({
|
|
181
|
+
status: 'indeterminate'
|
|
182
|
+
});
|
|
183
|
+
createTestTabSessionSync({
|
|
184
|
+
reconcileSession,
|
|
185
|
+
sourceTabID: 'receiving-tab'
|
|
186
|
+
});
|
|
187
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
188
|
+
key: 'payload:auth-session:tab-sync',
|
|
189
|
+
newValue: JSON.stringify({
|
|
190
|
+
affectedExpirationMs: 30_000,
|
|
191
|
+
refreshStartedAt: 100,
|
|
192
|
+
sentAt: 200,
|
|
193
|
+
sourceTabID: 'refreshing-tab',
|
|
194
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
195
|
+
})
|
|
196
|
+
}));
|
|
197
|
+
await Promise.resolve();
|
|
198
|
+
expect(reconcileSession).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
it('should ignore a late storage refresh whose request started before logout', () => {
|
|
201
|
+
vi.stubGlobal('BroadcastChannel', undefined);
|
|
202
|
+
const reconcileSession = vi.fn().mockResolvedValue({
|
|
203
|
+
status: 'indeterminate'
|
|
204
|
+
});
|
|
205
|
+
const onSessionLoggedOut = vi.fn();
|
|
206
|
+
createTestTabSessionSync({
|
|
207
|
+
reconcileSession,
|
|
208
|
+
onSessionLoggedOut,
|
|
209
|
+
sourceTabID: 'receiving-tab'
|
|
210
|
+
});
|
|
211
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
212
|
+
key: 'payload:auth-session:tab-sync',
|
|
213
|
+
newValue: JSON.stringify({
|
|
214
|
+
affectedExpirationMs: 0,
|
|
215
|
+
settlesSentAt: 200,
|
|
216
|
+
sentAt: 201,
|
|
217
|
+
sourceTabID: 'logout-tab',
|
|
218
|
+
type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT
|
|
219
|
+
})
|
|
220
|
+
}));
|
|
221
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
222
|
+
key: 'payload:auth-session:tab-sync',
|
|
223
|
+
newValue: JSON.stringify({
|
|
224
|
+
affectedExpirationMs: 30_000,
|
|
225
|
+
refreshStartedAt: 100,
|
|
226
|
+
sentAt: 300,
|
|
227
|
+
sourceTabID: 'refresh-tab',
|
|
228
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
229
|
+
})
|
|
230
|
+
}));
|
|
231
|
+
expect(onSessionLoggedOut).toHaveBeenCalledOnce();
|
|
232
|
+
expect(reconcileSession).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
it('should keep storage fallback notification-only while reconciling refresh and settling logout', async () => {
|
|
235
|
+
vi.stubGlobal('BroadcastChannel', undefined);
|
|
236
|
+
const reconcileSession = vi.fn().mockResolvedValue({
|
|
237
|
+
status: 'indeterminate'
|
|
238
|
+
});
|
|
239
|
+
const onSessionLoggedOut = vi.fn();
|
|
240
|
+
createTestTabSessionSync({
|
|
241
|
+
reconcileSession,
|
|
242
|
+
onSessionLoggedOut,
|
|
243
|
+
sourceTabID: 'receiving-tab'
|
|
244
|
+
});
|
|
245
|
+
const setItem = vi.spyOn(Storage.prototype, 'setItem');
|
|
246
|
+
const broadcaster = createTestTabSessionSync({
|
|
247
|
+
now: () => 123,
|
|
248
|
+
sourceTabID: 'broadcasting-tab'
|
|
249
|
+
});
|
|
250
|
+
broadcaster.broadcast({
|
|
251
|
+
refreshStartedAt: 100,
|
|
252
|
+
session: createSession({
|
|
253
|
+
expirationMs: 20_000,
|
|
254
|
+
token: 'sensitive-token'
|
|
255
|
+
}),
|
|
256
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
257
|
+
});
|
|
258
|
+
const [key, value] = setItem.mock.calls[0];
|
|
259
|
+
expect(JSON.parse(value)).toEqual({
|
|
260
|
+
affectedExpirationMs: 20_000,
|
|
261
|
+
isBroadcastChannelFallback: true,
|
|
262
|
+
refreshStartedAt: 100,
|
|
263
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED,
|
|
264
|
+
sentAt: 123,
|
|
265
|
+
sourceTabID: 'broadcasting-tab'
|
|
266
|
+
});
|
|
267
|
+
expect(value).not.toContain('sensitive-token');
|
|
268
|
+
expect(value).not.toContain('user');
|
|
269
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
270
|
+
key,
|
|
271
|
+
newValue: value
|
|
272
|
+
}));
|
|
273
|
+
await Promise.resolve();
|
|
274
|
+
expect(reconcileSession).toHaveBeenCalledOnce();
|
|
275
|
+
reconcileSession.mockClear();
|
|
276
|
+
const logoutPublication = broadcaster.broadcast({
|
|
277
|
+
type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT
|
|
278
|
+
});
|
|
279
|
+
expect(setItem).toHaveBeenCalledOnce();
|
|
280
|
+
expect(logoutPublication.type).toBe(TAB_SESSION_EVENT_TYPES.LOGGED_OUT);
|
|
281
|
+
if (logoutPublication.type !== TAB_SESSION_EVENT_TYPES.LOGGED_OUT) {
|
|
282
|
+
throw new Error('Expected a logged-out publication.');
|
|
283
|
+
}
|
|
284
|
+
broadcaster.broadcastLogoutSettlement(logoutPublication);
|
|
285
|
+
const [logoutKey, logoutValue] = setItem.mock.calls[1];
|
|
286
|
+
expect(JSON.parse(logoutValue)).toEqual({
|
|
287
|
+
affectedExpirationMs: 0,
|
|
288
|
+
isBroadcastChannelFallback: true,
|
|
289
|
+
settlesSentAt: 124,
|
|
290
|
+
sentAt: 125,
|
|
291
|
+
sourceTabID: 'broadcasting-tab',
|
|
292
|
+
type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT
|
|
293
|
+
});
|
|
294
|
+
expect(logoutValue).not.toContain('sensitive-token');
|
|
295
|
+
expect(logoutValue).not.toContain('user');
|
|
296
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
297
|
+
key: logoutKey,
|
|
298
|
+
newValue: logoutValue
|
|
299
|
+
}));
|
|
300
|
+
expect(onSessionLoggedOut).toHaveBeenCalledOnce();
|
|
301
|
+
expect(reconcileSession).not.toHaveBeenCalled();
|
|
302
|
+
await Promise.resolve();
|
|
303
|
+
expect(reconcileSession).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
it('should downgrade a failed channel and reconcile through storage', async () => {
|
|
306
|
+
const reconcileSession = vi.fn().mockResolvedValue({
|
|
307
|
+
status: 'indeterminate'
|
|
308
|
+
});
|
|
309
|
+
createTestTabSessionSync({
|
|
310
|
+
reconcileSession,
|
|
311
|
+
sourceTabID: 'storage-peer'
|
|
312
|
+
});
|
|
313
|
+
const addEventListener = vi.spyOn(window, 'addEventListener');
|
|
314
|
+
const setItem = vi.spyOn(Storage.prototype, 'setItem');
|
|
315
|
+
const broadcaster = createTestTabSessionSync({
|
|
316
|
+
sourceTabID: 'channel-broadcaster'
|
|
317
|
+
});
|
|
318
|
+
const failedChannel = getBroadcastChannel();
|
|
319
|
+
failedChannel.postMessage.mockImplementationOnce(() => {
|
|
320
|
+
throw new Error('channel closed');
|
|
321
|
+
});
|
|
322
|
+
broadcaster.broadcast({
|
|
323
|
+
refreshStartedAt: 100,
|
|
324
|
+
session: createSession({
|
|
325
|
+
expirationMs: 20_000
|
|
326
|
+
}),
|
|
327
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED
|
|
328
|
+
});
|
|
329
|
+
expect(failedChannel.close).toHaveBeenCalledOnce();
|
|
330
|
+
expect(addEventListener).toHaveBeenCalledWith('storage', expect.any(Function));
|
|
331
|
+
expect(setItem).toHaveBeenCalledOnce();
|
|
332
|
+
const [key, newValue] = setItem.mock.calls[0];
|
|
333
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
334
|
+
key,
|
|
335
|
+
newValue
|
|
336
|
+
}));
|
|
337
|
+
await Promise.resolve();
|
|
338
|
+
expect(reconcileSession).toHaveBeenCalledOnce();
|
|
339
|
+
});
|
|
340
|
+
it('should ignore a pending storage reconciliation after cleanup', async () => {
|
|
341
|
+
vi.stubGlobal('BroadcastChannel', undefined);
|
|
342
|
+
let resolveReconciliation;
|
|
343
|
+
const reconcileSession = vi.fn(() => new Promise(resolve => {
|
|
344
|
+
resolveReconciliation = resolve;
|
|
345
|
+
}));
|
|
346
|
+
const onTabSessionUnauthenticated = vi.fn();
|
|
347
|
+
const tabSessionSync = createTestTabSessionSync({
|
|
348
|
+
reconcileSession,
|
|
349
|
+
onTabSessionUnauthenticated
|
|
350
|
+
});
|
|
351
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
352
|
+
key: 'payload:auth-session:tab-sync',
|
|
353
|
+
newValue: JSON.stringify({
|
|
354
|
+
affectedExpirationMs: 40_000,
|
|
355
|
+
refreshStartedAt: 400,
|
|
356
|
+
type: TAB_SESSION_EVENT_TYPES.REFRESHED,
|
|
357
|
+
sentAt: 500,
|
|
358
|
+
sourceTabID: 'remote-tab'
|
|
359
|
+
})
|
|
360
|
+
}));
|
|
361
|
+
tabSessionSync.cleanup();
|
|
362
|
+
resolveReconciliation?.({
|
|
363
|
+
status: 'unauthenticated'
|
|
364
|
+
});
|
|
365
|
+
await Promise.resolve();
|
|
366
|
+
expect(onTabSessionUnauthenticated).not.toHaveBeenCalled();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
function createTestTabSessionSync({
|
|
370
|
+
reconcileSession = vi.fn().mockResolvedValue({
|
|
371
|
+
status: 'indeterminate'
|
|
372
|
+
}),
|
|
373
|
+
localExpirationMs,
|
|
374
|
+
now,
|
|
375
|
+
onSessionExpired = vi.fn(),
|
|
376
|
+
onSessionLoggedOut = vi.fn(),
|
|
377
|
+
onSessionRefreshed = vi.fn(),
|
|
378
|
+
onTabSessionUnauthenticated = vi.fn(),
|
|
379
|
+
sourceTabID = 'local-tab'
|
|
380
|
+
} = {}) {
|
|
381
|
+
const tabSessionSync = createTabSessionSync({
|
|
382
|
+
reconcileSession,
|
|
383
|
+
getTokenExpirationMs: () => localExpirationMs,
|
|
384
|
+
now,
|
|
385
|
+
onSessionExpired,
|
|
386
|
+
onSessionLoggedOut,
|
|
387
|
+
onSessionRefreshed,
|
|
388
|
+
onTabSessionUnauthenticated,
|
|
389
|
+
sourceTabID
|
|
390
|
+
});
|
|
391
|
+
tabSessionSyncCleanups.push(tabSessionSync.cleanup);
|
|
392
|
+
return tabSessionSync;
|
|
393
|
+
}
|
|
394
|
+
class MockBroadcastChannel {
|
|
395
|
+
static instances = [];
|
|
396
|
+
listeners = new Set();
|
|
397
|
+
addEventListener = vi.fn((_type, listener) => {
|
|
398
|
+
this.listeners.add(listener);
|
|
399
|
+
});
|
|
400
|
+
close = vi.fn();
|
|
401
|
+
postMessage = vi.fn();
|
|
402
|
+
removeEventListener = vi.fn((_type, listener) => {
|
|
403
|
+
this.listeners.delete(listener);
|
|
404
|
+
});
|
|
405
|
+
emit(message) {
|
|
406
|
+
for (const listener of this.listeners) {
|
|
407
|
+
listener(new MessageEvent('message', {
|
|
408
|
+
data: message
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function createTabSessionMessage(message) {
|
|
414
|
+
const {
|
|
415
|
+
sentAt = 100,
|
|
416
|
+
...messageWithoutTimestamp
|
|
417
|
+
} = message;
|
|
418
|
+
if (messageWithoutTimestamp.type === TAB_SESSION_EVENT_TYPES.REFRESHED) {
|
|
419
|
+
const {
|
|
420
|
+
refreshStartedAt = sentAt,
|
|
421
|
+
...refreshedMessage
|
|
422
|
+
} = messageWithoutTimestamp;
|
|
423
|
+
return {
|
|
424
|
+
...refreshedMessage,
|
|
425
|
+
refreshStartedAt,
|
|
426
|
+
sentAt
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
...messageWithoutTimestamp,
|
|
431
|
+
sentAt
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function createSession({
|
|
435
|
+
expirationMs,
|
|
436
|
+
token = 'token'
|
|
437
|
+
}) {
|
|
438
|
+
return {
|
|
439
|
+
exp: expirationMs / 1000,
|
|
440
|
+
token,
|
|
441
|
+
user: {
|
|
442
|
+
id: '1',
|
|
443
|
+
collection: 'users'
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function getBroadcastChannel() {
|
|
448
|
+
const channel = MockBroadcastChannel.instances.at(-1);
|
|
449
|
+
if (!channel) {
|
|
450
|
+
throw new Error('Expected a BroadcastChannel instance');
|
|
451
|
+
}
|
|
452
|
+
return channel;
|
|
453
|
+
}
|
|
454
|
+
function resetMockBroadcastChannels() {
|
|
455
|
+
MockBroadcastChannel.instances.length = 0;
|
|
456
|
+
vi.stubGlobal('BroadcastChannel', class extends MockBroadcastChannel {
|
|
457
|
+
constructor() {
|
|
458
|
+
super();
|
|
459
|
+
MockBroadcastChannel.instances.push(this);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
//# sourceMappingURL=index.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.spec.js","names":["afterEach","beforeEach","describe","expect","it","vi","TAB_SESSION_EVENT_TYPES","createTabSessionSync","tabSessionSyncCleanups","resetMockBroadcastChannels","cleanup","splice","restoreAllMocks","unstubAllGlobals","clearAllMocks","session","createSession","expirationMs","token","tabSessionSync","createTestTabSessionSync","now","broadcast","refreshStartedAt","type","REFRESHED","getBroadcastChannel","postMessage","toHaveBeenCalledWith","sourceTabID","sentAt","onSessionRefreshed","fn","localExpirationMs","channel","emit","createTabSessionMessage","onSessionExpired","expiredTokenAt","EXPIRED","not","toHaveBeenCalled","firstState","secondState","refreshMessage","logoutMessage","LOGGED_OUT","onSessionLoggedOut","firstChannel","secondChannel","toBe","toHaveBeenCalledOnce","appliedSession","refreshB","refreshA","toHaveBeenCalledTimes","toHaveBeenLastCalledWith","reconcileSession","mockResolvedValue","status","window","dispatchEvent","StorageEvent","key","newValue","JSON","stringify","affectedExpirationMs","Promise","resolve","stubGlobal","undefined","settlesSentAt","setItem","spyOn","Storage","prototype","broadcaster","value","mock","calls","parse","toEqual","isBroadcastChannelFallback","toContain","mockClear","logoutPublication","Error","broadcastLogoutSettlement","logoutKey","logoutValue","addEventListener","failedChannel","mockImplementationOnce","close","any","Function","resolveReconciliation","onTabSessionUnauthenticated","getTokenExpirationMs","push","MockBroadcastChannel","instances","listeners","Set","_type","listener","add","removeEventListener","delete","message","MessageEvent","data","messageWithoutTimestamp","refreshedMessage","exp","user","id","collection","at","length","constructor"],"sources":["../../../../src/providers/Auth/tabSessionSync/index.spec.ts"],"sourcesContent":["// @vitest-environment jsdom\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport type { UserWithToken } from '../types.js'\nimport type { TabSessionMessage, TabSessionReconciliationResult } from './index.js'\n\nimport { TAB_SESSION_EVENT_TYPES, createTabSessionSync } from './index.js'\n\nconst tabSessionSyncCleanups: Array<() => void> = []\n\nbeforeEach(() => {\n resetMockBroadcastChannels()\n})\n\nafterEach(() => {\n for (const cleanup of tabSessionSyncCleanups.splice(0)) {\n cleanup()\n }\n\n vi.restoreAllMocks()\n vi.unstubAllGlobals()\n vi.clearAllMocks()\n})\n\ndescribe('createTabSessionSync', () => {\n it('should broadcast refreshed sessions with source-tab and timing metadata', () => {\n const session = createSession({ expirationMs: 20_000, token: 'refreshed-token' })\n const tabSessionSync = createTestTabSessionSync({ now: () => 123 })\n\n tabSessionSync.broadcast({\n refreshStartedAt: 100,\n session,\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n\n expect(getBroadcastChannel().postMessage).toHaveBeenCalledWith({\n refreshStartedAt: 100,\n session,\n sourceTabID: 'local-tab',\n sentAt: 123,\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n })\n\n it('should apply a newer refresh when the provider shortens the token lifetime', () => {\n const onSessionRefreshed = vi.fn()\n const session = createSession({ expirationMs: 30_000 })\n\n createTestTabSessionSync({\n localExpirationMs: 40_000,\n onSessionRefreshed,\n })\n const channel = getBroadcastChannel()\n\n channel.emit(\n createTabSessionMessage({\n sentAt: 100,\n session,\n sourceTabID: 'refresh-tab',\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n }),\n )\n\n expect(onSessionRefreshed).toHaveBeenCalledWith(session)\n })\n\n it('should ignore expiration events for older sessions', () => {\n const onSessionExpired = vi.fn()\n\n createTestTabSessionSync({\n localExpirationMs: 40_000,\n onSessionExpired,\n })\n const channel = getBroadcastChannel()\n\n channel.emit(\n createTabSessionMessage({\n expiredTokenAt: 30_000,\n sourceTabID: 'stale-expiration-tab',\n type: TAB_SESSION_EVENT_TYPES.EXPIRED,\n }),\n )\n\n expect(onSessionExpired).not.toHaveBeenCalled()\n })\n\n it('should converge on logout when equal-time events arrive in opposite orders', () => {\n let firstState = 'initial'\n let secondState = 'initial'\n const refreshMessage = createTabSessionMessage({\n sentAt: 500,\n session: createSession({ expirationMs: 30_000, token: 'refreshed-token' }),\n sourceTabID: 'refresh-tab',\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n const logoutMessage = createTabSessionMessage({\n sentAt: 500,\n sourceTabID: 'logout-tab',\n type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT,\n })\n\n createTestTabSessionSync({\n onSessionLoggedOut: () => {\n firstState = 'logged-out'\n },\n onSessionRefreshed: () => {\n firstState = 'refreshed'\n },\n sourceTabID: 'first-local-tab',\n })\n const firstChannel = getBroadcastChannel()\n createTestTabSessionSync({\n onSessionLoggedOut: () => {\n secondState = 'logged-out'\n },\n onSessionRefreshed: () => {\n secondState = 'refreshed'\n },\n sourceTabID: 'second-local-tab',\n })\n const secondChannel = getBroadcastChannel()\n\n firstChannel.emit(refreshMessage)\n firstChannel.emit(logoutMessage)\n secondChannel.emit(logoutMessage)\n secondChannel.emit(refreshMessage)\n\n expect(firstState).toBe('logged-out')\n expect(secondState).toBe('logged-out')\n })\n\n it('should keep logout ahead of a refresh that started earlier and broadcast later', () => {\n const onSessionLoggedOut = vi.fn()\n const onSessionRefreshed = vi.fn()\n const refreshMessage = createTabSessionMessage({\n refreshStartedAt: 100,\n sentAt: 300,\n session: createSession({ expirationMs: 30_000, token: 'late-refreshed-token' }),\n sourceTabID: 'refresh-tab',\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n const logoutMessage = createTabSessionMessage({\n sentAt: 200,\n sourceTabID: 'logout-tab',\n type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT,\n })\n\n createTestTabSessionSync({\n onSessionLoggedOut,\n onSessionRefreshed,\n sourceTabID: 'receiving-tab',\n })\n const channel = getBroadcastChannel()\n\n channel.emit(logoutMessage)\n channel.emit(refreshMessage)\n\n expect(onSessionLoggedOut).toHaveBeenCalledOnce()\n expect(onSessionRefreshed).not.toHaveBeenCalled()\n })\n\n it('should apply refreshes in publication order instead of request-start ordering for refresh-vs-refresh', () => {\n let appliedSession: UserWithToken | undefined\n const onSessionRefreshed = vi.fn((session: UserWithToken) => {\n appliedSession = session\n })\n const refreshB = createTabSessionMessage({\n refreshStartedAt: 200,\n sentAt: 100,\n session: createSession({ expirationMs: 30_000, token: 'refresh-b' }),\n sourceTabID: 'refresh-b-tab',\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n const refreshA = createTabSessionMessage({\n refreshStartedAt: 100,\n sentAt: 200,\n session: createSession({ expirationMs: 40_000, token: 'refresh-a' }),\n sourceTabID: 'refresh-a-tab',\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n\n createTestTabSessionSync({ onSessionRefreshed, sourceTabID: 'receiving-tab' })\n const channel = getBroadcastChannel()\n\n channel.emit(refreshB)\n channel.emit(refreshA)\n\n expect(onSessionRefreshed).toHaveBeenCalledTimes(2)\n expect(onSessionRefreshed).toHaveBeenLastCalledWith(refreshA.session)\n expect(appliedSession).toBe(refreshA.session)\n })\n\n it('should ignore storage notifications while BroadcastChannel is healthy', async () => {\n const reconcileSession = vi.fn().mockResolvedValue({ status: 'indeterminate' } as const)\n\n createTestTabSessionSync({ reconcileSession, sourceTabID: 'receiving-tab' })\n\n window.dispatchEvent(\n new StorageEvent('storage', {\n key: 'payload:auth-session:tab-sync',\n newValue: JSON.stringify({\n affectedExpirationMs: 30_000,\n refreshStartedAt: 100,\n sentAt: 200,\n sourceTabID: 'refreshing-tab',\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n }),\n }),\n )\n await Promise.resolve()\n\n expect(reconcileSession).not.toHaveBeenCalled()\n })\n\n it('should ignore a late storage refresh whose request started before logout', () => {\n vi.stubGlobal('BroadcastChannel', undefined)\n const reconcileSession = vi.fn().mockResolvedValue({ status: 'indeterminate' } as const)\n const onSessionLoggedOut = vi.fn()\n\n createTestTabSessionSync({\n reconcileSession,\n onSessionLoggedOut,\n sourceTabID: 'receiving-tab',\n })\n\n window.dispatchEvent(\n new StorageEvent('storage', {\n key: 'payload:auth-session:tab-sync',\n newValue: JSON.stringify({\n affectedExpirationMs: 0,\n settlesSentAt: 200,\n sentAt: 201,\n sourceTabID: 'logout-tab',\n type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT,\n }),\n }),\n )\n window.dispatchEvent(\n new StorageEvent('storage', {\n key: 'payload:auth-session:tab-sync',\n newValue: JSON.stringify({\n affectedExpirationMs: 30_000,\n refreshStartedAt: 100,\n sentAt: 300,\n sourceTabID: 'refresh-tab',\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n }),\n }),\n )\n\n expect(onSessionLoggedOut).toHaveBeenCalledOnce()\n expect(reconcileSession).not.toHaveBeenCalled()\n })\n\n it('should keep storage fallback notification-only while reconciling refresh and settling logout', async () => {\n vi.stubGlobal('BroadcastChannel', undefined)\n const reconcileSession = vi.fn().mockResolvedValue({ status: 'indeterminate' })\n const onSessionLoggedOut = vi.fn()\n\n createTestTabSessionSync({\n reconcileSession,\n onSessionLoggedOut,\n sourceTabID: 'receiving-tab',\n })\n const setItem = vi.spyOn(Storage.prototype, 'setItem')\n const broadcaster = createTestTabSessionSync({\n now: () => 123,\n sourceTabID: 'broadcasting-tab',\n })\n\n broadcaster.broadcast({\n refreshStartedAt: 100,\n session: createSession({ expirationMs: 20_000, token: 'sensitive-token' }),\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n const [key, value] = setItem.mock.calls[0] as [string, string]\n\n expect(JSON.parse(value)).toEqual({\n affectedExpirationMs: 20_000,\n isBroadcastChannelFallback: true,\n refreshStartedAt: 100,\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n sentAt: 123,\n sourceTabID: 'broadcasting-tab',\n })\n expect(value).not.toContain('sensitive-token')\n expect(value).not.toContain('user')\n\n window.dispatchEvent(new StorageEvent('storage', { key, newValue: value }))\n await Promise.resolve()\n\n expect(reconcileSession).toHaveBeenCalledOnce()\n\n reconcileSession.mockClear()\n\n const logoutPublication = broadcaster.broadcast({\n type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT,\n })\n\n expect(setItem).toHaveBeenCalledOnce()\n expect(logoutPublication.type).toBe(TAB_SESSION_EVENT_TYPES.LOGGED_OUT)\n\n if (logoutPublication.type !== TAB_SESSION_EVENT_TYPES.LOGGED_OUT) {\n throw new Error('Expected a logged-out publication.')\n }\n\n broadcaster.broadcastLogoutSettlement(logoutPublication)\n\n const [logoutKey, logoutValue] = setItem.mock.calls[1] as [string, string]\n\n expect(JSON.parse(logoutValue)).toEqual({\n affectedExpirationMs: 0,\n isBroadcastChannelFallback: true,\n settlesSentAt: 124,\n sentAt: 125,\n sourceTabID: 'broadcasting-tab',\n type: TAB_SESSION_EVENT_TYPES.LOGGED_OUT,\n })\n expect(logoutValue).not.toContain('sensitive-token')\n expect(logoutValue).not.toContain('user')\n\n window.dispatchEvent(new StorageEvent('storage', { key: logoutKey, newValue: logoutValue }))\n\n expect(onSessionLoggedOut).toHaveBeenCalledOnce()\n expect(reconcileSession).not.toHaveBeenCalled()\n\n await Promise.resolve()\n\n expect(reconcileSession).not.toHaveBeenCalled()\n })\n\n it('should downgrade a failed channel and reconcile through storage', async () => {\n const reconcileSession = vi.fn().mockResolvedValue({ status: 'indeterminate' })\n\n createTestTabSessionSync({ reconcileSession, sourceTabID: 'storage-peer' })\n const addEventListener = vi.spyOn(window, 'addEventListener')\n const setItem = vi.spyOn(Storage.prototype, 'setItem')\n const broadcaster = createTestTabSessionSync({ sourceTabID: 'channel-broadcaster' })\n const failedChannel = getBroadcastChannel()\n\n failedChannel.postMessage.mockImplementationOnce(() => {\n throw new Error('channel closed')\n })\n broadcaster.broadcast({\n refreshStartedAt: 100,\n session: createSession({ expirationMs: 20_000 }),\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n })\n\n expect(failedChannel.close).toHaveBeenCalledOnce()\n expect(addEventListener).toHaveBeenCalledWith('storage', expect.any(Function))\n expect(setItem).toHaveBeenCalledOnce()\n\n const [key, newValue] = setItem.mock.calls[0] as [string, string]\n\n window.dispatchEvent(new StorageEvent('storage', { key, newValue }))\n await Promise.resolve()\n\n expect(reconcileSession).toHaveBeenCalledOnce()\n })\n\n it('should ignore a pending storage reconciliation after cleanup', async () => {\n vi.stubGlobal('BroadcastChannel', undefined)\n let resolveReconciliation: ((result: TabSessionReconciliationResult) => void) | undefined\n const reconcileSession = vi.fn(\n () =>\n new Promise<TabSessionReconciliationResult>((resolve) => {\n resolveReconciliation = resolve\n }),\n )\n const onTabSessionUnauthenticated = vi.fn()\n const tabSessionSync = createTestTabSessionSync({\n reconcileSession,\n onTabSessionUnauthenticated,\n })\n\n window.dispatchEvent(\n new StorageEvent('storage', {\n key: 'payload:auth-session:tab-sync',\n newValue: JSON.stringify({\n affectedExpirationMs: 40_000,\n refreshStartedAt: 400,\n type: TAB_SESSION_EVENT_TYPES.REFRESHED,\n sentAt: 500,\n sourceTabID: 'remote-tab',\n }),\n }),\n )\n tabSessionSync.cleanup()\n resolveReconciliation?.({ status: 'unauthenticated' })\n await Promise.resolve()\n\n expect(onTabSessionUnauthenticated).not.toHaveBeenCalled()\n })\n})\n\nfunction createTestTabSessionSync({\n reconcileSession = vi.fn().mockResolvedValue({ status: 'indeterminate' } as const),\n localExpirationMs,\n now,\n onSessionExpired = vi.fn(),\n onSessionLoggedOut = vi.fn(),\n onSessionRefreshed = vi.fn(),\n onTabSessionUnauthenticated = vi.fn(),\n sourceTabID = 'local-tab',\n}: {\n reconcileSession?: () => Promise<TabSessionReconciliationResult>\n localExpirationMs?: number\n now?: () => number\n onSessionExpired?: (expiredTokenAt: number) => void\n onSessionLoggedOut?: () => void\n onSessionRefreshed?: (session: UserWithToken) => void\n onTabSessionUnauthenticated?: () => void\n sourceTabID?: string\n} = {}) {\n const tabSessionSync = createTabSessionSync({\n reconcileSession,\n getTokenExpirationMs: () => localExpirationMs,\n now,\n onSessionExpired,\n onSessionLoggedOut,\n onSessionRefreshed,\n onTabSessionUnauthenticated,\n sourceTabID,\n })\n\n tabSessionSyncCleanups.push(tabSessionSync.cleanup)\n\n return tabSessionSync\n}\n\nclass MockBroadcastChannel {\n static instances: MockBroadcastChannel[] = []\n\n listeners = new Set<(event: MessageEvent<TabSessionMessage>) => void>()\n addEventListener = vi.fn(\n (_type: string, listener: (event: MessageEvent<TabSessionMessage>) => void) => {\n this.listeners.add(listener)\n },\n )\n close = vi.fn()\n postMessage = vi.fn()\n removeEventListener = vi.fn(\n (_type: string, listener: (event: MessageEvent<TabSessionMessage>) => void) => {\n this.listeners.delete(listener)\n },\n )\n\n emit(message: TabSessionMessage): void {\n for (const listener of this.listeners) {\n listener(new MessageEvent('message', { data: message }))\n }\n }\n}\n\nfunction createTabSessionMessage(\n message:\n | ({\n sentAt?: number\n } & Omit<\n Extract<TabSessionMessage, { type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED }>,\n 'sentAt'\n >)\n | ({\n sentAt?: number\n } & Omit<\n Extract<TabSessionMessage, { type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT }>,\n 'sentAt'\n >)\n | ({\n refreshStartedAt?: number\n sentAt?: number\n } & Omit<\n Extract<TabSessionMessage, { type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED }>,\n 'refreshStartedAt' | 'sentAt'\n >),\n): TabSessionMessage {\n const { sentAt = 100, ...messageWithoutTimestamp } = message\n\n if (messageWithoutTimestamp.type === TAB_SESSION_EVENT_TYPES.REFRESHED) {\n const { refreshStartedAt = sentAt, ...refreshedMessage } = messageWithoutTimestamp\n\n return { ...refreshedMessage, refreshStartedAt, sentAt }\n }\n\n return { ...messageWithoutTimestamp, sentAt } as TabSessionMessage\n}\n\nfunction createSession({\n expirationMs,\n token = 'token',\n}: {\n expirationMs: number\n token?: string\n}): UserWithToken {\n return {\n exp: expirationMs / 1000,\n token,\n user: { id: '1', collection: 'users' },\n }\n}\n\nfunction getBroadcastChannel(): MockBroadcastChannel {\n const channel = MockBroadcastChannel.instances.at(-1)\n\n if (!channel) {\n throw new Error('Expected a BroadcastChannel instance')\n }\n\n return channel\n}\n\nfunction resetMockBroadcastChannels(): void {\n MockBroadcastChannel.instances.length = 0\n vi.stubGlobal(\n 'BroadcastChannel',\n class extends MockBroadcastChannel {\n constructor() {\n super()\n MockBroadcastChannel.instances.push(this)\n }\n },\n )\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { UserWithToken } from '../types.js';
|
|
2
|
+
export declare const TAB_SESSION_EVENT_TYPES: {
|
|
3
|
+
readonly EXPIRED: 'session-expired';
|
|
4
|
+
readonly LOGGED_OUT: 'session-logged-out';
|
|
5
|
+
readonly REFRESHED: 'session-refreshed';
|
|
6
|
+
};
|
|
7
|
+
export type TabSessionEventType = (typeof TAB_SESSION_EVENT_TYPES)[keyof typeof TAB_SESSION_EVENT_TYPES];
|
|
8
|
+
export type TabSessionMessage = {
|
|
9
|
+
expiredTokenAt: number;
|
|
10
|
+
sentAt: number;
|
|
11
|
+
sourceTabID: string;
|
|
12
|
+
type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED;
|
|
13
|
+
} | {
|
|
14
|
+
refreshStartedAt: number;
|
|
15
|
+
sentAt: number;
|
|
16
|
+
session: UserWithToken;
|
|
17
|
+
sourceTabID: string;
|
|
18
|
+
type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED;
|
|
19
|
+
} | {
|
|
20
|
+
sentAt: number;
|
|
21
|
+
sourceTabID: string;
|
|
22
|
+
type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT;
|
|
23
|
+
};
|
|
24
|
+
export type TabSessionEvent = {
|
|
25
|
+
expiredTokenAt: number;
|
|
26
|
+
type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED;
|
|
27
|
+
} | {
|
|
28
|
+
refreshStartedAt: number;
|
|
29
|
+
session: UserWithToken;
|
|
30
|
+
type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED;
|
|
31
|
+
} | {
|
|
32
|
+
type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT;
|
|
33
|
+
};
|
|
34
|
+
export type TabSessionReconciliationOptions = {
|
|
35
|
+
isTabSessionEventStale: () => boolean;
|
|
36
|
+
};
|
|
37
|
+
export type TabSessionReconciliationResult<T = unknown> = {
|
|
38
|
+
expirationMs: number;
|
|
39
|
+
status: 'authenticated';
|
|
40
|
+
user: T;
|
|
41
|
+
} | {
|
|
42
|
+
status: 'indeterminate';
|
|
43
|
+
} | {
|
|
44
|
+
status: 'unauthenticated';
|
|
45
|
+
};
|
|
46
|
+
export type CreateTabSessionSyncArgs = {
|
|
47
|
+
getTokenExpirationMs: () => number | undefined;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
onSessionExpired: (expiredTokenAt: number) => void;
|
|
50
|
+
onSessionLoggedOut: () => void;
|
|
51
|
+
onSessionRefreshed: (session: UserWithToken) => void;
|
|
52
|
+
onTabSessionUnauthenticated: () => void;
|
|
53
|
+
reconcileSession: (options: TabSessionReconciliationOptions) => Promise<TabSessionReconciliationResult>;
|
|
54
|
+
sourceTabID: string;
|
|
55
|
+
};
|
|
56
|
+
export type TabSessionPublication = {
|
|
57
|
+
affectedExpirationMs: 0;
|
|
58
|
+
sentAt: number;
|
|
59
|
+
sourceTabID: string;
|
|
60
|
+
type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT;
|
|
61
|
+
} | {
|
|
62
|
+
affectedExpirationMs: number;
|
|
63
|
+
refreshStartedAt: number;
|
|
64
|
+
sentAt: number;
|
|
65
|
+
sourceTabID: string;
|
|
66
|
+
type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED;
|
|
67
|
+
} | {
|
|
68
|
+
affectedExpirationMs: number;
|
|
69
|
+
sentAt: number;
|
|
70
|
+
sourceTabID: string;
|
|
71
|
+
type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED;
|
|
72
|
+
};
|
|
73
|
+
export type TabSessionLogoutPublication = Extract<TabSessionPublication, {
|
|
74
|
+
type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT;
|
|
75
|
+
}>;
|
|
76
|
+
export type TabSessionLifecycleOrder = TabSessionPublication;
|
|
77
|
+
export type TabSessionStorageNotification = {
|
|
78
|
+
isBroadcastChannelFallback?: true;
|
|
79
|
+
} & (({
|
|
80
|
+
settlesSentAt: number;
|
|
81
|
+
} & TabSessionLogoutPublication) | Extract<TabSessionPublication, {
|
|
82
|
+
type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED;
|
|
83
|
+
}> | Extract<TabSessionPublication, {
|
|
84
|
+
type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED;
|
|
85
|
+
}>);
|
|
86
|
+
export type WriteStorageNotification = (args: {
|
|
87
|
+
notification: TabSessionStorageNotification;
|
|
88
|
+
}) => void;
|
|
89
|
+
export type OnExpiredSession = (args: {
|
|
90
|
+
lifecycleOrder: TabSessionLifecycleOrder;
|
|
91
|
+
message: Extract<TabSessionMessage, {
|
|
92
|
+
type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED;
|
|
93
|
+
}>;
|
|
94
|
+
}) => void;
|
|
95
|
+
export type OnLoggedOutSession = (args: {
|
|
96
|
+
lifecycleOrder: TabSessionLifecycleOrder;
|
|
97
|
+
}) => void;
|
|
98
|
+
export type OnRefreshedSession = (args: {
|
|
99
|
+
lifecycleOrder: TabSessionLifecycleOrder;
|
|
100
|
+
message: Extract<TabSessionMessage, {
|
|
101
|
+
type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED;
|
|
102
|
+
}>;
|
|
103
|
+
}) => void;
|
|
104
|
+
export type ReconcileStorageSession = (args: {
|
|
105
|
+
notification: TabSessionStorageNotification;
|
|
106
|
+
}) => void;
|
|
107
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/providers/Auth/tabSessionSync/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":["TAB_SESSION_EVENT_TYPES","EXPIRED","LOGGED_OUT","REFRESHED"],"sources":["../../../../src/providers/Auth/tabSessionSync/types.ts"],"sourcesContent":["import type { UserWithToken } from '../types.js'\n\nexport const TAB_SESSION_EVENT_TYPES = {\n EXPIRED: 'session-expired',\n LOGGED_OUT: 'session-logged-out',\n REFRESHED: 'session-refreshed',\n} as const\n\nexport type TabSessionEventType =\n (typeof TAB_SESSION_EVENT_TYPES)[keyof typeof TAB_SESSION_EVENT_TYPES]\n\nexport type TabSessionMessage =\n | {\n expiredTokenAt: number\n sentAt: number\n sourceTabID: string\n type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED\n }\n | {\n refreshStartedAt: number\n sentAt: number\n session: UserWithToken\n sourceTabID: string\n type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED\n }\n | {\n sentAt: number\n sourceTabID: string\n type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT\n }\n\nexport type TabSessionEvent =\n | {\n expiredTokenAt: number\n type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED\n }\n | {\n refreshStartedAt: number\n session: UserWithToken\n type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED\n }\n | {\n type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT\n }\n\nexport type TabSessionReconciliationOptions = {\n isTabSessionEventStale: () => boolean\n}\n\nexport type TabSessionReconciliationResult<T = unknown> =\n | {\n expirationMs: number\n status: 'authenticated'\n user: T\n }\n | {\n status: 'indeterminate'\n }\n | {\n status: 'unauthenticated'\n }\n\nexport type CreateTabSessionSyncArgs = {\n getTokenExpirationMs: () => number | undefined\n now?: () => number\n onSessionExpired: (expiredTokenAt: number) => void\n onSessionLoggedOut: () => void\n onSessionRefreshed: (session: UserWithToken) => void\n onTabSessionUnauthenticated: () => void\n reconcileSession: (\n options: TabSessionReconciliationOptions,\n ) => Promise<TabSessionReconciliationResult>\n sourceTabID: string\n}\n\nexport type TabSessionPublication =\n | {\n affectedExpirationMs: 0\n sentAt: number\n sourceTabID: string\n type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT\n }\n | {\n affectedExpirationMs: number\n refreshStartedAt: number\n sentAt: number\n sourceTabID: string\n type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED\n }\n | {\n affectedExpirationMs: number\n sentAt: number\n sourceTabID: string\n type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED\n }\n\nexport type TabSessionLogoutPublication = Extract<\n TabSessionPublication,\n { type: typeof TAB_SESSION_EVENT_TYPES.LOGGED_OUT }\n>\n\nexport type TabSessionLifecycleOrder = TabSessionPublication\n\nexport type TabSessionStorageNotification = {\n isBroadcastChannelFallback?: true\n} & (\n | ({ settlesSentAt: number } & TabSessionLogoutPublication)\n | Extract<TabSessionPublication, { type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED }>\n | Extract<TabSessionPublication, { type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED }>\n)\n\nexport type WriteStorageNotification = (args: {\n notification: TabSessionStorageNotification\n}) => void\n\nexport type OnExpiredSession = (args: {\n lifecycleOrder: TabSessionLifecycleOrder\n message: Extract<TabSessionMessage, { type: typeof TAB_SESSION_EVENT_TYPES.EXPIRED }>\n}) => void\n\nexport type OnLoggedOutSession = (args: { lifecycleOrder: TabSessionLifecycleOrder }) => void\n\nexport type OnRefreshedSession = (args: {\n lifecycleOrder: TabSessionLifecycleOrder\n message: Extract<TabSessionMessage, { type: typeof TAB_SESSION_EVENT_TYPES.REFRESHED }>\n}) => void\n\nexport type ReconcileStorageSession = (args: {\n notification: TabSessionStorageNotification\n}) => void\n"],"mappings":"AAEA,OAAO,MAAMA,uBAAA,GAA0B;EACrCC,OAAA,EAAS;EACTC,UAAA,EAAY;EACZC,SAAA,EAAW;AACb","ignoreList":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TabSessionEvent, TabSessionLogoutPublication, TabSessionPublication } from './index.js';
|
|
2
|
+
import { createTabSessionSync } from './index.js';
|
|
3
|
+
export type TabSessionSync = {
|
|
4
|
+
/** Broadcasts a session lifecycle event to the other browser tabs. */
|
|
5
|
+
broadcast: (event: TabSessionEvent) => TabSessionPublication | undefined;
|
|
6
|
+
/** Notifies storage-fallback tabs after the server-side logout request settles. */
|
|
7
|
+
broadcastLogoutSettlement: (publication: TabSessionLogoutPublication) => void;
|
|
8
|
+
};
|
|
9
|
+
/** Connects the Auth provider to one cross-tab session coordinator for the current browser tab. */
|
|
10
|
+
export declare function useTabSessionSync(options: Omit<Parameters<typeof createTabSessionSync>[0], 'sourceTabID'>): TabSessionSync;
|
|
11
|
+
//# sourceMappingURL=useTabSessionSync.d.ts.map
|