@kungfu-tech/buildchain 3.0.4-alpha.2 → 3.0.4-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 (49) hide show
  1. package/bin/buildchain.mjs +126 -72
  2. package/bin/internal/trust-release-cli.mjs +15 -537
  3. package/bin/internal/trust-release-command-handlers.mjs +14 -0
  4. package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
  5. package/bin/internal/trust-release-release-handlers.mjs +317 -0
  6. package/bin/internal/trust-release-verification-handlers.mjs +306 -0
  7. package/dist/site/buildchain-contract.json +35 -24
  8. package/dist/site/buildchain-site.json +22 -10
  9. package/dist/site/controller-registry.json +16 -3
  10. package/dist/site/kfd-claims.json +9 -7
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +2 -2
  13. package/dist/site/node-api-registry.json +7 -7
  14. package/dist/site/page-registry.json +9 -4
  15. package/dist/site/public-surface-audit.json +56 -8
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-model.json +7 -0
  18. package/dist/site/site-manifest.json +6 -6
  19. package/dist/site/workflow-registry.json +13 -5
  20. package/docs/MAP.md +1 -1
  21. package/docs/release-propagation.md +166 -8
  22. package/package.json +1 -1
  23. package/packages/core/buildchain-kfd-claims.js +1 -1
  24. package/packages/core/controller-evidence.js +8 -1
  25. package/packages/core/index.js +1 -13
  26. package/packages/core/paper-npm-bootstrap.js +492 -0
  27. package/packages/core/paper.js +271 -669
  28. package/packages/core/public-surface-cli.js +12 -1
  29. package/packages/core/release-passport.js +67 -71
  30. package/packages/core/release-propagation-common.js +64 -0
  31. package/packages/core/release-propagation-execution-profile.js +59 -0
  32. package/packages/core/release-propagation-release.js +196 -0
  33. package/packages/core/release-propagation-stage-evidence.js +364 -0
  34. package/packages/core/release-propagation-work-capture.js +64 -0
  35. package/packages/core/release-propagation-work-constants.js +34 -0
  36. package/packages/core/release-propagation-work-control.js +203 -0
  37. package/packages/core/release-propagation-work-transitions.js +145 -0
  38. package/packages/core/release-propagation-work.js +517 -0
  39. package/packages/core/release-propagation.js +34 -158
  40. package/scripts/aws-windows-jit-controller-core.mjs +269 -0
  41. package/scripts/aws-windows-jit-controller.mjs +502 -0
  42. package/scripts/check-internal-architecture.mjs +178 -52
  43. package/scripts/check-maintainability.mjs +76 -17
  44. package/scripts/generate-site-bundle.mjs +16 -7
  45. package/scripts/maintainability-metrics.mjs +24 -4
  46. package/scripts/release-propagation.mjs +126 -0
  47. package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
  48. package/scripts/web-surface-core.mjs +46 -207
  49. package/scripts/web-surface-routing.mjs +286 -0
