mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-06 15:14:37 +00:00
Some checks failed
CI / semgrep (push) Has been cancelled
The web server is long-lived, so cache what the per-invocation CLI cannot: - Cache the parsed local payload in-memory (single-flight, 180s TTL matched to the parser session cache, expired entries pruned on write). /api/usage and the local half of /api/devices now return from a Map hit after the first parse. - Prewarm today at startup and inline that payload into index.html as a bootstrap, so the SPA paints today's numbers with no round-trip. Only the local device is embedded; '<' is escaped so a name cannot break the script tag; served no-store; the seeded view refetches at once so live peers appear. - Default the web command and dashboard to today. - Cap the peer connect phase at 3s. req.setTimeout does not abort a stalled TCP connect, so an offline paired device hung ~75s on the OS timeout; it now degrades to an unreachable row in ~3s. The cap clears on TCP connect, so the TLS handshake and the 65s pairing-approval wait are unaffected. Measured: /api/usage 0.0007s warm (was ~0.22s), /api/devices ~3s with an offline peer (was ~75s), first paint instant.
111 lines
4.1 KiB
TypeScript
111 lines
4.1 KiB
TypeScript
import { request } from 'https'
|
|
import type { TLSSocket } from 'tls'
|
|
|
|
import { certFingerprint } from './pairing.js'
|
|
import type { Identity } from './identity.js'
|
|
import type { UsageQuery } from './share-server.js'
|
|
|
|
// req.setTimeout only arms once connected; it does not abort a stalled TCP
|
|
// connect, so an unreachable peer would otherwise ride the OS connect timeout
|
|
// (~75s on macOS). Cap the TCP-connect phase separately; it clears the instant
|
|
// the socket connects, so the TLS handshake and the read/approval timeouts
|
|
// (req.setTimeout) are unaffected even over a slow VPN link.
|
|
const CONNECT_TIMEOUT_MS = 3000
|
|
|
|
export type PeerEndpoint = {
|
|
identity: Identity // our own identity (we present our cert so the peer can bind a token to us)
|
|
host: string
|
|
port: number
|
|
// When set, the connection is aborted unless the peer's cert fingerprint matches.
|
|
expectedFingerprint?: string
|
|
}
|
|
|
|
export type Response = { status: number; serverFingerprint: string; json: unknown }
|
|
|
|
// One request to a peer. Self-signed certs are accepted at the TLS layer
|
|
// (rejectUnauthorized:false) but the peer is authenticated by pinning its cert
|
|
// fingerprint, the SSH/Syncthing trust-on-first-use model.
|
|
function call(
|
|
ep: PeerEndpoint,
|
|
method: string,
|
|
path: string,
|
|
headers: Record<string, string> = {},
|
|
body?: string,
|
|
timeoutMs = 15000,
|
|
): Promise<Response> {
|
|
return new Promise((resolve, reject) => {
|
|
const req = request(
|
|
{
|
|
host: ep.host,
|
|
port: ep.port,
|
|
method,
|
|
path,
|
|
key: ep.identity.key,
|
|
cert: ep.identity.cert,
|
|
rejectUnauthorized: false,
|
|
checkServerIdentity: () => undefined,
|
|
// Fresh socket per request so the pinned-fingerprint check always reads
|
|
// this connection's certificate, never a pooled/keep-alive one.
|
|
agent: false,
|
|
headers: { ...headers, ...(body ? { 'content-type': 'application/json' } : {}) },
|
|
},
|
|
(res) => {
|
|
const cert = (res.socket as TLSSocket).getPeerCertificate?.()
|
|
const serverFingerprint = cert?.raw ? certFingerprint(cert.raw) : ''
|
|
if (ep.expectedFingerprint && serverFingerprint !== ep.expectedFingerprint) {
|
|
res.destroy()
|
|
reject(new Error('server fingerprint mismatch'))
|
|
return
|
|
}
|
|
let data = ''
|
|
res.on('data', (chunk) => {
|
|
data += chunk
|
|
})
|
|
res.on('end', () => resolve({ status: res.statusCode ?? 0, serverFingerprint, json: safeJson(data) }))
|
|
},
|
|
)
|
|
req.on('error', reject)
|
|
req.setTimeout(timeoutMs, () => req.destroy(new Error('peer timed out')))
|
|
const connectTimer = setTimeout(() => req.destroy(new Error('peer unreachable')), CONNECT_TIMEOUT_MS)
|
|
connectTimer.unref()
|
|
req.once('socket', (socket) => {
|
|
const clear = () => clearTimeout(connectTimer)
|
|
socket.once('connect', clear)
|
|
socket.once('secureConnect', clear)
|
|
})
|
|
req.once('close', () => clearTimeout(connectTimer))
|
|
if (body) req.write(body)
|
|
req.end()
|
|
})
|
|
}
|
|
|
|
export function hello(ep: PeerEndpoint): Promise<Response> {
|
|
return call(ep, 'GET', '/api/peer/hello')
|
|
}
|
|
|
|
export function pair(ep: PeerEndpoint, pin: string, name: string): Promise<Response> {
|
|
return call(ep, 'POST', '/api/peer/pair', {}, JSON.stringify({ pin, name }))
|
|
}
|
|
|
|
// Approve-style pairing: no PIN. The peer prompts its user to approve; this
|
|
// request stays open until they accept or decline.
|
|
export function pairRequest(ep: PeerEndpoint, name: string): Promise<Response> {
|
|
// Stays open while the peer's user decides; give it longer than the server's
|
|
// 60s approval prompt.
|
|
return call(ep, 'POST', '/api/peer/pair-request', {}, JSON.stringify({ name }), 65_000)
|
|
}
|
|
|
|
export function fetchUsage(ep: PeerEndpoint, token: string, query: UsageQuery = {}): Promise<Response> {
|
|
const params = new URLSearchParams()
|
|
for (const [k, v] of Object.entries(query)) if (v) params.set(k, v)
|
|
const qs = params.toString()
|
|
return call(ep, 'GET', `/api/usage${qs ? `?${qs}` : ''}`, { authorization: `Bearer ${token}` })
|
|
}
|
|
|
|
function safeJson(s: string): unknown {
|
|
try {
|
|
return JSON.parse(s)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|