@agent-link/server 0.1.56 → 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 +13 -0
- package/dist/auth.js +65 -0
- package/dist/auth.js.map +1 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.js +5 -0
- package/dist/context.js.map +1 -1
- package/dist/ws-agent.js +13 -1
- package/dist/ws-agent.js.map +1 -1
- package/dist/ws-client.js +104 -5
- package/dist/ws-client.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +52 -1
- package/web/modules/connection.js +52 -2
- package/web/style.css +98 -0
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
|
package/dist/auth.js.map
ADDED
|
@@ -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
|
*/
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,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',
|
package/dist/ws-agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EACL,MAAM,EACN,cAAc,EACd,UAAU,EACV,iBAAiB,GAElB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;
|
|
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
|
-
//
|
|
31
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/ws-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EACL,MAAM,EACN,cAAc,EACd,UAAU,
|
|
1
|
+
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
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
|
|
|
@@ -289,6 +297,9 @@ const App = {
|
|
|
289
297
|
removeWorkDirHistory: sidebar.removeWorkDirHistory,
|
|
290
298
|
toggleWorkDirHistory: sidebar.toggleWorkDirHistory,
|
|
291
299
|
selectRecentDir: sidebar.selectRecentDir,
|
|
300
|
+
// Authentication
|
|
301
|
+
authRequired, authPassword, authError, authAttempts, authLocked,
|
|
302
|
+
submitPassword,
|
|
292
303
|
// File attachments
|
|
293
304
|
attachments, fileInputRef, dragOver,
|
|
294
305
|
triggerFileInput: fileAttach.triggerFileInput,
|
|
@@ -678,6 +689,46 @@ const App = {
|
|
|
678
689
|
</div>
|
|
679
690
|
</div>
|
|
680
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>
|
|
681
732
|
</div>
|
|
682
733
|
`
|
|
683
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
|
-
|
|
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
|
-
|
|
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
|
}
|
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;
|