#!/usr/bin/env node import { createHash, createPrivateKey, createPublicKey, sign } from 'node:crypto' import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises' import { basename, dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { randomUUID } from 'node:crypto' import { spawnSync } from 'node:child_process' import { canonicalJson } from '../lib/canonical-json.mjs' import { parseBootstrapVersion, validateBootstrapContentTransition } from '../lib/bootstrap-version.mjs' import { parseBootstrapManifest, parseEnvironmentManifest, parseOfficialDshPolicy, parseStable } from '../lib/contracts.mjs' import { validateSupportedTarget } from '../lib/supported-target.mjs' import { positiveSafeInteger } from '../lib/validation.mjs' import { validateKeyringTransition, verifyRecoveryKeyring } from '../stage0/lib/keyring.mjs' import { verifyDetached } from '../stage0/lib/signature.mjs' import { verifyManagementDependencies } from './verify-management-dependencies.mjs' const args = process.argv.slice(2) if (args.length !== 10) { console.error('usage: prepare-release.mjs ') process.exit(64) } const [targetArg, definitionArg, policyArg, trustArg, privateKeyArg, tarballArg, previousArg, sequenceArg, baseUrlArg, outputArg] = args const platformRoot = dirname(dirname(fileURLToPath(import.meta.url))) const containerRoot = dirname(platformRoot) const targetPath = resolve(targetArg) const definitionPath = resolve(definitionArg) const officialDshPolicy = parseOfficialDshPolicy(JSON.parse(await readFile(resolve(policyArg), 'utf8'))) const trustRoot = resolve(trustArg) const privateKeyPath = resolve(privateKeyArg) const tarballPath = resolve(tarballArg) const output = resolve(outputArg) const targetSequence = positiveSafeInteger(Number(sequenceArg), 'target sequence') const artifactBaseUrl = new URL(baseUrlArg.endsWith('/') ? baseUrlArg : `${baseUrlArg}/`) if (artifactBaseUrl.protocol !== 'https:') throw new Error('Artifact base URL must use HTTPS') const bootstrapVersion = parseBootstrapVersion(await readFile(join(platformRoot, 'bootstrap', 'VERSION'), 'utf8')) const target = validateSupportedTarget(await readFile(targetPath), await readFile(definitionPath)) const recoveryPublicKey = (await readFile(join(trustRoot, 'recovery-root.spki.base64'), 'utf8')).trim() const keyringBytes = await readFile(join(trustRoot, 'keyring.json')) const keyringSignatureBytes = await readFile(join(trustRoot, 'keyring.sig.json')) const keyring = verifyRecoveryKeyring(keyringBytes, JSON.parse(keyringSignatureBytes.toString('utf8')), recoveryPublicKey) let previousTarget let previousBootstrap if (previousArg === '-') { if (targetSequence !== 1) throw new Error('The first target sequence must be 1') } else { const previousRoot = resolve(previousArg) const previousKeyringBytes = await readFile(join(previousRoot, 'keyring.json')) const previousKeyring = verifyRecoveryKeyring( previousKeyringBytes, JSON.parse(await readFile(join(previousRoot, 'keyring.sig.json'), 'utf8')), recoveryPublicKey, ) const previousStableBytes = await readFile(join(previousRoot, 'stable.json')) previousTarget = parseStable(previousStableBytes) verifyDetached( previousStableBytes, JSON.parse(await readFile(join(previousRoot, 'stable.sig.json'), 'utf8')), previousKeyring.current.publicKey, ) if (previousTarget.keyringGeneration !== previousKeyring.generation) { throw new Error('previous stable target does not match its keyring generation') } if (keyring.generation < previousTarget.keyringGeneration) { throw new Error('keyring generation must not roll back from the previous stable target') } if (keyring.generation === previousKeyring.generation) { if (!keyringBytes.equals(previousKeyringBytes)) throw new Error('same keyring generation must be byte-identical') } else validateKeyringTransition(previousKeyring, keyring) if (targetSequence <= previousTarget.targetSequence) throw new Error('target sequence must increase') const manifestDescriptor = previousTarget.artifacts.find(({ id }) => id === previousTarget.desired.bootstrap.manifestArtifactId) const signatureDescriptor = previousTarget.artifacts.find(({ id }) => id === previousTarget.desired.bootstrap.signatureArtifactId) if (manifestDescriptor === undefined || signatureDescriptor === undefined) throw new Error('previous Bootstrap metadata is incomplete') const previousBootstrapBytes = await readFile(join(previousRoot, basename(new URL(manifestDescriptor.url).pathname))) const previousBootstrapSignatureBytes = await readFile(join(previousRoot, basename(new URL(signatureDescriptor.url).pathname))) if ( previousBootstrapBytes.byteLength !== manifestDescriptor.size || createHash('sha256').update(previousBootstrapBytes).digest('hex') !== manifestDescriptor.sha256 || previousBootstrapSignatureBytes.byteLength !== signatureDescriptor.size || createHash('sha256').update(previousBootstrapSignatureBytes).digest('hex') !== signatureDescriptor.sha256 ) throw new Error('previous Bootstrap metadata differs from signed Stable descriptors') verifyDetached( previousBootstrapBytes, JSON.parse(previousBootstrapSignatureBytes.toString('utf8')), previousKeyring.current.publicKey, ) previousBootstrap = parseBootstrapManifest(previousBootstrapBytes) if (previousBootstrap.version !== previousTarget.desired.bootstrap.version) { throw new Error('previous Stable and Bootstrap manifest versions differ') } if ( previousBootstrap.targetSequence !== previousTarget.targetSequence || previousBootstrap.keyringGeneration !== previousTarget.keyringGeneration ) throw new Error('previous Stable and Bootstrap manifest generations differ') } const privateKey = createPrivateKey(await readFile(privateKeyPath)) if (privateKey.asymmetricKeyType !== 'ed25519') throw new Error('Release private key must be Ed25519') const publicDer = createPublicKey(privateKey).export({ format: 'der', type: 'spki' }) const currentKeyId = createHash('sha256').update(publicDer).digest('hex') if (currentKeyId !== keyring.current.keyId) throw new Error('Release private key is not the keyring current key') const packageResult = spawnSync('tar', ['-xOf', tarballPath, 'package/package.json'], { encoding: 'utf8' }) if (packageResult.status !== 0) throw new Error(packageResult.stderr || 'Unable to read DSH npm package metadata') const packageMetadata = JSON.parse(packageResult.stdout) if (packageMetadata.name !== '@deepseek-ai/dsh' || packageMetadata.version !== target.latestSupportedDsh) { throw new Error('DSH npm tarball does not match the Supported Target') } const dshBytes = await readFile(tarballPath) const dshIntegrity = `sha512-${createHash('sha512').update(dshBytes).digest('base64')}` const staging = `${output}.${randomUUID()}.tmp` const environmentOutput = join(staging, '.environment') const metadataOutput = join(staging, '.metadata') const releaseConfigPath = join(staging, '.release-config.json') const sourceDateEpoch = process.env.SOURCE_DATE_EPOCH let issuedAt if (sourceDateEpoch === undefined) issuedAt = new Date().toISOString() else { const epoch = Number(sourceDateEpoch) if (!Number.isSafeInteger(epoch) || epoch < 0) throw new Error('SOURCE_DATE_EPOCH must be a non-negative safe integer') issuedAt = new Date(epoch * 1000).toISOString() } function run(command, commandArgs, label) { const result = spawnSync(command, commandArgs, { encoding: 'utf8', env: process.env }) if (result.status !== 0) throw new Error(result.stderr || `${label} failed`) } function signature(document) { return canonicalJson({ schema: 1, algorithm: 'Ed25519', keyId: currentKeyId, signature: sign(null, document, privateKey).toString('base64'), }) } async function descriptor(id, path, mediaType) { const bytes = await readFile(path) return { id, mediaType, sha256: createHash('sha256').update(bytes).digest('hex'), size: bytes.byteLength, url: new URL(basename(path), artifactBaseUrl).href, } } await lstat(output).then( () => { throw new Error('Release output already exists') }, error => { if (error?.code !== 'ENOENT') throw error }, ) await mkdir(dirname(output), { recursive: true }) await mkdir(staging, { recursive: false }) try { await verifyManagementDependencies(join(containerRoot, 'control-plane', 'services', 'management')) run(process.execPath, [ join(platformRoot, 'tools', 'package-environment.mjs'), definitionPath, environmentOutput, String(keyring.generation), String(targetSequence), artifactBaseUrl.href, 'flat', ], 'Environment packaging') const environmentManifestPath = join(staging, 'environment.manifest.json') await cp(join(environmentOutput, 'environment.manifest.json'), environmentManifestPath) for (const name of await readdir(join(environmentOutput, 'artifacts'))) { await cp(join(environmentOutput, 'artifacts', name), join(staging, name), { errorOnExist: true, force: false }) } const environmentManifest = await readFile(environmentManifestPath) const parsedEnvironment = parseEnvironmentManifest(environmentManifest) if (parsedEnvironment.version !== target.environment) throw new Error('Packaged Environment does not match the Supported Target') await writeFile(join(staging, 'environment.manifest.sig.json'), signature(environmentManifest), { flag: 'wx', mode: 0o600 }) const bootstrapPath = join(staging, 'bootstrap.tgz') run('tar', [ '--sort=name', '--mtime=@0', '--owner=0', '--group=0', '--numeric-owner', '-czf', bootstrapPath, '-C', containerRoot, 'platform/bootstrap', 'platform/lib', 'control-plane/definition.json', 'control-plane/hooks/recovery', 'control-plane/hooks/dsh-web-ready.mjs', 'control-plane/services/gateway', 'control-plane/services/access-manager', 'control-plane/services/outbound-proxy', 'control-plane/modules/log-manager', 'control-plane/services/management', 'control-plane/modules/patch-manager', 'control-plane/modules/plugin-manager', 'control-plane/modules/skill-manager', 'control-plane/modules/updater', 'control-plane/modules/file-manager', ], 'Bootstrap packaging') const bootstrapPackage = await descriptor('bootstrap-package', bootstrapPath, 'application/gzip') if (previousBootstrap !== undefined) { const previousBootstrapPackage = previousBootstrap.artifacts.find(({ id }) => id === 'bootstrap-package') if (previousBootstrapPackage === undefined) throw new Error('previous Bootstrap package is missing') validateBootstrapContentTransition({ previousVersion: previousBootstrap.version, previousSha256: previousBootstrapPackage.sha256, nextVersion: bootstrapVersion, nextSha256: bootstrapPackage.sha256, }) } const bootstrapManifest = canonicalJson({ schema: 1, manifestType: 'bootstrap', version: bootstrapVersion, keyringGeneration: keyring.generation, targetSequence, issuedAt, artifacts: [bootstrapPackage], bootstrapApi: 1, entrypoint: '/platform/bootstrap/index.mjs', }) parseBootstrapManifest(bootstrapManifest) await writeFile(join(staging, 'bootstrap.manifest.json'), bootstrapManifest, { flag: 'wx' }) await writeFile(join(staging, 'bootstrap.manifest.sig.json'), signature(bootstrapManifest), { flag: 'wx', mode: 0o600 }) const releaseConfig = { currentKeyId, keyringGeneration: keyring.generation, targetSequence, artifactBaseUrl: artifactBaseUrl.href, officialDshPolicy, artifacts: [ ['environment-manifest', environmentManifestPath, 'application/vnd.dsh-platform.manifest.v1+json'], ['environment-signature', join(staging, 'environment.manifest.sig.json'), 'application/vnd.dsh-platform.signature.v1+json'], ['bootstrap-manifest', join(staging, 'bootstrap.manifest.json'), 'application/vnd.dsh-platform.manifest.v1+json'], ['bootstrap-signature', join(staging, 'bootstrap.manifest.sig.json'), 'application/vnd.dsh-platform.signature.v1+json'], ].map(([id, path, mediaType]) => ({ id, path, mediaType })), desired: { bootstrap: { version: bootstrapVersion, manifestArtifactId: 'bootstrap-manifest', signatureArtifactId: 'bootstrap-signature' }, environment: { version: target.environment, manifestArtifactId: 'environment-manifest', signatureArtifactId: 'environment-signature' }, dsh: { version: target.latestSupportedDsh, integrity: dshIntegrity }, }, } await writeFile(releaseConfigPath, JSON.stringify(releaseConfig), { flag: 'wx' }) run(process.execPath, [join(platformRoot, 'tools', 'build-release.mjs'), releaseConfigPath, privateKeyPath, metadataOutput], 'Stable metadata build') await cp(join(metadataOutput, 'stable.json'), join(staging, 'stable.json')) await cp(join(metadataOutput, 'stable.sig.json'), join(staging, 'stable.sig.json')) await cp(join(trustRoot, 'keyring.json'), join(staging, 'keyring.json')) await cp(join(trustRoot, 'keyring.sig.json'), join(staging, 'keyring.sig.json')) const stableBytes = await readFile(join(staging, 'stable.json')) const stable = parseStable(stableBytes) verifyDetached(stableBytes, JSON.parse(await readFile(join(staging, 'stable.sig.json'), 'utf8')), keyring.current.publicKey) if (stable.desired.dsh.version !== target.latestSupportedDsh || stable.desired.environment.version !== target.environment) { throw new Error('Generated stable metadata differs from the Supported Target') } await rm(environmentOutput, { recursive: true, force: true }) await rm(metadataOutput, { recursive: true, force: true }) await rm(releaseConfigPath, { force: true }) await rename(staging, output) } catch (error) { await rm(staging, { recursive: true, force: true }) throw error }