@gwakko/shared-websocket 0.10.2 → 0.11.0
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/README.md +18 -5
- package/dist/SharedWebSocket.d.ts +43 -2
- package/dist/adapters/react.d.ts +34 -2
- package/dist/adapters/vue.d.ts +27 -2
- package/dist/{chunk-MJXKQYRZ.cjs → chunk-LIFZOQPW.cjs} +133 -12
- package/dist/chunk-LIFZOQPW.cjs.map +1 -0
- package/dist/{chunk-PKZXBX5I.js → chunk-VUTUECT2.js} +128 -7
- package/dist/chunk-VUTUECT2.js.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +1 -1
- package/dist/react.cjs +32 -10
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +30 -8
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/vue.cjs +26 -8
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +24 -6
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
- package/src/SharedWebSocket.ts +144 -6
- package/src/adapters/react.ts +60 -6
- package/src/adapters/vue.ts +45 -4
- package/src/types.ts +8 -0
- package/dist/chunk-MJXKQYRZ.cjs.map +0 -1
- package/dist/chunk-PKZXBX5I.js.map +0 -1
package/README.md
CHANGED
|
@@ -77,6 +77,11 @@ await withSocket('wss://api.example.com/ws', {
|
|
|
77
77
|
title: (n) => n.title, // + browser Notification
|
|
78
78
|
target: 'active', // active | leader | all
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
// Runtime auth — authenticate/deauthenticate without reconnecting
|
|
82
|
+
ws.authenticate(token); // → sends $auth:login to server
|
|
83
|
+
const chat = ws.channel('chat:private', { auth: true }); // auto-leaves on deauth
|
|
84
|
+
ws.deauthenticate(); // → auto-leaves auth channels/topics
|
|
80
85
|
});
|
|
81
86
|
```
|
|
82
87
|
|
|
@@ -86,6 +91,7 @@ await withSocket('wss://api.example.com/ws', {
|
|
|
86
91
|
import {
|
|
87
92
|
SharedWebSocketProvider,
|
|
88
93
|
useSharedWebSocket,
|
|
94
|
+
useAuth,
|
|
89
95
|
useSocketEvent,
|
|
90
96
|
useSocketStream,
|
|
91
97
|
useSocketSync,
|
|
@@ -110,6 +116,7 @@ function App() {
|
|
|
110
116
|
|
|
111
117
|
function Dashboard() {
|
|
112
118
|
const ws = useSharedWebSocket();
|
|
119
|
+
const { isAuthenticated, authenticate, deauthenticate } = useAuth();
|
|
113
120
|
const order = useSocketEvent<Order>('order.created');
|
|
114
121
|
const [cart, setCart] = useSocketSync('cart', { items: [] });
|
|
115
122
|
const { connected, tabRole } = useSocketStatus();
|
|
@@ -123,10 +130,11 @@ function Dashboard() {
|
|
|
123
130
|
useSocketLifecycle({
|
|
124
131
|
onConnect: () => toast.success('Connected'),
|
|
125
132
|
onActive: () => refreshData(),
|
|
133
|
+
onAuthChange: (auth) => !auth && navigate('/login'),
|
|
126
134
|
});
|
|
127
135
|
|
|
128
|
-
//
|
|
129
|
-
const chat = useChannel(`chat:${roomId}
|
|
136
|
+
// Auth-aware channel — auto-leaves on deauth
|
|
137
|
+
const chat = useChannel(`chat:${roomId}`, { auth: true });
|
|
130
138
|
|
|
131
139
|
// Topics
|
|
132
140
|
useTopics(['notifications:orders']);
|
|
@@ -155,6 +163,7 @@ app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {
|
|
|
155
163
|
<script setup lang="ts">
|
|
156
164
|
import {
|
|
157
165
|
useSharedWebSocket,
|
|
166
|
+
useAuth,
|
|
158
167
|
useSocketEvent,
|
|
159
168
|
useSocketSync,
|
|
160
169
|
useSocketLifecycle,
|
|
@@ -164,15 +173,18 @@ import {
|
|
|
164
173
|
} from '@gwakko/shared-websocket/vue';
|
|
165
174
|
|
|
166
175
|
const ws = useSharedWebSocket();
|
|
176
|
+
const { isAuthenticated, authenticate, deauthenticate } = useAuth();
|
|
167
177
|
const order = useSocketEvent<Order>('order.created');
|
|
168
178
|
const cart = useSocketSync('cart', { items: [] });
|
|
169
179
|
|
|
170
180
|
useSocketLifecycle({
|
|
171
181
|
onConnect: () => toast.success('Connected'),
|
|
172
182
|
onActive: () => refreshData(),
|
|
183
|
+
onAuthChange: (auth) => { if (!auth) router.push('/login'); },
|
|
173
184
|
});
|
|
174
185
|
|
|
175
|
-
|
|
186
|
+
// Auth-aware channel — auto-leaves on deauth
|
|
187
|
+
const chat = useChannel(`chat:${roomId}`, { auth: true });
|
|
176
188
|
useTopics(['notifications:orders']);
|
|
177
189
|
|
|
178
190
|
usePush('notification', {
|
|
@@ -196,10 +208,11 @@ usePush('notification', {
|
|
|
196
208
|
| **Worker Mode** | `useWorker: true` — WebSocket off main thread |
|
|
197
209
|
| **Custom Serialization** | `serialize`/`deserialize` — JSON, MessagePack, Protobuf |
|
|
198
210
|
| **Per-Event Serializers** | `ws.serializer(event, fn)` — binary for specific events |
|
|
199
|
-
| **
|
|
211
|
+
| **Runtime Auth** | `authenticate(token)` / `deauthenticate()` on existing connection |
|
|
212
|
+
| **Lifecycle Hooks** | onConnect, onDisconnect, onActive, onInactive, onLeaderChange, onAuthChange |
|
|
200
213
|
| **Debug/Logger** | `debug: true` + injectable logger (pino, Sentry) |
|
|
201
214
|
| **Event Protocol** | Configurable field names (Socket.IO, Phoenix, Laravel Echo) |
|
|
202
|
-
| **Auth** | `auth` callback
|
|
215
|
+
| **Auth** | URL param (`auth` callback / `authToken`) + runtime `authenticate()`/`deauthenticate()` |
|
|
203
216
|
| **Zero Dependencies** | Pure browser APIs |
|
|
204
217
|
|
|
205
218
|
## Processing Pipeline
|
|
@@ -31,9 +31,14 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
|
|
|
31
31
|
private incomingMiddleware;
|
|
32
32
|
private serializers;
|
|
33
33
|
private deserializers;
|
|
34
|
+
private _isAuthenticated;
|
|
35
|
+
private authChannels;
|
|
36
|
+
private authTopics;
|
|
34
37
|
constructor(url: string, options?: SharedWebSocketOptions<TEvents>);
|
|
35
38
|
get connected(): boolean;
|
|
36
39
|
get tabRole(): TabRole;
|
|
40
|
+
/** Whether the user is authenticated via runtime auth. */
|
|
41
|
+
get isAuthenticated(): boolean;
|
|
37
42
|
/** Whether this tab is currently visible/focused. */
|
|
38
43
|
get isActive(): boolean;
|
|
39
44
|
/** Start leader election and connect. */
|
|
@@ -54,6 +59,37 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
|
|
|
54
59
|
onInactive(fn: () => void): Unsubscribe;
|
|
55
60
|
/** Called on any visibility change. */
|
|
56
61
|
onVisibilityChange(fn: (isActive: boolean) => void): Unsubscribe;
|
|
62
|
+
/**
|
|
63
|
+
* Authenticate on an existing connection. Sends auth event to server,
|
|
64
|
+
* syncs auth state across all tabs. Use for login after guest connection.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const token = await loginApi(email, password);
|
|
68
|
+
* ws.authenticate(token);
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // React — via useAuth hook
|
|
72
|
+
* const { authenticate } = useAuth();
|
|
73
|
+
* authenticate(token);
|
|
74
|
+
*/
|
|
75
|
+
authenticate(token: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Deauthenticate — notifies server, auto-leaves all auth-required channels
|
|
78
|
+
* and topics, syncs state across tabs. Connection stays open for public events.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ws.deauthenticate(); // connection stays open, auth subscriptions cleaned up
|
|
82
|
+
*/
|
|
83
|
+
deauthenticate(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Called when auth state changes (authenticate, deauthenticate, or server revocation).
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ws.onAuthChange((authenticated) => {
|
|
89
|
+
* if (!authenticated) router.push('/login');
|
|
90
|
+
* });
|
|
91
|
+
*/
|
|
92
|
+
onAuthChange(fn: (authenticated: boolean) => void): Unsubscribe;
|
|
57
93
|
/**
|
|
58
94
|
* Add middleware to transform messages before send or after receive.
|
|
59
95
|
* Return null from middleware to drop the message.
|
|
@@ -129,7 +165,9 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
|
|
|
129
165
|
* const notifications = ws.channel(`tenant:${tenantId}:notifications`);
|
|
130
166
|
* notifications.on('alert', (alert) => showToast(alert));
|
|
131
167
|
*/
|
|
132
|
-
channel(name: string
|
|
168
|
+
channel(name: string, options?: {
|
|
169
|
+
auth?: boolean;
|
|
170
|
+
}): Channel;
|
|
133
171
|
/**
|
|
134
172
|
* Subscribe to a server-side topic. Server will start sending events for this topic.
|
|
135
173
|
* Sends topicSubscribe event (default: "$topic:subscribe").
|
|
@@ -139,7 +177,9 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
|
|
|
139
177
|
* ws.subscribe('notifications:payments');
|
|
140
178
|
* ws.subscribe(`user:${userId}:mentions`);
|
|
141
179
|
*/
|
|
142
|
-
subscribe(topic: string
|
|
180
|
+
subscribe(topic: string, options?: {
|
|
181
|
+
auth?: boolean;
|
|
182
|
+
}): void;
|
|
143
183
|
/**
|
|
144
184
|
* Unsubscribe from a server-side topic.
|
|
145
185
|
* Sends topicUnsubscribe event (default: "$topic:unsubscribe").
|
|
@@ -205,6 +245,7 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
|
|
|
205
245
|
disconnect(): void;
|
|
206
246
|
private createSocket;
|
|
207
247
|
private handleBecomeLeader;
|
|
248
|
+
private reAuthenticateOnReconnect;
|
|
208
249
|
private handleLoseLeadership;
|
|
209
250
|
[Symbol.dispose](): void;
|
|
210
251
|
}
|
package/dist/adapters/react.d.ts
CHANGED
|
@@ -41,6 +41,33 @@ export declare function SharedWebSocketProvider({ url, options, children }: Shar
|
|
|
41
41
|
* ws.send('chat.message', { text: 'Hello' });
|
|
42
42
|
*/
|
|
43
43
|
export declare function useSharedWebSocket(): SharedWebSocket;
|
|
44
|
+
/**
|
|
45
|
+
* Reactive auth state with authenticate/deauthenticate actions.
|
|
46
|
+
* Syncs across all tabs via BroadcastChannel.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* function LoginPage() {
|
|
50
|
+
* const { authenticate } = useAuth();
|
|
51
|
+
* const login = async (email: string, password: string) => {
|
|
52
|
+
* const { token } = await api.login(email, password);
|
|
53
|
+
* authenticate(token);
|
|
54
|
+
* };
|
|
55
|
+
* return <button onClick={() => login('user@test.com', 'pass')}>Login</button>;
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* function Header() {
|
|
60
|
+
* const { isAuthenticated, deauthenticate } = useAuth();
|
|
61
|
+
* return isAuthenticated
|
|
62
|
+
* ? <button onClick={deauthenticate}>Logout</button>
|
|
63
|
+
* : <Link to="/login">Login</Link>;
|
|
64
|
+
* }
|
|
65
|
+
*/
|
|
66
|
+
export declare function useAuth(): {
|
|
67
|
+
isAuthenticated: boolean;
|
|
68
|
+
authenticate: (token: string) => void;
|
|
69
|
+
deauthenticate: () => void;
|
|
70
|
+
};
|
|
44
71
|
/**
|
|
45
72
|
* Subscribe to a WebSocket event.
|
|
46
73
|
* - Without callback: returns the latest received value (reactive state).
|
|
@@ -136,6 +163,7 @@ export declare function useSocketCallback<T>(event: string, callback: (data: T)
|
|
|
136
163
|
export declare function useSocketStatus(): {
|
|
137
164
|
connected: boolean;
|
|
138
165
|
tabRole: TabRole;
|
|
166
|
+
isAuthenticated: boolean;
|
|
139
167
|
};
|
|
140
168
|
/**
|
|
141
169
|
* Lifecycle hooks — react to connection state changes.
|
|
@@ -163,7 +191,9 @@ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): v
|
|
|
163
191
|
* const notifications = useChannel(`tenant:${tenantId}:notifications`);
|
|
164
192
|
* useSocketCallback(`tenant:${tenantId}:notifications:alert`, showToast);
|
|
165
193
|
*/
|
|
166
|
-
export declare function useChannel(name: string
|
|
194
|
+
export declare function useChannel(name: string, options?: {
|
|
195
|
+
auth?: boolean;
|
|
196
|
+
}): import("..").Channel;
|
|
167
197
|
/**
|
|
168
198
|
* Subscribe to server-side topics. Auto-unsubscribes on unmount.
|
|
169
199
|
*
|
|
@@ -171,7 +201,9 @@ export declare function useChannel(name: string): import("..").Channel;
|
|
|
171
201
|
* useTopics(['notifications:orders', 'notifications:payments']);
|
|
172
202
|
* useTopics([`user:${userId}:mentions`]);
|
|
173
203
|
*/
|
|
174
|
-
export declare function useTopics(topics: string[]
|
|
204
|
+
export declare function useTopics(topics: string[], options?: {
|
|
205
|
+
auth?: boolean;
|
|
206
|
+
}): void;
|
|
175
207
|
/**
|
|
176
208
|
* Enable browser push notifications for an event. Auto-cleanup on unmount.
|
|
177
209
|
*
|
package/dist/adapters/vue.d.ts
CHANGED
|
@@ -23,6 +23,26 @@ export declare function createSharedWebSocketPlugin(url: string, options?: Share
|
|
|
23
23
|
* ws.send('chat.message', { text: 'Hello' });
|
|
24
24
|
*/
|
|
25
25
|
export declare function useSharedWebSocket(): SharedWebSocket;
|
|
26
|
+
/**
|
|
27
|
+
* Reactive auth state with authenticate/deauthenticate actions.
|
|
28
|
+
* Syncs across all tabs.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const { isAuthenticated, authenticate, deauthenticate } = useAuth();
|
|
32
|
+
*
|
|
33
|
+
* async function login(email: string, password: string) {
|
|
34
|
+
* const { token } = await api.login(email, password);
|
|
35
|
+
* authenticate(token);
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // In template: <button v-if="isAuthenticated" @click="deauthenticate">Logout</button>
|
|
40
|
+
*/
|
|
41
|
+
export declare function useAuth(): {
|
|
42
|
+
isAuthenticated: Ref<boolean>;
|
|
43
|
+
authenticate: (token: string) => void;
|
|
44
|
+
deauthenticate: () => void;
|
|
45
|
+
};
|
|
26
46
|
/**
|
|
27
47
|
* Subscribe to a WebSocket event.
|
|
28
48
|
* - Without callback: returns reactive ref with latest value.
|
|
@@ -100,6 +120,7 @@ export declare function useSocketCallback<T>(event: string, callback: (data: T)
|
|
|
100
120
|
export declare function useSocketStatus(): {
|
|
101
121
|
connected: Ref<boolean>;
|
|
102
122
|
tabRole: Ref<TabRole>;
|
|
123
|
+
isAuthenticated: Ref<boolean>;
|
|
103
124
|
};
|
|
104
125
|
/**
|
|
105
126
|
* Lifecycle hooks — react to connection state changes.
|
|
@@ -122,14 +143,18 @@ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): v
|
|
|
122
143
|
* // Listen via useSocketEvent('chat:room_123:message')
|
|
123
144
|
* // Send via chat.send('message', { text: 'Hello' })
|
|
124
145
|
*/
|
|
125
|
-
export declare function useChannel(name: string
|
|
146
|
+
export declare function useChannel(name: string, options?: {
|
|
147
|
+
auth?: boolean;
|
|
148
|
+
}): import("..").Channel;
|
|
126
149
|
/**
|
|
127
150
|
* Subscribe to server-side topics. Auto-unsubscribes on unmount.
|
|
128
151
|
*
|
|
129
152
|
* @example
|
|
130
153
|
* useTopics(['notifications:orders', 'notifications:payments']);
|
|
131
154
|
*/
|
|
132
|
-
export declare function useTopics(topics: string[]
|
|
155
|
+
export declare function useTopics(topics: string[], options?: {
|
|
156
|
+
auth?: boolean;
|
|
157
|
+
}): void;
|
|
133
158
|
/**
|
|
134
159
|
* Enable browser push notifications for an event. Auto-cleanup on unmount.
|
|
135
160
|
*
|
|
@@ -590,7 +590,10 @@ var DEFAULT_PROTOCOL = {
|
|
|
590
590
|
ping: { type: "ping" },
|
|
591
591
|
defaultEvent: "message",
|
|
592
592
|
topicSubscribe: "$topic:subscribe",
|
|
593
|
-
topicUnsubscribe: "$topic:unsubscribe"
|
|
593
|
+
topicUnsubscribe: "$topic:unsubscribe",
|
|
594
|
+
authLogin: "$auth:login",
|
|
595
|
+
authLogout: "$auth:logout",
|
|
596
|
+
authRevoked: "$auth:revoked"
|
|
594
597
|
};
|
|
595
598
|
var NOOP_LOGGER = {
|
|
596
599
|
debug() {
|
|
@@ -603,7 +606,7 @@ var NOOP_LOGGER = {
|
|
|
603
606
|
}
|
|
604
607
|
};
|
|
605
608
|
var SharedWebSocket = (_class6 = class {
|
|
606
|
-
constructor(url, options = {}) {;_class6.prototype.__init25.call(this);_class6.prototype.__init26.call(this);_class6.prototype.__init27.call(this);_class6.prototype.__init28.call(this);_class6.prototype.__init29.call(this);_class6.prototype.__init30.call(this);_class6.prototype.__init31.call(this);_class6.prototype.__init32.call(this);_class6.prototype.__init33.call(this);
|
|
609
|
+
constructor(url, options = {}) {;_class6.prototype.__init25.call(this);_class6.prototype.__init26.call(this);_class6.prototype.__init27.call(this);_class6.prototype.__init28.call(this);_class6.prototype.__init29.call(this);_class6.prototype.__init30.call(this);_class6.prototype.__init31.call(this);_class6.prototype.__init32.call(this);_class6.prototype.__init33.call(this);_class6.prototype.__init34.call(this);_class6.prototype.__init35.call(this);_class6.prototype.__init36.call(this);
|
|
607
610
|
this.url = url;
|
|
608
611
|
this.options = options;
|
|
609
612
|
this.proto = { ...DEFAULT_PROTOCOL, ...options.events };
|
|
@@ -660,6 +663,15 @@ var SharedWebSocket = (_class6 = class {
|
|
|
660
663
|
case "error":
|
|
661
664
|
this.subs.emit("$lifecycle:error", msg.error);
|
|
662
665
|
break;
|
|
666
|
+
case "auth": {
|
|
667
|
+
this._isAuthenticated = !!msg.authenticated;
|
|
668
|
+
if (!msg.authenticated) {
|
|
669
|
+
this.authChannels.clear();
|
|
670
|
+
this.authTopics.clear();
|
|
671
|
+
}
|
|
672
|
+
this.subs.emit("$lifecycle:auth", msg.authenticated);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
663
675
|
}
|
|
664
676
|
})
|
|
665
677
|
);
|
|
@@ -672,6 +684,20 @@ var SharedWebSocket = (_class6 = class {
|
|
|
672
684
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
673
685
|
this.cleanups.push(() => document.removeEventListener("visibilitychange", onVisibilityChange));
|
|
674
686
|
}
|
|
687
|
+
this.cleanups.push(
|
|
688
|
+
this.subs.on(this.proto.authRevoked, () => {
|
|
689
|
+
if (this.coordinator.isLeader) {
|
|
690
|
+
for (const [, ch] of this.authChannels) ch.leave();
|
|
691
|
+
for (const topic of this.authTopics) this.unsubscribe(topic);
|
|
692
|
+
}
|
|
693
|
+
this.authChannels.clear();
|
|
694
|
+
this.authTopics.clear();
|
|
695
|
+
this._isAuthenticated = false;
|
|
696
|
+
this.syncStore.delete("$auth:token");
|
|
697
|
+
this.subs.emit("$lifecycle:auth", false);
|
|
698
|
+
this.log.warn("[SharedWS] auth revoked by server");
|
|
699
|
+
})
|
|
700
|
+
);
|
|
675
701
|
if (typeof window !== "undefined") {
|
|
676
702
|
const onBeforeUnload = () => this[Symbol.dispose]();
|
|
677
703
|
window.addEventListener("beforeunload", onBeforeUnload);
|
|
@@ -694,12 +720,19 @@ var SharedWebSocket = (_class6 = class {
|
|
|
694
720
|
__init31() {this.incomingMiddleware = []}
|
|
695
721
|
__init32() {this.serializers = /* @__PURE__ */ new Map()}
|
|
696
722
|
__init33() {this.deserializers = /* @__PURE__ */ new Map()}
|
|
723
|
+
__init34() {this._isAuthenticated = false}
|
|
724
|
+
__init35() {this.authChannels = /* @__PURE__ */ new Map()}
|
|
725
|
+
__init36() {this.authTopics = /* @__PURE__ */ new Set()}
|
|
697
726
|
get connected() {
|
|
698
727
|
return _optionalChain([this, 'access', _30 => _30.socket, 'optionalAccess', _31 => _31.state]) === "connected" || !this.coordinator.isLeader;
|
|
699
728
|
}
|
|
700
729
|
get tabRole() {
|
|
701
730
|
return this.coordinator.isLeader ? "leader" : "follower";
|
|
702
731
|
}
|
|
732
|
+
/** Whether the user is authenticated via runtime auth. */
|
|
733
|
+
get isAuthenticated() {
|
|
734
|
+
return this._isAuthenticated;
|
|
735
|
+
}
|
|
703
736
|
/** Whether this tab is currently visible/focused. */
|
|
704
737
|
get isActive() {
|
|
705
738
|
return typeof document !== "undefined" ? !document.hidden : true;
|
|
@@ -745,6 +778,58 @@ var SharedWebSocket = (_class6 = class {
|
|
|
745
778
|
onVisibilityChange(fn) {
|
|
746
779
|
return this.subs.on("$lifecycle:active", fn);
|
|
747
780
|
}
|
|
781
|
+
// ─── Authentication ──────────────────────────────────
|
|
782
|
+
/**
|
|
783
|
+
* Authenticate on an existing connection. Sends auth event to server,
|
|
784
|
+
* syncs auth state across all tabs. Use for login after guest connection.
|
|
785
|
+
*
|
|
786
|
+
* @example
|
|
787
|
+
* const token = await loginApi(email, password);
|
|
788
|
+
* ws.authenticate(token);
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* // React — via useAuth hook
|
|
792
|
+
* const { authenticate } = useAuth();
|
|
793
|
+
* authenticate(token);
|
|
794
|
+
*/
|
|
795
|
+
authenticate(token) {
|
|
796
|
+
this._isAuthenticated = true;
|
|
797
|
+
this.syncStore.set("$auth:token", token);
|
|
798
|
+
this.bus.broadcast("ws:sync", { key: "$auth:token", value: token });
|
|
799
|
+
this.send(this.proto.authLogin, { token });
|
|
800
|
+
this.bus.broadcast("ws:lifecycle", { type: "auth", authenticated: true });
|
|
801
|
+
this.log.info("[SharedWS] authenticated");
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Deauthenticate — notifies server, auto-leaves all auth-required channels
|
|
805
|
+
* and topics, syncs state across tabs. Connection stays open for public events.
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* ws.deauthenticate(); // connection stays open, auth subscriptions cleaned up
|
|
809
|
+
*/
|
|
810
|
+
deauthenticate() {
|
|
811
|
+
for (const [, ch] of this.authChannels) ch.leave();
|
|
812
|
+
this.authChannels.clear();
|
|
813
|
+
for (const topic of this.authTopics) this.unsubscribe(topic);
|
|
814
|
+
this.authTopics.clear();
|
|
815
|
+
this._isAuthenticated = false;
|
|
816
|
+
this.send(this.proto.authLogout, {});
|
|
817
|
+
this.syncStore.delete("$auth:token");
|
|
818
|
+
this.bus.broadcast("ws:sync", { key: "$auth:token", value: void 0 });
|
|
819
|
+
this.bus.broadcast("ws:lifecycle", { type: "auth", authenticated: false });
|
|
820
|
+
this.log.info("[SharedWS] deauthenticated");
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Called when auth state changes (authenticate, deauthenticate, or server revocation).
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ws.onAuthChange((authenticated) => {
|
|
827
|
+
* if (!authenticated) router.push('/login');
|
|
828
|
+
* });
|
|
829
|
+
*/
|
|
830
|
+
onAuthChange(fn) {
|
|
831
|
+
return this.subs.on("$lifecycle:auth", fn);
|
|
832
|
+
}
|
|
748
833
|
// ─── Middleware ───────────────────────────────────────
|
|
749
834
|
/**
|
|
750
835
|
* Add middleware to transform messages before send or after receive.
|
|
@@ -864,11 +949,12 @@ var SharedWebSocket = (_class6 = class {
|
|
|
864
949
|
* const notifications = ws.channel(`tenant:${tenantId}:notifications`);
|
|
865
950
|
* notifications.on('alert', (alert) => showToast(alert));
|
|
866
951
|
*/
|
|
867
|
-
channel(name) {
|
|
952
|
+
channel(name, options) {
|
|
868
953
|
this.send(this.proto.channelJoin, { channel: name });
|
|
869
954
|
const self = this;
|
|
870
955
|
const unsubs = [];
|
|
871
|
-
|
|
956
|
+
const isAuth = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _32 => _32.auth]), () => ( false));
|
|
957
|
+
const ch = {
|
|
872
958
|
name,
|
|
873
959
|
on(event, handler) {
|
|
874
960
|
const unsub = self.subs.on(`${name}:${event}`, handler);
|
|
@@ -890,8 +976,13 @@ var SharedWebSocket = (_class6 = class {
|
|
|
890
976
|
self.send(self.proto.channelLeave, { channel: name });
|
|
891
977
|
for (const unsub of unsubs) unsub();
|
|
892
978
|
unsubs.length = 0;
|
|
979
|
+
if (isAuth) self.authChannels.delete(name);
|
|
893
980
|
}
|
|
894
981
|
};
|
|
982
|
+
if (isAuth) {
|
|
983
|
+
this.authChannels.set(name, ch);
|
|
984
|
+
}
|
|
985
|
+
return ch;
|
|
895
986
|
}
|
|
896
987
|
// ─── Topics ──────────────────────────────────────────
|
|
897
988
|
/**
|
|
@@ -903,9 +994,12 @@ var SharedWebSocket = (_class6 = class {
|
|
|
903
994
|
* ws.subscribe('notifications:payments');
|
|
904
995
|
* ws.subscribe(`user:${userId}:mentions`);
|
|
905
996
|
*/
|
|
906
|
-
subscribe(topic) {
|
|
997
|
+
subscribe(topic, options) {
|
|
907
998
|
this.send(this.proto.topicSubscribe, { topic });
|
|
908
|
-
|
|
999
|
+
if (_optionalChain([options, 'optionalAccess', _33 => _33.auth])) {
|
|
1000
|
+
this.authTopics.add(topic);
|
|
1001
|
+
}
|
|
1002
|
+
this.log.debug("[SharedWS] subscribe topic", topic);
|
|
909
1003
|
}
|
|
910
1004
|
/**
|
|
911
1005
|
* Unsubscribe from a server-side topic.
|
|
@@ -913,7 +1007,8 @@ var SharedWebSocket = (_class6 = class {
|
|
|
913
1007
|
*/
|
|
914
1008
|
unsubscribe(topic) {
|
|
915
1009
|
this.send(this.proto.topicUnsubscribe, { topic });
|
|
916
|
-
this.
|
|
1010
|
+
this.authTopics.delete(topic);
|
|
1011
|
+
this.log.debug("[SharedWS] unsubscribe topic", topic);
|
|
917
1012
|
}
|
|
918
1013
|
// ─── Push Notifications ─────────────────────────────
|
|
919
1014
|
/**
|
|
@@ -1029,8 +1124,8 @@ var SharedWebSocket = (_class6 = class {
|
|
|
1029
1124
|
}
|
|
1030
1125
|
}
|
|
1031
1126
|
const msg = data;
|
|
1032
|
-
const event = _nullishCoalesce(_optionalChain([msg, 'optionalAccess',
|
|
1033
|
-
let payload = _nullishCoalesce(_optionalChain([msg, 'optionalAccess',
|
|
1127
|
+
const event = _nullishCoalesce(_optionalChain([msg, 'optionalAccess', _34 => _34[this.proto.eventField]]), () => ( this.proto.defaultEvent));
|
|
1128
|
+
let payload = _nullishCoalesce(_optionalChain([msg, 'optionalAccess', _35 => _35[this.proto.dataField]]), () => ( data));
|
|
1034
1129
|
const eventDeserializer = this.deserializers.get(event);
|
|
1035
1130
|
if (eventDeserializer) {
|
|
1036
1131
|
payload = eventDeserializer(payload);
|
|
@@ -1043,6 +1138,7 @@ var SharedWebSocket = (_class6 = class {
|
|
|
1043
1138
|
switch (state) {
|
|
1044
1139
|
case "connected":
|
|
1045
1140
|
this.bus.broadcast("ws:lifecycle", { type: "connect" });
|
|
1141
|
+
this.reAuthenticateOnReconnect();
|
|
1046
1142
|
break;
|
|
1047
1143
|
case "closed":
|
|
1048
1144
|
this.bus.broadcast("ws:lifecycle", { type: "disconnect" });
|
|
@@ -1057,9 +1153,9 @@ var SharedWebSocket = (_class6 = class {
|
|
|
1057
1153
|
return new Promise((resolve) => {
|
|
1058
1154
|
const unsub = this.socket.onMessage((response) => {
|
|
1059
1155
|
const res = response;
|
|
1060
|
-
if (_optionalChain([res, 'optionalAccess',
|
|
1156
|
+
if (_optionalChain([res, 'optionalAccess', _36 => _36[this.proto.eventField]]) === req.event || _optionalChain([res, 'optionalAccess', _37 => _37.requestId])) {
|
|
1061
1157
|
unsub();
|
|
1062
|
-
resolve(_nullishCoalesce(_optionalChain([res, 'optionalAccess',
|
|
1158
|
+
resolve(_nullishCoalesce(_optionalChain([res, 'optionalAccess', _38 => _38[this.proto.dataField]]), () => ( response)));
|
|
1063
1159
|
}
|
|
1064
1160
|
});
|
|
1065
1161
|
this.socket.send({ event: req.event, data: req.data });
|
|
@@ -1068,6 +1164,29 @@ var SharedWebSocket = (_class6 = class {
|
|
|
1068
1164
|
);
|
|
1069
1165
|
this.socket.connect();
|
|
1070
1166
|
}
|
|
1167
|
+
reAuthenticateOnReconnect() {
|
|
1168
|
+
if (!this._isAuthenticated || !this.socket) return;
|
|
1169
|
+
const token = this.syncStore.get("$auth:token");
|
|
1170
|
+
if (token) {
|
|
1171
|
+
this.socket.send({
|
|
1172
|
+
[this.proto.eventField]: this.proto.authLogin,
|
|
1173
|
+
[this.proto.dataField]: { token }
|
|
1174
|
+
});
|
|
1175
|
+
this.log.debug("[SharedWS] re-authenticated after reconnect");
|
|
1176
|
+
}
|
|
1177
|
+
for (const name of this.authChannels.keys()) {
|
|
1178
|
+
this.socket.send({
|
|
1179
|
+
[this.proto.eventField]: this.proto.channelJoin,
|
|
1180
|
+
[this.proto.dataField]: { channel: name }
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
for (const topic of this.authTopics) {
|
|
1184
|
+
this.socket.send({
|
|
1185
|
+
[this.proto.eventField]: this.proto.topicSubscribe,
|
|
1186
|
+
[this.proto.dataField]: { topic }
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1071
1190
|
handleLoseLeadership() {
|
|
1072
1191
|
if (this.socket) {
|
|
1073
1192
|
this.socket[Symbol.dispose]();
|
|
@@ -1087,6 +1206,8 @@ var SharedWebSocket = (_class6 = class {
|
|
|
1087
1206
|
this.subs[Symbol.dispose]();
|
|
1088
1207
|
this.bus[Symbol.dispose]();
|
|
1089
1208
|
this.syncStore.clear();
|
|
1209
|
+
this.authChannels.clear();
|
|
1210
|
+
this.authTopics.clear();
|
|
1090
1211
|
}
|
|
1091
1212
|
}, _class6);
|
|
1092
1213
|
|
|
@@ -1098,4 +1219,4 @@ var SharedWebSocket = (_class6 = class {
|
|
|
1098
1219
|
|
|
1099
1220
|
|
|
1100
1221
|
exports.MessageBus = MessageBus; exports.TabCoordinator = TabCoordinator; exports.SharedSocket = SharedSocket; exports.WorkerSocket = WorkerSocket; exports.SubscriptionManager = SubscriptionManager; exports.SharedWebSocket = SharedWebSocket;
|
|
1101
|
-
//# sourceMappingURL=chunk-
|
|
1222
|
+
//# sourceMappingURL=chunk-LIFZOQPW.cjs.map
|