185 lines
7.6 KiB
JavaScript
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`)
|