@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 +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 +51 -28
- package/web/modules/connection.js +52 -4
- package/web/modules/sidebar.js +0 -49
- package/web/style.css +98 -71
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
|
@@ -68,9 +68,12 @@ const App = {
|
|
|
68
68
|
const deleteConfirmOpen = ref(false);
|
|
69
69
|
const deleteConfirmTitle = ref('');
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
const
|
|
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
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
>×</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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/web/modules/sidebar.js
CHANGED
|
@@ -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;
|