@kungfu-tech/buildchain 3.0.6-alpha.1 → 3.0.6-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +4 -4
  2. package/actions/promote-buildchain-ref/README.md +8 -0
  3. package/bin/buildchain.mjs +13 -1
  4. package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
  5. package/contracts/release-candidate-recovery-v1.schema.json +102 -0
  6. package/dist/site/buildchain-contract.json +129 -34
  7. package/dist/site/buildchain-site.json +171 -45
  8. package/dist/site/capability-registry.json +5 -5
  9. package/dist/site/cli-registry.json +40 -4
  10. package/dist/site/controller-registry.json +56 -4
  11. package/dist/site/kfd-claims.json +184 -20
  12. package/dist/site/kfd-upstream-aggregate.json +1 -1
  13. package/dist/site/manual-registry.json +10 -10
  14. package/dist/site/node-api-registry.json +1242 -121
  15. package/dist/site/page-registry.json +157 -31
  16. package/dist/site/public-surface-audit.json +422 -21
  17. package/dist/site/publication-authority-registry.json +66 -1
  18. package/dist/site/publication-registry.json +4 -4
  19. package/dist/site/release-provenance.json +2 -0
  20. package/dist/site/site-manifest.json +14 -14
  21. package/dist/site/workflow-registry.json +197 -15
  22. package/docs/MAP.md +1 -0
  23. package/docs/auditable-demo.md +58 -11
  24. package/docs/aws-us-elastic-runner-burst-plane.md +23 -19
  25. package/docs/cli-reference.md +154 -0
  26. package/docs/dev-alpha-candidate-patrol.md +13 -5
  27. package/docs/dev-delivery-warrant.md +158 -0
  28. package/docs/node-api-reference.md +96 -38
  29. package/docs/publication-authority.md +11 -0
  30. package/docs/publish-transaction.md +10 -1
  31. package/docs/release-candidate.md +106 -12
  32. package/docs/release-governance.md +52 -0
  33. package/docs/reusable-build-surface.md +11 -1
  34. package/docs/shifu-gate-profiles.md +12 -1
  35. package/docs/versioning.md +2 -0
  36. package/package.json +3 -1
  37. package/packages/core/buildchain-contract.js +30 -2
  38. package/packages/core/buildchain-publication-authority.js +3 -1
  39. package/packages/core/channel-candidate.js +2 -21
  40. package/packages/core/channel-promotion-baseline.js +199 -0
  41. package/packages/core/dev-delivery-candidate-identity.js +94 -0
  42. package/packages/core/dev-delivery-common.js +73 -0
  43. package/packages/core/dev-delivery-proof.js +252 -0
  44. package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
  45. package/packages/core/dev-delivery-warrant-settlement.js +73 -0
  46. package/packages/core/dev-delivery-warrant.js +591 -0
  47. package/packages/core/index.js +7 -0
  48. package/packages/core/release-candidate-recovery.js +386 -0
  49. package/scripts/auditable-demo-bundle-verification.mjs +148 -0
  50. package/scripts/auditable-demo-platform.mjs +86 -50
  51. package/scripts/auditable-demo-presentation.mjs +83 -0
  52. package/scripts/auditable-demo-renditions.mjs +264 -0
  53. package/scripts/auditable-demo.mjs +24 -30
  54. package/scripts/build-contract-core.mjs +58 -3
  55. package/scripts/buildchain-cli-help.mjs +8 -0
  56. package/scripts/buildchain-patrol.mjs +9 -0
  57. package/scripts/check-inventory.mjs +13 -2
  58. package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
  59. package/scripts/dev-delivery-proof.mjs +193 -0
  60. package/scripts/dev-delivery-warrant.mjs +426 -0
  61. package/scripts/dev-pr-auto-merge.mjs +488 -46
  62. package/scripts/dev-pr-delivery-warrant.mjs +209 -0
  63. package/scripts/gate-profile-core.mjs +24 -0
  64. package/scripts/generate-site-bundle.mjs +2 -2
  65. package/scripts/git-fetch-process-tree.mjs +142 -0
  66. package/scripts/lifecycle-substage-evidence.mjs +274 -0
  67. package/scripts/locked-source-checkout.mjs +6 -3
  68. package/scripts/release-candidate-resolver.mjs +35 -3
  69. package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
  70. package/scripts/resolve-build-contract.mjs +7 -0
  71. package/scripts/resume-from-candidate-run.mjs +498 -0
  72. package/scripts/route-offline-runners.mjs +1 -0
  73. package/scripts/run-lifecycle-core.mjs +9 -9
  74. package/scripts/shifu-gate-profile.mjs +10 -16
  75. package/scripts/site-capability-metadata.mjs +15 -0
