forked from QuantumNous/new-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth-session-sync.ts
More file actions
119 lines (104 loc) · 3.55 KB
/
Copy pathauth-session-sync.ts
File metadata and controls
119 lines (104 loc) · 3.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export type AuthSessionSyncEvent = {
kind: 'authenticated' | 'signed_out'
sid: string
source: string
nonce: string
timestamp: number
}
const AUTH_SYNC_CHANNEL = 'new-api:auth-session'
const AUTH_SYNC_STORAGE_KEY = 'new-api:auth-session:event'
function randomIdentifier(): string {
if (typeof globalThis.crypto?.randomUUID === 'function') {
return globalThis.crypto.randomUUID()
}
return `${Date.now()}-${Math.random().toString(36).slice(2)}`
}
const authSyncSource = randomIdentifier()
let authSyncPublisher: BroadcastChannel | null = null
function isAuthSessionSyncEvent(value: unknown): value is AuthSessionSyncEvent {
if (!value || typeof value !== 'object') return false
const event = value as Partial<AuthSessionSyncEvent>
return (
(event.kind === 'authenticated' || event.kind === 'signed_out') &&
typeof event.sid === 'string' &&
event.sid.length > 0 &&
typeof event.source === 'string' &&
typeof event.nonce === 'string' &&
typeof event.timestamp === 'number'
)
}
export function publishAuthSessionEvent(
kind: AuthSessionSyncEvent['kind'],
sid: string
): void {
if (typeof window === 'undefined' || !sid) return
const event: AuthSessionSyncEvent = {
kind,
sid,
source: authSyncSource,
nonce: randomIdentifier(),
timestamp: Date.now(),
}
if (typeof BroadcastChannel !== 'undefined') {
authSyncPublisher ??= new BroadcastChannel(AUTH_SYNC_CHANNEL)
authSyncPublisher.postMessage(event)
return
}
try {
window.localStorage.setItem(AUTH_SYNC_STORAGE_KEY, JSON.stringify(event))
window.localStorage.removeItem(AUTH_SYNC_STORAGE_KEY)
} catch {
// Cross-tab synchronization is best-effort when storage is unavailable.
}
}
export function subscribeAuthSessionEvents(
listener: (event: AuthSessionSyncEvent) => void
): () => void {
if (typeof window === 'undefined') return () => undefined
const deliver = (value: unknown) => {
if (
isAuthSessionSyncEvent(value) &&
value.source !== authSyncSource &&
Math.abs(Date.now() - value.timestamp) < 60_000
) {
listener(value)
}
}
if (typeof BroadcastChannel !== 'undefined') {
const channel = new BroadcastChannel(AUTH_SYNC_CHANNEL)
const handleMessage = (message: MessageEvent<unknown>) => {
deliver(message.data)
}
channel.addEventListener('message', handleMessage)
return () => {
channel.removeEventListener('message', handleMessage)
channel.close()
}
}
const handleStorage = (event: StorageEvent) => {
if (event.key !== AUTH_SYNC_STORAGE_KEY || !event.newValue) return
try {
deliver(JSON.parse(event.newValue))
} catch {
// Ignore malformed same-origin storage events.
}
}
window.addEventListener('storage', handleStorage)
return () => {
window.removeEventListener('storage', handleStorage)
}
}