@kungfu-tech/buildchain 4.0.2-alpha.0 → 4.0.2-alpha.2

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 (45) hide show
  1. package/architecture/agent-change-map.md +4 -4
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/maintainability-debt.json +41 -115
  4. package/architecture/maintainability-policy.json +12 -7
  5. package/architecture/release-tail-contract-inventory.json +4 -4
  6. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  7. package/architecture/v4-release-invocation-fixtures.json +119 -0
  8. package/architecture/v4-release-topology.json +238 -0
  9. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  10. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  11. package/contracts/v4-release-invocation-v1.schema.json +86 -0
  12. package/dist/site/buildchain-contract.json +9 -9
  13. package/dist/site/buildchain-site.json +7 -7
  14. package/dist/site/kfd-claims.json +7 -5
  15. package/dist/site/kfd-upstream-aggregate.json +1 -1
  16. package/dist/site/manual-registry.json +1 -1
  17. package/dist/site/node-api-registry.json +3 -3
  18. package/dist/site/page-registry.json +2 -2
  19. package/dist/site/public-surface-audit.json +17 -9
  20. package/dist/site/publication-authority-registry.json +2 -3
  21. package/dist/site/publication-registry.json +4 -4
  22. package/dist/site/site-manifest.json +5 -5
  23. package/dist/site/workflow-registry.json +19 -11
  24. package/docs/node-api-reference.md +3 -3
  25. package/package.json +2 -2
  26. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  27. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  28. package/packages/core/dev-delivery-warrant-state.js +12 -28
  29. package/packages/core/v4-canonical-contracts.js +8 -0
  30. package/packages/core/v4-floating-consumer-policy.js +4 -1
  31. package/packages/core/v4-release-invocation.js +356 -0
  32. package/scripts/audit-publication-control-plane.mjs +0 -1
  33. package/scripts/check-inventory.mjs +27 -59
  34. package/scripts/check-maintainability.mjs +13 -5
  35. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  36. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  37. package/scripts/check-v4-release-topology.mjs +237 -0
  38. package/scripts/dev-delivery-warrant.mjs +13 -4
  39. package/scripts/generate-channel-promotion-workflow.mjs +12 -54
  40. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  41. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  42. package/scripts/capture-package-release-propagation.mjs +0 -263
  43. package/scripts/publication-commit-evidence.mjs +0 -444
  44. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  45. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -1,263 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import crypto from "node:crypto";
