@kungfu-tech/buildchain 3.0.2-alpha.7 → 3.0.2-alpha.9

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 (54) hide show
  1. package/README.md +3 -2
  2. package/contracts/auditable-demo-media-profiles-v1.json +168 -0
  3. package/contracts/evidence/auditable-demo-web-delivery-v1.json +103 -0
  4. package/contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt +2 -0
  5. package/contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json +16 -0
  6. package/contracts/fixtures/auditable-demo-web-delivery-v1/scene.json +12 -0
  7. package/dist/site/buildchain-contract.json +44 -26
  8. package/dist/site/buildchain-site.json +42 -32
  9. package/dist/site/capability-registry.json +3 -3
  10. package/dist/site/controller-registry.json +11 -3
  11. package/dist/site/kfd-claims.json +74 -8
  12. package/dist/site/kfd-upstream-aggregate.json +1 -1
  13. package/dist/site/manual-registry.json +7 -7
  14. package/dist/site/node-api-registry.json +44 -5
  15. package/dist/site/page-registry.json +30 -20
  16. package/dist/site/public-surface-audit.json +38 -10
  17. package/dist/site/publication-authority-registry.json +26 -1
  18. package/dist/site/publication-registry.json +4 -4
  19. package/dist/site/release-provenance.json +3 -0
  20. package/dist/site/site-manifest.json +11 -11
  21. package/dist/site/workflow-registry.json +41 -7
  22. package/docs/MAP.md +2 -0
  23. package/docs/auditable-demo.md +55 -3
  24. package/docs/dev-alpha-candidate-patrol.md +9 -0
  25. package/docs/github-artifact-attestation.md +1 -1
  26. package/docs/release-governance.md +32 -0
  27. package/docs/reusable-build-surface.md +73 -58
  28. package/docs/runtime-train-validation.md +21 -0
  29. package/docs/versioning.md +1 -0
  30. package/package.json +5 -1
  31. package/packages/core/artifact-signing-result.js +228 -0
  32. package/packages/core/artifact-signing.js +412 -0
  33. package/packages/core/buildchain-config.js +58 -0
  34. package/packages/core/buildchain-contract.js +8 -0
  35. package/packages/core/buildchain-publication-authority.js +1 -0
  36. package/packages/core/detached-artifact-signature.js +121 -0
  37. package/packages/core/github-governance-authority.js +6 -0
  38. package/packages/core/index.js +27 -0
  39. package/scripts/auditable-demo.mjs +491 -22
  40. package/scripts/buildchain-channel-router.mjs +8 -2
  41. package/scripts/check-inventory.mjs +5 -0
  42. package/scripts/dev-alpha-candidate-patrol.mjs +18 -0
  43. package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
  44. package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
  45. package/scripts/generate-site-bundle.mjs +3 -0
  46. package/scripts/import-artifact-signing-results.mjs +76 -0
  47. package/scripts/inspect-artifact-signing-requests.mjs +101 -0
  48. package/scripts/materialize-artifact-signing-request.mjs +66 -0
  49. package/scripts/merge-artifact-signing-results.mjs +76 -0
  50. package/scripts/release-line-policy.mjs +27 -0
  51. package/scripts/runtime-ref-core.mjs +10 -6
  52. package/scripts/seal-artifact-signing-requests.mjs +368 -0
  53. package/scripts/sign-detached-artifact-requests.mjs +237 -0
  54. package/scripts/verify-artifact-signing-results.mjs +99 -0
