@agent-link/server 0.1.56 → 0.1.58

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/auth.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export declare function hashPassword(password: string): {
2
+ hash: string;
3
+ salt: string;
4
+ };
5
+ export declare function verifyPassword(submitted: string, storedHash: string, storedSalt: string): boolean;
6
+ export declare function generateAuthToken(sessionId: string): string;
7
+ export declare function verifyAuthToken(token: string, expectedSessionId: string): boolean;
8
+ export declare function isSessionLocked(sessionId: string): boolean;
9
+ export declare function recordFailure(sessionId: string): {
10
+ locked: boolean;
11
+ remaining: number;
12
+ };
13
+ export declare function clearFailures(sessionId: string): void;
package/dist/auth.js ADDED
@@ -0,0 +1,65 @@
1
+ import { scryptSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
2
+ import { authAttempts, serverSecret } from './context.js';
3
+ const MAX_FAILURES = 5;
4
+ const LOCKOUT_MS = 15 * 60 * 1000; // 15 minutes
5
+ const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
6
+ export function hashPassword(password) {
7
+ const salt = randomBytes(16).toString('base64');
8
+ const hash = scryptSync(password, salt, 32).toString('base64');
9
+ return { hash, salt };
10
+ }
11
+ export function verifyPassword(submitted, storedHash, storedSalt) {
12
+ const computed = scryptSync(submitted, storedSalt, 32);
13
+ const expected = Buffer.from(storedHash, 'base64');
14
+ if (computed.length !== expected.length)
15
+ return false;
16
+ return timingSafeEqual(computed, expected);
17
+ }
18
+ export function generateAuthToken(sessionId) {
19
+ const ts = Date.now().toString();
20
+ const hmac = createHmac('sha256', serverSecret).update(`${sessionId}:${ts}`).digest('base64url');
21
+ return `${sessionId}:${ts}:${hmac}`;
22
+ }
23
+ export function verifyAuthToken(token, expectedSessionId) {
24
+ const idx1 = token.indexOf(':');
25
+ const idx2 = token.indexOf(':', idx1 + 1);
26
+ if (idx1 === -1 || idx2 === -1)
27
+ return false;
28
+ const sessionId = token.slice(0, idx1);
29
+ const ts = token.slice(idx1 + 1, idx2);
30
+ const hmac = token.slice(idx2 + 1);
31
+ if (sessionId !== expectedSessionId)
32
+ return false;
33
+ const age = Date.now() - parseInt(ts, 10);
34
+ if (isNaN(age) || age < 0 || age > TOKEN_TTL_MS)
35
+ return false;
36
+ const expected = createHmac('sha256', serverSecret).update(`${sessionId}:${ts}`).digest('base64url');
37
+ return hmac === expected;
38
+ }
39
+ export function isSessionLocked(sessionId) {
40
+ const state = authAttempts.get(sessionId);
41
+ if (!state?.lockedUntil)
42
+ return false;
43
+ if (Date.now() > state.lockedUntil.getTime()) {
44
+ authAttempts.delete(sessionId);
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ export function recordFailure(sessionId) {
50
+ let state = authAttempts.get(sessionId);
51
+ if (!state) {
52
+ state = { failures: 0, lockedUntil: null };
53
+ authAttempts.set(sessionId, state);
54
+ }
55
+ state.failures++;
56
+ if (state.failures >= MAX_FAILURES) {
57
+ state.lockedUntil = new Date(Date.now() + LOCKOUT_MS);
58
+ return { locked: true, remaining: 0 };
59
+ }
60
+ return { locked: false, remaining: MAX_FAILURES - state.failures };
61
+ }
62
+ export function clearFailures(sessionId) {
63
+ authAttempts.delete(sessionId);
64
+ }
65
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/context.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface AgentSession {
10
10
  sessionKey: Uint8Array | null;
11
11
  connectedAt: Date;
12
12
  isAlive: boolean;
13
+ passwordHash: string | null;
14
+ passwordSalt: string | null;
13
15
  }
14
16
  export interface WebClient {
15
17
  ws: WebSocket;
@@ -22,6 +24,13 @@ export interface WebClient {
22
24
  export declare const agents: Map<string, AgentSession>;
23
25
  export declare const sessionToAgent: Map<string, string>;
24
26
  export declare const webClients: Map<string, WebClient>;
27
+ export interface AuthAttemptState {
28
+ failures: number;
29
+ lockedUntil: Date | null;
30
+ }
31
+ export declare const authAttempts: Map<string, AuthAttemptState>;
32
+ export declare const pendingAuth: Map<string, string>;
33
+ export declare const serverSecret: NonSharedBuffer;
25
34
  /**
26
35
  * Generate a short, URL-safe session ID
27
36
  */
package/dist/context.js CHANGED
@@ -5,6 +5,11 @@ export const agents = new Map();
5
5
  export const sessionToAgent = new Map();
6
6
  // Web clients: clientId → WebClient
7
7
  export const webClients = new Map();
8
+ export const authAttempts = new Map();
9
+ // Pending auth: clientId → sessionId (web clients awaiting password verification)
10
+ export const pendingAuth = new Map();
11
+ // Server secret for HMAC auth tokens (generated fresh on each server start)
12
+ export const serverSecret = randomBytes(32);
8
13
  /**
9
14
  * Generate a short, URL-safe session ID
10
15
  */
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAwBrC,yCAAyC;AACzC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,wCAAwC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExD,oCAAoC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AA0BrC,yCAAyC;AACzC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,wCAAwC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExD,oCAAoC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;AAOvD,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA4B,CAAC;AAEhE,kFAAkF;AAClF,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;AAErD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;AAE5C;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
package/dist/ws-agent.js CHANGED
@@ -2,6 +2,7 @@ import { WebSocket } from 'ws';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { agents, sessionToAgent, webClients, generateSessionId, } from './context.js';
4
4
  import { generateSessionKey, encodeKey, parseMessage, encryptAndSend } from './encryption.js';
5
+ import { hashPassword } from './auth.js';
5
6
  export function handleAgentConnection(ws, req) {
6
7
  const url = new URL(req.url || '/', `http://${req.headers.host}`);
7
8
  const agentId = url.searchParams.get('id') || randomUUID();
@@ -9,6 +10,15 @@ export function handleAgentConnection(ws, req) {
9
10
  const workDir = url.searchParams.get('workDir') || 'unknown';
10
11
  const hostname = url.searchParams.get('hostname') || '';
11
12
  const version = url.searchParams.get('version') || '';
13
+ const password = url.searchParams.get('password') || '';
14
+ // Hash password if provided (agent sends plaintext over WSS)
15
+ let passwordHash = null;
16
+ let passwordSalt = null;
17
+ if (password) {
18
+ const h = hashPassword(password);
19
+ passwordHash = h.hash;
20
+ passwordSalt = h.salt;
21
+ }
12
22
  // Reuse requested sessionId (agent reconnecting) or generate a new one
13
23
  const requestedSessionId = url.searchParams.get('sessionId');
14
24
  const sessionId = requestedSessionId || generateSessionId();
@@ -24,10 +34,12 @@ export function handleAgentConnection(ws, req) {
24
34
  sessionKey,
25
35
  connectedAt: new Date(),
26
36
  isAlive: true,
37
+ passwordHash,
38
+ passwordSalt,
27
39
  };
28
40
  agents.set(agentId, agent);
29
41
  sessionToAgent.set(sessionId, agentId);
30
- console.log(`[Agent] Registered: ${name} (${agentId}), session: ${sessionId}${requestedSessionId ? ' (reconnect)' : ''}`);
42
+ console.log(`[Agent] Registered: ${name} (${agentId}), session: ${sessionId}${requestedSessionId ? ' (reconnect)' : ''}${passwordHash ? ' (password protected)' : ''}`);
31
43
  // Send registration with session key (this initial message is plain text)
32
44
  ws.send(JSON.stringify({
33
45
  type: 'registered',
@@ -1 +1 @@
1
- {"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"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"}
package/dist/ws-client.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { WebSocket } from 'ws';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { createRequire } from 'module';
4
- import { agents, sessionToAgent, webClients, } from './context.js';
4
+ import { agents, sessionToAgent, webClients, pendingAuth, } from './context.js';
5
5
  import { generateSessionKey, encodeKey, parseMessage, encryptAndSend } from './encryption.js';
6
+ import { isSessionLocked, recordFailure, clearFailures, verifyPassword, generateAuthToken, verifyAuthToken, } from './auth.js';
6
7
  const require = createRequire(import.meta.url);
7
8
  const serverPkg = require('../package.json');
8
9
  export function handleWebConnection(ws, req) {
9
10
  const url = new URL(req.url || '/', `http://${req.headers.host}`);
10
11
  const sessionId = url.searchParams.get('sessionId');
12
+ const authToken = url.searchParams.get('authToken');
11
13
  const clientId = randomUUID();
12
14
  if (!sessionId) {
13
15
  ws.send(JSON.stringify({ type: 'error', message: 'Missing sessionId' }));
@@ -17,6 +19,99 @@ export function handleWebConnection(ws, req) {
17
19
  // Check if agent exists for this session
18
20
  const agentId = sessionToAgent.get(sessionId);
19
21
  const agent = agentId ? agents.get(agentId) : undefined;
22
+ // Password-protected session?
23
+ const requiresAuth = !!(agent?.passwordHash && agent?.passwordSalt);
24
+ if (requiresAuth) {
25
+ // Check saved auth token first
26
+ if (authToken && verifyAuthToken(authToken, sessionId)) {
27
+ completeConnection(ws, clientId, sessionId, agent);
28
+ return;
29
+ }
30
+ // Check lockout
31
+ if (isSessionLocked(sessionId)) {
32
+ ws.send(JSON.stringify({
33
+ type: 'auth_locked',
34
+ message: 'Too many failed attempts. Try again in 15 minutes.',
35
+ }));
36
+ ws.close();
37
+ return;
38
+ }
39
+ // Require authentication
40
+ pendingAuth.set(clientId, sessionId);
41
+ ws.send(JSON.stringify({ type: 'auth_required', sessionId }));
42
+ ws.on('message', (data) => {
43
+ handlePendingAuthMessage(clientId, ws, data.toString());
44
+ });
45
+ ws.on('close', () => {
46
+ pendingAuth.delete(clientId);
47
+ });
48
+ return;
49
+ }
50
+ // No auth required — proceed directly
51
+ completeConnection(ws, clientId, sessionId, agent);
52
+ }
53
+ function handlePendingAuthMessage(clientId, ws, raw) {
54
+ const sessionId = pendingAuth.get(clientId);
55
+ if (!sessionId)
56
+ return;
57
+ let msg;
58
+ try {
59
+ msg = JSON.parse(raw);
60
+ }
61
+ catch {
62
+ return;
63
+ }
64
+ if (msg.type !== 'authenticate' || typeof msg.password !== 'string')
65
+ return;
66
+ const agentId = sessionToAgent.get(sessionId);
67
+ const agent = agentId ? agents.get(agentId) : undefined;
68
+ if (!agent?.passwordHash || !agent?.passwordSalt) {
69
+ // Agent disconnected or password removed while authenticating
70
+ ws.send(JSON.stringify({ type: 'error', message: 'Session no longer available.' }));
71
+ pendingAuth.delete(clientId);
72
+ ws.close();
73
+ return;
74
+ }
75
+ // Check lockout (may have been triggered by another client)
76
+ if (isSessionLocked(sessionId)) {
77
+ ws.send(JSON.stringify({
78
+ type: 'auth_locked',
79
+ message: 'Too many failed attempts. Try again in 15 minutes.',
80
+ }));
81
+ pendingAuth.delete(clientId);
82
+ ws.close();
83
+ return;
84
+ }
85
+ const valid = verifyPassword(msg.password, agent.passwordHash, agent.passwordSalt);
86
+ if (!valid) {
87
+ const { locked, remaining } = recordFailure(sessionId);
88
+ if (locked) {
89
+ ws.send(JSON.stringify({
90
+ type: 'auth_locked',
91
+ message: 'Too many failed attempts. Try again in 15 minutes.',
92
+ }));
93
+ pendingAuth.delete(clientId);
94
+ ws.close();
95
+ }
96
+ else {
97
+ ws.send(JSON.stringify({
98
+ type: 'auth_failed',
99
+ message: 'Incorrect password.',
100
+ attemptsRemaining: remaining,
101
+ }));
102
+ }
103
+ return;
104
+ }
105
+ // Success
106
+ clearFailures(sessionId);
107
+ pendingAuth.delete(clientId);
108
+ const token = generateAuthToken(sessionId);
109
+ // Replace pending auth message handler with normal encrypted handler
110
+ ws.removeAllListeners('message');
111
+ ws.removeAllListeners('close');
112
+ completeConnection(ws, clientId, sessionId, agent, token);
113
+ }
114
+ function completeConnection(ws, clientId, sessionId, agent, authToken) {
20
115
  const sessionKey = generateSessionKey();
21
116
  const client = {
22
117
  ws,
@@ -27,8 +122,8 @@ export function handleWebConnection(ws, req) {
27
122
  isAlive: true,
28
123
  };
29
124
  webClients.set(clientId, client);
30
- // Send connection result with agent info and session key (plain text — key exchange)
31
- ws.send(JSON.stringify({
125
+ // Build connected payload
126
+ const payload = {
32
127
  type: 'connected',
33
128
  clientId,
34
129
  sessionKey: encodeKey(sessionKey),
@@ -40,8 +135,12 @@ export function handleWebConnection(ws, req) {
40
135
  workDir: agent.workDir,
41
136
  version: agent.version,
42
137
  } : null,
43
- }));
44
- console.log(`[Web] Client ${clientId.slice(0, 8)} connected to session ${sessionId}, agent: ${agent ? agent.name : 'none'}`);
138
+ };
139
+ if (authToken) {
140
+ payload.authToken = authToken;
141
+ }
142
+ ws.send(JSON.stringify(payload));
143
+ console.log(`[Web] Client ${clientId.slice(0, 8)} connected to session ${sessionId}, agent: ${agent ? agent.name : 'none'}${authToken ? ' (authenticated)' : ''}`);
45
144
  ws.on('message', (data) => {
46
145
  handleWebMessage(clientId, data.toString());
47
146
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-link/server",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "description": "AgentLink relay server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/web/app.js CHANGED
@@ -68,9 +68,12 @@ const App = {
68
68
  const deleteConfirmOpen = ref(false);
69
69
  const deleteConfirmTitle = ref('');
70
70
 
71
- // Working directory history state
72
- const workDirHistory = ref([]);
73
- const workDirHistoryOpen = ref(false);
71
+ // Authentication state
72
+ const authRequired = ref(false);
73
+ const authPassword = ref('');
74
+ const authError = ref('');
75
+ const authAttempts = ref(null);
76
+ const authLocked = ref(false);
74
77
 
75
78
  // File attachment state
76
79
  const attachments = ref([]);
@@ -145,15 +148,15 @@ const App = {
145
148
  folderPickerOpen, folderPickerPath, folderPickerEntries,
146
149
  folderPickerLoading, folderPickerSelected, streaming,
147
150
  deleteConfirmOpen, deleteConfirmTitle,
148
- workDirHistory, workDirHistoryOpen,
149
151
  });
150
152
 
151
- const { connect, wsSend, closeWs } = createConnection({
153
+ const { connect, wsSend, closeWs, submitPassword } = createConnection({
152
154
  status, agentName, hostname, workDir, sessionId, error,
153
155
  serverVersion, agentVersion,
154
156
  messages, isProcessing, isCompacting, visibleLimit,
155
157
  historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
156
158
  folderPickerLoading, folderPickerEntries, folderPickerPath,
159
+ authRequired, authPassword, authError, authAttempts, authLocked,
157
160
  streaming, sidebar, scrollToBottom,
158
161
  });
159
162
 
@@ -283,12 +286,9 @@ const App = {
283
286
  deleteSession: sidebar.deleteSession,
284
287
  confirmDeleteSession: sidebar.confirmDeleteSession,
285
288
  cancelDeleteSession: sidebar.cancelDeleteSession,
286
- // Working directory history
287
- workDirHistory, workDirHistoryOpen,
288
- selectWorkDirHistory: sidebar.selectWorkDirHistory,
289
- removeWorkDirHistory: sidebar.removeWorkDirHistory,
290
- toggleWorkDirHistory: sidebar.toggleWorkDirHistory,
291
- selectRecentDir: sidebar.selectRecentDir,
289
+ // Authentication
290
+ authRequired, authPassword, authError, authAttempts, authLocked,
291
+ submitPassword,
292
292
  // File attachments
293
293
  attachments, fileInputRef, dragOver,
294
294
  triggerFileInput: fileAttach.triggerFileInput,
@@ -621,23 +621,6 @@ const App = {
621
621
  </button>
622
622
  <input class="folder-picker-path-input" type="text" v-model="folderPickerPath" @keydown.enter="folderPickerGoToPath" placeholder="Enter path..." spellcheck="false" />
623
623
  </div>
624
- <div v-if="workDirHistory.length > 0" class="folder-picker-recent">
625
- <div class="folder-picker-recent-label">Recent</div>
626
- <div
627
- v-for="dir in workDirHistory" :key="dir"
628
- :class="['folder-picker-recent-item', { active: dir === workDir }]"
629
- @click="selectRecentDir(dir)"
630
- :title="dir"
631
- >
632
- <svg viewBox="0 0 24 24" width="13" height="13"><path fill="currentColor" d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
633
- <span class="folder-picker-recent-path">{{ dir }}</span>
634
- <button
635
- class="folder-picker-recent-remove"
636
- @click.stop="removeWorkDirHistory(dir)"
637
- title="Remove from history"
638
- >&times;</button>
639
- </div>
640
- </div>
641
624
  <div class="folder-picker-list">
642
625
  <div v-if="folderPickerLoading" class="folder-picker-loading">
643
626
  <div class="history-loading-spinner"></div>
@@ -678,6 +661,46 @@ const App = {
678
661
  </div>
679
662
  </div>
680
663
  </div>
664
+
665
+ <!-- Password Authentication Dialog -->
666
+ <div class="folder-picker-overlay" v-if="authRequired && !authLocked">
667
+ <div class="auth-dialog">
668
+ <div class="auth-dialog-header">
669
+ <svg viewBox="0 0 24 24" width="22" height="22"><path fill="currentColor" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
670
+ <span>Session Protected</span>
671
+ </div>
672
+ <div class="auth-dialog-body">
673
+ <p>This session requires a password to access.</p>
674
+ <input
675
+ type="password"
676
+ class="auth-password-input"
677
+ v-model="authPassword"
678
+ @keydown.enter="submitPassword"
679
+ placeholder="Enter password..."
680
+ autofocus
681
+ />
682
+ <p v-if="authError" class="auth-error">{{ authError }}</p>
683
+ <p v-if="authAttempts" class="auth-attempts">{{ authAttempts }}</p>
684
+ </div>
685
+ <div class="auth-dialog-footer">
686
+ <button class="auth-submit-btn" @click="submitPassword" :disabled="!authPassword.trim()">Unlock</button>
687
+ </div>
688
+ </div>
689
+ </div>
690
+
691
+ <!-- Auth Locked Out -->
692
+ <div class="folder-picker-overlay" v-if="authLocked">
693
+ <div class="auth-dialog auth-dialog-locked">
694
+ <div class="auth-dialog-header">
695
+ <svg viewBox="0 0 24 24" width="22" height="22"><path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
696
+ <span>Access Locked</span>
697
+ </div>
698
+ <div class="auth-dialog-body">
699
+ <p>{{ authError }}</p>
700
+ <p class="auth-locked-hint">Close this tab and try again later.</p>
701
+ </div>
702
+ </div>
703
+ </div>
681
704
  </div>
682
705
  `
683
706
  };
@@ -17,6 +17,7 @@ export function createConnection(deps) {
17
17
  messages, isProcessing, isCompacting, visibleLimit,
18
18
  historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
19
19
  folderPickerLoading, folderPickerEntries, folderPickerPath,
20
+ authRequired, authPassword, authError, authAttempts, authLocked,
20
21
  streaming, sidebar,
21
22
  scrollToBottom,
22
23
  } = deps;
@@ -113,7 +114,12 @@ export function createConnection(deps) {
113
114
  error.value = '';
114
115
 
115
116
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
116
- const wsUrl = `${protocol}//${window.location.host}/?type=web&sessionId=${sid}`;
117
+ let wsUrl = `${protocol}//${window.location.host}/?type=web&sessionId=${sid}`;
118
+ // Include saved auth token for automatic re-authentication
119
+ const savedToken = localStorage.getItem(`agentlink-auth-${sid}`);
120
+ if (savedToken) {
121
+ wsUrl += `&authToken=${encodeURIComponent(savedToken)}`;
122
+ }
117
123
  ws = new WebSocket(wsUrl);
118
124
 
119
125
  ws.onopen = () => { error.value = ''; reconnectAttempts = 0; };
@@ -122,6 +128,30 @@ export function createConnection(deps) {
122
128
  let msg;
123
129
  const parsed = JSON.parse(event.data);
124
130
 
131
+ // Auth messages are always plaintext (before session key exchange)
132
+ if (parsed.type === 'auth_required') {
133
+ authRequired.value = true;
134
+ authError.value = '';
135
+ authLocked.value = false;
136
+ status.value = 'Authentication Required';
137
+ return;
138
+ }
139
+ if (parsed.type === 'auth_failed') {
140
+ authError.value = parsed.message || 'Incorrect password.';
141
+ authAttempts.value = parsed.attemptsRemaining != null
142
+ ? `${parsed.attemptsRemaining} attempt${parsed.attemptsRemaining !== 1 ? 's' : ''} remaining`
143
+ : null;
144
+ authPassword.value = '';
145
+ return;
146
+ }
147
+ if (parsed.type === 'auth_locked') {
148
+ authLocked.value = true;
149
+ authRequired.value = false;
150
+ authError.value = parsed.message || 'Too many failed attempts.';
151
+ status.value = 'Locked';
152
+ return;
153
+ }
154
+
125
155
  if (parsed.type === 'connected') {
126
156
  msg = parsed;
127
157
  if (typeof parsed.sessionKey === 'string') {
@@ -138,6 +168,16 @@ export function createConnection(deps) {
138
168
  }
139
169
 
140
170
  if (msg.type === 'connected') {
171
+ // Reset auth state
172
+ authRequired.value = false;
173
+ authPassword.value = '';
174
+ authError.value = '';
175
+ authAttempts.value = null;
176
+ authLocked.value = false;
177
+ // Save auth token for automatic re-authentication
178
+ if (msg.authToken) {
179
+ localStorage.setItem(`agentlink-auth-${sessionId.value}`, msg.authToken);
180
+ }
141
181
  if (msg.serverVersion) serverVersion.value = msg.serverVersion;
142
182
  if (msg.agent) {
143
183
  status.value = 'Connected';
@@ -145,7 +185,6 @@ export function createConnection(deps) {
145
185
  hostname.value = msg.agent.hostname || '';
146
186
  workDir.value = msg.agent.workDir;
147
187
  agentVersion.value = msg.agent.version || '';
148
- sidebar.addToWorkDirHistory(msg.agent.workDir);
149
188
  const savedDir = localStorage.getItem('agentlink-workdir');
150
189
  if (savedDir && savedDir !== msg.agent.workDir) {
151
190
  wsSend({ type: 'change_workdir', workDir: savedDir });
@@ -342,7 +381,6 @@ export function createConnection(deps) {
342
381
  } else if (msg.type === 'workdir_changed') {
343
382
  workDir.value = msg.workDir;
344
383
  localStorage.setItem('agentlink-workdir', msg.workDir);
345
- sidebar.addToWorkDirHistory(msg.workDir);
346
384
  messages.value = [];
347
385
  toolMsgMap.clear();
348
386
  visibleLimit.value = 50;
@@ -366,6 +404,9 @@ export function createConnection(deps) {
366
404
  isProcessing.value = false;
367
405
  isCompacting.value = false;
368
406
 
407
+ // Don't auto-reconnect if auth-locked or still in auth prompt
408
+ if (authLocked.value || authRequired.value) return;
409
+
369
410
  if (wasConnected || reconnectAttempts > 0) {
370
411
  scheduleReconnect(scheduleHighlight);
371
412
  }
@@ -393,5 +434,12 @@ export function createConnection(deps) {
393
434
  if (ws) ws.close();
394
435
  }
395
436
 
396
- return { connect, wsSend, closeWs };
437
+ function submitPassword() {
438
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
439
+ const pwd = authPassword.value.trim();
440
+ if (!pwd) return;
441
+ ws.send(JSON.stringify({ type: 'authenticate', password: pwd }));
442
+ }
443
+
444
+ return { connect, wsSend, closeWs, submitPassword };
397
445
  }
@@ -31,53 +31,6 @@ export function createSidebar(deps) {
31
31
  folderPickerLoading, folderPickerSelected, streaming,
32
32
  } = deps;
33
33
 
34
- // ── Working directory history ──
35
-
36
- const WORKDIR_HISTORY_KEY = 'agentlink-workdir-history';
37
- const MAX_HISTORY = 10;
38
- const workDirHistory = deps.workDirHistory;
39
- const workDirHistoryOpen = deps.workDirHistoryOpen;
40
-
41
- // Load from localStorage on init
42
- try {
43
- const saved = localStorage.getItem(WORKDIR_HISTORY_KEY);
44
- if (saved) workDirHistory.value = JSON.parse(saved);
45
- } catch { /* ignore */ }
46
-
47
- function addToWorkDirHistory(dir) {
48
- if (!dir) return;
49
- const list = workDirHistory.value.filter(d => d !== dir);
50
- list.unshift(dir);
51
- if (list.length > MAX_HISTORY) list.length = MAX_HISTORY;
52
- workDirHistory.value = list;
53
- localStorage.setItem(WORKDIR_HISTORY_KEY, JSON.stringify(list));
54
- }
55
-
56
- function selectWorkDirHistory(dir) {
57
- workDirHistoryOpen.value = false;
58
- if (dir === workDir.value) return;
59
- wsSend({ type: 'change_workdir', workDir: dir });
60
- }
61
-
62
- function selectRecentDir(dir) {
63
- if (dir === workDir.value) {
64
- folderPickerOpen.value = false;
65
- return;
66
- }
67
- folderPickerOpen.value = false;
68
- wsSend({ type: 'change_workdir', workDir: dir });
69
- }
70
-
71
- function removeWorkDirHistory(dir) {
72
- const list = workDirHistory.value.filter(d => d !== dir);
73
- workDirHistory.value = list;
74
- localStorage.setItem(WORKDIR_HISTORY_KEY, JSON.stringify(list));
75
- }
76
-
77
- function toggleWorkDirHistory() {
78
- workDirHistoryOpen.value = !workDirHistoryOpen.value;
79
- }
80
-
81
34
  // ── Session management ──
82
35
 
83
36
  function requestSessionList() {
@@ -254,8 +207,6 @@ export function createSidebar(deps) {
254
207
  return {
255
208
  requestSessionList, resumeSession, newConversation, toggleSidebar,
256
209
  deleteSession, confirmDeleteSession, cancelDeleteSession,
257
- addToWorkDirHistory, selectWorkDirHistory, removeWorkDirHistory, toggleWorkDirHistory,
258
- selectRecentDir,
259
210
  openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
260
211
  folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
261
212
  groupedSessions,
package/web/style.css CHANGED
@@ -534,6 +534,104 @@ body {
534
534
  background: #dc2626;
535
535
  }
536
536
 
537
+ /* ── Auth Dialog ── */
538
+ .auth-dialog {
539
+ background: var(--bg-secondary);
540
+ border: 1px solid var(--border);
541
+ border-radius: 12px;
542
+ width: 360px;
543
+ max-width: 90vw;
544
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
545
+ }
546
+
547
+ .auth-dialog-header {
548
+ display: flex;
549
+ align-items: center;
550
+ gap: 10px;
551
+ padding: 20px 24px 16px;
552
+ font-size: 1rem;
553
+ font-weight: 600;
554
+ color: var(--text-primary);
555
+ }
556
+
557
+ .auth-dialog-header svg {
558
+ color: var(--accent);
559
+ }
560
+
561
+ .auth-dialog-locked .auth-dialog-header svg {
562
+ color: var(--error);
563
+ }
564
+
565
+ .auth-dialog-body {
566
+ padding: 0 24px 16px;
567
+ }
568
+
569
+ .auth-dialog-body p {
570
+ margin: 0 0 12px 0;
571
+ color: var(--text-secondary);
572
+ font-size: 0.85rem;
573
+ }
574
+
575
+ .auth-password-input {
576
+ width: 100%;
577
+ padding: 10px 12px;
578
+ background: var(--bg-tertiary);
579
+ border: 1px solid var(--border);
580
+ border-radius: 8px;
581
+ color: var(--text-primary);
582
+ font-size: 0.9rem;
583
+ outline: none;
584
+ box-sizing: border-box;
585
+ margin-bottom: 4px;
586
+ }
587
+
588
+ .auth-password-input:focus {
589
+ border-color: var(--accent);
590
+ }
591
+
592
+ .auth-error {
593
+ color: var(--error) !important;
594
+ font-size: 0.82rem !important;
595
+ margin-top: 8px !important;
596
+ }
597
+
598
+ .auth-attempts {
599
+ color: var(--text-secondary) !important;
600
+ font-size: 0.78rem !important;
601
+ }
602
+
603
+ .auth-locked-hint {
604
+ font-size: 0.78rem !important;
605
+ color: var(--text-secondary) !important;
606
+ }
607
+
608
+ .auth-dialog-footer {
609
+ padding: 12px 24px 20px;
610
+ display: flex;
611
+ justify-content: flex-end;
612
+ }
613
+
614
+ .auth-submit-btn {
615
+ background: var(--accent);
616
+ color: #fff;
617
+ border: none;
618
+ padding: 8px 24px;
619
+ border-radius: 8px;
620
+ font-size: 0.85rem;
621
+ font-weight: 600;
622
+ cursor: pointer;
623
+ transition: background 0.15s;
624
+ }
625
+
626
+ .auth-submit-btn:hover:not(:disabled) {
627
+ background: var(--accent-hover);
628
+ }
629
+
630
+ .auth-submit-btn:disabled {
631
+ opacity: 0.4;
632
+ cursor: not-allowed;
633
+ }
634
+
537
635
  /* ── Chat area (message list + input) ── */
538
636
  .chat-area {
539
637
  flex: 1;
@@ -1747,77 +1845,6 @@ body {
1747
1845
  cursor: not-allowed;
1748
1846
  }
1749
1847
 
1750
- /* ── Folder Picker: Recent directories ── */
1751
- .folder-picker-recent {
1752
- border-bottom: 1px solid var(--border);
1753
- padding: 6px 0;
1754
- }
1755
-
1756
- .folder-picker-recent-label {
1757
- padding: 4px 16px 2px;
1758
- font-size: 0.7rem;
1759
- font-weight: 600;
1760
- text-transform: uppercase;
1761
- letter-spacing: 0.04em;
1762
- color: var(--text-secondary);
1763
- }
1764
-
1765
- .folder-picker-recent-item {
1766
- display: flex;
1767
- align-items: center;
1768
- gap: 8px;
1769
- padding: 5px 16px;
1770
- font-size: 0.82rem;
1771
- cursor: pointer;
1772
- color: var(--text-primary);
1773
- transition: background 0.1s;
1774
- user-select: none;
1775
- }
1776
-
1777
- .folder-picker-recent-item:hover {
1778
- background: var(--bg-tertiary);
1779
- }
1780
-
1781
- .folder-picker-recent-item.active {
1782
- color: var(--accent);
1783
- }
1784
-
1785
- .folder-picker-recent-item svg {
1786
- flex-shrink: 0;
1787
- color: var(--text-secondary);
1788
- }
1789
-
1790
- .folder-picker-recent-item.active svg {
1791
- color: var(--accent);
1792
- }
1793
-
1794
- .folder-picker-recent-path {
1795
- flex: 1;
1796
- overflow: hidden;
1797
- text-overflow: ellipsis;
1798
- white-space: nowrap;
1799
- }
1800
-
1801
- .folder-picker-recent-remove {
1802
- background: none;
1803
- border: none;
1804
- color: var(--text-secondary);
1805
- font-size: 1rem;
1806
- cursor: pointer;
1807
- padding: 0 4px;
1808
- line-height: 1;
1809
- opacity: 0;
1810
- transition: opacity 0.15s, color 0.15s;
1811
- }
1812
-
1813
- .folder-picker-recent-item:hover .folder-picker-recent-remove {
1814
- opacity: 1;
1815
- }
1816
-
1817
- .folder-picker-recent-remove:hover {
1818
- color: var(--error);
1819
- }
1820
-
1821
1848
  /* ── File Upload: Attachment Bar ── */
1822
1849
  .attachment-bar {
1823
1850
  display: flex;