2026-08-13 18:32:38 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import crypto from 'node:crypto'
|
|
|
|
|
import fs from 'node:fs'
|
|
|
|
|
import os from 'node:os'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
2026-08-13 18:37:30 +08:00
|
|
|
import { createReleaseServer, isMainModule, loadRuntimeState } from '../server/release-broadcast/server.mjs'
|
2026-08-13 18:32:38 +08:00
|
|
|
|
|
|
|
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex')
|
|
|
|
|
|
|
|
|
|
const writeJson = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
|
|
|
|
|
|
|
|
|
|
async function withServer(state, callback) {
|
|
|
|
|
const server = createReleaseServer(state)
|
|
|
|
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
|
|
|
const address = server.address()
|
|
|
|
|
try {
|
|
|
|
|
return await callback(`http://127.0.0.1:${address.port}`)
|
|
|
|
|
} finally {
|
|
|
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildReleaseRoot() {
|
|
|
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-broadcast-'))
|
|
|
|
|
const releaseDirectory = path.join(root, 'releases', '0.2.0')
|
|
|
|
|
fs.mkdirSync(releaseDirectory, { recursive: true })
|
|
|
|
|
const packageBytes = Buffer.from('signed-notarized-updater-placeholder')
|
|
|
|
|
const packageName = 'HoloLake.app.tar.gz'
|
|
|
|
|
fs.writeFileSync(path.join(releaseDirectory, packageName), packageBytes, { mode: 0o600 })
|
|
|
|
|
writeJson(path.join(releaseDirectory, 'HOLOLAKE-CODESIGN.json'), {
|
|
|
|
|
schema: 'hololake.platform-code-signature-receipt/v1',
|
|
|
|
|
state: 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED',
|
2026-08-13 18:37:30 +08:00
|
|
|
sourceCommit: '1'.repeat(40),
|
2026-08-13 18:32:38 +08:00
|
|
|
})
|
|
|
|
|
writeJson(path.join(releaseDirectory, 'HOLOLAKE-NOTARIZATION.json'), {
|
|
|
|
|
schema: 'hololake.apple-notarization-receipt/v1',
|
|
|
|
|
state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED',
|
2026-08-13 18:37:30 +08:00
|
|
|
sourceCommit: '1'.repeat(40),
|
2026-08-13 18:32:38 +08:00
|
|
|
})
|
|
|
|
|
const broadcast = {
|
|
|
|
|
schema: 'hololake.release-broadcast/v1',
|
|
|
|
|
releaseId: 'GH-HOLOLAKE-RELEASE-0.2.0',
|
|
|
|
|
version: '0.2.0',
|
|
|
|
|
pub_date: '2026-08-13T10:00:00.000Z',
|
|
|
|
|
notes: 'Signed release',
|
|
|
|
|
platforms: {
|
|
|
|
|
'darwin-aarch64': {
|
|
|
|
|
url: `https://release.guanghu.test/releases/0.2.0/${packageName}`,
|
|
|
|
|
signature: 'trusted-updater-signature',
|
|
|
|
|
size: packageBytes.length,
|
|
|
|
|
sha256: sha256(packageBytes),
|
|
|
|
|
platformCodeSignatureReceipt: 'HOLOLAKE-CODESIGN',
|
|
|
|
|
notarizationReceipt: 'HOLOLAKE-NOTARIZATION',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
hololake: {
|
|
|
|
|
features: ['Stage one'],
|
|
|
|
|
fixes: [],
|
|
|
|
|
compatibility: { minimumVersion: '0.1.0', dataMigrationRequired: false },
|
|
|
|
|
restart: { required: true, automaticAllowed: false },
|
|
|
|
|
rollback: { supported: true, healthReceiptRequired: true, previousVersion: '0.1.0' },
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
const broadcastPath = path.join(releaseDirectory, 'latest.json')
|
|
|
|
|
writeJson(broadcastPath, broadcast)
|
|
|
|
|
const broadcastSha256 = sha256(fs.readFileSync(broadcastPath))
|
|
|
|
|
writeJson(path.join(releaseDirectory, 'pipeline-receipt.json'), {
|
|
|
|
|
schema: 'hololake.signed-release-pipeline-receipt/v1',
|
|
|
|
|
state: 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD',
|
2026-08-13 18:37:30 +08:00
|
|
|
sourceCommit: '1'.repeat(40),
|
2026-08-13 18:32:38 +08:00
|
|
|
broadcastSha256,
|
|
|
|
|
automaticUpload: false,
|
|
|
|
|
automaticActivation: false,
|
|
|
|
|
})
|
|
|
|
|
writeJson(path.join(root, 'ACTIVE.json'), {
|
|
|
|
|
schema: 'hololake.release-broadcast-activation/v1',
|
|
|
|
|
state: 'HUMAN_APPROVED_SIGNED_NOTARIZED_RELEASE_ACTIVE',
|
|
|
|
|
releaseId: broadcast.releaseId,
|
|
|
|
|
version: broadcast.version,
|
|
|
|
|
broadcastRelativePath: 'releases/0.2.0/latest.json',
|
|
|
|
|
broadcastSha256,
|
|
|
|
|
pipelineReceiptRelativePath: 'releases/0.2.0/pipeline-receipt.json',
|
|
|
|
|
humanApprovalReceipt: 'GH-HUMAN-RELEASE-APPROVAL-001',
|
2026-08-13 18:37:30 +08:00
|
|
|
humanApprovalReceiptRelativePath: 'releases/0.2.0/human-approval.json',
|
|
|
|
|
})
|
|
|
|
|
writeJson(path.join(releaseDirectory, 'human-approval.json'), {
|
|
|
|
|
schema: 'hololake.release-broadcast-human-approval/v1',
|
|
|
|
|
state: 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE',
|
|
|
|
|
approvalId: 'GH-HUMAN-RELEASE-APPROVAL-001',
|
|
|
|
|
releaseId: broadcast.releaseId,
|
|
|
|
|
version: broadcast.version,
|
|
|
|
|
broadcastSha256,
|
2026-08-13 18:32:38 +08:00
|
|
|
})
|
|
|
|
|
return { root, packageBytes }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('empty release root stays healthy but returns Tauri-compatible 204 no update', async () => {
|
|
|
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-empty-'))
|
|
|
|
|
const state = loadRuntimeState(root)
|
|
|
|
|
assert.equal(state.state, 'EMPTY_FAIL_CLOSED')
|
|
|
|
|
await withServer(state, async (base) => {
|
|
|
|
|
const health = await fetch(`${base}/health`)
|
|
|
|
|
assert.equal(health.status, 200)
|
|
|
|
|
assert.deepEqual((await health.json()).upstreamUpdateSources, [])
|
|
|
|
|
assert.equal((await fetch(`${base}/latest.json`)).status, 204)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('invalid activation locks the endpoint instead of falling back to an update', async () => {
|
|
|
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-invalid-'))
|
|
|
|
|
writeJson(path.join(root, 'ACTIVE.json'), { schema: 'wrong', state: 'ACTIVE' })
|
|
|
|
|
const state = loadRuntimeState(root)
|
|
|
|
|
assert.equal(state.state, 'LOCKED_INVALID_RELEASE_EVIDENCE')
|
|
|
|
|
await withServer(state, async (base) => {
|
|
|
|
|
assert.equal((await fetch(`${base}/health`)).status, 503)
|
|
|
|
|
assert.equal((await fetch(`${base}/latest.json`)).status, 503)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('only an exact human-approved signed notarized evidence chain becomes readable', async () => {
|
|
|
|
|
const fixture = buildReleaseRoot()
|
|
|
|
|
const state = loadRuntimeState(fixture.root)
|
|
|
|
|
assert.equal(state.state, 'READY_SIGNED_NOTARIZED_BROADCAST')
|
|
|
|
|
await withServer(state, async (base) => {
|
|
|
|
|
const latest = await fetch(`${base}/latest.json`)
|
|
|
|
|
assert.equal(latest.status, 200)
|
|
|
|
|
assert.equal((await latest.json()).releaseId, 'GH-HOLOLAKE-RELEASE-0.2.0')
|
|
|
|
|
const updater = await fetch(`${base}/releases/0.2.0/HoloLake.app.tar.gz`)
|
|
|
|
|
assert.equal(updater.status, 200)
|
|
|
|
|
assert.deepEqual(Buffer.from(await updater.arrayBuffer()), fixture.packageBytes)
|
|
|
|
|
assert.equal((await fetch(`${base}/unknown`)).status, 404)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('package tampering after pipeline output locks the whole release at startup', () => {
|
|
|
|
|
const fixture = buildReleaseRoot()
|
|
|
|
|
fs.appendFileSync(path.join(fixture.root, 'releases', '0.2.0', 'HoloLake.app.tar.gz'), 'tampered')
|
|
|
|
|
assert.equal(loadRuntimeState(fixture.root).state, 'LOCKED_INVALID_RELEASE_EVIDENCE')
|
|
|
|
|
})
|
2026-08-13 18:37:30 +08:00
|
|
|
|
|
|
|
|
test('a symlinked immutable current directory is recognized as the intended executable', () => {
|
|
|
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-symlink-'))
|
|
|
|
|
const versionDirectory = path.join(root, 'immutable')
|
|
|
|
|
fs.mkdirSync(versionDirectory)
|
|
|
|
|
const source = fs.readFileSync(new URL('../server/release-broadcast/server.mjs', import.meta.url), 'utf8')
|
|
|
|
|
const copy = path.join(versionDirectory, 'server.mjs')
|
|
|
|
|
fs.writeFileSync(copy, source, { mode: 0o500 })
|
|
|
|
|
const current = path.join(root, 'current')
|
|
|
|
|
fs.symlinkSync(versionDirectory, current)
|
|
|
|
|
assert.equal(isMainModule(path.join(current, 'server.mjs'), new URL(`file://${copy}`)), true)
|
|
|
|
|
})
|