@agent-link/server 0.1.55 → 0.1.57

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.55",
3
+ "version": "0.1.57",
4
4
  "description": "AgentLink relay server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/web/app.js CHANGED
@@ -72,6 +72,13 @@ const App = {
72
72
  const workDirHistory = ref([]);
73
73
  const workDirHistoryOpen = ref(false);
74
74
 
75
+ // Authentication state
76
+ const authRequired = ref(false);
77
+ const authPassword = ref('');
78
+ const authError = ref('');
79
+ const authAttempts = ref(null);
80
+ const authLocked = ref(false);
81
+
75
82
  // File attachment state
76
83
  const attachments = ref([]);
77
84
  const fileInputRef = ref(null);
@@ -148,12 +155,13 @@ const App = {
148
155
  workDirHistory, workDirHistoryOpen,
149
156
  });
150
157
 
151
- const { connect, wsSend, closeWs } = createConnection({
158
+ const { connect, wsSend, closeWs, submitPassword } = createConnection({
152
159
  status, agentName, hostname, workDir, sessionId, error,
153
160
  serverVersion, agentVersion,
154
161
  messages, isProcessing, isCompacting, visibleLimit,
155
162
  historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
156
163
  folderPickerLoading, folderPickerEntries, folderPickerPath,
164
+ authRequired, authPassword, authError, authAttempts, authLocked,
157
165
  streaming, sidebar, scrollToBottom,
158
166
  });
159
167
 
@@ -288,6 +296,10 @@ const App = {
288
296
  selectWorkDirHistory: sidebar.selectWorkDirHistory,
289
297
  removeWorkDirHistory: sidebar.removeWorkDirHistory,
290
298
  toggleWorkDirHistory: sidebar.toggleWorkDirHistory,
299
+ selectRecentDir: sidebar.selectRecentDir,
300
+ // Authentication
301
+ authRequired, authPassword, authError, authAttempts, authLocked,
302
+ submitPassword,
291
303
  // File attachments
292
304
  attachments, fileInputRef, dragOver,
293
305
  triggerFileInput: fileAttach.triggerFileInput,
@@ -345,33 +357,11 @@ const App = {
345
357
  </div>
346
358
  <div class="sidebar-workdir-header">
347
359
  <div class="sidebar-workdir-label">Working Directory</div>
348
- <div class="sidebar-workdir-actions">
349
- <button class="sidebar-change-dir-btn" @click="toggleWorkDirHistory" title="Recent directories" :disabled="isProcessing" v-if="workDirHistory.length > 1">
350
- <svg viewBox="0 0 24 24" width="12" height="12"><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>
351
- </button>
352
- <button class="sidebar-change-dir-btn" @click="openFolderPicker" title="Change working directory" :disabled="isProcessing">
353
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
354
- </button>
355
- </div>
360
+ <button class="sidebar-change-dir-btn" @click="openFolderPicker" title="Change working directory" :disabled="isProcessing">
361
+ <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
362
+ </button>
356
363
  </div>
357
364
  <div class="sidebar-workdir-path" :title="workDir">{{ workDir }}</div>
358
- <!-- Workdir history dropdown -->
359
- <div v-if="workDirHistoryOpen && workDirHistory.length > 1" class="workdir-history-dropdown">
360
- <div
361
- v-for="dir in workDirHistory" :key="dir"
362
- :class="['workdir-history-item', { active: dir === workDir }]"
363
- @click="selectWorkDirHistory(dir)"
364
- :title="dir"
365
- >
366
- <span class="workdir-history-path">{{ dir }}</span>
367
- <button
368
- v-if="dir !== workDir"
369
- class="workdir-history-remove"
370
- @click.stop="removeWorkDirHistory(dir)"
371
- title="Remove from history"
372
- >&times;</button>
373
- </div>
374
- </div>
375
365
  </div>
376
366
  </div>
377
367
 
@@ -642,6 +632,23 @@ const App = {
642
632
  </button>
643
633
  <input class="folder-picker-path-input" type="text" v-model="folderPickerPath" @keydown.enter="folderPickerGoToPath" placeholder="Enter path..." spellcheck="false" />
644
634
  </div>
635
+ <div v-if="workDirHistory.length > 0" class="folder-picker-recent">
636
+ <div class="folder-picker-recent-label">Recent</div>
637
+ <div
638
+ v-for="dir in workDirHistory" :key="dir"
639
+ :class="['folder-picker-recent-item', { active: dir === workDir }]"
640
+ @click="selectRecentDir(dir)"
641
+ :title="dir"
642
+ >
643
+ <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>
644
+ <span class="folder-picker-recent-path">{{ dir }}</span>
645
+ <button
646
+ class="folder-picker-recent-remove"
647
+ @click.stop="removeWorkDirHistory(dir)"
648
+ title="Remove from history"
649
+ >&times;</button>
650
+ </div>
651
+ </div>
645
652
  <div class="folder-picker-list">
646
653
  <div v-if="folderPickerLoading" class="folder-picker-loading">
647
654
  <div class="history-loading-spinner"></div>
@@ -682,6 +689,46 @@ const App = {
682
689
  </div>
683
690
  </div>
684
691
  </div>
692
+
693
+ <!-- Password Authentication Dialog -->
694
+ <div class="folder-picker-overlay" v-if="authRequired && !authLocked">
695
+ <div class="auth-dialog">
696
+ <div class="auth-dialog-header">
697
+ <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>
698
+ <span>Session Protected</span>
699
+ </div>
700
+ <div class="auth-dialog-body">
701
+ <p>This session requires a password to access.</p>
702
+ <input
703
+ type="password"
704
+ class="auth-password-input"
705
+ v-model="authPassword"
706
+ @keydown.enter="submitPassword"
707
+ placeholder="Enter password..."
708
+ autofocus
709
+ />
710
+ <p v-if="authError" class="auth-error">{{ authError }}</p>
711
+ <p v-if="authAttempts" class="auth-attempts">{{ authAttempts }}</p>
712
+ </div>
713
+ <div class="auth-dialog-footer">
714
+ <button class="auth-submit-btn" @click="submitPassword" :disabled="!authPassword.trim()">Unlock</button>
715
+ </div>
716
+ </div>
717
+ </div>
718
+
719
+ <!-- Auth Locked Out -->
720
+ <div class="folder-picker-overlay" v-if="authLocked">
721
+ <div class="auth-dialog auth-dialog-locked">
722
+ <div class="auth-dialog-header">
723
+ <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>
724
+ <span>Access Locked</span>
725
+ </div>
726
+ <div class="auth-dialog-body">
727
+ <p>{{ authError }}</p>
728
+ <p class="auth-locked-hint">Close this tab and try again later.</p>
729
+ </div>
730
+ </div>
731
+ </div>
685
732
  </div>
686
733
  `
687
734
  };
@@ -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';
@@ -366,6 +406,9 @@ export function createConnection(deps) {
366
406
  isProcessing.value = false;
367
407
  isCompacting.value = false;
368
408
 
409
+ // Don't auto-reconnect if auth-locked or still in auth prompt
410
+ if (authLocked.value || authRequired.value) return;
411
+
369
412
  if (wasConnected || reconnectAttempts > 0) {
370
413
  scheduleReconnect(scheduleHighlight);
371
414
  }
@@ -393,5 +436,12 @@ export function createConnection(deps) {
393
436
  if (ws) ws.close();
394
437
  }
395
438
 
396
- return { connect, wsSend, closeWs };
439
+ function submitPassword() {
440
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
441
+ const pwd = authPassword.value.trim();
442
+ if (!pwd) return;
443
+ ws.send(JSON.stringify({ type: 'authenticate', password: pwd }));
444
+ }
445
+
446
+ return { connect, wsSend, closeWs, submitPassword };
397
447
  }
@@ -59,6 +59,15 @@ export function createSidebar(deps) {
59
59
  wsSend({ type: 'change_workdir', workDir: dir });
60
60
  }
61
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
+
62
71
  function removeWorkDirHistory(dir) {
63
72
  const list = workDirHistory.value.filter(d => d !== dir);
64
73
  workDirHistory.value = list;
@@ -246,6 +255,7 @@ export function createSidebar(deps) {
246
255
  requestSessionList, resumeSession, newConversation, toggleSidebar,
247
256
  deleteSession, confirmDeleteSession, cancelDeleteSession,
248
257
  addToWorkDirHistory, selectWorkDirHistory, removeWorkDirHistory, toggleWorkDirHistory,
258
+ selectRecentDir,
249
259
  openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
250
260
  folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
251
261
  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;
@@ -1543,79 +1641,6 @@ body {
1543
1641
  cursor: not-allowed;
1544
1642
  }
1545
1643
 
1546
- .sidebar-workdir-actions {
1547
- display: flex;
1548
- gap: 4px;
1549
- }
1550
-
1551
- /* ── Workdir history dropdown ── */
1552
- .workdir-history-dropdown {
1553
- margin-top: 6px;
1554
- border: 1px solid var(--border);
1555
- border-radius: 6px;
1556
- background: var(--bg-secondary);
1557
- max-height: 200px;
1558
- overflow-y: auto;
1559
- }
1560
-
1561
- .workdir-history-item {
1562
- display: flex;
1563
- align-items: center;
1564
- padding: 6px 8px;
1565
- font-size: 0.75rem;
1566
- font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
1567
- color: var(--text-primary);
1568
- cursor: pointer;
1569
- transition: background 0.1s;
1570
- gap: 4px;
1571
- }
1572
-
1573
- .workdir-history-item:hover {
1574
- background: var(--bg-tertiary);
1575
- }
1576
-
1577
- .workdir-history-item.active {
1578
- color: var(--accent);
1579
- font-weight: 600;
1580
- }
1581
-
1582
- .workdir-history-item + .workdir-history-item {
1583
- border-top: 1px solid var(--border);
1584
- }
1585
-
1586
- .workdir-history-path {
1587
- flex: 1;
1588
- overflow: hidden;
1589
- text-overflow: ellipsis;
1590
- white-space: nowrap;
1591
- }
1592
-
1593
- .workdir-history-remove {
1594
- flex-shrink: 0;
1595
- display: none;
1596
- align-items: center;
1597
- justify-content: center;
1598
- width: 18px;
1599
- height: 18px;
1600
- background: none;
1601
- border: none;
1602
- border-radius: 3px;
1603
- color: var(--text-secondary);
1604
- font-size: 0.85rem;
1605
- cursor: pointer;
1606
- padding: 0;
1607
- line-height: 1;
1608
- }
1609
-
1610
- .workdir-history-item:hover .workdir-history-remove {
1611
- display: flex;
1612
- }
1613
-
1614
- .workdir-history-remove:hover {
1615
- color: var(--error);
1616
- background: rgba(239, 68, 68, 0.1);
1617
- }
1618
-
1619
1644
  /* ── Folder Picker Modal ── */
1620
1645
  .folder-picker-overlay {
1621
1646
  position: fixed;
@@ -1820,6 +1845,77 @@ body {
1820
1845
  cursor: not-allowed;
1821
1846
  }
1822
1847
 
1848
+ /* ── Folder Picker: Recent directories ── */
1849
+ .folder-picker-recent {
1850
+ border-bottom: 1px solid var(--border);
1851
+ padding: 6px 0;
1852
+ }
1853
+
1854
+ .folder-picker-recent-label {
1855
+ padding: 4px 16px 2px;
1856
+ font-size: 0.7rem;
1857
+ font-weight: 600;
1858
+ text-transform: uppercase;
1859
+ letter-spacing: 0.04em;
1860
+ color: var(--text-secondary);
1861
+ }
1862
+
1863
+ .folder-picker-recent-item {
1864
+ display: flex;
1865
+ align-items: center;
1866
+ gap: 8px;
1867
+ padding: 5px 16px;
1868
+ font-size: 0.82rem;
1869
+ cursor: pointer;
1870
+ color: var(--text-primary);
1871
+ transition: background 0.1s;
1872
+ user-select: none;
1873
+ }
1874
+
1875
+ .folder-picker-recent-item:hover {
1876
+ background: var(--bg-tertiary);
1877
+ }
1878
+
1879
+ .folder-picker-recent-item.active {
1880
+ color: var(--accent);
1881
+ }
1882
+
1883
+ .folder-picker-recent-item svg {
1884
+ flex-shrink: 0;
1885
+ color: var(--text-secondary);
1886
+ }
1887
+
1888
+ .folder-picker-recent-item.active svg {
1889
+ color: var(--accent);
1890
+ }
1891
+
1892
+ .folder-picker-recent-path {
1893
+ flex: 1;
1894
+ overflow: hidden;
1895
+ text-overflow: ellipsis;
1896
+ white-space: nowrap;
1897
+ }
1898
+
1899
+ .folder-picker-recent-remove {
1900
+ background: none;
1901
+ border: none;
1902
+ color: var(--text-secondary);
1903
+ font-size: 1rem;
1904
+ cursor: pointer;
1905
+ padding: 0 4px;
1906
+ line-height: 1;
1907
+ opacity: 0;
1908
+ transition: opacity 0.15s, color 0.15s;
1909
+ }
1910
+
1911
+ .folder-picker-recent-item:hover .folder-picker-recent-remove {
1912
+ opacity: 1;
1913
+ }
1914
+
1915
+ .folder-picker-recent-remove:hover {
1916
+ color: var(--error);
1917
+ }
1918
+
1823
1919
  /* ── File Upload: Attachment Bar ── */
1824
1920
  .attachment-bar {
1825
1921
  display: flex;