@@ -96,6 +96,10 @@ export function enumerateCliCommandsFromBin({
96
96
  } = {}) {
97
97
  const binSource = readText(root, binPath);
98
98
  const helpSource = readText(root, "scripts/buildchain-cli-help.mjs");
99
+ const registrySource = readText(
100
+ root,
101
+ "bin/internal/command-registry.mjs",
102
+ );
99
103
  const dispatchSource = [
100
104
  binSource,
101
105
  ...listFiles(root, "bin/internal", (name) => name.endsWith(".mjs")).map(
@@ -106,7 +110,14 @@ export function enumerateCliCommandsFromBin({
106
110
  const dispatch = [
107
111
  ...dispatchSource.matchAll(/if\s*\(\s*command\s*===\s*"([^"]+)"/g),
108
112
  ].map((match) => commandId(match[1]));
109
- return uniqueSorted([...usage.map((entry) => entry.id), ...dispatch]).map(
113
+ const registered = [
114
+ ...registrySource.matchAll(/\{\s*id:\s*"([^"]+)"/g),
115
+ ].map((match) => commandId(match[1]));
116
+ return uniqueSorted([
117
+ ...usage.map((entry) => entry.id),
118
+ ...dispatch,
119
+ ...registered,
120
+ ]).map(
110
121
  (id) => ({
111
122
  id,
112
123
  source: "bin/buildchain.mjs",
@@ -1189,6 +1189,28 @@ export function createArtifactEvidence({ assets = [], repository = "", tag = "",
1189
1189
  };
1190
1190
  }
1191
1191
 
1192
+ function firstTruthy(...values) {
1193
+ for (const value of values) {
1194
+ if (value) return value;
1195
+ }
1196
+ return values.at(-1);
1197
+ }
1198
+
1199
+ function releaseField(release, camelKey, snakeKey, ...fallbacks) {
1200
+ return optionalString(firstTruthy(release[camelKey], release[snakeKey], ...fallbacks));
1201
+ }
1202
+
1203
+ function optionalSections(entries) {
1204
+ return Object.fromEntries(entries.filter(([, value]) => value && (!Array.isArray(value) || value.length > 0)));
1205
+ }
1206
+
1207
+ function acceptedControllerSourceShas(input) {
1208
+ const { treeEquivalent, builtSourceSha, promotionChannelSha, sourceSha, recoveryTreeEquivalent } = input;
1209
+ return treeEquivalent && builtSourceSha && (promotionChannelSha === sourceSha || recoveryTreeEquivalent)
1210
+ ? [builtSourceSha]
1211
+ : [];
1212
+ }
1213
+
1192
1214
  export function createReleasePassport({
1193
1215
  cwd = process.cwd(),
1194
1216
  repository = "",
@@ -1272,14 +1294,10 @@ export function createReleasePassport({
1272
1294
  kfd1Section: normalizedKfd1?.passportSection,
1273
1295
  kfd3Section: normalizedKfd3?.passportSection,
1274
1296
  });
1275
- const builtSourceSha = optionalString(release.builtSourceSha || release.built_source_sha);
1276
- const builtSourceTreeSha = optionalString(
1277
- release.builtSourceTreeSha || release.built_source_tree_sha,
1278
- );
1279
- const promotionChannelSha = optionalString(release.promotionChannelSha || release.promotion_channel_sha);
1280
- const promotionChannelTreeSha = optionalString(
1281
- release.promotionChannelTreeSha || release.promotion_channel_tree_sha,
1282
- );
1297
+ const builtSourceSha = releaseField(release, "builtSourceSha", "built_source_sha");
1298
+ const builtSourceTreeSha = releaseField(release, "builtSourceTreeSha", "built_source_tree_sha");
1299
+ const promotionChannelSha = releaseField(release, "promotionChannelSha", "promotion_channel_sha");
1300
+ const promotionChannelTreeSha = releaseField(release, "promotionChannelTreeSha", "promotion_channel_tree_sha");
1283
1301
  const treeEquivalent = release.treeEquivalent === true;
1284
1302
  const recoveryTreeEquivalent = Boolean(
1285
1303
  treeEquivalent &&
@@ -1291,11 +1309,13 @@ export function createReleasePassport({
1291
1309
  receipts: controllerReceipts,
1292
1310
  references: controllerReceiptReferences,
1293
1311
  expectedSourceSha: sourceSha,
1294
- acceptedSourceShas: treeEquivalent && builtSourceSha && (
1295
- promotionChannelSha === sourceSha || recoveryTreeEquivalent
1296
- )
1297
- ? [builtSourceSha]
1298
- : [],
1312
+ acceptedSourceShas: acceptedControllerSourceShas({
1313
+ treeEquivalent,
1314
+ builtSourceSha,
1315
+ promotionChannelSha,
1316
+ sourceSha,
1317
+ recoveryTreeEquivalent,
1318
+ }),
1299
1319
  requirePassed: true,
1300
1320
  });
1301
1321
  const normalizedGitHubArtifactAttestations = (githubArtifactAttestations || [])
@@ -1306,37 +1326,13 @@ export function createReleasePassport({
1306
1326
  publishEvidence: normalizedPublishEvidence,
1307
1327
  publish,
1308
1328
  });
1309
- const releaseMaterialSha = optionalString(
1310
- release.releaseMaterialSha ||
1311
- release.release_material_sha ||
1312
- normalizedPublishEvidence?.releaseMaterialSha ||
1313
- normalizedTransaction?.releaseMaterialSha,
1314
- );
1315
- const releaseSha = optionalString(
1316
- release.releaseSha ||
1317
- release.release_sha ||
1318
- normalizedPublishEvidence?.releaseSha ||
1319
- normalizedTransaction?.releaseSha,
1320
- );
1321
- const targetRef = optionalString(release.targetRef || release.target_ref || normalizedPublishEvidence?.targetRef);
1322
- const packageDisplayVersion = optionalString(
1323
- packageVersion ||
1324
- normalizedPackageSet?.main?.version ||
1325
- normalizedPublishSummary?.packages?.find((entry) => entry.role === "main")?.publishedVersion ||
1326
- normalizedPublishEvidence?.version ||
1327
- normalizedTransaction?.version ||
1328
- readPackageVersion(cwd),
1329
- );
1330
- const publishedVersion = optionalString(
1331
- release.publishedVersion ||
1332
- release.published_version ||
1333
- packageDisplayVersion,
1334
- );
1335
- const internalVersion = optionalString(
1336
- release.internalVersion ||
1337
- release.internal_version ||
1338
- normalizedTransaction?.version,
1339
- );
1329
+ const releaseMaterialSha = releaseField(release, "releaseMaterialSha", "release_material_sha", normalizedPublishEvidence?.releaseMaterialSha, normalizedTransaction?.releaseMaterialSha);
1330
+ const releaseSha = releaseField(release, "releaseSha", "release_sha", normalizedPublishEvidence?.releaseSha, normalizedTransaction?.releaseSha);
1331
+ const targetRef = releaseField(release, "targetRef", "target_ref", normalizedPublishEvidence?.targetRef);
1332
+ const mainPublishedVersion = normalizedPublishSummary?.packages?.find((entry) => entry.role === "main")?.publishedVersion;
1333
+ const packageDisplayVersion = optionalString(firstTruthy(packageVersion, normalizedPackageSet?.main?.version, mainPublishedVersion, normalizedPublishEvidence?.version, normalizedTransaction?.version, readPackageVersion(cwd)));
1334
+ const publishedVersion = releaseField(release, "publishedVersion", "published_version", packageDisplayVersion);
1335
+ const internalVersion = releaseField(release, "internalVersion", "internal_version", normalizedTransaction?.version);
1340
1336
  return {
1341
1337
  schemaVersion: 1,
1342
1338
  contract: RELEASE_PASSPORT_CONTRACT,
@@ -1344,7 +1340,7 @@ export function createReleasePassport({
1344
1340
  surfaceTimestampPolicy: createSurfaceTimestampPolicy({
1345
1341
  generatedAt,
1346
1342
  publishedAt: optionalString(release.publishedAt || release.published_at || normalizedPublishEvidence?.publishedAt),
1347
- sourceRevision: optionalString(sourceSha || releaseSha || normalizedTransaction?.releaseSha),
1343
+ sourceRevision: optionalString(firstTruthy(sourceSha, releaseSha, normalizedTransaction?.releaseSha)),
1348
1344
  timestampPolicy: "ci-injected",
1349
1345
  deterministicInputs: [
1350
1346
  "release.sourceSha",
@@ -1366,14 +1362,14 @@ export function createReleasePassport({
1366
1362
  },
1367
1363
  release: {
1368
1364
  tag: normalizedTag,
1369
- publicTag: optionalString(release.publicTag || release.public_tag || normalizedTag),
1370
- internalTag: optionalString(release.internalTag || release.internal_tag || normalizedTag),
1365
+ publicTag: releaseField(release, "publicTag", "public_tag", normalizedTag),
1366
+ internalTag: releaseField(release, "internalTag", "internal_tag", normalizedTag),
1371
1367
  internalVersion,
1372
1368
  publishedVersion,
1373
- versionLabel: optionalString(release.versionLabel || release.version_label || publishedVersion || normalizedTag),
1369
+ versionLabel: releaseField(release, "versionLabel", "version_label", publishedVersion, normalizedTag),
1374
1370
  line: optionalString(line),
1375
1371
  sourceSha: optionalString(sourceSha),
1376
- channel: optionalString(release.channel || normalizedPublishEvidence?.channel || publish.channel),
1372
+ channel: optionalString(firstTruthy(release.channel, normalizedPublishEvidence?.channel, publish.channel)),
1377
1373
  targetRef,
1378
1374
  releaseSha,
1379
1375
  releaseMaterialSha,
@@ -1411,28 +1407,28 @@ export function createReleasePassport({
1411
1407
  compatibilityFixture: "self-hosted",
1412
1408
  note: "Runner facts are recorded in artifact evidence; the protocol does not require self-hosted runners.",
1413
1409
  },
1414
- ...(normalizedPackageSet ? { packageSet: normalizedPackageSet } : {}),
1415
- ...(normalizedPublishSummary ? { publish: normalizedPublishSummary } : {}),
1416
- ...(anchorManifest ? { anchorManifest } : {}),
1417
- ...(versionMaterial ? { versionMaterial } : {}),
1418
- ...(normalizedTrustedPublishing ? { trustedPublishing: normalizedTrustedPublishing } : {}),
1419
- ...(normalizedTransaction ? { transaction: normalizedTransaction } : {}),
1420
- ...(normalizedPromotionRouting ? { promotionRouting: normalizedPromotionRouting } : {}),
1421
- ...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
1422
- ...(normalizedBuildFacts.length > 0 ? { buildFacts: normalizedBuildFacts } : {}),
1423
- ...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
1424
- ...(normalizedDistTagPromotionEvidence ? { distTagPromotion: normalizedDistTagPromotionEvidence } : {}),
1425
- ...(normalizedKfd1 ? { [normalizedKfd1.key || kfd1Metadata.key]: normalizedKfd1.passportSection } : {}),
1426
- ...(normalizedKfd2 ? { "kfd-2": normalizedKfd2 } : {}),
1427
- ...(normalizedKfd3 ? { [normalizedKfd3.key || "kfd-3"]: normalizedKfd3.passportSection } : {}),
1428
- ...(normalizedKfdSupport ? { kfdSupport: normalizedKfdSupport } : {}),
1429
- ...(normalizedKfdAgentHub ? { kfdAgentHub: normalizedKfdAgentHub } : {}),
1430
- ...(invariantPassports ? { invariantPassports } : {}),
1431
- ...(releaseEvidence.length > 0 ? { releaseEvidence } : {}),
1432
- ...(normalizedControllerReceipts.length > 0 ? { controllerReceipts: normalizedControllerReceipts } : {}),
1433
- ...(normalizedGitHubArtifactAttestations.length > 0
1434
- ? { githubArtifactAttestations: normalizedGitHubArtifactAttestations }
1435
- : {}),
1410
+ ...optionalSections([
1411
+ ["packageSet", normalizedPackageSet],
1412
+ ["publish", normalizedPublishSummary],
1413
+ ["anchorManifest", anchorManifest],
1414
+ ["versionMaterial", versionMaterial],
1415
+ ["trustedPublishing", normalizedTrustedPublishing],
1416
+ ["transaction", normalizedTransaction],
1417
+ ["promotionRouting", normalizedPromotionRouting],
1418
+ ["buildSummary", normalizedBuildSummary],
1419
+ ["buildFacts", normalizedBuildFacts],
1420
+ ["platformArtifactManifests", normalizedPlatformArtifactManifests],
1421
+ ["distTagPromotion", normalizedDistTagPromotionEvidence],
1422
+ [firstTruthy(normalizedKfd1?.key, kfd1Metadata.key), normalizedKfd1?.passportSection],
1423
+ ["kfd-2", normalizedKfd2],
1424
+ [firstTruthy(normalizedKfd3?.key, "kfd-3"), normalizedKfd3?.passportSection],
1425
+ ["kfdSupport", normalizedKfdSupport],
1426
+ ["kfdAgentHub", normalizedKfdAgentHub],
1427
+ ["invariantPassports", invariantPassports],
1428
+ ["releaseEvidence", releaseEvidence],
1429
+ ["controllerReceipts", normalizedControllerReceipts],
1430
+ ["githubArtifactAttestations", normalizedGitHubArtifactAttestations],
1431
+ ]),
1436
1432
  versionImpact: normalizedImpact.versionImpact,
1437
1433
  surfaceImpacts: normalizedImpact.surfaceImpacts,
1438
1434
  artifacts: [
@@ -0,0 +1,64 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const SUPPORTED_CHANNELS = new Set(["alpha", "release"]);
4
+
5
+ export const RELEASE_PROPAGATION_PLAN_CONTRACT = "kungfu-buildchain-release-propagation-plan";
6
+
7
+ export function stableJson(value) {
8
+ return `${JSON.stringify(sortJson(value), null, 2)}\n`;
9
+ }
10
+
11
+ export function sortJson(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map(sortJson);
14
+ }
15
+ if (!value || typeof value !== "object") {
16
+ return value;
17
+ }
18
+ return Object.fromEntries(
19
+ Object.entries(value)
20
+ .sort(([left], [right]) => left.localeCompare(right))
21
+ .map(([key, entry]) => [key, sortJson(entry)]),
22
+ );
23
+ }
24
+
25
+ export function sha256Json(value) {
26
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
27
+ }
28
+
29
+ export function assertPlainObject(value, label) {
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
31
+ throw new Error(`${label} must be an object`);
32
+ }
33
+ return value;
34
+ }
35
+
36
+ export function assertString(value, label) {
37
+ if (typeof value !== "string" || value.trim() === "") {
38
+ throw new Error(`${label} must be a non-empty string`);
39
+ }
40
+ return value.trim();
41
+ }
42
+
43
+ export function optionalString(value) {
44
+ return value === undefined || value === null ? "" : String(value).trim();
45
+ }
46
+
47
+ export function normalizeChannel(value, label) {
48
+ const channel = assertString(value, label);
49
+ if (!SUPPORTED_CHANNELS.has(channel)) {
50
+ throw new Error(`${label} must be alpha or release`);
51
+ }
52
+ return channel;
53
+ }
54
+
55
+
56
+ export function assertExactFields(value, fields, label) {
57
+ const object = assertPlainObject(value, label);
58
+ const actual = Object.keys(object).sort();
59
+ const expected = [...fields].sort();
60
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
61
+ throw new Error(`${label} has an invalid field set`);
62
+ }
63
+ return object;
64
+ }
@@ -0,0 +1,59 @@
1
+ import { assertExactFields, assertString } from "./release-propagation-common.js";
2
+
3
+ const WEB_SURFACE_EXECUTION_PROFILE_CONTRACT = "kungfu-buildchain-github-web-surface-execution";
4
+
5
+ export function normalizeExecutionProfile(value, label) {
6
+ if (value === undefined || value === null) {
7
+ return null;
8
+ }
9
+ const profile = assertExactFields(value, [
10
+ "contract",
11
+ "workflow",
12
+ "productionReleaseLabel",
13
+ "productionReleaseHeadPrefix",
14
+ "productionStatusUrl",
15
+ "readbackUrls",
16
+ "updateCommand",
17
+ "prepareCommand",
18
+ "verifyCommand",
19
+ ], label);
20
+ if (profile.contract !== WEB_SURFACE_EXECUTION_PROFILE_CONTRACT) {
21
+ throw new Error(`${label}.contract must be ${WEB_SURFACE_EXECUTION_PROFILE_CONTRACT}`);
22
+ }
23
+ const productionStatusUrl = assertString(profile.productionStatusUrl, `${label}.productionStatusUrl`);
24
+ if (!/^https:\/\/[^?\s]+$/.test(productionStatusUrl)) {
25
+ throw new Error(`${label}.productionStatusUrl must be a stable HTTPS URL without query parameters`);
26
+ }
27
+ if (!Array.isArray(profile.readbackUrls) || profile.readbackUrls.length === 0) {
28
+ throw new Error(`${label}.readbackUrls must be a non-empty array`);
29
+ }
30
+ const readbackUrls = [...new Set(profile.readbackUrls.map((entry, index) => {
31
+ const url = assertString(entry, `${label}.readbackUrls[${index}]`);
32
+ if (!/^https:\/\/[^?\s]+$/.test(url)) {
33
+ throw new Error(`${label}.readbackUrls[${index}] must be a stable HTTPS URL without query parameters`);
34
+ }
35
+ return url;
36
+ }))].sort();
37
+ if (JSON.stringify(readbackUrls) !== JSON.stringify(profile.readbackUrls)) {
38
+ throw new Error(`${label}.readbackUrls must be sorted and unique`);
39
+ }
40
+ const command = (entry, field) => {
41
+ const text = assertString(entry, `${label}.${field}`);
42
+ if (/[\r\n]/.test(text)) {
43
+ throw new Error(`${label}.${field} must be a single-line command`);
44
+ }
45
+ return text;
46
+ };
47
+ return {
48
+ contract: WEB_SURFACE_EXECUTION_PROFILE_CONTRACT,
49
+ workflow: assertString(profile.workflow, `${label}.workflow`),
50
+ productionReleaseLabel: assertString(profile.productionReleaseLabel, `${label}.productionReleaseLabel`),
51
+ productionReleaseHeadPrefix: assertString(profile.productionReleaseHeadPrefix, `${label}.productionReleaseHeadPrefix`),
52
+ productionStatusUrl,
53
+ readbackUrls,
54
+ updateCommand: command(profile.updateCommand, "updateCommand"),
55
+ prepareCommand: command(profile.prepareCommand, "prepareCommand"),
56
+ verifyCommand: command(profile.verifyCommand, "verifyCommand"),
57
+ };
58
+ }
59
+
@@ -0,0 +1,196 @@
1
+ import {
2
+ assertPlainObject,
3
+ assertString,
4
+ normalizeChannel,
5
+ optionalString,
6
+ } from "./release-propagation-common.js";
7
+ import { assertCommitSha } from "./release-propagation-work-control.js";
8
+
9
+ function normalizeReleasePassport(passport = {}) {
10
+ if (!passport || typeof passport !== "object" || Array.isArray(passport)) {
11
+ throw new Error("upstreamRelease.releasePassport must be an object");
12
+ }
13
+ const normalized = {
14
+ url: assertString(passport.url, "upstreamRelease.releasePassport.url"),
15
+ sha256: assertString(passport.sha256 || passport.digest, "upstreamRelease.releasePassport.sha256"),
16
+ };
17
+ if (!/^[0-9a-f]{64}$/.test(normalized.sha256)) {
18
+ throw new Error("upstreamRelease.releasePassport.sha256 must be an exact SHA-256 digest");
19
+ }
20
+ if (/[?&](?:token|signature|x-amz-|x-goog-)/i.test(normalized.url)) {
21
+ throw new Error("upstreamRelease.releasePassport.url must not contain signed or credential parameters");
22
+ }
23
+ return normalized;
24
+ }
25
+
26
+ function normalizePackageFact(pkg = {}) {
27
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
28
+ throw new Error("upstreamRelease.package must be an object");
29
+ }
30
+ const normalized = {
31
+ name: assertString(pkg.name, "upstreamRelease.package.name"),
32
+ version: assertString(pkg.version, "upstreamRelease.package.version"),
33
+ integrity: assertString(pkg.integrity, "upstreamRelease.package.integrity"),
34
+ gitHead: assertCommitSha(pkg.gitHead || pkg.git_head, "upstreamRelease.package.gitHead"),
35
+ };
36
+ if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(normalized.integrity)) {
37
+ throw new Error("upstreamRelease.package.integrity must be an exact sha512 SRI value");
38
+ }
39
+ return normalized;
40
+ }
41
+
42
+ function normalizeDigestUrlFact(value = {}, label) {
43
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
44
+ throw new Error(`${label} must be an object`);
45
+ }
46
+ const normalized = {
47
+ url: assertString(value.url, `${label}.url`),
48
+ sha256: assertString(value.sha256 || value.digest, `${label}.sha256`),
49
+ };
50
+ if (!/^[0-9a-f]{64}$/.test(normalized.sha256)) {
51
+ throw new Error(`${label}.sha256 must be an exact SHA-256 digest`);
52
+ }
53
+ if (/[?&](?:token|signature|x-amz-|x-goog-)/i.test(normalized.url)) {
54
+ throw new Error(`${label}.url must not contain signed or credential parameters`);
55
+ }
56
+ return normalized;
57
+ }
58
+
59
+ function normalizeOptionalPathDigestUrlFact(value = undefined, label) {
60
+ if (value === undefined || value === null) {
61
+ return undefined;
62
+ }
63
+ const normalized = normalizeDigestUrlFact(value, label);
64
+ return {
65
+ path: optionalString(value.path),
66
+ bytes: value.bytes === undefined ? undefined : Number(value.bytes),
67
+ ...normalized,
68
+ };
69
+ }
70
+
71
+ function normalizePublicationArtifactFact(value = undefined) {
72
+ if (value === undefined || value === null) {
73
+ return undefined;
74
+ }
75
+ const artifact = assertPlainObject(value, "upstreamRelease.publicationArtifact");
76
+ return {
77
+ id: optionalString(artifact.id),
78
+ kind: optionalString(artifact.kind),
79
+ version: assertString(artifact.version, "upstreamRelease.publicationArtifact.version"),
80
+ canonicalUrl: assertString(
81
+ artifact.canonicalUrl || artifact.canonical_url,
82
+ "upstreamRelease.publicationArtifact.canonicalUrl",
83
+ ),
84
+ latestUrl: assertString(
85
+ artifact.latestUrl || artifact.latest_url,
86
+ "upstreamRelease.publicationArtifact.latestUrl",
87
+ ),
88
+ latestEvidenceUrl: optionalString(artifact.latestEvidenceUrl || artifact.latest_evidence_url),
89
+ immutableVersionUrl: assertString(
90
+ artifact.immutableVersionUrl || artifact.immutable_version_url || artifact.immutableVersionPrefix || artifact.immutable_version_prefix,
91
+ "upstreamRelease.publicationArtifact.immutableVersionUrl",
92
+ ),
93
+ immutableVersionPrefix: optionalString(artifact.immutableVersionPrefix || artifact.immutable_version_prefix),
94
+ registry: normalizeDigestUrlFact(artifact.registry, "upstreamRelease.publicationArtifact.registry"),
95
+ manifest: normalizeDigestUrlFact(artifact.manifest, "upstreamRelease.publicationArtifact.manifest"),
96
+ passport: normalizeDigestUrlFact(artifact.passport, "upstreamRelease.publicationArtifact.passport"),
97
+ primaryArtifact: normalizeOptionalPathDigestUrlFact(
98
+ artifact.primaryArtifact || artifact.primary_artifact,
99
+ "upstreamRelease.publicationArtifact.primaryArtifact",
100
+ ),
101
+ sourceBundle: normalizeOptionalPathDigestUrlFact(
102
+ artifact.sourceBundle || artifact.source_bundle,
103
+ "upstreamRelease.publicationArtifact.sourceBundle",
104
+ ),
105
+ };
106
+ }
107
+
108
+ export function normalizeUpstreamRelease(input) {
109
+ const release = assertPlainObject(input, "upstreamRelease");
110
+ const publicationArtifact = normalizePublicationArtifactFact(release.publicationArtifact || release.publication_artifact);
111
+ const packageFact = release.package === undefined ? undefined : normalizePackageFact(release.package);
112
+ if (!packageFact && !publicationArtifact) {
113
+ throw new Error("upstreamRelease requires package or publicationArtifact");
114
+ }
115
+ const repository = assertString(release.repository, "upstreamRelease.repository");
116
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repository)) {
117
+ throw new Error("upstreamRelease.repository must be owner/repo");
118
+ }
119
+ const sourceSha = assertCommitSha(release.sourceSha || release.source_sha, "upstreamRelease.sourceSha");
120
+ const tagTargetSha = assertCommitSha(
121
+ release.tagTargetSha || release.tag_target_sha,
122
+ "upstreamRelease.tagTargetSha",
123
+ );
124
+ const version = packageFact?.version || publicationArtifact?.version || "";
125
+ const tag = assertString(release.tag, "upstreamRelease.tag");
126
+ if (tag !== `v${version}`) {
127
+ throw new Error("upstreamRelease.tag must exactly match the published version");
128
+ }
129
+ if (tagTargetSha !== sourceSha) {
130
+ throw new Error("upstreamRelease tag target must match sourceSha");
131
+ }
132
+ if (packageFact && packageFact.gitHead !== sourceSha) {
133
+ throw new Error("upstreamRelease package gitHead must match sourceSha");
134
+ }
135
+ if (packageFact && publicationArtifact && packageFact.version !== publicationArtifact.version) {
136
+ throw new Error("upstreamRelease package and publication artifact versions must match");
137
+ }
138
+ return {
139
+ repository,
140
+ channel: normalizeChannel(release.channel, "upstreamRelease.channel"),
141
+ tag,
142
+ tagTargetSha,
143
+ sourceSha,
144
+ package: packageFact,
145
+ publicationArtifact,
146
+ releasePassport: normalizeReleasePassport(release.releasePassport || release.release_passport),
147
+ siteBundle: release.siteBundle || release.site_bundle
148
+ ? {
149
+ manifestSha256: assertString(
150
+ release.siteBundle?.manifestSha256 || release.site_bundle?.manifest_sha256,
151
+ "upstreamRelease.siteBundle.manifestSha256",
152
+ ),
153
+ }
154
+ : undefined,
155
+ };
156
+ }
157
+
158
+ export function verifyReleaseLockBinding({ release, lock, downstream, propagationKey }) {
159
+ const normalizedRelease = normalizeUpstreamRelease(release);
160
+ const lockRelease = normalizeUpstreamRelease({
161
+ repository: lock.upstream?.repository,
162
+ channel: lock.upstream?.channel,
163
+ tag: lock.upstream?.tag,
164
+ tagTargetSha: lock.upstream?.tagTargetSha,
165
+ sourceSha: lock.upstream?.sourceSha,
166
+ package: lock.upstream?.package,
167
+ publicationArtifact: lock.upstream?.publicationArtifact,
168
+ releasePassport: lock.upstream?.releasePassport,
169
+ siteBundle: lock.upstream?.siteBundle,
170
+ });
171
+ if (JSON.stringify(lockRelease) !== JSON.stringify(normalizedRelease)) {
172
+ throw new Error("release propagation work upstream release disagrees with its lock");
173
+ }
174
+ const expectedCoordinates = {
175
+ target: lock.downstream?.node,
176
+ repository: lock.downstream?.repository,
177
+ channel: lock.downstream?.channel,
178
+ baseRef: lock.downstream?.baseRef,
179
+ branch: lock.propagation?.branch,
180
+ lockPath: lock.downstream?.lockPath,
181
+ propagationKey: lock.propagation?.propagationKey,
182
+ };
183
+ const actualCoordinates = {
184
+ target: downstream.target,
185
+ repository: downstream.repository,
186
+ channel: downstream.channel,
187
+ baseRef: downstream.baseRef,
188
+ branch: downstream.branch,
189
+ lockPath: downstream.lockPath,
190
+ propagationKey,
191
+ };
192
+ if (JSON.stringify(actualCoordinates) !== JSON.stringify(expectedCoordinates)) {
193
+ throw new Error("release propagation work downstream coordinates disagree with its lock");
194
+ }
195
+ return normalizedRelease;
196
+ }