mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-09 16:44:34 +00:00
* feat(sharing): pairing, token, and device-identity core First piece of local device sharing: self-cert fingerprint identity (trust-on-first-use), a one-time expiring pairing PIN, fingerprint-bound tokens, and a peer store that authorizes a pull only when both the token and the TLS peer fingerprint match the same paired device. Pure logic, fully unit-tested; the TLS share server and host pull build on this. * feat(sharing): secure mutual-TLS transport + pairing handshake Add device identity (self-signed cert, persisted; fingerprint = sha256 of the cert DER), an HTTPS share server (mutual TLS: presents its cert, reads the client's, and serves /api/usage only when the bearer token AND the client fingerprint match the same paired peer), a one-time-PIN pairing endpoint, and a fingerprint-pinning client. Verified end to end on loopback: PIN pairing, pinned authed pull, and rejection of a wrong PIN, a token replayed from another device, and a mismatched server fingerprint. Adds the selfsigned dep (Node cannot generate certs natively). * feat(sharing): share + devices CLI (pair, pull, combine) Phase 3 terminal flow: codeburn share runs the secure server on-demand (stops after 10 min idle; --always to persist, --pair to add a device), and codeburn devices add <host> --pin <pin> pairs and pins a remote. codeburn devices pulls this machine plus every paired device, keeps each separate, and prints a per-device table with a simple summed Combined row (no server-side merge). Persists identity and peers under the config dir. Host pair-and-pull flow covered by a loopback integration test. * feat(sharing): mDNS discovery + approve-style (no-PIN) pairing Add bonjour-service discovery (advertise/browse over the LAN), a short confirmation code derived from both cert fingerprints (Bluetooth-style 'do these match?' check), and an approve-style pairing endpoint that prompts the owner instead of requiring a typed PIN. Loopback-tested: approved device gets a working token with a matching code on both sides, declined device is rejected. * feat(sharing): AirDrop-style discover + approve UX codeburn share now advertises on the LAN and approves incoming devices interactively (confirm the matching code, no typed PIN). codeburn devices add (no args) discovers nearby devices, lets you pick one, shows the confirmation code, and waits for the owner to approve. Manual add <host> --pin stays as a fallback for networks that block mDNS. * feat(sharing): share only aggregates, never project names or paths Sanitize each device's payload before it leaves the machine: drop topProjects and topSessions (project names + session detail) and send only aggregate numbers (cost, tokens, models, tools, activities, daily). What you are working on stays local; only the totals travel.
58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
import * as selfsigned from 'selfsigned'
|
|
import { X509Certificate } from 'crypto'
|
|
import { readFile, writeFile, mkdir } from 'fs/promises'
|
|
import { existsSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { hostname } from 'os'
|
|
|
|
import { certFingerprint } from './pairing.js'
|
|
|
|
// A device's stable identity: a self-signed TLS keypair whose certificate
|
|
// fingerprint is the trust anchor (trust-on-first-use). No CA.
|
|
export type Identity = {
|
|
key: string // private key PEM
|
|
cert: string // certificate PEM
|
|
fingerprint: string // SHA-256 hex of the certificate DER
|
|
name: string // human label (defaults to the hostname)
|
|
}
|
|
|
|
export async function generateIdentity(name: string = hostname()): Promise<Identity> {
|
|
const attrs = [{ name: 'commonName', value: 'codeburn-device' }]
|
|
// @types/selfsigned is missing `days`; the runtime accepts it. selfsigned >=5
|
|
// resolves a Promise of { private, public, cert, fingerprint }.
|
|
const genOpts = { days: 3650, keySize: 2048, algorithm: 'sha256' } as unknown as Parameters<
|
|
typeof selfsigned.generate
|
|
>[1]
|
|
const pems = (await (selfsigned.generate(attrs, genOpts) as unknown as Promise<{ private: string; cert: string }>))
|
|
const der = new X509Certificate(pems.cert).raw
|
|
return { key: pems.private, cert: pems.cert, fingerprint: certFingerprint(der), name }
|
|
}
|
|
|
|
// Load the device identity from `dir`, creating and persisting it on first run.
|
|
export async function loadOrCreateIdentity(dir: string, name?: string): Promise<Identity> {
|
|
const keyPath = join(dir, 'device-key.pem')
|
|
const certPath = join(dir, 'device-cert.pem')
|
|
const namePath = join(dir, 'device-name')
|
|
|
|
if (existsSync(keyPath) && existsSync(certPath)) {
|
|
const [key, cert] = await Promise.all([readFile(keyPath, 'utf8'), readFile(certPath, 'utf8')])
|
|
let resolvedName = name ?? hostname()
|
|
try {
|
|
const stored = (await readFile(namePath, 'utf8')).trim()
|
|
if (stored) resolvedName = name ?? stored
|
|
} catch {
|
|
/* no stored name yet */
|
|
}
|
|
const der = new X509Certificate(cert).raw
|
|
return { key, cert, fingerprint: certFingerprint(der), name: resolvedName }
|
|
}
|
|
|
|
const id = await generateIdentity(name)
|
|
await mkdir(dir, { recursive: true })
|
|
await Promise.all([
|
|
writeFile(keyPath, id.key, { mode: 0o600 }),
|
|
writeFile(certPath, id.cert),
|
|
writeFile(namePath, id.name),
|
|
])
|
|
return id
|
|
}
|