@myrialabs/clopen 0.2.0 → 0.2.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.
@@ -580,6 +580,28 @@ class WSServer {
580
580
  debug.log('websocket', `Connection ${id} auth cleared`);
581
581
  }
582
582
 
583
+ /**
584
+ * Clear authentication state for ALL active connections.
585
+ * Used when switching auth mode to 'required' — invalidates every
586
+ * connection's in-memory auth so the auth gate blocks subsequent messages.
587
+ * Returns the number of connections that were cleared.
588
+ */
589
+ clearAllAuth(): number {
590
+ let cleared = 0;
591
+ for (const [id, state] of this.connectionState) {
592
+ if (state.authenticated) {
593
+ state.authenticated = false;
594
+ state.role = null;
595
+ state.sessionTokenHash = null;
596
+ cleared++;
597
+ }
598
+ }
599
+ if (cleared > 0) {
600
+ debug.log('websocket', `Cleared auth on ${cleared} active connection(s)`);
601
+ }
602
+ return cleared;
603
+ }
604
+
583
605
  /**
584
606
  * Check if connection can receive data (backpressure check)
585
607
  */
@@ -14,4 +14,8 @@ export const authRouter = createRouter()
14
14
  .emit('auth:error', t.Object({
15
15
  error: t.String(),
16
16
  blockedAction: t.String()
17
+ }))
18
+ // Declare auth:force-logout event (emitted when auth mode switches to required)
19
+ .emit('auth:force-logout', t.Object({
20
+ reason: t.String()
17
21
  }));
@@ -2,6 +2,7 @@ import { t } from 'elysia';
2
2
  import { createRouter } from '$shared/utils/ws-server';
3
3
  import {
4
4
  createAdmin,
5
+ getAuthMode,
5
6
  loginWithToken,
6
7
  createUserFromInvite,
7
8
  logout,
@@ -96,6 +97,10 @@ export const loginHandler = createRouter()
96
97
  expiresAt: t.String()
97
98
  })
98
99
  }, async ({ conn }) => {
100
+ if (getAuthMode() !== 'none') {
101
+ throw new Error('Auto-login is only available in no-auth mode');
102
+ }
103
+
99
104
  const result = createOrGetNoAuthAdmin();
100
105
 
101
106
  // Set auth on connection
@@ -254,6 +259,15 @@ export const loginHandler = createRouter()
254
259
  })
255
260
  }, async () => {
256
261
  const count = logoutAllSessions();
262
+
263
+ // Broadcast force-logout to ALL connected clients before clearing auth.
264
+ // This ensures clients receive the event while still connected.
265
+ ws.emit.global('auth:force-logout', { reason: 'Auth mode changed' });
266
+
267
+ // Clear in-memory auth state on all active WebSocket connections.
268
+ // After this, the auth gate will block any further messages.
269
+ ws.clearAllAuth();
270
+
257
271
  return { count };
258
272
  })
259
273
 
@@ -31,6 +31,18 @@ let sessionToken = $state<string | null>(null);
31
31
  let personalAccessToken = $state<string | null>(null);
32
32
  let authMode = $state<AuthMode>('required');
33
33
 
34
+ // Listen for server-side force-logout (auth mode switched to required)
35
+ ws.on('auth:force-logout', (payload) => {
36
+ debug.log('auth', `Force logout received: ${payload.reason}`);
37
+ currentUser = null;
38
+ sessionToken = null;
39
+ personalAccessToken = null;
40
+ localStorage.removeItem(SESSION_TOKEN_KEY);
41
+ ws.setSessionToken(null);
42
+ authMode = 'required';
43
+ authState = 'login';
44
+ });
45
+
34
46
  export const authStore = {
35
47
  get authState() { return authState; },
36
48
  get currentUser() { return currentUser; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myrialabs/clopen",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
5
5
  "author": "Myria Labs",
6
6
  "license": "MIT",