Files
LinkPort/scripts/verify-package.mjs
T
2026-09-09 14:34:04 +08:00

68 lines
4.2 KiB
JavaScript

import assert from 'node:assert/strict'
import { spawn, execFileSync } from 'node:child_process'
import { X509Certificate, createHash, createPrivateKey, randomBytes } from 'node:crypto'
import { readFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs'
import { createServer } from 'node:net'
import { tmpdir } from 'node:os'
import { resolve, join, relative, isAbsolute } from 'node:path'
import { setTimeout as delay } from 'node:timers/promises'
const directory = resolve(process.argv[2] || '')
assert(process.argv[2], 'Provide an extracted package directory')
const binary = join(directory, 'agentdb.exe')
const certTool = join(directory, 'devcert.exe')
for (const name of ['agentdb.exe', 'devcert.exe', 'README.md', '.env.example', 'build-info.json', 'SHA256SUMS.txt']) assert(existsSync(join(directory, name)), `Missing ${name}`)
for (const line of readFileSync(join(directory, 'SHA256SUMS.txt'), 'utf8').trim().split(/\r?\n/)) {
const match = /^([a-f0-9]{64}) (.+)$/.exec(line)
assert(match, 'Invalid checksum entry')
const filename = resolve(directory, match[2])
const within = relative(directory, filename)
assert(within && !within.startsWith('..') && !isAbsolute(within), 'Checksum path outside package')
assert.equal(createHash('sha256').update(readFileSync(filename)).digest('hex'), match[1], `Checksum mismatch: ${match[2]}`)
}
const key = execFileSync(binary, ['generate-key'], { encoding: 'utf8', windowsHide: true }).trim()
assert.equal(Buffer.from(key, 'base64').length, 32)
const temp = mkdtempSync(join(tmpdir(), 'agentdb-package-smoke-'))
let child
try {
const tlsDir = join(temp, 'tls')
execFileSync(certTool, [tlsDir], { windowsHide: true })
const cert = new X509Certificate(readFileSync(join(tlsDir, 'cert.pem')))
assert(cert.checkHost('localhost'))
assert(cert.checkPrivateKey(createPrivateKey(readFileSync(join(tlsDir, 'key.pem')))))
const listener = createServer()
await new Promise((resolve, reject) => { listener.once('error', reject); listener.listen(0, '127.0.0.1', resolve) })
const port = listener.address().port
await new Promise(resolve => listener.close(resolve))
const password = randomBytes(24).toString('base64url')
child = spawn(binary, [], { cwd: directory, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'], env: { ...process.env, AGENTDB_AUTH_ENABLED: 'true', AGENTDB_MASTER_KEY: key, AGENTDB_ADMIN_USER: 'package-test', AGENTDB_ADMIN_PASSWORD: password, AGENTDB_DATA: join(temp, 'test.sqlite'), AGENTDB_HTTP_ADDR: `127.0.0.1:${port}`, AGENTDB_SECURE_COOKIE: 'false', AGENTDB_TLS_CERT: join(tlsDir, 'cert.pem'), AGENTDB_TLS_KEY: join(tlsDir, 'key.pem') } })
let startupError = ''
child.stderr.on('data', b => { startupError += b.toString() })
child.on('error', e => { startupError += e.message })
const base = `http://127.0.0.1:${port}`
let healthy = false
for (let i = 0; i < 60; i++) {
try { healthy = (await fetch(`${base}/healthz`, { signal: AbortSignal.timeout(500) })).ok } catch {}
if (healthy) break
if (child.exitCode !== null) throw new Error(`Package exited during startup: ${startupError}`)
await delay(100)
}
assert(healthy, 'Packaged application did not start')
const page = await fetch(base)
assert.equal(page.status, 200)
const html = await page.text()
const assets = [...html.matchAll(/(?:src|href)="(\/assets\/[^" ]+)"/g)].map(m => m[1])
assert(assets.length >= 2, 'Built frontend was not embedded')
for (const asset of assets) assert.equal((await fetch(base + asset)).status, 200, `Missing embedded ${asset}`)
const auth = await fetch(`${base}/api/v1/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'package-test', password }) })
assert.equal(auth.status, 200)
const cookie = auth.headers.get('set-cookie').split(';')[0]
const proxies = await fetch(`${base}/api/v1/proxies`, { headers: { Cookie: cookie } })
assert.equal(proxies.status, 200)
assert.deepEqual(await proxies.json(), [])
console.log('Package verified: file checksums, key generation, certificate utility, isolated startup, embedded web assets and administrator login.')
} finally {
if (child && child.exitCode === null) { await new Promise(resolve => { child.once('exit', resolve); child.kill() }) }
rmSync(temp, { recursive: true, force: true })
}