@@ -71,6 +71,11 @@ const requiredPaths = [
71
71
  "docs/github-artifact-attestation.md",
72
72
  "docs/shifu-gate-profiles.md",
73
73
  "docs/auditable-demo.md",
74
+ "contracts/auditable-demo-media-profiles-v1.json",
75
+ "contracts/evidence/auditable-demo-web-delivery-v1.json",
76
+ "contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt",
77
+ "contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json",
78
+ "contracts/fixtures/auditable-demo-web-delivery-v1/scene.json",
74
79
  "docs/release-propagation.md",
75
80
  "docs/site-bundle-contract.md",
76
81
  "docs/toolkit-observability.md",
@@ -54,6 +54,17 @@ function integer(value, fallback) {
54
54
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
55
55
  }
56
56
 
57
+ function pullRequestBodyPrefix(value) {
58
+ const normalized = String(value ?? "").trim();
59
+ if (normalized.length > 32768)
60
+ throw new Error("pullRequestBodyPrefix exceeds 32768 characters");
61
+ if (normalized.includes(STATE_MARKER_START))
62
+ throw new Error(
63
+ "pullRequestBodyPrefix must not contain the managed candidate state marker",
64
+ );
65
+ return normalized;
66
+ }
67
+
57
68
  function canonical(value) {
58
69
  if (Array.isArray(value)) return value.map(canonical);
59
70
  if (value && typeof value === "object") {
@@ -210,6 +221,10 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
210
221
  process.env.BUILDCHAIN_CHANNEL_PATROL_MAX_AGE_SECONDS,
211
222
  7 * 24 * 60 * 60,
212
223
  ),
224
+ pullRequestBodyPrefix: pullRequestBodyPrefix(
225
+ options.pullRequestBodyPrefix ??
226
+ process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX,
227
+ ),
213
228
  createPullRequest,
214
229
  settlementAuthorized: bool(
215
230
  options.settlementAuthorized ??
@@ -405,6 +420,9 @@ function pullRequestBody({
405
420
  state,
406
421
  }) {
407
422
  return [
423
+ ...(options.pullRequestBodyPrefix
424
+ ? [options.pullRequestBodyPrefix, ""]
425
+ : []),
408
426
  LEGACY_BODY_MARKER,
409
427
  "",
410
428
  `- Source branch: \`${options.sourceBranch}\``,
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
6
+
7
+ function required(value, label) {
8
+ const normalized = String(value || "").trim();
9
+ if (!normalized) throw new Error(`${label} is required`);
10
+ return normalized;
11
+ }
12
+
13
+ function repository(value, label) {
14
+ const normalized = required(value, label);
15
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(normalized)) throw new Error(`${label} must be owner/repository`);
16
+ return normalized;
17
+ }
18
+
19
+ const TRANSIENT_GITHUB_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
20
+
21
+ function retryDelayMs(attempt) {
22
+ return Math.min(1_000 * (2 ** (attempt - 1)), 10_000);
23
+ }
24
+
25
+ export async function githubRequest(url, {
26
+ token,
27
+ method = "GET",
28
+ body,
29
+ fetchImpl = fetch,
30
+ delayImpl = delay,
31
+ maxAttempts = 5,
32
+ warnImpl = console.warn,
33
+ } = {}) {
34
+ const methodName = String(method).toUpperCase();
35
+ const retrySafe = methodName === "GET";
36
+ const requestedAttempts = Number(maxAttempts);
37
+ const attemptLimit = retrySafe && Number.isSafeInteger(requestedAttempts) && requestedAttempts > 0
38
+ ? requestedAttempts
39
+ : 1;
40
+ for (let attempt = 1; attempt <= attemptLimit; attempt += 1) {
41
+ let response;
42
+ try {
43
+ response = await fetchImpl(`https://api.github.com${url}`, {
44
+ method: methodName,
45
+ headers: {
46
+ Accept: "application/vnd.github+json",
47
+ Authorization: `Bearer ${token}`,
48
+ "X-GitHub-Api-Version": "2022-11-28",
49
+ "User-Agent": "kungfu-buildchain-artifact-signing",
50
+ ...(body ? { "Content-Type": "application/json" } : {}),
51
+ },
52
+ ...(body ? { body: JSON.stringify(body) } : {}),
53
+ });
54
+ } catch (error) {
55
+ if (retrySafe && attempt < attemptLimit) {
56
+ warnImpl(`Buildchain signing authority: GitHub API GET transport failure; retry ${attempt + 1}/${attemptLimit}`);
57
+ await delayImpl(retryDelayMs(attempt));
58
+ continue;
59
+ }
60
+ throw error;
61
+ }
62
+ if (!response.ok) {
63
+ const text = await response.text();
64
+ if (retrySafe && TRANSIENT_GITHUB_STATUSES.has(response.status) && attempt < attemptLimit) {
65
+ warnImpl(`Buildchain signing authority: GitHub API GET returned ${response.status}; retry ${attempt + 1}/${attemptLimit}`);
66
+ await delayImpl(retryDelayMs(attempt));
67
+ continue;
68
+ }
69
+ throw new Error(`GitHub API ${methodName} ${url} failed (${response.status}): ${text.slice(0, 500)}`);
70
+ }
71
+ if (response.status === 204) return {};
72
+ try {
73
+ return await response.json();
74
+ } catch (error) {
75
+ if (retrySafe && attempt < attemptLimit) {
76
+ warnImpl(`Buildchain signing authority: GitHub API GET response failed to decode; retry ${attempt + 1}/${attemptLimit}`);
77
+ await delayImpl(retryDelayMs(attempt));
78
+ continue;
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+ throw new Error("GitHub API retry loop exhausted");
84
+ }
85
+
86
+ export async function dispatchArtifactSigningAuthority({
87
+ token = process.env.BUILDCHAIN_AUTHORITY_DISPATCH_TOKEN,
88
+ authorityRepository = process.env.BUILDCHAIN_AUTHORITY_REPOSITORY || "kungfu-systems/buildchain",
89
+ authorityRef = process.env.BUILDCHAIN_AUTHORITY_REF,
90
+ sourceRepository = process.env.GITHUB_REPOSITORY,
91
+ sourceRunId = process.env.GITHUB_RUN_ID,
92
+ sourceRunAttempt = process.env.GITHUB_RUN_ATTEMPT || "1",
93
+ requestArtifact = process.env.BUILDCHAIN_SIGNING_REQUEST_ARTIFACT,
94
+ runtimeSha = process.env.BUILDCHAIN_RUNTIME_SHA,
95
+ resultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT,
96
+ timeoutSeconds = process.env.BUILDCHAIN_SIGNING_TIMEOUT_SECONDS || "7200",
97
+ } = {}) {
98
+ const authToken = required(token, "Buildchain authority dispatch token");
99
+ const authorityRepo = repository(authorityRepository, "authority repository");
100
+ const sourceRepo = repository(sourceRepository, "source repository");
101
+ const ref = required(authorityRef, "authority ref");
102
+ const runtime = required(runtimeSha, "Buildchain runtime SHA");
103
+ if (!/^[0-9a-f]{40}$/u.test(runtime)) throw new Error("Buildchain runtime SHA must be exact");
104
+ const correlationId = `${required(sourceRunId, "source run ID")}-${required(sourceRunAttempt, "source run attempt")}-${runtime.slice(0, 12)}-${required(requestArtifact, "request artifact").replace(/[^A-Za-z0-9._-]+/gu, "-")}`;
105
+ const resultName = required(resultArtifact, "result artifact");
106
+ const workflow = "artifact-signing-authority.yml";
107
+ const startedAt = Date.now() - 30_000;
108
+ await githubRequest(`/repos/${authorityRepo}/actions/workflows/${workflow}/dispatches`, {
109
+ token: authToken,
110
+ method: "POST",
111
+ body: {
112
+ ref,
113
+ inputs: {
114
+ "source-repository": sourceRepo,
115
+ "source-run-id": String(sourceRunId),
116
+ "request-artifact-pattern": required(requestArtifact, "request artifact"),
117
+ "result-artifact-name": resultName,
118
+ "correlation-id": correlationId,
119
+ "expected-runtime-sha": runtime,
120
+ },
121
+ },
122
+ });
123
+ const deadline = Date.now() + Number(timeoutSeconds) * 1000;
124
+ let run;
125
+ while (Date.now() < deadline) {
126
+ const response = await githubRequest(`/repos/${authorityRepo}/actions/workflows/${workflow}/runs?event=workflow_dispatch&per_page=50`, { token: authToken });
127
+ run = (response.workflow_runs || []).find((entry) =>
128
+ new Date(entry.created_at).getTime() >= startedAt &&
129
+ String(entry.display_title || "").includes(correlationId),
130
+ );
131
+ if (run?.status === "completed") break;
132
+ await delay(10_000);
133
+ }
134
+ if (!run || run.status !== "completed") throw new Error("timed out waiting for Buildchain signing authority");
135
+ if (run.conclusion !== "success") throw new Error(`Buildchain signing authority failed: ${run.html_url}`);
136
+ writeGitHubOutputs({
137
+ "authority-run-id": String(run.id),
138
+ "authority-run-url": run.html_url,
139
+ "result-artifact": resultName,
140
+ "correlation-id": correlationId,
141
+ });
142
+ return { runId: run.id, runUrl: run.html_url, resultArtifact: resultName, correlationId };
143
+ }
144
+
145
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
146
+ try {
147
+ await dispatchArtifactSigningAuthority();
148
+ } catch (error) {
149
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
150
+ process.exitCode = 1;
151
+ }
152
+ }
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { createArtifactSigningReceipt, validateArtifactSigningRequest } from "../packages/core/artifact-signing.js";
8
+ import { artifactSigningEvidenceDigest, createArtifactSigningResult } from "../packages/core/artifact-signing-result.js";
9
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
10
+
11
+ function required(value, label) {
12
+ const normalized = String(value || "").trim();
13
+ if (!normalized) throw new Error(`${label} is required`);
14
+ return normalized;
15
+ }
16
+
17
+ function sha256File(filePath) {
18
+ return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
19
+ }
20
+
21
+ function resolveBelow(root, relative, label) {
22
+ const target = path.resolve(root, required(relative, label));
23
+ const rel = path.relative(path.resolve(root), target);
24
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`${label} escapes its root`);
25
+ return target;
26
+ }
27
+
28
+ export function finalizeNativeArtifactSigningResult({
29
+ requestRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT,
30
+ requestPath = process.env.BUILDCHAIN_SIGNING_REQUEST_PATH,
31
+ signedPayload = process.env.BUILDCHAIN_SIGNED_PAYLOAD,
32
+ evidencePath = process.env.BUILDCHAIN_SIGNING_EVIDENCE,
33
+ outputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_ROOT,
34
+ checks = process.env.BUILDCHAIN_SIGNING_VERIFICATION_CHECKS,
35
+ } = {}) {
36
+ const requests = path.resolve(required(requestRoot, "signing request root"));
37
+ const request = JSON.parse(fs.readFileSync(resolveBelow(requests, requestPath, "request path"), "utf8"));
38
+ const requestCheck = validateArtifactSigningRequest(request);
39
+ if (!requestCheck.ok) throw new Error(`invalid signing request: ${requestCheck.issues.join(", ")}`);
40
+ if (request.signature.semantics !== "native-platform-signature") {
41
+ throw new Error("native result finalizer rejects non-native signature profiles");
42
+ }
43
+ const payloadSource = path.resolve(required(signedPayload, "signed payload"));
44
+ const evidenceSource = path.resolve(required(evidencePath, "signing evidence"));
45
+ const resultDirectory = path.resolve(required(outputRoot, "signing result root"));
46
+ fs.mkdirSync(path.join(resultDirectory, "payload"), { recursive: true });
47
+ const payloadPath = path.join(resultDirectory, "payload", path.basename(payloadSource));
48
+ fs.copyFileSync(payloadSource, payloadPath, fs.constants.COPYFILE_EXCL);
49
+ const evidenceOutput = path.join(resultDirectory, "provider-evidence.json");
50
+ fs.copyFileSync(evidenceSource, evidenceOutput, fs.constants.COPYFILE_EXCL);
51
+ const evidenceDocument = JSON.parse(fs.readFileSync(evidenceOutput, "utf8"));
52
+ if (evidenceDocument.status !== "passed" || evidenceDocument.provider !== request.signature.provider) {
53
+ throw new Error("provider evidence does not prove the requested native signature");
54
+ }
55
+ const evidence = [{ kind: `${request.signature.profile}-verification`, path: "provider-evidence.json", digest: sha256File(evidenceOutput) }];
56
+ const payloadDigest = sha256File(payloadPath);
57
+ const receipt = createArtifactSigningReceipt({
58
+ request,
59
+ result: { artifactDigest: payloadDigest, evidenceDigest: artifactSigningEvidenceDigest(evidence) },
60
+ signatures: [{ kind: request.signature.profile, digest: evidence[0].digest }],
61
+ });
62
+ fs.writeFileSync(path.join(resultDirectory, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`);
63
+ const verificationChecks = String(checks || "").split(",").map((value) => value.trim()).filter(Boolean);
64
+ const result = createArtifactSigningResult({
65
+ request,
66
+ receipt,
67
+ payload: { path: `payload/${path.basename(payloadPath)}`, bytes: fs.statSync(payloadPath).size, digest: payloadDigest },
68
+ evidence,
69
+ verification: { status: "passed", provider: request.signature.provider, checks: verificationChecks },
70
+ });
71
+ fs.writeFileSync(path.join(resultDirectory, "result.json"), `${JSON.stringify(result, null, 2)}\n`);
72
+ const index = {
73
+ schemaVersion: 1,
74
+ contract: "kungfu-buildchain-artifact-signing-result-index/v1",
75
+ results: [{
76
+ id: request.artifact.id,
77
+ requestDigest: request.digest,
78
+ resultDigest: result.digest,
79
+ result: "result.json",
80
+ payload: result.artifact.path,
81
+ receipt: "receipt.json",
82
+ }],
83
+ };
84
+ fs.writeFileSync(path.join(resultDirectory, "index.json"), `${JSON.stringify(index, null, 2)}\n`);
85
+ writeGitHubOutputs({ "result-root": resultDirectory, "result-index": path.join(resultDirectory, "index.json") });
86
+ return index;
87
+ }
88
+
89
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
90
+ try {
91
+ finalizeNativeArtifactSigningResult();
92
+ } catch (error) {
93
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
94
+ process.exitCode = 1;
95
+ }
96
+ }
@@ -533,6 +533,9 @@ function nodeApiMeta(exportName) {
533
533
  "./github-governance-authority": { group: "governance-versioning", summary: "Fail-closed GitHub ownership, effective-policy, managed-zone admission, rollout-plan, and immutable receipt APIs." },
534
534
  "./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
535
535
  "./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
536
+ "./artifact-signing": { group: "reusable-build", summary: "Credential-free artifact signing declarations, source-bound requests, authority receipts, profile resolution, and fail-closed validation APIs." },
537
+ "./artifact-signing-result": { group: "release-passport-trust", summary: "Immutable signed-result, final-payload, receipt, evidence-root, and provider-verification binding APIs." },
538
+ "./detached-artifact-signature": { group: "release-passport-trust", summary: "Buildchain-authority Ed25519 detached signature and verification APIs for arbitrary binary artifacts." },
536
539
  "./anchored-version-material": { group: "reusable-build", summary: "Anchored/manual derived version material preflight, exact-tree binding, and digest evidence APIs." },
537
540
  "./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
538
541
  "./github-artifact-attestation": { group: "release-passport-trust", summary: "GitHub keyless artifact attestation policy, predicate, provider evidence, and fail-closed verification APIs." },
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { validateArtifactSigningRequest } from "../packages/core/artifact-signing.js";
7
+ import { verifyArtifactSigningResults } from "./verify-artifact-signing-results.mjs";
8
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
9
+
10
+ function required(value, label) {
11
+ const normalized = String(value || "").trim();
12
+ if (!normalized) throw new Error(`${label} is required`);
13
+ return normalized;
14
+ }
15
+
16
+ function resolveBelow(root, relative, label) {
17
+ const target = path.resolve(root, required(relative, label));
18
+ const rel = path.relative(path.resolve(root), target);
19
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`${label} must resolve below its root`);
20
+ return target;
21
+ }
22
+
23
+ export function importArtifactSigningResults({
24
+ workspace = process.env.GITHUB_WORKSPACE || process.cwd(),
25
+ cwd = process.env.BUILDCHAIN_SIGNING_CWD || ".",
26
+ requestRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT,
27
+ resultRoot = process.env.BUILDCHAIN_SIGNING_RESULT_ROOT,
28
+ evidenceRoot = process.env.BUILDCHAIN_SIGNING_IMPORTED_EVIDENCE_ROOT || ".buildchain/artifacts/signing",
29
+ } = {}) {
30
+ const sourceRoot = path.resolve(workspace, cwd);
31
+ const requests = path.resolve(required(requestRoot, "signing request root"));
32
+ const results = path.resolve(required(resultRoot, "signing result root"));
33
+ const verification = verifyArtifactSigningResults({ requestRoot: requests, resultRoot: results });
34
+ const requestIndex = JSON.parse(fs.readFileSync(path.join(requests, "index.json"), "utf8"));
35
+ const resultIndex = JSON.parse(fs.readFileSync(path.join(results, "index.json"), "utf8"));
36
+ const byId = new Map();
37
+ for (const entry of requestIndex.requests || []) {
38
+ const request = JSON.parse(fs.readFileSync(resolveBelow(requests, entry.path, "request path"), "utf8"));
39
+ const check = validateArtifactSigningRequest(request);
40
+ if (!check.ok || request.digest !== entry.digest) throw new Error(`invalid request during result import: ${entry.id}`);
41
+ byId.set(entry.id, request);
42
+ }
43
+ const imported = [];
44
+ for (const entry of resultIndex.results || []) {
45
+ const request = byId.get(entry.id);
46
+ if (!request) throw new Error(`result has no local sealed request: ${entry.id}`);
47
+ const resultPath = resolveBelow(results, entry.result, "result path");
48
+ const result = JSON.parse(fs.readFileSync(resultPath, "utf8"));
49
+ const payload = resolveBelow(path.dirname(resultPath), result.artifact.path, "signed payload");
50
+ const target = resolveBelow(sourceRoot, request.artifact.path, "consumer artifact path");
51
+ const mode = fs.statSync(target).mode;
52
+ fs.copyFileSync(payload, target);
53
+ fs.chmodSync(target, mode);
54
+ const destination = path.resolve(sourceRoot, evidenceRoot, entry.id);
55
+ const rel = path.relative(sourceRoot, destination);
56
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error("imported signing evidence escapes consumer source root");
57
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
58
+ fs.cpSync(path.dirname(resultPath), destination, { recursive: true, force: false, errorOnExist: true });
59
+ imported.push({ id: entry.id, path: request.artifact.path, resultDigest: result.digest });
60
+ }
61
+ writeGitHubOutputs({
62
+ "imported-count": String(imported.length),
63
+ "imported-json": JSON.stringify(imported),
64
+ "evidence-root": path.resolve(sourceRoot, evidenceRoot),
65
+ });
66
+ return { ...verification, imported };
67
+ }
68
+
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
70
+ try {
71
+ importArtifactSigningResults();
72
+ } catch (error) {
73
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
74
+ process.exitCode = 1;
75
+ }
76
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { validateArtifactSigningRequest } from "../packages/core/artifact-signing.js";
7
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
8
+
9
+ function required(value, label) {
10
+ const normalized = String(value || "").trim();
11
+ if (!normalized) throw new Error(`${label} is required`);
12
+ return normalized;
13
+ }
14
+
15
+ function safeId(value) {
16
+ const normalized = String(value || "")
17
+ .replace(/[^A-Za-z0-9._-]+/gu, "-")
18
+ .replace(/^-+|-+$/gu, "");
19
+ if (!normalized || normalized === "." || normalized === "..") {
20
+ throw new Error("unsafe signing request id");
21
+ }
22
+ return normalized;
23
+ }
24
+
25
+ function walk(root, name, output = []) {
26
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
27
+ const child = path.join(root, entry.name);
28
+ if (entry.isDirectory()) walk(child, name, output);
29
+ else if (entry.isFile() && entry.name === name) output.push(child);
30
+ }
31
+ return output;
32
+ }
33
+
34
+ export function inspectArtifactSigningRequests({
35
+ inputRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT,
36
+ expectedRepository = process.env.BUILDCHAIN_SIGNING_SOURCE_REPOSITORY,
37
+ expectedRuntimeSha = process.env.BUILDCHAIN_RUNTIME_SHA,
38
+ } = {}) {
39
+ const root = path.resolve(required(inputRoot, "signing request root"));
40
+ const indexes = walk(root, "index.json");
41
+ if (indexes.length === 0) throw new Error("no artifact signing request indexes found");
42
+ const seen = new Set();
43
+ const matrices = {
44
+ detached: [],
45
+ macos: [],
46
+ windows: [],
47
+ };
48
+ for (const indexPath of indexes) {
49
+ const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
50
+ if (index.contract !== "kungfu-buildchain-artifact-signing-request-index/v1") continue;
51
+ for (const entry of index.requests || []) {
52
+ const requestPath = path.resolve(path.dirname(indexPath), entry.path);
53
+ const relative = path.relative(root, requestPath);
54
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
55
+ throw new Error("signing request path escapes the authority intake root");
56
+ }
57
+ const request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
58
+ const check = validateArtifactSigningRequest(request);
59
+ if (!check.ok || request.digest !== entry.digest) {
60
+ throw new Error(`invalid artifact signing request: ${entry.id}`);
61
+ }
62
+ if (expectedRepository && request.source.repository !== expectedRepository) {
63
+ throw new Error("signing request source repository mismatch");
64
+ }
65
+ if (expectedRuntimeSha && request.runtime.sha !== expectedRuntimeSha) {
66
+ throw new Error("signing request runtime SHA mismatch");
67
+ }
68
+ const key = `${request.source.sha}:${request.artifact.id}:${request.artifact.platform}`;
69
+ if (seen.has(key)) throw new Error(`duplicate artifact signing request: ${key}`);
70
+ seen.add(key);
71
+ const item = {
72
+ id: request.artifact.id,
73
+ slug: safeId(request.artifact.id),
74
+ request: path.relative(root, requestPath).split(path.sep).join("/"),
75
+ directory: path.relative(root, path.dirname(requestPath)).split(path.sep).join("/"),
76
+ indexRoot: path.relative(root, path.dirname(indexPath)).split(path.sep).join("/") || ".",
77
+ };
78
+ if (request.signature.profile === "detached-signature-v1") matrices.detached.push(item);
79
+ else if (request.signature.profile === "apple-developer-id") matrices.macos.push(item);
80
+ else if (request.signature.profile === "windows-authenticode") matrices.windows.push(item);
81
+ else throw new Error(`unsupported signing authority profile: ${request.signature.profile}`);
82
+ }
83
+ }
84
+ for (const entries of Object.values(matrices)) entries.sort((a, b) => a.request.localeCompare(b.request));
85
+ writeGitHubOutputs({
86
+ "detached-matrix": JSON.stringify(matrices.detached),
87
+ "macos-matrix": JSON.stringify(matrices.macos),
88
+ "windows-matrix": JSON.stringify(matrices.windows),
89
+ "request-count": String(seen.size),
90
+ });
91
+ return matrices;
92
+ }
93
+
94
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
95
+ try {
96
+ inspectArtifactSigningRequests();
97
+ } catch (error) {
98
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
99
+ process.exitCode = 1;
100
+ }
101
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { validateArtifactSigningRequest } from "../packages/core/artifact-signing.js";
8
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
9
+
10
+ function required(value, label) {
11
+ const normalized = String(value || "").trim();
12
+ if (!normalized) throw new Error(`${label} is required`);
13
+ return normalized;
14
+ }
15
+
16
+ function resolveBelow(root, relative, label) {
17
+ const target = path.resolve(root, required(relative, label));
18
+ const rel = path.relative(path.resolve(root), target);
19
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`${label} must resolve below its root`);
20
+ return target;
21
+ }
22
+
23
+ function sha256File(filePath) {
24
+ return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
25
+ }
26
+
27
+ export function materializeArtifactSigningRequest({
28
+ requestRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT,
29
+ requestPath = process.env.BUILDCHAIN_SIGNING_REQUEST_PATH,
30
+ expectedProfile = process.env.BUILDCHAIN_SIGNING_EXPECTED_PROFILE,
31
+ outputPath = process.env.BUILDCHAIN_UNSIGNED_OUTPUT,
32
+ } = {}) {
33
+ const root = path.resolve(required(requestRoot, "signing request root"));
34
+ const requestFile = resolveBelow(root, requestPath, "request path");
35
+ const request = JSON.parse(fs.readFileSync(requestFile, "utf8"));
36
+ const check = validateArtifactSigningRequest(request);
37
+ if (!check.ok) throw new Error(`invalid artifact signing request: ${check.issues.join(", ")}`);
38
+ if (expectedProfile && request.signature.profile !== expectedProfile) throw new Error("artifact signing profile mismatch");
39
+ if (request.artifact.transport?.format !== "exact-file") throw new Error("native executable signing requires exact-file transport");
40
+ const requestIndexRoot = path.dirname(path.dirname(requestFile));
41
+ const payload = resolveBelow(requestIndexRoot, request.artifact.transport.file, "transport payload");
42
+ const stat = fs.statSync(payload);
43
+ const payloadDigest = sha256File(payload);
44
+ if (!stat.isFile() || stat.size !== request.artifact.transport.bytes || stat.size !== request.artifact.bytes || payloadDigest !== request.artifact.transport.digest || payloadDigest !== request.artifact.digest) {
45
+ throw new Error("signing payload does not match its sealed request");
46
+ }
47
+ const output = path.resolve(required(outputPath, "unsigned output path"));
48
+ fs.mkdirSync(path.dirname(output), { recursive: true });
49
+ fs.copyFileSync(payload, output, fs.constants.COPYFILE_EXCL);
50
+ writeGitHubOutputs({
51
+ "artifact-id": request.artifact.id,
52
+ "payload-path": output,
53
+ "request-digest": request.digest,
54
+ "signature-provider": request.signature.provider,
55
+ });
56
+ return { request, output };
57
+ }
58
+
59
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
60
+ try {
61
+ materializeArtifactSigningRequest();
62
+ } catch (error) {
63
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
64
+ process.exitCode = 1;
65
+ }
66
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
7
+
8
+ function required(value, label) {
9
+ const normalized = String(value || "").trim();
10
+ if (!normalized) throw new Error(`${label} is required`);
11
+ return normalized;
12
+ }
13
+
14
+ function safeId(value) {
15
+ const normalized = String(value || "").replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "");
16
+ if (!normalized || normalized === "." || normalized === "..") throw new Error("unsafe signing result id");
17
+ return normalized;
18
+ }
19
+
20
+ function walk(root, output = []) {
21
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
22
+ const child = path.join(root, entry.name);
23
+ if (entry.isDirectory()) walk(child, output);
24
+ else if (entry.isFile() && entry.name === "index.json") output.push(child);
25
+ }
26
+ return output;
27
+ }
28
+
29
+ export function mergeArtifactSigningResults({
30
+ inputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_INPUT_ROOT,
31
+ outputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_ROOT,
32
+ } = {}) {
33
+ const input = path.resolve(required(inputRoot, "signing result input root"));
34
+ const output = path.resolve(required(outputRoot, "signing result output root"));
35
+ fs.mkdirSync(output, { recursive: true });
36
+ const merged = [];
37
+ const seen = new Set();
38
+ for (const indexPath of walk(input)) {
39
+ const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
40
+ if (index.contract !== "kungfu-buildchain-artifact-signing-result-index/v1") continue;
41
+ for (const entry of index.results || []) {
42
+ if (seen.has(entry.id)) throw new Error(`duplicate signing result: ${entry.id}`);
43
+ seen.add(entry.id);
44
+ const sourceResult = path.resolve(path.dirname(indexPath), entry.result);
45
+ const sourceDirectory = path.dirname(sourceResult);
46
+ const relative = path.relative(path.dirname(indexPath), sourceDirectory);
47
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("signing result escapes provider root");
48
+ const destinationName = safeId(entry.id);
49
+ const destination = path.join(output, destinationName);
50
+ fs.cpSync(sourceDirectory, destination, { recursive: true, errorOnExist: true, force: false });
51
+ merged.push({
52
+ ...entry,
53
+ result: `${destinationName}/${path.basename(sourceResult)}`,
54
+ ...(entry.payload ? { payload: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.payload)).split(path.sep).join("/")}` } : {}),
55
+ ...(entry.envelope ? { envelope: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.envelope)).split(path.sep).join("/")}` } : {}),
56
+ ...(entry.receipt ? { receipt: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.receipt)).split(path.sep).join("/")}` } : {}),
57
+ });
58
+ }
59
+ }
60
+ if (merged.length === 0) throw new Error("no artifact signing results found");
61
+ merged.sort((a, b) => a.id.localeCompare(b.id));
62
+ const index = { schemaVersion: 1, contract: "kungfu-buildchain-artifact-signing-result-index/v1", results: merged };
63
+ const indexPath = path.join(output, "index.json");
64
+ fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
65
+ writeGitHubOutputs({ "result-count": String(merged.length), "result-index": indexPath, "result-root": output });
66
+ return index;
67
+ }
68
+
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
70
+ try {
71
+ mergeArtifactSigningResults();
72
+ } catch (error) {
73
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
74
+ process.exitCode = 1;
75
+ }
76
+ }