hololake-system-architecture/product-source/hololake-native-desktop/scripts/prepare-public-distribution-release.mjs

185 lines
7.6 KiB
JavaScript

#!/usr/bin/env node
import { createHash, createPrivateKey, createPublicKey, sign } from 'node:crypto'
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { basename, join } from 'node:path'
import process from 'node:process'
const root = new URL('..', import.meta.url).pathname
const args = Object.fromEntries(process.argv.slice(2).map((item) => {
const [key, ...rest] = item.replace(/^--/, '').split('=')
return [key, rest.join('=')]
}))
const output = args.out
const epoch = Number(args.epoch)
const sourceRevision = args['source-revision']
const privateKeyBase64 = process.env.HOLOLAKE_ZERO_ORIGIN_PKCS8_BASE64
if (!output || !Number.isSafeInteger(epoch) || epoch < 1 || !/^[a-f0-9]{40}$/.test(sourceRevision || '')) {
throw new Error('usage: --out=/absolute/path --epoch=1 --source-revision=<40 hex git revision>')
}
if (!privateKeyBase64) throw new Error('HOLOLAKE_ZERO_ORIGIN_PKCS8_BASE64 is required')
const privateKey = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' })
const publicSpki = createPublicKey(privateKey).export({ format: 'der', type: 'spki' })
const publicKeyBase64 = Buffer.from(publicSpki).subarray(-32).toString('base64')
const releaseId = `public-distribution-${epoch}-${sourceRevision.slice(0, 12)}`
const generatedAt = new Date().toISOString()
function sha256(bytes) {
return createHash('sha256').update(bytes).digest('hex')
}
function canonical(value) {
if (Array.isArray(value)) return value.map(canonical)
if (value && typeof value === 'object') {
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]))
}
return value
}
function jsonBytes(value) {
return Buffer.from(`${JSON.stringify(value, null, 2)}\n`)
}
function exactSignature(bytes) {
return sign(null, bytes, privateKey).toString('base64')
}
await rm(output, { recursive: true, force: true })
await mkdir(join(output, 'zero-core'), { recursive: true })
await mkdir(join(output, 'marketplace', 'artifacts'), { recursive: true })
const artifact = {
schema: 'hololake.public-zero-core-artifact/v1',
planeNumber: 'HLP-DIST-PLANE-0001',
epoch,
version: '1.0.0',
minimumHostVersion: '0.5.0',
protocol: {
gracePeriodDays: 7,
lighthouseAnchorUrl: 'https://guanghu.chat/api/hololake/zero-core/lamp',
lighthouseResolveUrl: 'https://guanghulab.com/api/ai/v1/resolve?id=',
enterpriseResolveUrl: 'https://guanghu.chat/api/hololake/enterprise/resolve',
coreChannelSource: 'https://guanghulab.com/code/bingshuo/guanghu-ice-heart',
origin: 'HLP-PUBLIC-ZERO-CORE-TRUST-001 · public scope only',
},
}
const artifactRaw = jsonBytes(artifact)
const lamp = {
schema: 'hololake.public-zero-core-lamp/v1',
planeNumber: 'HLP-DIST-PLANE-0001',
epoch,
version: '1.0.0',
contentRootSha256: sha256(artifactRaw),
artifactManifestUrl: 'https://guanghu.chat/api/hololake/zero-core/artifact',
signatureUrl: 'https://guanghu.chat/api/hololake/zero-core/lamp.sig',
publishedAt: generatedAt,
minimumHostVersion: '0.5.0',
}
const lampRaw = jsonBytes(lamp)
await writeFile(join(output, 'zero-core', 'artifact.json'), artifactRaw)
await writeFile(join(output, 'zero-core', 'lamp.json'), lampRaw)
const physicalDefinitions = [
{
file: 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod',
summary: '把当前账号的知识目录投影为只读结构视图;不改写知识原件。',
},
{
file: 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod',
summary: '教育文档、表格、作业导入与需本人确认的自动化工作台。',
},
]
const entries = []
for (const definition of physicalDefinitions) {
const source = join(root, 'fixtures', 'module-packages', definition.file)
const signatureSource = `${source}.sig`
const raw = await readFile(source)
const packageValue = JSON.parse(raw)
const manifest = packageValue.manifest
const artifactSha256 = sha256(raw)
const artifactFile = `${artifactSha256}.ghmod`
const signatureFile = `${artifactSha256}.ghmod.sig`
await copyFile(source, join(output, 'marketplace', 'artifacts', artifactFile))
await copyFile(signatureSource, join(output, 'marketplace', 'artifacts', signatureFile))
entries.push({
itemNumber: manifest.moduleNumber,
artifactKind: 'PHYSICAL_MODULE',
displayName: manifest.displayName,
summary: definition.summary,
version: manifest.version,
minimumHostVersion: manifest.minimumHostVersion,
sourceRepository: 'https://guanghulab.com/code/bingshuo/hololake-system-architecture',
sourceRevision,
sourcePath: `product-source/hololake-native-desktop/fixtures/module-packages/${definition.file}`,
artifactUrl: `https://guanghu.chat/api/hololake/marketplace/artifacts/${artifactFile}`,
artifactSha256,
signatureUrl: `https://guanghu.chat/api/hololake/marketplace/artifacts/${signatureFile}`,
adapter: manifest.adapter,
permissions: manifest.permissions,
executionAuthority: false,
skillReadonlyGuarantee: false,
})
}
const skillFiles = [
'HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill',
'HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill',
]
for (const file of skillFiles) {
const source = join(root, 'marketplace', 'skills', file)
const raw = await readFile(source)
const skill = JSON.parse(raw)
const artifactSha256 = sha256(raw)
const artifactFile = `${artifactSha256}.ghskill`
await copyFile(source, join(output, 'marketplace', 'artifacts', artifactFile))
entries.push({
itemNumber: skill.manifest.skillNumber,
artifactKind: 'COGNITIVE_SKILL',
displayName: skill.manifest.displayName,
summary: skill.payload.purpose,
version: skill.manifest.version,
minimumHostVersion: skill.manifest.minimumHostVersion,
sourceRepository: 'https://guanghulab.com/code/bingshuo/hololake-system-architecture',
sourceRevision,
sourcePath: `product-source/hololake-native-desktop/marketplace/skills/${file}`,
artifactUrl: `https://guanghu.chat/api/hololake/marketplace/artifacts/${artifactFile}`,
artifactSha256,
signatureUrl: null,
adapter: null,
permissions: [],
executionAuthority: false,
skillReadonlyGuarantee: true,
})
}
entries.sort((left, right) => left.itemNumber.localeCompare(right.itemNumber))
const catalog = {
schema: 'hololake.marketplace.catalog/v1',
planeNumber: 'HLP-DIST-PLANE-0003',
epoch,
generatedAt,
minimumHostVersion: '0.5.0',
contentRootSha256: sha256(Buffer.from(JSON.stringify(canonical(entries)))),
entries,
}
const catalogRaw = jsonBytes(catalog)
await writeFile(join(output, 'marketplace', 'catalog.json'), catalogRaw)
const authorization = {
schema: 'hololake.origin-release-authorization/v1',
releaseId,
sourceRevision,
epoch,
signer: {
signerId: 'HLP-SIGNER-ZERO-POINT-ORIGIN-PUBLIC-0001',
signerClass: 'ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER',
algorithm: 'Ed25519',
publicKeyBase64,
},
signedObjects: [
{ name: 'zero-core/lamp.json', sha256: sha256(lampRaw), signatureBase64: exactSignature(lampRaw) },
{ name: 'marketplace/catalog.json', sha256: sha256(catalogRaw), signatureBase64: exactSignature(catalogRaw) },
],
}
await writeFile(join(output, 'origin-authorization.json'), jsonBytes(authorization))
await writeFile(join(output, 'release-metadata.json'), jsonBytes({ schema: 'hololake.public-distribution-release/v1', releaseId, epoch, sourceRevision, generatedAt, originPublicKeyBase64: publicKeyBase64, artifacts: entries.map((entry) => ({ name: basename(entry.artifactUrl), sha256: entry.artifactSha256 })) }))
process.stdout.write(`${JSON.stringify({ releaseId, epoch, sourceRevision, itemCount: entries.length, originPublicKeyBase64: publicKeyBase64 })}\n`)