@@ -0,0 +1,386 @@
1
+ import {
2
+ createControllerReceiptReference,
3
+ validateControllerReceipt,
4
+ } from "./controller-evidence.js";
5
+ import {
6
+ sha256Json,
7
+ validateReleaseCandidatePassport,
8
+ } from "./release-candidate.js";
9
+ import { releaseTransactionId } from "./publish-transaction.js";
10
+
11
+ export const RELEASE_CANDIDATE_RECOVERY_CONTRACT =
12
+ "kungfu-buildchain-release-candidate-recovery/v1";
13
+
14
+ const SHA_PATTERN = /^[0-9a-f]{40}$/;
15
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
16
+ const TRUSTED_ASSOCIATIONS = new Set(["COLLABORATOR", "MEMBER", "OWNER"]);
17
+
18
+ export class ReleaseCandidateRecoveryError extends Error {
19
+ constructor(code, message, nextAction) {
20
+ super(message);
21
+ this.name = "ReleaseCandidateRecoveryError";
22
+ this.code = code;
23
+ this.nextAction = nextAction;
24
+ }
25
+ }
26
+
27
+ function fail(code, message, nextAction) {
28
+ throw new ReleaseCandidateRecoveryError(code, message, nextAction);
29
+ }
30
+
31
+ function required(value, label, code = "invalid-recovery-input") {
32
+ const normalized = String(value || "").trim();
33
+ if (!normalized) fail(code, `${label} is required`, "Correct the recovery dispatch inputs and run a fresh recovery event.");
34
+ return normalized;
35
+ }
36
+
37
+ function exactSha(value, label) {
38
+ const normalized = required(value, label).toLowerCase();
39
+ if (!SHA_PATTERN.test(normalized)) {
40
+ fail("invalid-recovery-input", `${label} must be a 40-character Git SHA`, "Supply the exact immutable Git SHA.");
41
+ }
42
+ return normalized;
43
+ }
44
+
45
+ function contentRoot(value, label) {
46
+ const normalized = required(value, label).toLowerCase();
47
+ if (!SHA256_PATTERN.test(normalized)) {
48
+ fail("invalid-recovery-input", `${label} must be a sha256 content root`, "Supply the exact sha256 root from the sealed candidate evidence.");
49
+ }
50
+ return normalized;
51
+ }
52
+
53
+ function normalizeArtifact(record, index) {
54
+ const name = required(record?.name, `artifacts[${index}].name`);
55
+ if (record?.expired === true) {
56
+ fail("artifact-expired", `candidate artifact is expired: ${name}`, "Create a new candidate explicitly; recovery never falls back to rebuilding.");
57
+ }
58
+ if (record?.missing === true) {
59
+ fail("artifact-missing", `candidate artifact is missing: ${name}`, "Restore the immutable artifact if possible or explicitly create a new candidate.");
60
+ }
61
+ const size = Number(record?.size);
62
+ const downloadedSize = Number(record?.downloadedSize);
63
+ if (!Number.isSafeInteger(size) || size < 0 || !Number.isSafeInteger(downloadedSize) || downloadedSize < 0) {
64
+ fail("artifact-metadata-invalid", `candidate artifact size is invalid: ${name}`, "Inspect the candidate run artifact metadata and retry with a complete artifact set.");
65
+ }
66
+ const digest = contentRoot(record?.digest, `artifacts[${index}].digest`);
67
+ const downloadedDigest = contentRoot(record?.downloadedDigest, `artifacts[${index}].downloadedDigest`);
68
+ if (size !== downloadedSize || digest !== downloadedDigest) {
69
+ fail("artifact-digest-mismatch", `candidate artifact archive differs from GitHub metadata: ${name}`, "Do not publish; preserve the run and investigate artifact corruption or replacement.");
70
+ }
71
+ const files = (record?.files || []).map((file, fileIndex) => {
72
+ const fileSize = Number(file?.size);
73
+ if (!Number.isSafeInteger(fileSize) || fileSize < 0) {
74
+ fail("artifact-manifest-mismatch", `${name} file ${fileIndex} has an invalid size`, "Inspect the platform manifest and product payload bytes.");
75
+ }
76
+ return {
77
+ path: required(file?.path, `${name}.files[${fileIndex}].path`),
78
+ size: fileSize,
79
+ sha256: contentRoot(file?.sha256, `${name}.files[${fileIndex}].sha256`),
80
+ };
81
+ }).sort((left, right) => left.path.localeCompare(right.path));
82
+ if (new Set(files.map((file) => file.path)).size !== files.length) {
83
+ fail("artifact-manifest-mismatch", `candidate artifact contains duplicate file paths: ${name}`, "Regenerate a valid candidate explicitly.");
84
+ }
85
+ return { name, size, digest, files, kind: String(record?.kind || "payload") };
86
+ }
87
+
88
+ function assertEqual(actual, expected, code, label, nextAction) {
89
+ if (actual !== expected) fail(code, `${label} mismatch: expected ${expected}, got ${actual || "<empty>"}`, nextAction);
90
+ }
91
+
92
+ function normalizedRef(value) {
93
+ return String(value || "").replace(/^refs\/heads\//, "").trim();
94
+ }
95
+
96
+ function isBuildchainDiagnosticsEnvelope(file) {
97
+ return String(file?.path || "").replace(/\\/g, "/").startsWith(".buildchain/");
98
+ }
99
+
100
+ function validatePlatformPayloads(passport, artifacts, platformManifests) {
101
+ const byName = new Map(artifacts.map((artifact) => [artifact.name, artifact]));
102
+ const manifestsByArtifact = new Map((platformManifests || []).map((manifest) => [manifest.artifactName, manifest]));
103
+ if (manifestsByArtifact.size !== (passport.platformMatrix || []).length) {
104
+ fail("platform-matrix-mismatch", "platform manifest count differs from the Passport platform matrix", "Restore every exact platform manifest from the candidate run.");
105
+ }
106
+ for (const platform of passport.platformMatrix || []) {
107
+ const payload = byName.get(platform.artifactName);
108
+ if (!payload) {
109
+ fail("artifact-missing", `platform payload artifact is missing: ${platform.artifactName}`, "Restore the exact candidate artifact or explicitly create a new candidate.");
110
+ }
111
+ const passportFiles = (platform.artifacts || []).map((file) => ({
112
+ path: String(file.path || file.name || ""),
113
+ size: Number(file.size),
114
+ sha256: `sha256:${String(file.sha256 || "").replace(/^sha256:/, "")}`,
115
+ })).sort((left, right) => left.path.localeCompare(right.path));
116
+ const manifest = manifestsByArtifact.get(platform.artifactName);
117
+ if (!manifest) {
118
+ fail("artifact-missing", `platform manifest is missing for ${platform.artifactName}`, "Restore the exact platform manifest artifact.");
119
+ }
120
+ const manifestFiles = (manifest.files || []).map((file) => ({
121
+ path: String(file.path || file.name || ""),
122
+ size: Number(file.size ?? file.bytes),
123
+ sha256: `sha256:${String(file.sha256 || "").replace(/^sha256:/, "")}`,
124
+ })).sort((left, right) => left.path.localeCompare(right.path));
125
+ const productPayloadFiles = payload.files.filter((file) => !isBuildchainDiagnosticsEnvelope(file));
126
+ const manifestProductFiles = manifestFiles.filter((file) => !isBuildchainDiagnosticsEnvelope(file));
127
+ if (JSON.stringify(productPayloadFiles) !== JSON.stringify(manifestProductFiles)) {
128
+ fail("artifact-manifest-mismatch", `platform payload bytes differ from the uploaded platform manifest: ${platform.artifactName}`, "Do not publish; preserve the mismatched manifest and payload evidence.");
129
+ }
130
+ if (passportFiles.length > 0 && JSON.stringify(manifestFiles) !== JSON.stringify(passportFiles)) {
131
+ fail("artifact-manifest-mismatch", `platform manifest differs from Passport inventory: ${platform.artifactName}`, "Do not publish; preserve the mismatched manifest and Passport evidence.");
132
+ }
133
+ if (
134
+ Number(platform.summary?.fileCount) !== manifestFiles.length ||
135
+ Number(platform.summary?.totalBytes) !== manifestFiles.reduce((total, file) => total + file.size, 0)
136
+ ) {
137
+ fail("platform-matrix-mismatch", `platform summary differs from manifest inventory: ${platform.artifactName}`, "Use the exact summary, manifest, and payload uploaded by the successful candidate run.");
138
+ }
139
+ }
140
+ }
141
+
142
+ function validateProductPayloads({ passport, artifacts, productPayloadManifests, candidateRoot, buildSummaryRoot, runtimeSha }) {
143
+ const byName = new Map(artifacts.map((artifact) => [artifact.name, artifact]));
144
+ for (const [index, manifest] of (productPayloadManifests || []).entries()) {
145
+ if (manifest?.contract !== "kungfu-buildchain-product-payload-manifest/v1") {
146
+ fail("artifact-manifest-mismatch", `product payload manifest ${index} has an invalid contract`, "Use the exact Buildchain-produced product payload manifest.");
147
+ }
148
+ const { root: _root, ...rootInput } = manifest;
149
+ assertEqual(manifest.root, `sha256:${sha256Json(rootInput)}`, "artifact-manifest-mismatch", "product payload manifest root", "Use an untampered product payload manifest from the candidate run.");
150
+ assertEqual(manifest.candidateRoot, candidateRoot, "candidate-root-mismatch", "product payload candidate root", "Use the product payload sealed for this exact candidate.");
151
+ assertEqual(manifest.buildSummaryRoot, buildSummaryRoot, "build-summary-root-mismatch", "product payload build summary root", "Use the payload produced from the exact candidate summary.");
152
+ assertEqual(manifest.source?.tree, passport.source?.treeHash, "source-tree-mismatch", "product payload source tree", "Use payload bytes produced from the exact candidate tree.");
153
+ assertEqual(manifest.runtimeSha, runtimeSha, "runtime-mismatch", "product payload candidate runtime", "Use payload evidence created by the candidate runtime.");
154
+ const artifact = byName.get(manifest.artifactName);
155
+ if (!artifact) fail("artifact-missing", `product payload artifact is missing: ${manifest.artifactName}`, "Restore the exact product payload artifact.");
156
+ const manifestName = String(manifest.manifestPath || "product-payload-manifest.json");
157
+ const payloadFiles = artifact.files.filter((file) => file.path !== manifestName);
158
+ const expectedFiles = (manifest.files || []).map((file) => ({
159
+ path: String(file.path || ""),
160
+ size: Number(file.size),
161
+ sha256: String(file.sha256 || ""),
162
+ })).sort((left, right) => left.path.localeCompare(right.path));
163
+ if (JSON.stringify(payloadFiles) !== JSON.stringify(expectedFiles)) {
164
+ fail("artifact-manifest-mismatch", `product payload bytes differ from their manifest: ${manifest.artifactName}`, "Do not publish; preserve the manifest and uploaded product bytes.");
165
+ }
166
+ }
167
+ }
168
+
169
+ function validateCandidateProvenance({
170
+ candidateRepository,
171
+ targetRepository,
172
+ expectedRunId,
173
+ expectedWorkflowFile,
174
+ expectedWorkflowName,
175
+ targetRef,
176
+ run,
177
+ workflow,
178
+ pullRequest,
179
+ ancestry,
180
+ }) {
181
+ const repository = required(candidateRepository, "candidateRepository");
182
+ assertEqual(repository, required(targetRepository, "targetRepository"), "repository-mismatch", "candidate repository", "Dispatch recovery in the repository that created the candidate.");
183
+ assertEqual(String(run?.id || ""), required(expectedRunId, "expectedRunId"), "run-mismatch", "candidate run ID", "Select the exact successful candidate run.");
184
+ assertEqual(run?.repository, repository, "repository-mismatch", "run repository", "Use a candidate run from the target repository.");
185
+ assertEqual(run?.headRepository, repository, "provenance-insufficient", "run head repository", "Fork candidates are not recoverable; create a same-repository candidate.");
186
+ if (run?.status !== "completed" || run?.conclusion !== "success" || run?.event !== "pull_request") {
187
+ fail("untrusted-build-run", "candidate run must be a successful completed pull_request build", "Choose a successful allowed Build workflow run.");
188
+ }
189
+ const workflowFile = required(expectedWorkflowFile, "expectedWorkflowFile").replace(/^\.github\/workflows\//, "");
190
+ const actualWorkflowPath = String(workflow?.path || run?.path || "").split("@")[0].replace(/^\.github\/workflows\//, "");
191
+ assertEqual(actualWorkflowPath, workflowFile, "workflow-mismatch", "candidate workflow file", "Select a run from the documented trusted Build workflow.");
192
+ assertEqual(String(workflow?.name || run?.name || ""), required(expectedWorkflowName, "expectedWorkflowName"), "workflow-mismatch", "candidate workflow name", "Select a run from the documented trusted Build workflow.");
193
+ if (workflow?.state && workflow.state !== "active") fail("untrusted-build-run", `candidate workflow is not active: ${workflow.state}`, "Restore or select an allowed active Build workflow.");
194
+ if (!TRUSTED_ASSOCIATIONS.has(String(pullRequest?.authorAssociation || ""))) fail("permission-evidence-insufficient", "candidate PR author lacks trusted repository association evidence", "Have a repository member create or approve a same-repository candidate PR.");
195
+ assertEqual(pullRequest?.headRepository, repository, "provenance-insufficient", "candidate PR head repository", "Fork candidates are outside the recovery permission boundary.");
196
+ if (!pullRequest?.merged) fail("pr-identity-invalid", "candidate PR is not merged", "Use a merged channel candidate PR.");
197
+ assertEqual(normalizedRef(pullRequest?.baseRef), normalizedRef(targetRef), "target-mismatch", "candidate PR target ref", "Use the original release channel target.");
198
+ const boundByNumber = Array.isArray(run?.pullRequestNumbers) && run.pullRequestNumbers.includes(Number(pullRequest?.number));
199
+ const boundByHead = run?.headSha === pullRequest?.headSha && normalizedRef(run?.headBranch) === normalizedRef(pullRequest?.headRef);
200
+ if (!boundByNumber && !boundByHead) fail("pr-identity-invalid", "candidate run is not bound to the merged candidate PR", "Select the exact PR-stage Build run.");
201
+ if (!ancestry?.mergeIsAncestor && ancestry?.status !== "identical") fail("ancestry-invalid", "candidate merge is not an ancestor of the promotion SHA", "Promote a descendant of the verified merged candidate identity.");
202
+ return { repository, workflowFile };
203
+ }
204
+
205
+ function validateCandidateIdentity({
206
+ repository,
207
+ channel,
208
+ targetSha,
209
+ targetTree,
210
+ expectedSourceTree,
211
+ expectedRuntimeSha,
212
+ currentToolingSha,
213
+ passport,
214
+ buildSummary,
215
+ }) {
216
+ const sha = exactSha(targetSha, "targetSha");
217
+ const tree = exactSha(targetTree, "targetTree");
218
+ const runtimeSha = exactSha(expectedRuntimeSha, "expectedRuntimeSha");
219
+ const toolingSha = exactSha(currentToolingSha, "currentToolingSha");
220
+ const validation = validateReleaseCandidatePassport({ passport, repository, targetChannel: channel, buildSummary });
221
+ if (!validation.ok) fail("passport-invalid", `Release Candidate Passport validation failed: ${validation.errors.join("; ")}`, "Preserve the candidate evidence and explicitly create a new candidate after fixing the producer.");
222
+ const expectedPassportHash = sha256Json({
223
+ repository: passport.repository,
224
+ target: passport.target,
225
+ source: passport.source,
226
+ platformMatrix: passport.platformMatrix,
227
+ buildchain: passport.buildchain,
228
+ ...(passport.gateProfileEvidence ? { gateProfileEvidence: passport.gateProfileEvidence } : {}),
229
+ ...(passport.familyEvidence ? { familyEvidence: passport.familyEvidence } : {}),
230
+ ...(passport.controllerReceipts ? { controllerReceipts: passport.controllerReceipts } : {}),
231
+ });
232
+ if (passport.candidateHash !== expectedPassportHash) fail("candidate-root-mismatch", "Release Candidate Passport candidate hash does not match its content", "Preserve the run evidence and explicitly create a new candidate after fixing the producer.");
233
+ assertEqual(passport.source?.treeHash, tree, "source-tree-mismatch", "promotion Git tree", "Select a promotion SHA with the exact candidate Git tree or create a new candidate explicitly.");
234
+ if (expectedSourceTree) assertEqual(passport.source?.treeHash, exactSha(expectedSourceTree, "expectedSourceTree"), "source-tree-mismatch", "expected source tree", "Correct the expected tree or create a new candidate.");
235
+ assertEqual(passport.buildchain?.sha, runtimeSha, "runtime-mismatch", "candidate Buildchain runtime SHA", "Run recovery with the exact trusted Buildchain runtime recorded by the candidate.");
236
+ return { sha, tree, runtimeSha, toolingSha };
237
+ }
238
+
239
+ function validateRecoveryTransaction({ existingTransaction, expectedTransactionId, repository, passport, sha, targetRef, candidateRoot }) {
240
+ const actualTransactionId = String(existingTransaction?.id || "");
241
+ if (expectedTransactionId && !actualTransactionId) fail("transaction-identity-conflict", `expected transaction ${expectedTransactionId} does not exist`, "Remove the stale transaction identity only if no durable transaction was ever sealed; otherwise preserve evidence and enter repair_required.");
242
+ if (expectedTransactionId && expectedTransactionId !== actualTransactionId) fail("transaction-identity-conflict", `existing transaction ${actualTransactionId} conflicts with expected ${expectedTransactionId}`, "Enter repair_required and inspect the durable transaction before any retry.");
243
+ if (existingTransaction) {
244
+ const expectedIdentity = releaseTransactionId({ repository, version: passport.target.version, sourceSha: sha, targetRef: normalizedRef(targetRef) });
245
+ assertEqual(existingTransaction.id, expectedIdentity, "transaction-identity-conflict", "durable transaction identity", "Enter repair_required; the durable transaction does not belong to this exact publication target.");
246
+ assertEqual(existingTransaction.repository, repository, "transaction-identity-conflict", "durable transaction repository", "Enter repair_required; never cross repository transaction boundaries.");
247
+ assertEqual(normalizedRef(existingTransaction.target_ref), normalizedRef(targetRef), "transaction-identity-conflict", "durable transaction target ref", "Enter repair_required; never retarget a sealed transaction.");
248
+ assertEqual(existingTransaction.source_sha, sha, "transaction-identity-conflict", "durable transaction source SHA", "Resume with the transaction's exact promotion SHA or enter repair_required.");
249
+ assertEqual(existingTransaction.version, passport.target.version, "transaction-identity-conflict", "durable transaction version", "Enter repair_required; never change a sealed publication version.");
250
+ assertEqual(existingTransaction.channel, passport.target.channel, "transaction-identity-conflict", "durable transaction channel", "Enter repair_required; never move a transaction between channels.");
251
+ }
252
+ if (existingTransaction?.candidateRoot && existingTransaction.candidateRoot !== candidateRoot) fail("transaction-identity-conflict", "existing transaction is sealed to a different candidate root", "Enter repair_required; never reuse the conflicting transaction.");
253
+ return actualTransactionId;
254
+ }
255
+
256
+ export function verifyReleaseCandidateRecovery({
257
+ candidateRepository,
258
+ targetRepository,
259
+ expectedRunId,
260
+ expectedWorkflowFile,
261
+ expectedWorkflowName,
262
+ channel,
263
+ targetRef,
264
+ targetSha,
265
+ targetTree,
266
+ expectedSourceTree = "",
267
+ expectedCandidateRoot = "",
268
+ expectedRuntimeSha,
269
+ expectedTransactionId = "",
270
+ existingTransaction = undefined,
271
+ run,
272
+ workflow,
273
+ pullRequest,
274
+ ancestry,
275
+ passport,
276
+ buildSummary,
277
+ controllerReceipts = [],
278
+ platformManifests = [],
279
+ productPayloadManifests = [],
280
+ artifacts = [],
281
+ currentToolingSha,
282
+ recoveryRunId = "",
283
+ createdAt = new Date().toISOString(),
284
+ } = {}) {
285
+ const { repository, workflowFile } = validateCandidateProvenance({
286
+ candidateRepository, targetRepository, expectedRunId, expectedWorkflowFile,
287
+ expectedWorkflowName, targetRef, run, workflow, pullRequest, ancestry,
288
+ });
289
+ const { sha, tree, runtimeSha, toolingSha } = validateCandidateIdentity({
290
+ repository, channel, targetSha, targetTree, expectedSourceTree,
291
+ expectedRuntimeSha, currentToolingSha, passport, buildSummary,
292
+ });
293
+
294
+ const recoveredArtifacts = artifacts.map(normalizeArtifact).sort((left, right) => left.name.localeCompare(right.name));
295
+ if (recoveredArtifacts.length === 0) fail("artifact-missing", "candidate artifact set is empty", "Select a retained run with the complete candidate artifact set.");
296
+ if (new Set(recoveredArtifacts.map((artifact) => artifact.name)).size !== recoveredArtifacts.length) {
297
+ fail("artifact-count-mismatch", "candidate artifact names are not unique", "Inspect the candidate run and remove ambiguity by creating a new candidate.");
298
+ }
299
+ validatePlatformPayloads(passport, recoveredArtifacts, platformManifests);
300
+
301
+ const references = new Map((passport.controllerReceipts || []).map((reference) => [reference.controllerId, reference]));
302
+ for (const receipt of controllerReceipts) {
303
+ const receiptValidation = validateControllerReceipt(receipt, {
304
+ expectedSourceSha: passport.source?.headSha,
305
+ expectedRuntimeSha: runtimeSha,
306
+ });
307
+ if (!receiptValidation.ok || !receiptValidation.qualifying) {
308
+ fail("controller-receipt-invalid", `controller receipt is not qualifying: ${receiptValidation.issues.join("; ")}`, "Use a successful candidate with complete controller evidence.");
309
+ }
310
+ const actualReference = createControllerReceiptReference(receipt);
311
+ const expectedReference = references.get(actualReference.controllerId);
312
+ if (!expectedReference || sha256Json(actualReference) !== sha256Json(expectedReference)) {
313
+ fail("controller-receipt-mismatch", `controller receipt does not match Passport reference: ${actualReference.controllerId}`, "Use the exact controller receipt artifact uploaded by the candidate run.");
314
+ }
315
+ }
316
+ if (references.size !== controllerReceipts.length) {
317
+ fail("controller-receipt-missing", "not every Passport controller receipt was recovered", "Restore every referenced controller receipt artifact before recovery.");
318
+ }
319
+
320
+ const candidateRoot = `sha256:${passport.candidateHash}`;
321
+ if (expectedCandidateRoot) assertEqual(candidateRoot, contentRoot(expectedCandidateRoot, "expectedCandidateRoot"), "candidate-root-mismatch", "candidate root", "Use the exact candidate root recorded by the original run.");
322
+ const artifactInventory = recoveredArtifacts.map(({ name, size, digest, files }) => ({ name, size, digest, files }));
323
+ const artifactRoot = `sha256:${sha256Json(artifactInventory)}`;
324
+ const artifactArchiveRoot = `sha256:${sha256Json(recoveredArtifacts.map(({ name, size, digest }) => ({ name, size, digest })))}`;
325
+ validateProductPayloads({
326
+ passport,
327
+ artifacts: recoveredArtifacts,
328
+ productPayloadManifests,
329
+ candidateRoot,
330
+ buildSummaryRoot: `sha256:${passport.diagnostics.buildSummaryHash}`,
331
+ runtimeSha,
332
+ });
333
+
334
+ const actualTransactionId = validateRecoveryTransaction({
335
+ existingTransaction, expectedTransactionId, repository, passport, sha, targetRef, candidateRoot,
336
+ });
337
+
338
+ const receipt = {
339
+ schemaVersion: 1,
340
+ contract: RELEASE_CANDIDATE_RECOVERY_CONTRACT,
341
+ action: "reused",
342
+ createdAt,
343
+ repository,
344
+ recoveryRunId: String(recoveryRunId || ""),
345
+ originalCandidate: {
346
+ runId: String(run.id),
347
+ workflowFile,
348
+ workflowName: String(workflow?.name || run?.name || ""),
349
+ pullRequest: Number(pullRequest.number),
350
+ sourceSha: passport.source.headSha,
351
+ mergeSha: String(pullRequest.mergeSha || ""),
352
+ tree: passport.source.treeHash,
353
+ },
354
+ target: { channel: passport.target.channel, ref: normalizedRef(targetRef), sha, tree },
355
+ recovered: {
356
+ candidateRoot,
357
+ buildSummaryRoot: `sha256:${passport.diagnostics.buildSummaryHash}`,
358
+ artifactRoot,
359
+ artifactArchiveRoot,
360
+ artifactCount: recoveredArtifacts.length,
361
+ artifacts: recoveredArtifacts.map(({ name, size, digest }) => ({ name, size, digest })),
362
+ },
363
+ skippedBuildStages: ["install", "build", "verify", "platform-matrix"],
364
+ payloadBytes: "unchanged",
365
+ buildchainToolingSha: toolingSha,
366
+ transaction: {
367
+ identity: expectedTransactionId || actualTransactionId || "",
368
+ state: String(existingTransaction?.state || "absent"),
369
+ publicationState: String(existingTransaction?.publication_state || existingTransaction?.state || "absent"),
370
+ },
371
+ };
372
+ receipt.root = `sha256:${sha256Json(receipt)}`;
373
+ return { receipt, artifacts: recoveredArtifacts };
374
+ }
375
+
376
+ export function recoveryFailure(error) {
377
+ if (error instanceof ReleaseCandidateRecoveryError) {
378
+ return { ok: false, code: error.code, reason: error.message, nextAction: error.nextAction };
379
+ }
380
+ return {
381
+ ok: false,
382
+ code: "recovery-internal-error",
383
+ reason: String(error?.message || error),
384
+ nextAction: "Preserve the run logs and candidate evidence; do not rebuild automatically.",
385
+ };
386
+ }
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import crypto from "node:crypto";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { readRendererManifest } from "./auditable-demo-renditions.mjs";
8
+
9
+ const DIGEST = /^sha256:[0-9a-f]{64}$/u;
10
+ const MAX_METADATA_MEMBER_BYTES = 8 * 1024 * 1024;
11
+ const MAX_LONG_FORM_MANIFEST_BYTES = 32 * 1024 * 1024;
12
+ const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
13
+ const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
14
+ const DIGEST_BUFFER_BYTES = 64 * 1024;
15
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
16
+
17
+ function fail(message) {
18
+ throw new Error(`auditable demo platform: ${message}`);
19
+ }
20
+
21
+ function requireValue(condition, message) {
22
+ if (!condition) fail(message);
23
+ }
24
+
25
+ function rootBytes(value) {
26
+ return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
27
+ }
28
+
29
+ function boundedRegular(file, label, maximum) {
30
+ const metadata = fs.lstatSync(file);
31
+ requireValue(metadata.isFile() && !metadata.isSymbolicLink() && metadata.size <= maximum, `${label} must be a bounded regular file`);
32
+ return metadata;
33
+ }
34
+
35
+ function readRegular(file, label, maximum = MAX_METADATA_MEMBER_BYTES) {
36
+ boundedRegular(file, label, maximum);
37
+ return fs.readFileSync(file);
38
+ }
39
+
40
+ function digestRegular(file, label, maximum) {
41
+ const expected = boundedRegular(file, label, maximum);
42
+ const descriptor = fs.openSync(file, "r");
43
+ const hash = crypto.createHash("sha256");
44
+ const buffer = Buffer.allocUnsafe(DIGEST_BUFFER_BYTES);
45
+ let bytes = 0;
46
+ try {
47
+ while (true) {
48
+ const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
49
+ if (count === 0) break;
50
+ bytes += count;
51
+ requireValue(bytes <= maximum, `${label} must be a bounded regular file`);
52
+ hash.update(buffer.subarray(0, count));
53
+ }
54
+ } finally {
55
+ fs.closeSync(descriptor);
56
+ }
57
+ requireValue(bytes === expected.size, `${label} changed while it was verified`);
58
+ return { bytes, root: `sha256:${hash.digest("hex")}` };
59
+ }
60
+
61
+ function decodeUtf8(bytes, label) {
62
+ try {
63
+ return UTF8.decode(bytes);
64
+ } catch {
65
+ fail(`${label} must be valid UTF-8`);
66
+ }
67
+ }
68
+
69
+ function inside(root, relative, label) {
70
+ requireValue(typeof relative === "string" && relative && !path.isAbsolute(relative), `${label} must be relative`);
71
+ const resolvedRoot = path.resolve(root);
72
+ const resolved = path.resolve(resolvedRoot, relative);
73
+ requireValue(resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`), `${label} escapes its root`);
74
+ return resolved;
75
+ }
76
+
77
+ function listBundleFiles(root, prefix = "") {
78
+ const entries = fs.readdirSync(path.join(root, prefix), { withFileTypes: true })
79
+ .sort((left, right) => left.name.localeCompare(right.name, "en"));
80
+ const files = [];
81
+ for (const entry of entries) {
82
+ requireValue(!entry.isSymbolicLink(), `bundle member must not be a symbolic link: ${path.join(prefix, entry.name)}`);
83
+ const relative = path.join(prefix, entry.name);
84
+ if (entry.isDirectory()) files.push(...listBundleFiles(root, relative));
85
+ else {
86
+ requireValue(entry.isFile(), `bundle member must be a regular file: ${relative}`);
87
+ files.push(relative.split(path.sep).join("/"));
88
+ }
89
+ }
90
+ return files;
91
+ }
92
+
93
+ const RENDERER_MANIFEST_HELPERS = {
94
+ decodeUtf8,
95
+ digestPattern: DIGEST,
96
+ invariant: requireValue,
97
+ maxBytes: MAX_TERMINAL_CAPTURE_BYTES,
98
+ maxEvents: MAX_TERMINAL_CAPTURE_EVENTS,
99
+ readRegular,
100
+ };
101
+
102
+ export function verifyBundleChecksums(root, label, options = {}) {
103
+ const resolved = path.resolve(root);
104
+ const checksums = readRegular(path.join(resolved, "checksums.sha256"), `${label} checksums`);
105
+ const declared = new Set();
106
+ const members = [];
107
+ let bundleBytes = 0;
108
+ for (const row of checksums.toString("utf8").split("\n").filter(Boolean)) {
109
+ const match = /^([0-9a-f]{64}) ([^\0\r\n]+)$/u.exec(row);
110
+ requireValue(match, `${label} checksum row is invalid`);
111
+ const target = inside(resolved, match[2], `${label} checksum member`);
112
+ requireValue(!declared.has(match[2]), `${label} checksum member is repeated`);
113
+ declared.add(match[2]);
114
+ const metadataMember = match[2].endsWith(".json") || match[2].endsWith(".sha256");
115
+ const maximum = options.allowLongFormRendererManifest && match[2] === "manifest.json"
116
+ ? MAX_LONG_FORM_MANIFEST_BYTES
117
+ : metadataMember
118
+ ? MAX_METADATA_MEMBER_BYTES
119
+ : (options.maximumMemberBytes || MAX_METADATA_MEMBER_BYTES);
120
+ bundleBytes += boundedRegular(target, `${label} member`, maximum).size;
121
+ requireValue(bundleBytes <= options.maximumBundleBytes, `${label} exceeds its aggregate byte budget`);
122
+ members.push({ expectedRoot: `sha256:${match[1]}`, maximum, name: match[2], target });
123
+ }
124
+ for (const member of members) {
125
+ const verified = options.allowLongFormRendererManifest && member.name === "manifest.json"
126
+ ? (() => {
127
+ const value = readRendererManifest(member.target, RENDERER_MANIFEST_HELPERS).bytes;
128
+ return { bytes: value.length, root: rootBytes(value) };
129
+ })()
130
+ : digestRegular(member.target, `${label} member`, member.maximum);
131
+ requireValue(verified.root === member.expectedRoot, `${label} checksum mismatch: ${member.name}`);
132
+ if (member.name === "manifest.json" && options.rendererManifestRoot) {
133
+ requireValue(verified.root === options.rendererManifestRoot, `${label} renderer manifest root mismatch`);
134
+ }
135
+ }
136
+ const actual = listBundleFiles(resolved).filter((name) => name !== "checksums.sha256");
137
+ requireValue(JSON.stringify([...declared].sort()) === JSON.stringify(actual), `${label} checksum member set is not exact`);
138
+ return rootBytes(checksums);
139
+ }
140
+
141
+ export function copyVerifiedRegular(source, destination, label, maximum) {
142
+ const sourceDigest = digestRegular(source, label, maximum);
143
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
144
+ fs.copyFileSync(source, destination);
145
+ const destinationDigest = digestRegular(destination, `${label} copy`, maximum);
146
+ requireValue(destinationDigest.bytes === sourceDigest.bytes && destinationDigest.root === sourceDigest.root, `${label} copy differs from its verified source`);
147
+ return { path: path.basename(destination), bytes: destinationDigest.bytes, root: destinationDigest.root };
148
+ }