const { createHash, createPublicKey, sign, verify } = require('node:crypto') const { closeSync, fstatSync, openSync, readFileSync, readSync, writeFileSync } = require('node:fs') const { basename, resolve } = require('node:path') const { crc32 } = require('node:zlib') const { productionSigningKey, publicKeySpkiBase64, readTrustedKeyRegistry, redactSecrets } = require('./agent-bundle.cjs') const { canonicalJson, descriptorContentDigest, signatureDomain: packageSignatureDomain } = require('./agent-package.cjs') const { localFileHeaderSignature, readExactZipBytes, readZipCentralDirectory } = require('./zip-central-directory.cjs') const root = resolve(__dirname, '..') function supportedTarget(platform, architecture) { return (platform === 'linux' && ['x64', 'arm64'].includes(architecture)) || (platform === 'darwin' && architecture === 'arm64') } const catalogName = 'agent-catalog.json' const catalogSignatureName = 'agent-catalog.sig' const catalogSignatureDomain = Buffer.from( 'GoodBuddy Agent Package Catalog Signature v1\0', 'utf8' ) const semanticVersionPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*)))*)?$/u const sha256Pattern = /^[a-f0-9]{64}$/u const maximumPackageBytes = 512 * 1024 * 1024 const maximumPackageEntries = 50_002 const maximumPackageEntryBytes = 384 * 1024 * 1024 const maximumExpandedPackageBytes = 1024 * 1024 * 1024 const maximumMetadataBytes = 1024 * 1024 const maximumDescriptorBytes = 4 * 1024 * 1024 const maximumCentralDirectoryBytes = 32 * 1024 * 1024 const maximumCatalogEntries = 200 const windowsReservedNamePattern = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu function canonicalBytes(value) { return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8') } function trustedProductionKey(registry, keyId) { const key = registry.keys.find( (candidate) => candidate.keyId === keyId && candidate.environment === 'production' ) if ( !key || registry.revocations.some( (revocation) => revocation.keyId === keyId ) ) { throw new Error(`Agent signing key is not trusted: ${keyId}`) } return key } function verifyDetachedSignature( bytes, signatureText, key, domain, label ) { const signature = Buffer.from( signatureText.toString('utf8').trim(), 'base64' ) const publicKey = createPublicKey({ key: Buffer.from(key.publicKeySpkiBase64, 'base64'), format: 'der', type: 'spki' }) if ( signature.length !== 64 || !verify( null, Buffer.concat([domain, bytes]), publicKey, signature ) ) { throw new Error(`${label} signature verification failed`) } } function parseCanonicalJson(bytes, label) { const value = JSON.parse( new TextDecoder('utf-8', { fatal: true }).decode(bytes) ) if (!canonicalBytes(value).equals(bytes)) { throw new Error(`${label} is not canonical`) } return value } function assertProtocol(value, label) { if ( typeof value !== 'object' || value === null || !Number.isSafeInteger(value.major) || value.major < 1 || !Number.isSafeInteger(value.minor) || value.minor < 0 ) { throw new Error(`${label} is invalid`) } } function assertPackageDescriptor(descriptor) { if ( typeof descriptor !== 'object' || descriptor === null || descriptor.format !== 'goodbuddy-agent-package' || descriptor.formatVersion !== 1 || descriptor.product !== 'GoodBuddy' || descriptor.component !== 'agent' || !semanticVersionPattern.test(descriptor.version ?? '') || !semanticVersionPattern.test( descriptor.minimumDesktopVersion ?? '' ) || !supportedTarget(descriptor.platform, descriptor.architecture) || !['x64', 'arm64'].includes(descriptor.architecture) || typeof descriptor.signingKeyId !== 'string' || !/^sha256:[a-f0-9]{64}$/u.test( descriptor.contentDigest ?? '' ) || !Array.isArray(descriptor.files) || descriptor.files.length === 0 || descriptor.files.length > 50_000 || typeof descriptor.remoteRuntime !== 'object' || descriptor.remoteRuntime === null || !['opencode', 'continue'].includes(descriptor.remoteRuntime.runtimeId) || descriptor.remoteRuntime.provider !== descriptor.remoteRuntime.runtimeId || !semanticVersionPattern.test( descriptor.remoteRuntime.version ?? '' ) || !/^sha256:[a-f0-9]{64}$/u.test( descriptor.remoteRuntime.bundleDigest ?? '' ) ) { throw new Error('Agent package descriptor is invalid') } assertProtocol(descriptor.agentProtocol, 'Agent protocol') assertAdditionalRuntimes(descriptor) assertProtocol( descriptor.remoteRuntime.protocol, 'Remote Runtime protocol' ) const paths = new Set() for (const file of descriptor.files) { if ( typeof file !== 'object' || file === null || !safePackagePath(file.path) || !Number.isSafeInteger(file.size) || file.size < 0 || !sha256Pattern.test(file.sha256 ?? '') || !['0644', '0755'].includes(file.mode) || paths.has(file.path) ) { throw new Error('Agent package file inventory is invalid') } paths.add(file.path) } if ( descriptor.contentDigest !== descriptorContentDigest(descriptor) ) { throw new Error('Agent package content identity is invalid') } } function readPackageMetadata(archivePath, registry) { const archive = resolve(archivePath) const streamed = streamPackageMetadata(archive) const descriptorBytes = streamed.descriptor const signatureBytes = streamed.signature if (!descriptorBytes || !signatureBytes) { throw new Error('Agent package metadata is missing') } const descriptor = parseCanonicalJson( Buffer.from(descriptorBytes), 'Agent package descriptor' ) assertPackageDescriptor(descriptor) verifyPackageFileInventory(descriptor, streamed.files) const key = trustedProductionKey( registry, descriptor.signingKeyId ) verifyDetachedSignature( Buffer.from(descriptorBytes), Buffer.from(signatureBytes), key, packageSignatureDomain, 'Agent package' ) const expectedArchive = `goodbuddy-agent-${descriptor.version}` + `-${descriptor.platform}-${descriptor.architecture}.gbagent` if (basename(archive) !== expectedArchive) { throw new Error( `Agent package archive must be named ${expectedArchive}` ) } return { archive: expectedArchive, descriptor, size: streamed.size, sha256: streamed.sha256 } } function safePackagePath(path) { return ( typeof path === 'string' && path.length > 0 && path.length <= 512 && !path.startsWith('/') && !path.endsWith('/') && !path.includes('\\') && path.split('/').every( (part) => part.length > 0 && part !== '.' && part !== '..' && !part.endsWith('.') && !part.endsWith(' ') && !part.includes(':') && [...part].every( (character) => character.charCodeAt(0) > 0x1f ) && !windowsReservedNamePattern.test(part) ) ) } function verifyPackageFileInventory(descriptor, actualFiles) { const expected = new Set([ ...descriptor.files.map((file) => file.path), 'agent-package.json', 'agent-package.sig' ]) if ( actualFiles.size !== expected.size || [...actualFiles.keys()].some((path) => !expected.has(path)) ) { throw new Error( 'Agent package contains undeclared or missing files' ) } for (const file of descriptor.files) { const actual = actualFiles.get(file.path) if ( !actual || actual.size !== file.size || actual.sha256 !== file.sha256 ) { throw new Error( `Agent package payload verification failed: ${file.path}` ) } } } function centralDirectoryEntries(handle, archiveSize) { const parsed = readZipCentralDirectory(handle, archiveSize, { label: 'Agent package', minimumEntries: 1, maximumEntries: maximumPackageEntries, maximumCentralDirectoryBytes }) const entries = [] const seen = new Set() let expandedBytes = 0 for (const entry of parsed.entries) { if ( entry.flags !== 0x0800 || entry.compression !== 0 || entry.compressedSize === 0xffffffff || entry.size === 0xffffffff || entry.compressedSize !== entry.size || entry.size > maximumPackageEntryBytes || entry.diskStart !== 0 || entry.localOffset === 0xffffffff ) { throw new Error('Agent package ZIP entry is unsupported') } if ( !safePackagePath(entry.name) || seen.has(entry.name) ) { throw new Error( 'Agent package contains an unsafe or duplicate path' ) } seen.add(entry.name) expandedBytes += entry.size if (expandedBytes > maximumExpandedPackageBytes) { throw new Error( 'Agent package expanded payload exceeds its limit' ) } const local = readExactZipBytes( handle, 30, entry.localOffset, 'Agent package local file header' ) const localNameLength = local.readUInt16LE(26) const localExtraLength = local.readUInt16LE(28) if ( local.readUInt32LE(0) !== localFileHeaderSignature || local.readUInt16LE(6) !== entry.flags || local.readUInt16LE(8) !== entry.compression || local.readUInt32LE(14) !== entry.checksum || local.readUInt32LE(18) !== entry.compressedSize || local.readUInt32LE(22) !== entry.size || localNameLength !== entry.nameBytes.length ) { throw new Error('Agent package local file header is invalid') } const localName = readExactZipBytes( handle, localNameLength, entry.localOffset + 30, 'Agent package local file name' ) if (!localName.equals(entry.nameBytes)) { throw new Error('Agent package local file name is invalid') } const dataOffset = entry.localOffset + 30 + localNameLength + localExtraLength const recordEnd = dataOffset + entry.compressedSize if (recordEnd > parsed.centralOffset) { throw new Error('Agent package entry exceeds its data region') } entries.push({ checksum: entry.checksum, dataOffset, name: entry.name, recordStart: entry.localOffset, recordEnd }) } entries.sort( (left, right) => left.recordStart - right.recordStart ) for (let index = 1; index < entries.length; index += 1) { if (entries[index].recordStart < entries[index - 1].recordEnd) { throw new Error('Agent package ZIP entries overlap') } } return entries } function sameFileSnapshot(left, right) { return ( left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs ) } function streamPackageMetadata(archivePath) { const handle = openSync(archivePath, 'r') const values = new Map() const files = new Map() try { const initialStatus = fstatSync(handle) if ( !initialStatus.isFile() || initialStatus.size <= 0 || initialStatus.size > maximumPackageBytes ) { throw new Error( 'Agent package archive is not a bounded file' ) } const entries = centralDirectoryEntries( handle, initialStatus.size ) const states = entries.map((entry) => { const wanted = entry.name === 'agent-package.json' || entry.name === 'agent-package.sig' const size = entry.recordEnd - entry.dataOffset const maximum = entry.name === 'agent-package.json' ? maximumDescriptorBytes : maximumMetadataBytes if (wanted && size > maximum) { throw new Error('Agent package metadata exceeds its limit') } return { ...entry, bytes: 0, contents: wanted ? Buffer.allocUnsafe(size) : undefined, crc32: 0, hash: createHash('sha256') } }) const archiveHash = createHash('sha256') const buffer = Buffer.allocUnsafe(1024 * 1024) let position = 0 let firstEntry = 0 while (position < initialStatus.size) { const bytesRead = readSync( handle, buffer, 0, Math.min(buffer.length, initialStatus.size - position), position ) if (bytesRead <= 0) { throw new Error('Agent package archive is truncated') } const chunk = buffer.subarray(0, bytesRead) const chunkEnd = position + bytesRead archiveHash.update(chunk) while ( firstEntry < states.length && states[firstEntry].recordEnd <= position ) { firstEntry += 1 } for ( let index = firstEntry; index < states.length && states[index].dataOffset < chunkEnd; index += 1 ) { const state = states[index] const dataStart = Math.max(position, state.dataOffset) const dataEnd = Math.min(chunkEnd, state.recordEnd) if (dataStart >= dataEnd) { continue } const data = chunk.subarray( dataStart - position, dataEnd - position ) state.hash.update(data) state.crc32 = crc32(data, state.crc32) if (state.contents) { data.copy( state.contents, dataStart - state.dataOffset ) } state.bytes += data.byteLength } position = chunkEnd } if (!sameFileSnapshot(initialStatus, fstatSync(handle))) { throw new Error( 'Agent package archive changed during verification' ) } for (const state of states) { const size = state.recordEnd - state.dataOffset if ( state.bytes !== size || (state.crc32 >>> 0) !== state.checksum ) { throw new Error( `Agent package ZIP checksum verification failed: ${state.name}` ) } files.set(state.name, { size, sha256: state.hash.digest('hex') }) if (state.contents) { values.set(state.name, state.contents) } } return { descriptor: values.get('agent-package.json'), signature: values.get('agent-package.sig'), files, sha256: archiveHash.digest('hex'), size: initialStatus.size } } finally { closeSync(handle) } } function catalogEntry(metadata) { const descriptor = metadata.descriptor const { contentDigest: _contentDigest, files: _files, signingKeyId: _signingKeyId, ...identity } = descriptor void _contentDigest void _files void _signingKeyId return { ...identity, archive: metadata.archive, size: metadata.size, sha256: metadata.sha256, downloads: { github: { url: 'https://github.com/mesalogo/goodbuddy/releases/download/' + `agent-v${descriptor.version}/${metadata.archive}` }, mirror: { url: 'https://goodbuddy.oss-cn-beijing.aliyuncs.com/' + `agent-releases/v${descriptor.version}/${metadata.archive}` } } } } function assertCatalog(catalog) { if ( typeof catalog !== 'object' || catalog === null || catalog.formatVersion !== 1 || catalog.product !== 'GoodBuddy' || catalog.component !== 'agent' || typeof catalog.signingKeyId !== 'string' || typeof catalog.generatedAt !== 'string' || !Number.isFinite(Date.parse(catalog.generatedAt)) || new Date(catalog.generatedAt).toISOString() !== catalog.generatedAt || !Array.isArray(catalog.entries) || catalog.entries.length === 0 || catalog.entries.length > maximumCatalogEntries ) { throw new Error('Agent catalog is invalid') } const identities = new Set() for (const entry of catalog.entries) { if ( typeof entry !== 'object' || entry === null || !semanticVersionPattern.test(entry.version ?? '') || !semanticVersionPattern.test( entry.minimumDesktopVersion ?? '' ) || !supportedTarget(entry.platform, entry.architecture) || !['x64', 'arm64'].includes(entry.architecture) || entry.archive !== `goodbuddy-agent-${entry.version}` + `-${entry.platform}-${entry.architecture}.gbagent` || !Number.isSafeInteger(entry.size) || entry.size <= 0 || !sha256Pattern.test(entry.sha256 ?? '') || entry.downloads?.github?.url !== 'https://github.com/mesalogo/goodbuddy/releases/download/' + `agent-v${entry.version}/${entry.archive}` || entry.downloads?.mirror?.url !== 'https://goodbuddy.oss-cn-beijing.aliyuncs.com/' + `agent-releases/v${entry.version}/${entry.archive}` || entry.format !== 'goodbuddy-agent-package' || entry.formatVersion !== 1 || entry.product !== 'GoodBuddy' || entry.component !== 'agent' || !['opencode', 'continue'].includes(entry.remoteRuntime?.runtimeId) || entry.remoteRuntime?.provider !== entry.remoteRuntime?.runtimeId || !semanticVersionPattern.test( entry.remoteRuntime?.version ?? '' ) || !/^sha256:[a-f0-9]{64}$/u.test( entry.remoteRuntime?.bundleDigest ?? '' ) ) { throw new Error('Agent catalog entry is invalid') } assertProtocol(entry.agentProtocol, 'Agent protocol') assertAdditionalRuntimes(entry) assertProtocol( entry.remoteRuntime.protocol, 'Remote Runtime protocol' ) const identity = `${entry.version}:${entry.platform}:${entry.architecture}` if (identities.has(identity)) { throw new Error('Agent catalog contains duplicate entries') } identities.add(identity) } } function readVerifiedCatalog(catalogPath, signaturePath, registry) { const bytes = readFileSync(resolve(catalogPath)) const catalog = parseCanonicalJson(bytes, 'Agent catalog') assertCatalog(catalog) const key = trustedProductionKey( registry, catalog.signingKeyId ) verifyDetachedSignature( bytes, readFileSync(resolve(signaturePath)), key, catalogSignatureDomain, 'Agent catalog' ) return catalog } function assertAdditionalRuntimes(value) { if (value.additionalRuntimes === undefined) return if (!Array.isArray(value.additionalRuntimes) || value.additionalRuntimes.length > 1) throw new Error('Invalid additional Runtimes') for (const runtime of value.additionalRuntimes) { if (!runtime || !['opencode', 'continue'].includes(runtime.runtimeId) || runtime.runtimeId === value.remoteRuntime.runtimeId || runtime.provider !== runtime.runtimeId || !semanticVersionPattern.test(runtime.version ?? '') || !/^sha256:[a-f0-9]{64}$/u.test(runtime.bundleDigest ?? '')) { throw new Error('Invalid additional Runtime identity') } assertProtocol(runtime.protocol, 'Additional Runtime protocol') } } function parseVersion(value) { const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/u .exec(value) if (!match) { throw new Error(`Invalid semantic version: ${value}`) } return [ BigInt(match[1]), BigInt(match[2]), BigInt(match[3]), match[4]?.split('.') ] } function compareVersions(left, right) { const leftParts = parseVersion(left) const rightParts = parseVersion(right) for (const index of [0, 1, 2]) { if (leftParts[index] !== rightParts[index]) { return leftParts[index] > rightParts[index] ? 1 : -1 } } const leftPre = leftParts[3] const rightPre = rightParts[3] if (!leftPre && !rightPre) return 0 if (!leftPre) return 1 if (!rightPre) return -1 for ( let index = 0; index < Math.max(leftPre.length, rightPre.length); index += 1 ) { const leftValue = leftPre[index] const rightValue = rightPre[index] if (leftValue === undefined) return -1 if (rightValue === undefined) return 1 if (leftValue === rightValue) continue const leftNumeric = /^\d+$/u.test(leftValue) const rightNumeric = /^\d+$/u.test(rightValue) if (leftNumeric && rightNumeric) { return BigInt(leftValue) > BigInt(rightValue) ? 1 : -1 } if (leftNumeric !== rightNumeric) { return leftNumeric ? -1 : 1 } return leftValue > rightValue ? 1 : -1 } return 0 } function createCatalog(options) { const projectRoot = options.projectRoot ?? root const registry = options.registry ?? readTrustedKeyRegistry(projectRoot) const signingIdentity = options.signingIdentity ?? productionSigningKey(options.environment ?? process.env) const signingRecord = trustedProductionKey( registry, signingIdentity.keyId ) if ( publicKeySpkiBase64(signingIdentity.privateKey) !== signingRecord.publicKeySpkiBase64 ) { throw new Error( 'Agent catalog signing key does not match the trusted registry' ) } const packages = [ readPackageMetadata(options.x64Package, registry), readPackageMetadata(options.arm64Package, registry), ...(options.darwinArm64Package ? [readPackageMetadata(options.darwinArm64Package, registry)] : []) ] const [first, second] = packages const additionalIdentity = descriptor => canonicalJson((descriptor.additionalRuntimes ?? []).map(({ bundleDigest: _digest, ...identity }) => { void _digest; return identity })) if (packages.some(value => additionalIdentity(value.descriptor) !== additionalIdentity(first.descriptor))) { throw new Error('Agent package matrix has inconsistent additional Runtimes') } const { bundleDigest: _firstRuntimeDigest, ...firstRuntimeIdentity } = first.descriptor.remoteRuntime const { bundleDigest: _secondRuntimeDigest, ...secondRuntimeIdentity } = second.descriptor.remoteRuntime void _firstRuntimeDigest void _secondRuntimeDigest if ( first.descriptor.version !== second.descriptor.version || first.descriptor.minimumDesktopVersion !== second.descriptor.minimumDesktopVersion || canonicalJson(first.descriptor.agentProtocol) !== canonicalJson(second.descriptor.agentProtocol) || canonicalJson(firstRuntimeIdentity) !== canonicalJson(secondRuntimeIdentity) || new Set(packages.map( (metadata) => metadata.descriptor.architecture ).slice(0, 2)).size !== 2 || first.descriptor.platform !== 'linux' || second.descriptor.platform !== 'linux' ) { throw new Error( 'Agent package matrix does not describe one coherent release' ) } if (packages[2]) { const mac = packages[2].descriptor const { bundleDigest: _digest, ...runtimeIdentity } = mac.remoteRuntime void _digest if ( mac.platform !== 'darwin' || mac.architecture !== 'arm64' || mac.version !== first.descriptor.version || mac.minimumDesktopVersion !== first.descriptor.minimumDesktopVersion || canonicalJson(mac.agentProtocol) !== canonicalJson(first.descriptor.agentProtocol) || canonicalJson(runtimeIdentity) !== canonicalJson(firstRuntimeIdentity) ) throw new Error('Agent package matrix does not describe one coherent release') } const previous = options.previousCatalog && options.previousSignature ? readVerifiedCatalog( options.previousCatalog, options.previousSignature, registry ) : undefined if ( Boolean(options.previousCatalog) !== Boolean(options.previousSignature) ) { throw new Error( 'Previous Agent catalog and signature must be provided together' ) } const entries = new Map() for (const entry of previous?.entries ?? []) { entries.set(`${entry.version}:${entry.platform}:${entry.architecture}`, entry) } for (const metadata of packages) { const entry = catalogEntry(metadata) const identity = `${entry.version}:${entry.platform}:${entry.architecture}` const existing = entries.get(identity) if ( existing && canonicalJson(existing) !== canonicalJson(entry) ) { throw new Error( `Agent release identity is immutable: ${identity}` ) } entries.set(identity, entry) } const sortedEntries = [...entries.values()].sort( (left, right) => compareVersions(right.version, left.version) || left.platform.localeCompare(right.platform, 'en') || left.architecture.localeCompare(right.architecture, 'en') ) if (sortedEntries.length > maximumCatalogEntries) { throw new Error('Agent catalog exceeds its retention limit') } const catalog = { formatVersion: 1, product: 'GoodBuddy', component: 'agent', signingKeyId: signingIdentity.keyId, generatedAt: new Date(options.generatedAt).toISOString(), entries: sortedEntries } assertCatalog(catalog) const bytes = canonicalBytes(catalog) const signature = sign( null, Buffer.concat([catalogSignatureDomain, bytes]), signingIdentity.privateKey ) writeFileSync(resolve(options.outputCatalog), bytes) writeFileSync( resolve(options.outputSignature), `${signature.toString('base64')}\n`, 'utf8' ) return { version: first.descriptor.version, catalog: basename(options.outputCatalog), signature: basename(options.outputSignature), entries: sortedEntries.length } } function parseArguments(argv) { const command = argv[0] if (!['create', 'verify'].includes(command)) { throw new Error( 'Usage: agent-catalog.cjs [options]' ) } const options = {} for (let index = 1; index < argv.length; index += 2) { const flag = argv[index] const value = argv[index + 1] if ( ![ '--x64-package', '--arm64-package', '--darwin-arm64-package', '--previous-catalog', '--previous-signature', '--catalog', '--signature', '--generated-at' ].includes(flag) || !value || value.startsWith('--') || options[flag] !== undefined ) { throw new Error('Invalid Agent catalog argument') } options[flag] = value } for (const flag of ['--catalog', '--signature']) { if (!options[flag]) { throw new Error(`Agent catalog argument is required: ${flag}`) } } if (command === 'verify') { return { command, catalog: resolve(options['--catalog']), signature: resolve(options['--signature']) } } for (const flag of [ '--x64-package', '--arm64-package', '--generated-at' ]) { if (!options[flag]) { throw new Error(`Agent catalog argument is required: ${flag}`) } } return { command, x64Package: resolve(options['--x64-package']), arm64Package: resolve(options['--arm64-package']), ...(options['--darwin-arm64-package'] ? { darwinArm64Package: resolve(options['--darwin-arm64-package']) } : {}), ...(options['--previous-catalog'] ? { previousCatalog: resolve( options['--previous-catalog'] ) } : {}), ...(options['--previous-signature'] ? { previousSignature: resolve( options['--previous-signature'] ) } : {}), outputCatalog: resolve(options['--catalog']), outputSignature: resolve(options['--signature']), generatedAt: options['--generated-at'] } } function main(argv = process.argv.slice(2)) { const options = parseArguments(argv) const registry = readTrustedKeyRegistry(root) const result = options.command === 'create' ? createCatalog(options) : { entries: readVerifiedCatalog( options.catalog, options.signature, registry ).entries.length } process.stdout.write(`${JSON.stringify(result)}\n`) } module.exports = { catalogName, catalogSignatureDomain, catalogSignatureName, canonicalBytes, catalogEntry, createCatalog, parseArguments, readPackageMetadata, readVerifiedCatalog } if (require.main === module) { try { main() } catch (error) { process.stderr.write( `${redactSecrets( error instanceof Error ? error.message : String(error) )}\n` ) process.exitCode = 1 } }