mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
197 lines
6.5 KiB
JavaScript
197 lines
6.5 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { createReadStream, createWriteStream } from 'node:fs'
|
|
import { rm, stat } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { pipeline } from 'node:stream/promises'
|
|
import { constants as zlibConstants, createBrotliCompress, createBrotliDecompress } from 'node:zlib'
|
|
|
|
import { create, Parser } from 'tar'
|
|
|
|
import { createSshRelayRuntimeZip, inspectSshRelayRuntimeZip } from './ssh-relay-runtime-zip.mjs'
|
|
|
|
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024
|
|
const ARCHIVE_TIMEOUT_MS = 5 * 60 * 1000
|
|
const BROTLI_CHUNK_BYTES = 64 * 1024
|
|
const BROTLI_QUALITY = 9
|
|
const BROTLI_WINDOW_BITS = 20
|
|
|
|
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.br'
|
|
return `orca-ssh-relay-runtime-v1-${tuple}-${match[1]}.${suffix}`
|
|
}
|
|
|
|
async function compressRuntimeTree({
|
|
runtimeRoot,
|
|
paths,
|
|
sourceDateEpoch,
|
|
archivePath,
|
|
signal,
|
|
markOutputCreated
|
|
}) {
|
|
const archiveOutput = createWriteStream(archivePath, { flags: 'wx', mode: 0o600 })
|
|
archiveOutput.once('open', markOutputCreated)
|
|
const tarStream = create(
|
|
{
|
|
cwd: runtimeRoot,
|
|
portable: true,
|
|
noPax: true,
|
|
noDirRecurse: true,
|
|
mtime: new Date(sourceDateEpoch * 1000)
|
|
},
|
|
paths
|
|
)
|
|
const compression = createBrotliCompress({
|
|
chunkSize: BROTLI_CHUNK_BYTES,
|
|
params: {
|
|
[zlibConstants.BROTLI_PARAM_QUALITY]: BROTLI_QUALITY,
|
|
[zlibConstants.BROTLI_PARAM_LGWIN]: BROTLI_WINDOW_BITS
|
|
}
|
|
})
|
|
await pipeline(tarStream, compression, archiveOutput, { signal })
|
|
}
|
|
|
|
async function decompressArchive(archivePath, destination, signal) {
|
|
await pipeline(
|
|
createReadStream(archivePath, { highWaterMark: BROTLI_CHUNK_BYTES }),
|
|
createBrotliDecompress({ chunkSize: BROTLI_CHUNK_BYTES }),
|
|
destination,
|
|
{ signal }
|
|
)
|
|
}
|
|
|
|
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 name = archiveName(identity.tupleId, identity.contentId)
|
|
const archivePath = join(outputDirectory, name)
|
|
const timeoutSignal = AbortSignal.timeout(ARCHIVE_TIMEOUT_MS)
|
|
const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal
|
|
effectiveSignal.throwIfAborted()
|
|
let outputCreated = false
|
|
try {
|
|
const paths = identity.entries.map((entry) => entry.path).sort()
|
|
await compressRuntimeTree({
|
|
runtimeRoot,
|
|
paths,
|
|
sourceDateEpoch,
|
|
archivePath,
|
|
signal: effectiveSignal,
|
|
// Why: an exclusive-open failure must never delete an output owned by another build.
|
|
markOutputCreated: () => {
|
|
outputCreated = true
|
|
}
|
|
})
|
|
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) {
|
|
if (outputCreated) {
|
|
await rm(archivePath, { force: true })
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
const timeoutSignal = AbortSignal.timeout(ARCHIVE_TIMEOUT_MS)
|
|
const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal
|
|
effectiveSignal.throwIfAborted()
|
|
await decompressArchive(archivePath, parser, effectiveSignal)
|
|
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
|
|
}
|
|
}
|
|
|
|
export const SSH_RELAY_RUNTIME_POSIX_ARCHIVE_LIMITS = Object.freeze({
|
|
chunkBytes: BROTLI_CHUNK_BYTES,
|
|
quality: BROTLI_QUALITY,
|
|
windowBits: BROTLI_WINDOW_BITS
|
|
})
|