local-agent-dashboard 0.0.0-stage → 0.3.2

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.
@@ -0,0 +1,3 @@
1
+ import { deflateRawSync } from 'node:zlib';
2
+ // The fragment stays out of HTTP requests. It wraps the existing short-lived invitation.
3
+ export function pairingLink(descriptor) { return `agentsignal://pair#${deflateRawSync(Buffer.from(descriptor), { level: 9 }).toString('base64url')}`; }
@@ -0,0 +1,22 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { fileURLToPath } from 'node:url';
4
+ export function qrExecutable() {
5
+ const packaged = fileURLToPath(new URL('../../native/qr', import.meta.url));
6
+ return existsSync(packaged) ? packaged : fileURLToPath(new URL('../../.tools/qr', import.meta.url));
7
+ }
8
+ export function qr(payload) {
9
+ return new Promise((done, reject) => {
10
+ const child = spawn(qrExecutable(), [], { stdio: ['pipe', 'pipe', 'ignore'] });
11
+ const chunks = [];
12
+ let size = 0;
13
+ child.stdout.on('data', c => { size += c.length; if (size > 2 * 1024 * 1024)
14
+ child.kill();
15
+ else
16
+ chunks.push(c); });
17
+ child.on('error', reject);
18
+ child.on('exit', code => code === 0 ? done(Buffer.concat(chunks)) : reject(new Error('QR generator unavailable')));
19
+ child.stdin.on('error', reject);
20
+ child.stdin.end(payload);
21
+ });
22
+ }
@@ -0,0 +1,6 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ // npm/npx caches are disposable; computer identity belongs to the user.
4
+ export function userRuntimeRoot(home = homedir()) {
5
+ return join(home, 'Library', 'Application Support', 'Agent Signal');
6
+ }
package/src/server.js ADDED
@@ -0,0 +1,74 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { resolve } from 'node:path';
5
+ import { body, reply } from "./transport/lan/server.js";
6
+ import { randomBytes } from 'node:crypto';
7
+ const publicRoot = fileURLToPath(new URL('../public/', import.meta.url));
8
+ const files = { '/': ['index.html', 'text/html; charset=utf-8'], '/app.js': ['app.js', 'text/javascript; charset=utf-8'], '/style.css': ['style.css', 'text/css; charset=utf-8'] };
9
+ export function createDashboard(snapshot, management) {
10
+ const csrf = randomBytes(32).toString('base64url');
11
+ let inFlight = null;
12
+ const server = createServer(async (req, res) => {
13
+ const address = server.address();
14
+ const expectedHost = typeof address === 'object' && address ? `127.0.0.1:${address.port}` : '';
15
+ res.setHeader('Cache-Control', 'no-store');
16
+ res.setHeader('Referrer-Policy', 'no-referrer');
17
+ res.setHeader('X-Content-Type-Options', 'nosniff');
18
+ res.setHeader('X-Frame-Options', 'DENY');
19
+ res.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'");
20
+ const fail = (status, message) => { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ error: message })); };
21
+ if (req.headers.host !== expectedHost)
22
+ return fail(403, 'Host rejected');
23
+ if (req.headers.origin && req.headers.origin !== `http://${expectedHost}`)
24
+ return fail(403, 'Origin rejected');
25
+ const path = req.url?.split('?')[0] ?? '';
26
+ if (management && path.startsWith('/api/manage/')) {
27
+ try {
28
+ if (req.method === 'GET' && path === '/api/manage/status')
29
+ return reply(res, 200, { csrf, ...await management('status', {}) });
30
+ if (req.method !== 'POST' || req.headers.origin !== `http://${expectedHost}` || req.headers['x-agent-signal-csrf'] !== csrf)
31
+ return fail(403, 'Management session rejected');
32
+ if (!['invite', 'approve', 'revoke', 'interfaces'].includes(path.slice(12)))
33
+ return fail(404, 'Not found');
34
+ return reply(res, 200, await management(path.slice(12), await body(req)));
35
+ }
36
+ catch {
37
+ return fail(409, '操作未完成;请检查所选地址、端口占用或申请是否过期。');
38
+ }
39
+ }
40
+ if (req.method !== 'GET')
41
+ return fail(405, 'Read-only endpoint');
42
+ if (path === '/api/snapshot') {
43
+ try {
44
+ inFlight ??= Promise.resolve().then(snapshot).finally(() => { inFlight = null; });
45
+ const data = await inFlight;
46
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
47
+ res.end(JSON.stringify(data));
48
+ }
49
+ catch {
50
+ fail(503, '采集暂不可用,请重试');
51
+ }
52
+ }
53
+ else if (files[path]) {
54
+ try {
55
+ const [file, type] = files[path];
56
+ const body = await readFile(resolve(publicRoot, file));
57
+ res.writeHead(200, { 'Content-Type': type });
58
+ res.end(body);
59
+ }
60
+ catch {
61
+ fail(503, '页面资源暂不可用');
62
+ }
63
+ }
64
+ else
65
+ fail(404, 'Not found');
66
+ });
67
+ server.requestTimeout = 10_000;
68
+ server.headersTimeout = 10_000;
69
+ server.maxConnections = 20;
70
+ return { server };
71
+ }
72
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
73
+ import("./composition/main.js").then(m => m.main()).catch(e => { console.error(`启动失败:${e.message}`); process.exitCode = 1; });
74
+ }
package/src/state.js ADDED
@@ -0,0 +1,2 @@
1
+ // Compatibility entry for the original probe; native format interpretation belongs to adapters.
2
+ export * from "./adapters/agents/log-state.js";
@@ -0,0 +1,98 @@
1
+ import { createServer } from 'node:https';
2
+ import { ProtocolError } from "../../application/pairing.js";
3
+ export async function body(req) {
4
+ if (req.headers['content-type'] !== 'application/json')
5
+ throw new ProtocolError(415, 'json_required');
6
+ let bytes = 0;
7
+ const chunks = [];
8
+ for await (const chunk of req) {
9
+ bytes += chunk.length;
10
+ if (bytes > 8192)
11
+ throw new ProtocolError(413, 'request_too_large');
12
+ chunks.push(chunk);
13
+ }
14
+ try {
15
+ const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
16
+ if (!result || typeof result !== 'object' || Array.isArray(result))
17
+ throw 0;
18
+ return result;
19
+ }
20
+ catch {
21
+ throw new ProtocolError(400, 'invalid_json');
22
+ }
23
+ }
24
+ export function reply(res, code, value) { res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' }); res.end(JSON.stringify(value)); }
25
+ export const tlsPolicy = { minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3',
26
+ ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256', honorCipherOrder: true };
27
+ export function createLan(identity, pairing, snapshots, allowedHosts) {
28
+ const deviceSockets = new Map();
29
+ const attempts = new Map();
30
+ const unsubscribe = pairing.subscribeRevoke(id => { for (const socket of deviceSockets.get(id) ?? [])
31
+ socket.destroy(); deviceSockets.delete(id); });
32
+ const server = createServer({ key: identity.key, cert: identity.cert, ...tlsPolicy, handshakeTimeout: 10000 }, async (req, res) => {
33
+ try {
34
+ if (!allowedHosts().includes(req.headers.host ?? '') || req.headers.origin)
35
+ throw new ProtocolError(403, 'origin_rejected');
36
+ if (req.url?.includes('?') || req.url?.includes('#'))
37
+ throw new ProtocolError(400, 'query_rejected');
38
+ const ip = req.socket.remoteAddress ?? 'unknown', now = Date.now();
39
+ for (const [key, v] of attempts)
40
+ if (now - v.start > 60000)
41
+ attempts.delete(key);
42
+ if (!attempts.has(ip) && attempts.size >= 256)
43
+ throw new ProtocolError(429, 'busy');
44
+ const bucket = attempts.get(ip) ?? { start: now, count: 0 };
45
+ attempts.set(ip, bucket);
46
+ if (++bucket.count > 120)
47
+ throw new ProtocolError(429, 'rate_limited');
48
+ if (req.method === 'GET' && req.url === '/v1/identity')
49
+ return reply(res, 200, { protocol: 1, computerId: identity.computerId, fingerprint: identity.fingerprint });
50
+ if (req.method === 'POST' && req.url === '/v1/pair/request') {
51
+ const b = await body(req);
52
+ return reply(res, 200, pairing.request(String(b.invitation ?? ''), String(b.challenge ?? ''), String(b.name ?? '')));
53
+ }
54
+ if (req.method === 'POST' && req.url === '/v1/pair/claim') {
55
+ const b = await body(req);
56
+ return reply(res, 200, pairing.claim(String(b.requestId ?? ''), String(b.challenge ?? '')));
57
+ }
58
+ const authorization = req.headers.authorization ?? '';
59
+ if (!authorization.startsWith('Bearer '))
60
+ throw new ProtocolError(401, 'repair_required');
61
+ const token = authorization.slice(7), device = pairing.authenticate(token);
62
+ const socket = req.socket;
63
+ if (!deviceSockets.has(device.id))
64
+ deviceSockets.set(device.id, new Set());
65
+ if (!deviceSockets.get(device.id).has(socket)) {
66
+ deviceSockets.get(device.id).add(socket);
67
+ socket.once('close', () => deviceSockets.get(device.id)?.delete(socket));
68
+ }
69
+ if (req.method === 'GET' && req.url === '/v1/snapshot') {
70
+ const snapshot = await snapshots.get();
71
+ pairing.authenticate(token); // revocation may have happened while collecting
72
+ device.issued = { instanceId: snapshot.instanceId, revision: snapshot.revision, generatedAt: snapshot.generatedAt };
73
+ return reply(res, 200, snapshot);
74
+ }
75
+ if (req.method === 'POST' && req.url === '/v1/ack') {
76
+ const b = await body(req);
77
+ pairing.ack(device, b.instanceId, b.revision);
78
+ return reply(res, 200, { accepted: true });
79
+ }
80
+ throw new ProtocolError(404, 'not_found');
81
+ }
82
+ catch (e) {
83
+ if (!res.destroyed)
84
+ reply(res, e instanceof ProtocolError ? e.status : 503, { error: e instanceof ProtocolError ? e.code : 'unavailable' });
85
+ }
86
+ });
87
+ server.requestTimeout = 10000;
88
+ server.headersTimeout = 10000;
89
+ server.maxConnections = 32;
90
+ server.keepAliveTimeout = 5000;
91
+ const rawSockets = new Set();
92
+ server.on('connection', socket => { rawSockets.add(socket); socket.once('close', () => rawSockets.delete(socket)); });
93
+ const closeHTTP = server.closeAllConnections.bind(server);
94
+ server.closeAllConnections = () => { closeHTTP(); for (const socket of rawSockets)
95
+ socket.destroy(); };
96
+ server.once('close', () => { unsubscribe(); deviceSockets.clear(); });
97
+ return server;
98
+ }