4
- import fs from "node:fs";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { execFileSync } from "node:child_process";
8
- import { fileURLToPath } from "node:url";
9
- import {
10
- createPackageReleasePropagationCapture,
11
- normalizePackageReleasePropagationConfig,
12
- } from "../packages/core/release-propagation-capture.js";
13
- import { stableJson } from "../packages/core/release-propagation-common.js";
14
-
15
- function parseArgs(argv) {
16
- const args = {};
17
- for (let index = 0; index < argv.length; index += 1) {
18
- const token = argv[index];
19
- if (!token.startsWith("--")) throw new Error(`unexpected argument: ${token}`);
20
- const key = token.slice(2);
21
- const value = argv[index + 1];
22
- if (!value || value.startsWith("--")) throw new Error(`${token} requires a value`);
23
- args[key] = value;
24
- index += 1;
25
- }
26
- return args;
27
- }
28
-
29
- function required(args, key) {
30
- const value = String(args[key] || "").trim();
31
- if (!value) throw new Error(`--${key} is required`);
32
- return value;
33
- }
34
-
35
- function sha256(bytes) {
36
- return crypto.createHash("sha256").update(bytes).digest("hex");
37
- }
38
-
39
- function commandJson(command, args, options = {}) {
40
- const text = execFileSync(command, args, { encoding: "utf8", ...options });
41
- return JSON.parse(text);
42
- }
43
-
44
- function assertSourcePath(value) {
45
- const sourcePath = String(value || "").trim();
46
- if (!sourcePath || path.isAbsolute(sourcePath) || sourcePath.split("/").includes("..") || /[\r\n]/.test(sourcePath)) {
47
- throw new Error("release propagation config path must be a safe repository-relative path");
48
- }
49
- return sourcePath;
50
- }
51
-
52
- function hasCommit(sourceSha, cwd) {
53
- try {
54
- execFileSync("git", ["cat-file", "-e", `${sourceSha}^{commit}`], {
55
- cwd,
56
- stdio: "ignore",
57
- });
58
- return true;
59
- } catch {
60
- return false;
61
- }
62
- }
63
-
64
- export function readConfigAtSource(sourceSha, configPath, cwd) {
65
- if (!hasCommit(sourceSha, cwd)) {
66
- try {
67
- execFileSync("git", ["fetch", "--no-tags", "--depth=1", "origin", sourceSha], {
68
- cwd,
69
- stdio: ["ignore", "pipe", "pipe"],
70
- });
71
- } catch (error) {
72
- const detail = String(error.stderr || error.message || "unknown git fetch failure").trim();
73
- throw new Error(`exact release source ${sourceSha} is unavailable from origin: ${detail}`);
74
- }
75
- }
76
- const bytes = execFileSync("git", ["show", `${sourceSha}:${configPath}`], {
77
- cwd,
78
- encoding: "utf8",
79
- });
80
- const parsed = JSON.parse(bytes);
81
- return { bytes, parsed, normalized: normalizePackageReleasePropagationConfig(parsed) };
82
- }
83
-
84
- function resolveTagTarget(repository, tag) {
85
- let object = commandJson("gh", ["api", `repos/${repository}/git/ref/tags/${tag}`]).object;
86
- for (let depth = 0; depth < 8 && object?.type === "tag"; depth += 1) {
87
- object = commandJson("gh", ["api", `repos/${repository}/git/tags/${object.sha}`]).object;
88
- }
89
- if (object?.type !== "commit" || !/^[0-9a-f]{40}$/i.test(object.sha || "")) {
90
- throw new Error(`release tag ${tag} does not resolve to one exact commit`);
91
- }
92
- return object.sha.toLowerCase();
93
- }
94
-
95
- function resolvePackageFact(packageName, version) {
96
- const fact = commandJson("npm", [
97
- "view",
98
- `${packageName}@${version}`,
99
- "version",
100
- "dist.integrity",
101
- "gitHead",
102
- "--json",
103
- "--registry=https://registry.npmjs.org/",
104
- ]);
105
- return {
106
- name: packageName,
107
- version: String(fact.version || ""),
108
- integrity: String(fact.dist?.integrity || fact["dist.integrity"] || ""),
109
- gitHead: String(fact.gitHead || "").toLowerCase(),
110
- };
111
- }
112
-
113
- function verifyPublicReleasePassport({ repository, tag, localPath }) {
114
- const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-release-passport-"));
115
- try {
116
- execFileSync("gh", [
117
- "release", "download", tag,
118
- "--repo", repository,
119
- "--pattern", "buildchain.release.json",
120
- "--dir", temporary,
121
- ], { stdio: ["ignore", "pipe", "pipe"] });
122
- const localBytes = fs.readFileSync(localPath);
123
- const remotePath = path.join(temporary, "buildchain.release.json");
124
- const remoteBytes = fs.readFileSync(remotePath);
125
- const localDigest = sha256(localBytes);
126
- const remoteDigest = sha256(remoteBytes);
127
- if (localDigest !== remoteDigest) {
128
- throw new Error("public release passport bytes disagree with finalized local passport");
129
- }
130
- return {
131
- url: `https://github.com/${repository}/releases/download/${tag}/buildchain.release.json`,
132
- sha256: remoteDigest,
133
- };
134
- } finally {
135
- fs.rmSync(temporary, { recursive: true, force: true });
136
- }
137
- }
138
-
139
- function resolveBaseShas(config) {
140
- return Object.fromEntries(config.targets.map((targetId) => {
141
- const node = config.graph.nodes.find((entry) => entry.id === targetId);
142
- if (!node?.baseRef) throw new Error(`configured propagation target ${targetId} has no baseRef`);
143
- const response = commandJson("gh", ["api", `repos/${node.repository}/commits/${node.baseRef}`]);
144
- const sha = String(response.sha || "").toLowerCase();
145
- if (!/^[0-9a-f]{40}$/.test(sha)) {
146
- throw new Error(`configured propagation target ${targetId} baseRef did not resolve exactly`);
147
- }
148
- return [targetId, sha];
149
- }));
150
- }
151
-
152
- function writeOutput(name, value) {
153
- if (!process.env.GITHUB_OUTPUT) return;
154
- fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
155
- }
156
-
157
- export function capturePackageReleasePropagation({
158
- config,
159
- upstreamRelease,
160
- expectedBaseShas,
161
- outputDir,
162
- configBytes = "",
163
- } = {}) {
164
- const captured = createPackageReleasePropagationCapture({
165
- config,
166
- upstreamRelease,
167
- expectedBaseShas,
168
- });
169
- fs.mkdirSync(outputDir, { recursive: true });
170
- fs.writeFileSync(
171
- path.join(outputDir, "config.json"),
172
- configBytes || stableJson(captured.config),
173
- );
174
- fs.writeFileSync(path.join(outputDir, "upstream-release.json"), stableJson(captured.upstreamRelease));
175
- fs.writeFileSync(path.join(outputDir, "plan.json"), stableJson(captured.plan));
176
- for (const item of captured.works) {
177
- const targetDir = path.join(outputDir, "work", item.propagationKey);
178
- fs.mkdirSync(targetDir, { recursive: true });
179
- fs.writeFileSync(path.join(targetDir, "work.json"), stableJson(item.work));
180
- fs.writeFileSync(path.join(targetDir, "status.json"), stableJson(item.status));
181
- }
182
- return captured;
183
- }
184
-
185
- export function main(argv = process.argv.slice(2)) {
186
- const args = parseArgs(argv);
187
- const repository = required(args, "repository");
188
- const channel = required(args, "channel");
189
- const sourceSha = required(args, "source-sha").toLowerCase();
190
- const tag = required(args, "tag");
191
- const configPath = assertSourcePath(required(args, "config-path"));
192
- const releasePassportPath = path.resolve(required(args, "release-passport-path"));
193
- const outputDir = path.resolve(required(args, "output-dir"));
194
- if (!/^[0-9a-f]{40}$/.test(sourceSha)) throw new Error("--source-sha must be an exact commit SHA");
195
- if (tag !== `v${tag.replace(/^v/, "")}`) throw new Error("--tag must be an exact v-prefixed release tag");
196
- const version = tag.slice(1);
197
- const configSource = readConfigAtSource(sourceSha, configPath, process.cwd());
198
- const sourceNode = configSource.normalized.graph.nodes.find(
199
- (node) => node.id === configSource.normalized.sourceNode,
200
- );
201
- const packageFact = resolvePackageFact(sourceNode.package, version);
202
- const tagTargetSha = resolveTagTarget(repository, tag);
203
- const releasePassport = verifyPublicReleasePassport({
204
- repository,
205
- tag,
206
- localPath: releasePassportPath,
207
- });
208
- const upstreamRelease = {
209
- repository,
210
- channel,
211
- tag,
212
- sourceSha,
213
- tagTargetSha,
214
- package: packageFact,
215
- releasePassport,
216
- };
217
- const expectedBaseShas = resolveBaseShas(configSource.normalized);
218
- const captured = capturePackageReleasePropagation({
219
- config: configSource.parsed,
220
- configBytes: configSource.bytes,
221
- upstreamRelease,
222
- expectedBaseShas,
223
- outputDir,
224
- });
225
- const artifactName = `package-propagation-work-${version}-${sourceSha}`;
226
- const workRoots = captured.works.map((item) => item.work.contentRoot);
227
- writeOutput("configured", "true");
228
- writeOutput("artifact-name", artifactName);
229
- writeOutput("work-roots-json", JSON.stringify(workRoots));
230
- process.stdout.write(stableJson({
231
- schemaVersion: 1,
232
- contract: "kungfu-buildchain-package-release-propagation-capture-result",
233
- artifactName,
234
- release: {
235
- repository,
236
- channel,
237
- tag,
238
- sourceSha,
239
- tagTargetSha,
240
- package: packageFact,
241
- releasePassport,
242
- },
243
- workCount: captured.works.length,
244
- works: captured.works.map((item) => ({
245
- target: item.target,
246
- repository: item.repository,
247
- propagationKey: item.propagationKey,
248
- workId: item.work.workId,
249
- workRoot: item.work.contentRoot,
250
- nextAction: item.status.nextAction,
251
- })),
252
- }));
253
- }
254
-
255
- const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
256
- if (isMain) {
257
- try {
258
- main();
259
- } catch (error) {
260
- console.error(error.message);
261
- process.exitCode = 1;
262
- }
263
- }
@@ -1,444 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import { createHash } from "node:crypto";
5
- import path from "node:path";
6
- import { pathToFileURL } from "node:url";
7
-
8
- const SCHEMA = "kungfu-buildchain-publication-commit-evidence/v1";
9
- const INSTALLER_BUNDLE_SCHEMA = "kungfu.installer-publication-bundle/v1";
10
-
11
- function requiredString(value, label) {
12
- if (typeof value !== "string" || value.trim() === "") {
13
- throw new Error(`${label} is required`);
14
- }
15
- return value.trim();
16
- }
17
-
18
- function sha256Root(value, label) {
19
- const normalized = requiredString(value, label);
20
- if (!/^sha256:[0-9a-f]{64}$/.test(normalized)) {
21
- throw new Error(`${label} must be a lowercase sha256 root`);
22
- }
23
- return normalized;
24
- }
25
-
26
- function exactSha(value, label) {
27
- const normalized = requiredString(value, label);
28
- if (!/^[0-9a-f]{40}$/.test(normalized)) {
29
- throw new Error(`${label} must be an exact Git SHA`);
30
- }
31
- return normalized;
32
- }
33
-
34
- function publicHttps(value, label) {
35
- const normalized = requiredString(value, label);
36
- let url;
37
- try {
38
- url = new URL(normalized);
39
- } catch {
40
- throw new Error(`${label} must be a public HTTPS URL`);
41
- }
42
- if (
43
- url.protocol !== "https:" ||
44
- !url.hostname ||
45
- url.username ||
46
- url.password ||
47
- url.search ||
48
- url.hash
49
- ) {
50
- throw new Error(
51
- `${label} must be a public HTTPS URL without credentials, query, or fragment`,
52
- );
53
- }
54
- return normalized;
55
- }
56
-
57
- function canonical(value) {
58
- if (Array.isArray(value)) return value.map(canonical);
59
- if (value && typeof value === "object") {
60
- return Object.fromEntries(
61
- Object.entries(value)
62
- .filter(([, item]) => item !== undefined)
63
- .sort(([left], [right]) => left.localeCompare(right))
64
- .map(([key, item]) => [key, canonical(item)]),
65
- );
66
- }
67
- return value;
68
- }
69
-
70
- function digest(bytes) {
71
- return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
72
- }
73
-
74
- function semanticRoot(value) {
75
- return digest(Buffer.from(JSON.stringify(canonical(value))));
76
- }
77
-
78
- function expectedContentType(assetPath) {
79
- if (assetPath.endsWith(".json")) return "application/json; charset=utf-8";
80
- if (assetPath.endsWith(".sh")) return "text/x-shellscript; charset=utf-8";
81
- if (assetPath.endsWith(".ps1")) return "text/plain; charset=utf-8";
82
- throw new Error(`installer bundle asset type is unsupported: ${assetPath}`);
83
- }
84
-
85
- function validateInstallerBundle(evidence, expected) {
86
- const bundle = evidence.publication?.installerBundle;
87
- if (!bundle) return null;
88
- if (bundle.schema !== INSTALLER_BUNDLE_SCHEMA) {
89
- throw new Error(
90
- `installer bundle schema must be ${INSTALLER_BUNDLE_SCHEMA}`,
91
- );
92
- }
93
- const bundleRoot = sha256Root(
94
- bundle.bundleRoot,
95
- "installerBundle.bundleRoot",
96
- );
97
- if (
98
- bundleRoot !== evidence.publication.payloadRoot ||
99
- bundleRoot !== evidence.readback?.payloadRoot
100
- ) {
101
- throw new Error(
102
- "installer bundle root must be the publication payload root",
103
- );
104
- }
105
- if (
106
- exactSha(bundle.sourceCommit, "installerBundle.sourceCommit") !==
107
- expected.sourceSha ||
108
- !["alpha", "stable"].includes(bundle.channel)
109
- ) {
110
- throw new Error("installer bundle release identity mismatch");
111
- }
112
- sha256Root(bundle.channelPayloadRoot, "installerBundle.channelPayloadRoot");
113
- sha256Root(bundle.channelFileDigest, "installerBundle.channelFileDigest");
114
- sha256Root(
115
- bundle.releasePassport?.root,
116
- "installerBundle.releasePassport.root",
117
- );
118
- sha256Root(bundle.manifestDigest, "installerBundle.manifestDigest");
119
- if (
120
- evidence.readback?.manifestDigest !== bundle.manifestDigest ||
121
- !Array.isArray(bundle.assets) ||
122
- bundle.assets.length !== 7
123
- ) {
124
- throw new Error("installer bundle read-back or asset set is incomplete");
125
- }
126
- const paths = new Set();
127
- const topLevel = new Set([
128
- "installer-publication.json",
129
- "channel-index.json",
130
- "trusted-keys.json",
131
- "install.sh",
132
- "install.ps1",
133
- ]);
134
- const expectedRoles = new Map([
135
- ["installer-publication.json", "publication-manifest"],
136
- ["channel-index.json", "signed-channel-index"],
137
- ["trusted-keys.json", "public-trust-anchors"],
138
- ["install.sh", "friendly-installer"],
139
- ["install.ps1", "friendly-installer"],
140
- ]);
141
- const immutableDirectories = new Set();
142
- let immutableShell = 0;
143
- let immutablePowerShell = 0;
144
- const releaseBaseUrl =
145
- `https://github.com/kungfu-systems/kungfu/releases/download/` +
146
- expected.releaseTag;
147
- for (const asset of bundle.assets) {
148
- const assetPath = requiredString(
149
- asset.path,
150
- "installer bundle asset path",
151
- ).replaceAll("\\", "/");
152
- if (
153
- assetPath.startsWith("/") ||
154
- assetPath.endsWith("/") ||
155
- assetPath.split("/").some((part) => part === "" || part === "..") ||
156
- paths.has(assetPath)
157
- ) {
158
- throw new Error(
159
- `unsafe or duplicate installer bundle asset: ${assetPath}`,
160
- );
161
- }
162
- paths.add(assetPath);
163
- topLevel.delete(assetPath);
164
- if (assetPath.includes("/") && assetPath.endsWith("/install.sh")) {
165
- immutableShell += 1;
166
- immutableDirectories.add(path.posix.dirname(assetPath));
167
- }
168
- if (assetPath.includes("/") && assetPath.endsWith("/install.ps1")) {
169
- immutablePowerShell += 1;
170
- immutableDirectories.add(path.posix.dirname(assetPath));
171
- }
172
- if (!Number.isSafeInteger(asset.size) || asset.size < 1) {
173
- throw new Error(`installer bundle asset size is invalid: ${assetPath}`);
174
- }
175
- sha256Root(asset.digest, `installer bundle asset digest: ${assetPath}`);
176
- const releaseAsset = requiredString(
177
- asset.releaseAsset,
178
- `installer bundle release asset: ${assetPath}`,
179
- );
180
- if (
181
- asset.contentType !== expectedContentType(assetPath) ||
182
- publicHttps(
183
- asset.releaseUrl,
184
- `installer bundle asset URL: ${assetPath}`,
185
- ) !== `${releaseBaseUrl}/${releaseAsset}` ||
186
- !/^kungfu-[a-z0-9.-]+$/.test(releaseAsset)
187
- ) {
188
- throw new Error(
189
- `installer bundle asset transport metadata is invalid: ${assetPath}`,
190
- );
191
- }
192
- const expectedRole = assetPath.includes("/")
193
- ? "immutable-installer"
194
- : expectedRoles.get(assetPath);
195
- if (asset.role !== expectedRole) {
196
- throw new Error(`installer bundle asset role is invalid: ${assetPath}`);
197
- }
198
- }
199
- const immutableDirectory = [...immutableDirectories][0] || "";
200
- const immutableParts = immutableDirectory.split("/");
201
- if (
202
- topLevel.size !== 0 ||
203
- immutableShell !== 1 ||
204
- immutablePowerShell !== 1 ||
205
- immutableDirectories.size !== 1 ||
206
- immutableParts.length !== 5 ||
207
- immutableParts[0] !== "installers" ||
208
- immutableParts[1] !== "v1" ||
209
- immutableParts[2] !== bundle.channel ||
210
- immutableParts[3] !== expected.version ||
211
- !/^[a-f0-9]{64}$/.test(immutableParts[4])
212
- ) {
213
- throw new Error("installer bundle asset topology is incomplete");
214
- }
215
- if (
216
- bundle.cachePolicy?.friendly !== "public,max-age=300,must-revalidate" ||
217
- bundle.cachePolicy?.immutable !== "public,max-age=31536000,immutable"
218
- ) {
219
- throw new Error("installer bundle cache policy is invalid");
220
- }
221
- if (
222
- evidence.siteHandoff?.state !== "deferred-to-site-owned-consumer" ||
223
- evidence.siteHandoff?.productionAvailable !== false ||
224
- evidence.siteHandoff?.requiredBundleRoot !== bundleRoot
225
- ) {
226
- throw new Error("installer bundle site handoff must remain deferred");
227
- }
228
- return {
229
- schema: bundle.schema,
230
- bundleRoot,
231
- manifestDigest: bundle.manifestDigest,
232
- channel: bundle.channel,
233
- channelPayloadRoot: bundle.channelPayloadRoot,
234
- channelFileDigest: bundle.channelFileDigest,
235
- releasePassport: bundle.releasePassport,
236
- cachePolicy: bundle.cachePolicy,
237
- immutablePath: immutableDirectory,
238
- assets: bundle.assets,
239
- };
240
- }
241
-
242
- export function validatePublicationCommitEvidence(
243
- evidence,
244
- { version, sourceSha, releaseSha, releaseTag } = {},
245
- ) {
246
- if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
247
- throw new Error("publication commit evidence must be an object");
248
- }
249
- if (evidence.schema !== SCHEMA) {
250
- throw new Error(`publication commit evidence schema must be ${SCHEMA}`);
251
- }
252
- if (evidence.status !== "passed") {
253
- throw new Error("publication commit evidence status must be passed");
254
- }
255
- const identity = evidence.identity || {};
256
- const expected = {
257
- version: requiredString(version, "expected version"),
258
- sourceSha: exactSha(sourceSha, "expected sourceSha"),
259
- releaseSha: exactSha(releaseSha, "expected releaseSha"),
260
- releaseTag: requiredString(releaseTag, "expected releaseTag"),
261
- };
262
- for (const [field, value] of Object.entries(expected)) {
263
- if (identity[field] !== value) {
264
- throw new Error(`publication commit evidence ${field} mismatch`);
265
- }
266
- }
267
- const publicUrl = publicHttps(evidence.publication?.url, "publication.url");
268
- const payloadRoot = sha256Root(
269
- evidence.publication?.payloadRoot,
270
- "publication.payloadRoot",
271
- );
272
- if (
273
- evidence.readback?.status !== "passed" ||
274
- publicHttps(evidence.readback?.url, "readback.url") !== publicUrl ||
275
- sha256Root(evidence.readback?.payloadRoot, "readback.payloadRoot") !==
276
- payloadRoot
277
- ) {
278
- throw new Error(
279
- "publication read-back must pass at the canonical URL with the exact payload root",
280
- );
281
- }
282
- const previousAuthority = evidence.recovery?.previousAuthority;
283
- const rollbackReference = requiredString(
284
- evidence.recovery?.rollbackReference,
285
- "recovery.rollbackReference",
286
- );
287
- if (!["preserved", "none"].includes(previousAuthority)) {
288
- throw new Error(
289
- "publication recovery must preserve or explicitly declare no previous authority",
290
- );
291
- }
292
- const installerBundle = validateInstallerBundle(evidence, expected);
293
- return {
294
- schema: SCHEMA,
295
- status: "passed",
296
- publicUrl,
297
- payloadRoot,
298
- identity: expected,
299
- recovery: {
300
- previousAuthority,
301
- rollbackReference,
302
- },
303
- ...(installerBundle ? { installerBundle } : {}),
304
- };
305
- }
306
-
307
- export async function verifyInstallerBundleReadback(
308
- result,
309
- fetchImpl = globalThis.fetch,
310
- ) {
311
- const bundle = result.installerBundle;
312
- if (!bundle) return null;
313
- if (typeof fetchImpl !== "function") {
314
- throw new Error("installer bundle read-back requires fetch");
315
- }
316
- const manifestResponse = await fetchImpl(result.publicUrl, {
317
- redirect: "manual",
318
- cache: "no-store",
319
- });
320
- if (manifestResponse.status !== 200) {
321
- throw new Error(
322
- `installer bundle manifest read-back failed: HTTP ${manifestResponse.status}`,
323
- );
324
- }
325
- const manifestBytes = Buffer.from(await manifestResponse.arrayBuffer());
326
- if (digest(manifestBytes) !== bundle.manifestDigest) {
327
- throw new Error("installer bundle manifest digest mismatch");
328
- }
329
- const manifest = JSON.parse(manifestBytes);
330
- const unsigned = Object.fromEntries(
331
- Object.entries(manifest).filter(([key]) => key !== "bundleRoot"),
332
- );
333
- if (
334
- manifest.schema !== INSTALLER_BUNDLE_SCHEMA ||
335
- manifest.bundleRoot !== bundle.bundleRoot ||
336
- semanticRoot(unsigned) !== bundle.bundleRoot ||
337
- manifest.package?.name !== "@kungfu-tech/site" ||
338
- typeof manifest.package?.version !== "string" ||
339
- manifest.identity?.sourceCommit !== result.identity.sourceSha ||
340
- manifest.identity?.releaseSha !== result.identity.releaseSha ||
341
- manifest.identity?.releaseTag !== result.identity.releaseTag ||
342
- manifest.identity?.version !== result.identity.version ||
343
- manifest.identity?.channel !== bundle.channel ||
344
- manifest.identity?.channelPayloadRoot !== bundle.channelPayloadRoot ||
345
- manifest.identity?.channelFileDigest !== bundle.channelFileDigest ||
346
- manifest.identity?.releasePassport?.root !== bundle.releasePassport.root ||
347
- manifest.distribution?.repository !== "kungfu-systems/kungfu" ||
348
- manifest.routes?.immutablePath !== bundle.immutablePath ||
349
- manifest.routes?.friendly?.["install.sh"] !==
350
- "https://kungfu.tech/install.sh" ||
351
- manifest.routes?.friendly?.["install.ps1"] !==
352
- "https://kungfu.tech/install.ps1" ||
353
- JSON.stringify(canonical(manifest.cachePolicy)) !==
354
- JSON.stringify(canonical(bundle.cachePolicy)) ||
355
- JSON.stringify(canonical(manifest.assets)) !==
356
- JSON.stringify(canonical(bundle.assets)) ||
357
- `${manifest.distribution?.releaseBaseUrl}/` +
358
- manifest.distribution?.manifestAsset !==
359
- result.publicUrl
360
- ) {
361
- throw new Error("installer bundle manifest root mismatch");
362
- }
363
- const observations = [];
364
- const byUrl = new Map();
365
- for (const asset of bundle.assets) {
366
- let observation = byUrl.get(asset.releaseUrl);
367
- if (!observation) {
368
- const response = await fetchImpl(asset.releaseUrl, {
369
- redirect: "manual",
370
- cache: "no-store",
371
- });
372
- if (response.status !== 200) {
373
- throw new Error(
374
- `installer bundle asset read-back failed: HTTP ${response.status}`,
375
- );
376
- }
377
- const bytes = Buffer.from(await response.arrayBuffer());
378
- observation = {
379
- releaseUrl: asset.releaseUrl,
380
- size: bytes.length,
381
- digest: digest(bytes),
382
- };
383
- byUrl.set(asset.releaseUrl, observation);
384
- }
385
- if (
386
- observation.size !== asset.size ||
387
- observation.digest !== asset.digest
388
- ) {
389
- throw new Error(`installer bundle asset drifted: ${asset.path}`);
390
- }
391
- observations.push({ path: asset.path, ...observation });
392
- }
393
- const seal = {
394
- schema: "kungfu-buildchain-installer-publication-bundle-seal/v1",
395
- bundleRoot: bundle.bundleRoot,
396
- manifestDigest: bundle.manifestDigest,
397
- sourceCommit: result.identity.sourceSha,
398
- releaseTag: result.identity.releaseTag,
399
- releasePassport: bundle.releasePassport,
400
- observations,
401
- };
402
- return { ...seal, sealRoot: semanticRoot(seal) };
403
- }
404
-
405
- async function main(args) {
406
- const options = {};
407
- for (let index = 0; index < args.length; index += 1) {
408
- const value = args[index];
409
- if (value === "--evidence") options.evidence = args[++index];
410
- else if (value === "--version") options.version = args[++index];
411
- else if (value === "--source-sha") options.sourceSha = args[++index];
412
- else if (value === "--release-sha") options.releaseSha = args[++index];
413
- else if (value === "--release-tag") options.releaseTag = args[++index];
414
- else throw new Error(`unknown argument: ${value}`);
415
- }
416
- const evidencePath = path.resolve(
417
- requiredString(options.evidence, "--evidence"),
418
- );
419
- const result = validatePublicationCommitEvidence(
420
- JSON.parse(fs.readFileSync(evidencePath, "utf8")),
421
- options,
422
- );
423
- const installerBundleSeal = await verifyInstallerBundleReadback(result);
424
- process.stdout.write(
425
- `${JSON.stringify({
426
- ...result,
427
- ...(installerBundleSeal ? { installerBundleSeal } : {}),
428
- })}\n`,
429
- );
430
- }
431
-
432
- if (
433
- process.argv[1] &&
434
- import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
435
- ) {
436
- main(process.argv.slice(2)).catch((error) => {
437
- console.error(
438
- `publication commit evidence failed: ${
439
- error instanceof Error ? error.message : String(error)
440
- }`,
441
- );
442
- process.exit(1);
443
- });
444
- }