"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.releaseCanonicalJson = releaseCanonicalJson; exports.releaseJsonChecksum = releaseJsonChecksum; exports.assertReleaseArtifactPath = assertReleaseArtifactPath; exports.compiledArtifactDigestFromEntries = compiledArtifactDigestFromEntries; exports.calculateReleaseCompiledDigest = calculateReleaseCompiledDigest; exports.captureReleaseDependencies = captureReleaseDependencies; exports.createReleaseArtifactBundle = createReleaseArtifactBundle; exports.inspectReleaseArtifactBundle = inspectReleaseArtifactBundle; exports.verifyLocalReleaseArtifact = verifyLocalReleaseArtifact; exports.signReleaseEvidence = signReleaseEvidence; exports.verifyReleaseEvidence = verifyReleaseEvidence; exports.validateReleaseDependencyManifest = validateReleaseDependencyManifest; exports.captureReleaseSourceInputs = captureReleaseSourceInputs; exports.materializeCommittedReleaseSource = materializeCommittedReleaseSource; const fs_1 = require("fs"); const path_1 = require("path"); const child_process_1 = require("child_process"); const crypto_1 = require("crypto"); const MAX_BUNDLE_BYTES = 64 * 1024 * 1024; const MAX_FILES = 10000; const MAX_DEPENDENCIES = 150000; const SHA = /^[a-f0-9]{64}$/; const SIGNATURE_DOMAINS = { build: 'COOL_SAAS_RELEASE_BUILD_V2\0', task: 'COOL_SAAS_RELEASE_TASK_V2\0', run: 'COOL_SAAS_RELEASE_RUN_V2\0', claim: 'COOL_SAAS_RELEASE_CLAIM_V2\0', }; function ensure(value, message) { if (!value) throw new Error(`Release artifact refused: ${message}`); } function releaseCanonicalJson(value) { if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value); if (typeof value === 'number') { ensure(Number.isFinite(value) && !Object.is(value, -0), 'invalid JSON number'); return JSON.stringify(value); } if (Array.isArray(value)) return `[${value.map(releaseCanonicalJson).join(',')}]`; ensure(value && typeof value === 'object' && [Object.prototype, null].includes(Object.getPrototypeOf(value)), 'invalid JSON value'); return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${releaseCanonicalJson(value[key])}`).join(',')}}`; } function releaseJsonChecksum(value) { return (0, crypto_1.createHash)('sha256').update(releaseCanonicalJson(value), 'utf8').digest('hex'); } function assertReleaseArtifactPath(name) { ensure(typeof name === 'string' && name.length > 0 && name.length <= 1024 && !/[\\:\x00-\x1f]/.test(name), 'invalid relative artifact path'); ensure(!name.startsWith('/') && name.split('/').every(part => part && part !== '.' && part !== '..' && !/[. ]$/.test(part) && !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)), 'unsafe relative artifact path'); return name; } function inside(root, target) { const name = (0, path_1.relative)(root, target); return name === '' || (!(0, path_1.isAbsolute)(name) && name !== '..' && !name.startsWith(`..${path_1.sep}`)); } function regularDirectoryFiles(root) { var _a; ensure(((_a = (0, fs_1.lstatSync)(root, { throwIfNoEntry: false })) === null || _a === void 0 ? void 0 : _a.isDirectory()) && !(0, fs_1.lstatSync)(root).isSymbolicLink(), 'artifact directory is missing or linked'); const files = []; const visit = (directory, depth = 0) => { ensure(depth <= 64, 'artifact directory nesting is too deep'); for (const name of (0, fs_1.readdirSync)(directory).sort()) { const absolute = (0, path_1.resolve)(directory, name); const stat = (0, fs_1.lstatSync)(absolute); ensure(!stat.isSymbolicLink(), 'artifact must not contain symbolic links'); if (stat.isDirectory()) visit(absolute, depth + 1); else { ensure(stat.isFile(), 'artifact contains a non-file entry'); files.push({ path: assertReleaseArtifactPath((0, path_1.relative)(root, absolute).replace(/\\/g, '/')), absolute }); ensure(files.length <= MAX_FILES, 'too many artifact files'); } } }; visit(root); ensure(files.length > 0, 'compiled artifact is empty'); return files.sort((left, right) => left.path.localeCompare(right.path)); } function compiledArtifactDigestFromEntries(entries) { ensure(entries.length > 0, 'compiled artifact is empty'); const hash = (0, crypto_1.createHash)('sha256').update('COOL_SAAS_ARTIFACT_DIRECTORY_V1\0', 'utf8'); const seen = new Set(); for (const { path, bytes } of [...entries].sort((left, right) => left.path.localeCompare(right.path))) { assertReleaseArtifactPath(path); ensure(!seen.has(path.toLowerCase()), 'duplicate or case-colliding compiled path'); seen.add(path.toLowerCase()); hash.update(`${Buffer.byteLength(path, 'utf8')}:`, 'utf8').update(path, 'utf8').update(`:${bytes.length}:`, 'utf8').update(bytes); } return `sha256:${hash.digest('hex')}`; } function calculateReleaseCompiledDigest(artifactDir) { return compiledArtifactDigestFromEntries(regularDirectoryFiles((0, path_1.resolve)(artifactDir)).map(file => ({ path: file.path, bytes: (0, fs_1.readFileSync)(file.absolute) }))); } /** Captures the installed dependency bytes, including pnpm's physical store. * Links must resolve inside node_modules. Absolute OS link text is normalized * to its target relative to that root so copying the same tree is portable. */ function captureReleaseDependencies(projectDir) { const root = (0, path_1.resolve)(projectDir, 'node_modules'); const rootStat = (0, fs_1.lstatSync)(root, { throwIfNoEntry: false }); ensure((rootStat === null || rootStat === void 0 ? void 0 : rootStat.isDirectory()) && !rootStat.isSymbolicLink(), 'node_modules must be an independent local directory'); const actualRoot = (0, fs_1.realpathSync)(root); const entries = []; const visit = (directory, depth = 0) => { ensure(depth <= 64, 'dependency nesting is too deep'); for (const name of (0, fs_1.readdirSync)(directory).sort()) { const absolute = (0, path_1.resolve)(directory, name); const entryPath = assertReleaseArtifactPath((0, path_1.relative)(root, absolute).replace(/\\/g, '/')); const stat = (0, fs_1.lstatSync)(absolute); if (stat.isSymbolicLink()) { const target = (0, fs_1.realpathSync)(absolute); ensure(inside(actualRoot, target) && target !== actualRoot, 'dependency link escapes node_modules'); entries.push({ path: entryPath, type: 'link', target: assertReleaseArtifactPath((0, path_1.relative)(actualRoot, target).replace(/\\/g, '/')) }); } else if (stat.isDirectory()) visit(absolute, depth + 1); else { ensure(stat.isFile() && stat.size <= 256 * 1024 * 1024, 'invalid or oversized dependency file'); const bytes = (0, fs_1.readFileSync)(absolute); entries.push({ path: entryPath, type: 'file', size: bytes.length, sha256: (0, crypto_1.createHash)('sha256').update(bytes).digest('hex') }); } ensure(entries.length <= MAX_DEPENDENCIES, 'too many dependency entries'); } }; visit(root); ensure(entries.length > 0, 'dependencies are empty'); return { formatVersion: 1, entries: entries.sort((a, b) => a.path.localeCompare(b.path)) }; } function createReleaseArtifactBundle(projectDir) { const root = (0, path_1.resolve)(projectDir); const files = []; const required = ['.cool-saas-candidate.json', '.cool-saas-service.json', 'bootstrap.js', 'release-environment.js', 'start-stable.js', 'start-frozen.js', 'release-artifact.js', 'package.json', 'scripts/published-packages.json']; const locks = ['pnpm-lock.yaml', 'package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'].filter(name => (0, fs_1.lstatSync)((0, path_1.resolve)(root, name), { throwIfNoEntry: false })); ensure(locks.length === 1, 'exactly one dependency lock is required'); for (const name of [...required, ...locks]) { const absolute = (0, path_1.resolve)(root, name); const stat = (0, fs_1.lstatSync)(absolute, { throwIfNoEntry: false }); ensure((stat === null || stat === void 0 ? void 0 : stat.isFile()) && !stat.isSymbolicLink(), `required deployment file is missing or linked: ${name}`); ensure(inside((0, fs_1.realpathSync)(root), (0, fs_1.realpathSync)(absolute)), 'deployment file escapes project'); files.push({ path: name, content: (0, fs_1.readFileSync)(absolute).toString('base64') }); } for (const file of regularDirectoryFiles((0, path_1.resolve)(root, 'dist'))) files.push({ path: `dist/${file.path}`, content: (0, fs_1.readFileSync)(file.absolute).toString('base64') }); const dependencies = captureReleaseDependencies(root); files.push({ path: 'release-dependencies.json', content: Buffer.from(releaseCanonicalJson(dependencies), 'utf8').toString('base64') }); const bundle = { formatVersion: 1, files: files.sort((a, b) => a.path.localeCompare(b.path)) }; inspectReleaseArtifactBundle(bundle); return bundle; } function inspectReleaseArtifactBundle(bundle) { ensure(bundle && Object.keys(bundle).sort().join(',') === 'files,formatVersion' && bundle.formatVersion === 1 && Array.isArray(bundle.files), 'invalid bundle shape'); ensure(bundle.files.length > 0 && bundle.files.length <= MAX_FILES, 'invalid bundle file count'); ensure(Buffer.byteLength(releaseCanonicalJson(bundle), 'utf8') <= MAX_BUNDLE_BYTES, 'artifact bundle exceeds size limit'); const files = new Map(); const names = new Set(); for (const file of bundle.files) { ensure(file && Object.keys(file).sort().join(',') === 'content,path', 'invalid artifact file shape'); assertReleaseArtifactPath(file.path); ensure(typeof file.content === 'string', 'invalid artifact content'); const bytes = Buffer.from(file.content, 'base64'); ensure(bytes.toString('base64') === file.content, 'non-canonical base64 artifact content'); ensure(!names.has(file.path.toLowerCase()), 'duplicate or case-colliding artifact path'); names.add(file.path.toLowerCase()); files.set(file.path, bytes); } const compiled = [...files].filter(([name]) => name.startsWith('dist/')).map(([name, bytes]) => ({ path: name.slice(5), bytes })); return { files, artifactDigest: compiledArtifactDigestFromEntries(compiled), bundleDigest: `sha256:${releaseJsonChecksum(bundle)}` }; } function verifyLocalReleaseArtifact(projectDir, bundle) { const inspected = inspectReleaseArtifactBundle(bundle); const dependencyBytes = inspected.files.get('release-dependencies.json'); ensure(dependencyBytes, 'installed dependency evidence is missing'); const expected = JSON.parse(dependencyBytes.toString('utf8')); const actual = captureReleaseDependencies(projectDir); ensure(releaseJsonChecksum(expected) === releaseJsonChecksum(actual), 'installed dependency bytes or link targets differ from the signed build'); for (const [name, bytes] of inspected.files) { if (name === 'release-dependencies.json') continue; const absolute = (0, path_1.resolve)(projectDir, name); const stat = (0, fs_1.lstatSync)(absolute, { throwIfNoEntry: false }); ensure((stat === null || stat === void 0 ? void 0 : stat.isFile()) && !stat.isSymbolicLink() && inside((0, fs_1.realpathSync)(projectDir), (0, fs_1.realpathSync)(absolute)), `deployment file is missing or linked: ${name}`); ensure((0, fs_1.readFileSync)(absolute).equals(bytes), `deployment file differs from signed build: ${name}`); } ensure(calculateReleaseCompiledDigest((0, path_1.resolve)(projectDir, 'dist')) === inspected.artifactDigest, 'compiled file set differs from signed build'); return inspected; } function signReleaseEvidence(kind, keyId, claims, privateKeyPem) { ensure(/^[a-z0-9][a-z0-9._-]{0,63}$/.test(keyId), 'invalid signing key id'); const key = (0, crypto_1.createPrivateKey)(privateKeyPem); ensure(key.asymmetricKeyType === 'ed25519', 'release evidence requires Ed25519'); const unsigned = { algorithm: 'Ed25519', keyId, claims }; const bytes = Buffer.from(SIGNATURE_DOMAINS[kind] + releaseCanonicalJson(unsigned), 'utf8'); return { ...unsigned, signature: (0, crypto_1.sign)(null, bytes, key).toString('base64url') }; } function verifyReleaseEvidence(kind, envelope, trustedKeys) { ensure(envelope && Object.keys(envelope).sort().join(',') === 'algorithm,claims,keyId,signature' && envelope.algorithm === 'Ed25519', 'invalid signed evidence envelope'); ensure(Object.prototype.hasOwnProperty.call(trustedKeys, envelope.keyId), 'untrusted evidence signer'); const signature = Buffer.from(envelope.signature || '', 'base64url'); ensure(signature.length === 64 && signature.toString('base64url') === envelope.signature, 'invalid evidence signature encoding'); const key = (0, crypto_1.createPublicKey)(trustedKeys[envelope.keyId]); ensure(key.asymmetricKeyType === 'ed25519', 'invalid trusted evidence key'); const unsigned = { algorithm: envelope.algorithm, keyId: envelope.keyId, claims: envelope.claims }; ensure((0, crypto_1.verify)(null, Buffer.from(SIGNATURE_DOMAINS[kind] + releaseCanonicalJson(unsigned), 'utf8'), key, signature), 'release evidence signature does not match'); return envelope.claims; } function validateReleaseDependencyManifest(value) { ensure(value && value.formatVersion === 1 && Array.isArray(value.entries) && value.entries.length > 0 && value.entries.length <= MAX_DEPENDENCIES, 'invalid dependency manifest'); const paths = new Set(); for (const entry of value.entries) { assertReleaseArtifactPath(entry.path); ensure(!paths.has(entry.path.toLowerCase()), 'duplicate dependency path'); paths.add(entry.path.toLowerCase()); if (entry.type === 'file') ensure(Number.isSafeInteger(entry.size) && entry.size >= 0 && SHA.test(entry.sha256 || ''), 'invalid dependency file proof'); else { ensure(entry.type === 'link', 'invalid dependency entry type'); assertReleaseArtifactPath(entry.target); } } } /** Captures every authored input, including unanticipated build-script inputs. * Only conventional generated output roots are excluded. */ function captureReleaseSourceInputs(projectDir) { const root = (0, fs_1.realpathSync)(projectDir); const files = []; const walk = (directory) => { for (const entry of (0, fs_1.readdirSync)(directory, { withFileTypes: true })) { const absolute = (0, path_1.resolve)(directory, entry.name); const name = (0, path_1.relative)(root, absolute).replace(/\\/g, '/'); if (/^(?:dist|node_modules|logs?|coverage|\.cache|\.git)(?:\/|$)/.test(name) || /^[^/]+\.tsbuildinfo$/.test(name)) continue; assertReleaseArtifactPath(name); const stat = (0, fs_1.lstatSync)(absolute); ensure(!stat.isSymbolicLink(), `source input is linked: ${name}`); if (stat.isDirectory()) walk(absolute); else { ensure(stat.isFile(), `source input is not a regular file: ${name}`); files.push({ path: name, sha256: (0, crypto_1.createHash)('sha256').update((0, fs_1.readFileSync)(absolute)).digest('hex') }); } } }; walk(root); return files.sort((a, b) => a.path.localeCompare(b.path)); } /** Reads exact Git blobs instead of a mutable checkout while a build runs. */ function materializeCommittedReleaseSource(repository, commit, destination) { ensure(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit), 'invalid source commit'); ensure(!(0, fs_1.lstatSync)(destination, { throwIfNoEntry: false }), 'source snapshot destination already exists'); const entries = (0, child_process_1.execFileSync)('git', ['ls-tree', '-r', '-z', commit], { cwd: repository, windowsHide: true, maxBuffer: 32 * 1024 * 1024 }).toString('utf8').split('\0').filter(Boolean); ensure(entries.length > 0 && entries.length <= 30000, 'invalid source snapshot size'); (0, fs_1.mkdirSync)(destination, { recursive: true }); for (const entry of entries) { const match = /^(100644|100755) blob ([a-f0-9]{40,64})\t([\s\S]+)$/.exec(entry); ensure(match, 'source snapshot contains a link or submodule'); const name = assertReleaseArtifactPath(match[3]); // Credentials are never build inputs, even if an old repository tracked one. ensure(!/(^|\/)(?:\.env(?:\..*)?|.*\.pem|.*\.private\.(?:json|txt))$/i.test(name), 'private configuration must not be tracked in a release'); if (/(^|\/)\.npmrc$/i.test(name)) continue; ensure(!name.startsWith('node_modules/') && !name.startsWith('dist/'), 'compiled dependencies/artifacts must not be tracked as source'); const target = (0, path_1.resolve)(destination, name); ensure(inside((0, path_1.resolve)(destination), target), 'source snapshot path escaped destination'); const bytes = (0, child_process_1.execFileSync)('git', ['cat-file', 'blob', match[2]], { cwd: repository, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }); (0, fs_1.mkdirSync)((0, path_1.dirname)(target), { recursive: true }); (0, fs_1.writeFileSync)(target, bytes, { flag: 'wx', mode: match[1] === '100755' ? 0o755 : 0o644 }); } }