mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
217 lines
7.1 KiB
JavaScript
217 lines
7.1 KiB
JavaScript
import { spawn } from 'node:child_process'
|
|
import { createHash } from 'node:crypto'
|
|
import { createReadStream, createWriteStream } from 'node:fs'
|
|
import { mkdtemp, rm, stat } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pipeline } from 'node:stream/promises'
|
|
|
|
import { create, Parser } from 'tar'
|
|
|
|
import { createSshRelayRuntimeZip, inspectSshRelayRuntimeZip } from './ssh-relay-runtime-zip.mjs'
|
|
|
|
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024
|
|
const MAX_DIAGNOSTIC_BYTES = 64 * 1024
|
|
const XZ_TIMEOUT_MS = 5 * 60 * 1000
|
|
|
|
function archiveName(tuple, contentId) {
|
|
const match = /^sha256:([0-9a-f]{64})$/.exec(contentId)
|
|
if (!match) {
|
|
throw new Error('Runtime content identity is not a SHA-256 digest')
|
|
}
|
|
const suffix = tuple.startsWith('win32-') ? 'zip' : 'tar.xz'
|
|
return `orca-ssh-relay-runtime-v1-${tuple}-${match[1]}.${suffix}`
|
|
}
|
|
|
|
async function compressTar(tarPath, archivePath, signal) {
|
|
const child = spawn(
|
|
'xz',
|
|
['--compress', '--stdout', '--threads=1', '--check=crc64', '-9e', '--', tarPath],
|
|
{ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, signal }
|
|
)
|
|
let stderr = ''
|
|
child.stderr.setEncoding('utf8')
|
|
child.stderr.on('data', (chunk) => {
|
|
stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES)
|
|
})
|
|
const completion = new Promise((resolve, reject) => {
|
|
child.once('error', reject)
|
|
child.once('close', (code, closeSignal) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`xz failed (${code ?? closeSignal ?? 'unknown'}): ${stderr.trim()}`))
|
|
}
|
|
})
|
|
})
|
|
try {
|
|
await Promise.all([
|
|
pipeline(child.stdout, createWriteStream(archivePath, { flags: 'wx', mode: 0o600 }), {
|
|
signal
|
|
}),
|
|
completion
|
|
])
|
|
} catch (error) {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill()
|
|
}
|
|
await completion.catch(() => {})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function decompressArchive(archivePath, destination, signal) {
|
|
const child = spawn('xz', ['--decompress', '--stdout', '--single-stream', '--', archivePath], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
signal
|
|
})
|
|
let stderr = ''
|
|
child.stderr.setEncoding('utf8')
|
|
child.stderr.on('data', (chunk) => {
|
|
stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES)
|
|
})
|
|
const completion = new Promise((resolve, reject) => {
|
|
child.once('error', reject)
|
|
child.once('close', (code, closeSignal) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`xz failed (${code ?? closeSignal ?? 'unknown'}): ${stderr.trim()}`))
|
|
}
|
|
})
|
|
})
|
|
try {
|
|
await Promise.all([pipeline(child.stdout, destination, { signal }), completion])
|
|
} catch (error) {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill()
|
|
}
|
|
await completion.catch(() => {})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export async function createSshRelayRuntimeArchive({
|
|
runtimeRoot,
|
|
outputDirectory,
|
|
identity,
|
|
sourceDateEpoch,
|
|
signal
|
|
}) {
|
|
if (identity.tupleId.startsWith('win32-')) {
|
|
return createSshRelayRuntimeZip({
|
|
runtimeRoot,
|
|
outputDirectory,
|
|
identity,
|
|
sourceDateEpoch,
|
|
signal
|
|
})
|
|
}
|
|
if (!Number.isSafeInteger(sourceDateEpoch) || sourceDateEpoch < 0) {
|
|
throw new Error('Runtime archive SOURCE_DATE_EPOCH must be a non-negative safe integer')
|
|
}
|
|
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-tar-'))
|
|
const tarPath = join(temporaryDirectory, 'runtime.tar')
|
|
const name = archiveName(identity.tupleId, identity.contentId)
|
|
const archivePath = join(outputDirectory, name)
|
|
const effectiveSignal = signal ?? AbortSignal.timeout(XZ_TIMEOUT_MS)
|
|
try {
|
|
const paths = identity.entries.map((entry) => entry.path).sort()
|
|
await create(
|
|
{
|
|
cwd: runtimeRoot,
|
|
file: tarPath,
|
|
portable: true,
|
|
noPax: true,
|
|
noDirRecurse: true,
|
|
mtime: new Date(sourceDateEpoch * 1000)
|
|
},
|
|
paths
|
|
)
|
|
await compressTar(tarPath, archivePath, effectiveSignal)
|
|
const metadata = await stat(archivePath)
|
|
if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_ARCHIVE_BYTES) {
|
|
throw new Error('Runtime archive exceeds the release-manifest compressed-size limit')
|
|
}
|
|
const digest = createHash('sha256')
|
|
for await (const chunk of createReadStream(archivePath)) {
|
|
digest.update(chunk)
|
|
}
|
|
return {
|
|
name,
|
|
path: archivePath,
|
|
size: metadata.size,
|
|
sha256: `sha256:${digest.digest('hex')}`
|
|
}
|
|
} catch (error) {
|
|
await rm(archivePath, { force: true })
|
|
throw error
|
|
} finally {
|
|
await rm(temporaryDirectory, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
export async function inspectSshRelayRuntimeArchive(archivePath, identity, { signal } = {}) {
|
|
if (identity.tupleId.startsWith('win32-')) {
|
|
return inspectSshRelayRuntimeZip(archivePath, identity, { signal })
|
|
}
|
|
const metadata = await stat(archivePath)
|
|
if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_ARCHIVE_BYTES) {
|
|
throw new Error('Runtime archive exceeds the release-manifest compressed-size limit')
|
|
}
|
|
const expected = new Map(identity.entries.map((entry) => [entry.path, entry]))
|
|
const seen = new Set()
|
|
const pendingFiles = []
|
|
const parser = new Parser({ strict: true })
|
|
parser.on('entry', (entry) => {
|
|
const path = entry.path.endsWith('/') ? entry.path.slice(0, -1) : entry.path
|
|
const expectedEntry = expected.get(path)
|
|
try {
|
|
if (!expectedEntry || seen.has(path)) {
|
|
throw new Error(`Runtime archive has extra or duplicate entry: ${path}`)
|
|
}
|
|
seen.add(path)
|
|
const expectedType = expectedEntry.type === 'file' ? 'File' : 'Directory'
|
|
if (entry.type !== expectedType || (entry.mode & 0o777) !== expectedEntry.mode) {
|
|
throw new Error(`Runtime archive type or mode mismatch: ${path}`)
|
|
}
|
|
if (expectedEntry.type === 'file') {
|
|
if (entry.size !== expectedEntry.size) {
|
|
throw new Error(`Runtime archive size mismatch: ${path}`)
|
|
}
|
|
const digest = createHash('sha256')
|
|
entry.on('data', (chunk) => digest.update(chunk))
|
|
pendingFiles.push(
|
|
new Promise((resolve, reject) => {
|
|
entry.once('error', reject)
|
|
entry.once('end', () => {
|
|
const actual = `sha256:${digest.digest('hex')}`
|
|
if (actual !== expectedEntry.sha256) {
|
|
reject(new Error(`Runtime archive SHA-256 mismatch: ${path}`))
|
|
} else {
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
)
|
|
} else {
|
|
entry.resume()
|
|
}
|
|
} catch (error) {
|
|
parser.abort(error)
|
|
}
|
|
})
|
|
await decompressArchive(archivePath, parser, signal ?? AbortSignal.timeout(XZ_TIMEOUT_MS))
|
|
await Promise.all(pendingFiles)
|
|
const missing = [...expected.keys()].filter((path) => !seen.has(path))
|
|
if (missing.length > 0) {
|
|
throw new Error(`Runtime archive is missing declared entry: ${missing[0]}`)
|
|
}
|
|
return {
|
|
entries: seen.size,
|
|
files: identity.entries.filter((entry) => entry.type === 'file').length,
|
|
expandedBytes: identity.expandedSize
|
|
}
|
|
}
|