@kungfu-tech/buildchain 3.0.2-alpha.7 → 3.0.2-alpha.8
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.
- package/README.md +3 -2
- package/contracts/auditable-demo-media-profiles-v1.json +168 -0
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +103 -0
- package/contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt +2 -0
- package/contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json +16 -0
- package/contracts/fixtures/auditable-demo-web-delivery-v1/scene.json +12 -0
- package/dist/site/buildchain-contract.json +44 -26
- package/dist/site/buildchain-site.json +42 -32
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +11 -3
- package/dist/site/kfd-claims.json +74 -8
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -7
- package/dist/site/node-api-registry.json +44 -5
- package/dist/site/page-registry.json +30 -20
- package/dist/site/public-surface-audit.json +38 -10
- package/dist/site/publication-authority-registry.json +26 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +41 -7
- package/docs/MAP.md +2 -0
- package/docs/auditable-demo.md +55 -3
- package/docs/dev-alpha-candidate-patrol.md +9 -0
- package/docs/github-artifact-attestation.md +1 -1
- package/docs/release-governance.md +32 -0
- package/docs/reusable-build-surface.md +73 -58
- package/docs/runtime-train-validation.md +21 -0
- package/docs/versioning.md +1 -0
- package/package.json +5 -1
- package/packages/core/artifact-signing-result.js +228 -0
- package/packages/core/artifact-signing.js +412 -0
- package/packages/core/buildchain-config.js +58 -0
- package/packages/core/buildchain-contract.js +8 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/detached-artifact-signature.js +121 -0
- package/packages/core/github-governance-authority.js +6 -0
- package/packages/core/index.js +27 -0
- package/scripts/auditable-demo.mjs +491 -22
- package/scripts/buildchain-channel-router.mjs +8 -2
- package/scripts/check-inventory.mjs +5 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +18 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
- package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
- package/scripts/generate-site-bundle.mjs +3 -0
- package/scripts/import-artifact-signing-results.mjs +76 -0
- package/scripts/inspect-artifact-signing-requests.mjs +101 -0
- package/scripts/materialize-artifact-signing-request.mjs +66 -0
- package/scripts/merge-artifact-signing-results.mjs +76 -0
- package/scripts/release-line-policy.mjs +27 -0
- package/scripts/runtime-ref-core.mjs +10 -6
- package/scripts/seal-artifact-signing-requests.mjs +368 -0
- package/scripts/sign-detached-artifact-requests.mjs +237 -0
- package/scripts/verify-artifact-signing-results.mjs +99 -0
package/packages/core/index.js
CHANGED
|
@@ -262,6 +262,33 @@ export {
|
|
|
262
262
|
verifyArtifactVerificationEnvelope,
|
|
263
263
|
} from "./artifact-verification-envelope.js";
|
|
264
264
|
|
|
265
|
+
export {
|
|
266
|
+
ARTIFACT_SIGNING_AUTHORITY_CONTRACT,
|
|
267
|
+
ARTIFACT_SIGNING_RECEIPT_CONTRACT,
|
|
268
|
+
ARTIFACT_SIGNING_REQUEST_CONTRACT,
|
|
269
|
+
artifactSigningDigest,
|
|
270
|
+
createArtifactSigningReceipt,
|
|
271
|
+
createArtifactSigningRequest,
|
|
272
|
+
listArtifactSigningProfiles,
|
|
273
|
+
resolveArtifactSigningProfile,
|
|
274
|
+
validateArtifactSigningReceipt,
|
|
275
|
+
validateArtifactSigningRequest,
|
|
276
|
+
} from "./artifact-signing.js";
|
|
277
|
+
|
|
278
|
+
export {
|
|
279
|
+
ARTIFACT_SIGNING_RESULT_CONTRACT,
|
|
280
|
+
artifactSigningEvidenceDigest,
|
|
281
|
+
createArtifactSigningResult,
|
|
282
|
+
validateArtifactSigningResult,
|
|
283
|
+
verifyArtifactSigningResultFiles,
|
|
284
|
+
} from "./artifact-signing-result.js";
|
|
285
|
+
|
|
286
|
+
export {
|
|
287
|
+
DETACHED_ARTIFACT_SIGNATURE_CONTRACT,
|
|
288
|
+
signDetachedArtifactRequest,
|
|
289
|
+
verifyDetachedArtifactSignature,
|
|
290
|
+
} from "./detached-artifact-signature.js";
|
|
291
|
+
|
|
265
292
|
export {
|
|
266
293
|
BUILDCHAIN_CONTRACT_LOCK,
|
|
267
294
|
BUILDCHAIN_RUNTIME_CONTRACT_WORLD,
|
|
@@ -11,6 +11,12 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
12
12
|
const IMAGE_PATTERN = /^[a-z0-9][a-z0-9./_-]*@sha256:[0-9a-f]{64}$/;
|
|
13
13
|
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
14
|
+
const MAX_BUNDLE_MEMBER_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
const SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const MEDIA_PROFILE_CATALOG = path.resolve(
|
|
17
|
+
SCRIPT_DIRECTORY,
|
|
18
|
+
"../contracts/auditable-demo-media-profiles-v1.json",
|
|
19
|
+
);
|
|
14
20
|
const REQUIRED_ADAPTER_FILES = [
|
|
15
21
|
"complete-transcript.txt",
|
|
16
22
|
"public-projection.json",
|
|
@@ -119,7 +125,10 @@ function verifyChecksums(root, checksumName = "checksums.sha256") {
|
|
|
119
125
|
const target = resolveInside(root, member, "checksum member");
|
|
120
126
|
invariant(!declared.has(member), `duplicate checksum member: ${member}`);
|
|
121
127
|
declared.add(member);
|
|
122
|
-
invariant(
|
|
128
|
+
invariant(
|
|
129
|
+
sha256(readRegular(target, member, MAX_BUNDLE_MEMBER_BYTES)).slice(7) === match[1],
|
|
130
|
+
`checksum mismatch: ${member}`,
|
|
131
|
+
);
|
|
123
132
|
}
|
|
124
133
|
const actual = listFiles(root).filter((name) => name !== checksumName);
|
|
125
134
|
invariant(
|
|
@@ -392,40 +401,465 @@ function prepareSmoke(values) {
|
|
|
392
401
|
writeJson(path.join(output, "public-projection.json"), projection);
|
|
393
402
|
}
|
|
394
403
|
|
|
395
|
-
function
|
|
404
|
+
function semanticRoot(value) {
|
|
405
|
+
return sha256(Buffer.from(stableJson(value)));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function validateBudgetBasis(entry, label) {
|
|
409
|
+
exactKeys(
|
|
410
|
+
entry.budgetBasis,
|
|
411
|
+
["evidence", "observedPath", "observedBytes", "multiplier", "rounding"],
|
|
412
|
+
[],
|
|
413
|
+
`${label}.budgetBasis`,
|
|
414
|
+
);
|
|
415
|
+
invariant(
|
|
416
|
+
entry.budgetBasis.evidence === "contracts/evidence/auditable-demo-web-delivery-v1.json",
|
|
417
|
+
`${label}.budgetBasis.evidence is unsupported`,
|
|
418
|
+
);
|
|
419
|
+
text(entry.budgetBasis.observedPath, 1, 128, `${label}.budgetBasis.observedPath`);
|
|
420
|
+
const observedBytes = integer(
|
|
421
|
+
entry.budgetBasis.observedBytes,
|
|
422
|
+
1,
|
|
423
|
+
MAX_BUNDLE_MEMBER_BYTES,
|
|
424
|
+
`${label}.budgetBasis.observedBytes`,
|
|
425
|
+
);
|
|
426
|
+
const multiplier = integer(entry.budgetBasis.multiplier, 1, 64, `${label}.budgetBasis.multiplier`);
|
|
427
|
+
invariant(entry.budgetBasis.rounding === "next-power-of-two", `${label}.budgetBasis.rounding is unsupported`);
|
|
428
|
+
const expected = 2 ** Math.ceil(Math.log2(observedBytes * multiplier));
|
|
429
|
+
invariant(entry.maximumBytes === expected, `${label}.maximumBytes does not match its measured budget basis`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function loadMediaProfile(profileId) {
|
|
433
|
+
const catalog = readJson(MEDIA_PROFILE_CATALOG, "auditable demo media profile catalog");
|
|
434
|
+
invariant(
|
|
435
|
+
catalog.schema === "buildchain.auditable-demo-media-profiles/v1",
|
|
436
|
+
"unsupported media profile catalog",
|
|
437
|
+
);
|
|
438
|
+
invariant(catalog.profiles && typeof catalog.profiles === "object", "media profile catalog is invalid");
|
|
439
|
+
const seen = new Set();
|
|
440
|
+
const resolve = (identifier) => {
|
|
441
|
+
invariant(!seen.has(identifier), `media profile inheritance cycle: ${identifier}`);
|
|
442
|
+
const declared = catalog.profiles[identifier];
|
|
443
|
+
invariant(declared && typeof declared === "object", `unsupported media profile: ${identifier}`);
|
|
444
|
+
seen.add(identifier);
|
|
445
|
+
const inherited = declared.extends ? resolve(declared.extends) : {
|
|
446
|
+
mode: "archive",
|
|
447
|
+
renditions: [],
|
|
448
|
+
singletonRoles: [],
|
|
449
|
+
additionalRenditions: null,
|
|
450
|
+
};
|
|
451
|
+
seen.delete(identifier);
|
|
452
|
+
const byPath = new Map(inherited.renditions.map((entry) => [entry.path, entry]));
|
|
453
|
+
for (const entry of declared.renditions || []) {
|
|
454
|
+
invariant(entry && typeof entry === "object" && typeof entry.path === "string", `${identifier} rendition is invalid`);
|
|
455
|
+
byPath.set(entry.path, { ...(byPath.get(entry.path) || {}), ...entry });
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
mode: declared.mode || inherited.mode,
|
|
459
|
+
renditions: [...byPath.values()],
|
|
460
|
+
singletonRoles: [...new Set([...(inherited.singletonRoles || []), ...(declared.singletonRoles || [])])],
|
|
461
|
+
additionalRenditions: declared.additionalRenditions || inherited.additionalRenditions || null,
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
const resolved = resolve(profileId);
|
|
465
|
+
const profile = { id: profileId, ...resolved };
|
|
466
|
+
for (const [index, entry] of profile.renditions.entries()) {
|
|
467
|
+
if (profile.mode === "web-delivery") {
|
|
468
|
+
invariant(entry.budgetBasis, `${profileId}.renditions[${index}].budgetBasis is required`);
|
|
469
|
+
validateBudgetBasis(entry, `${profileId}.renditions[${index}]`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
catalog,
|
|
474
|
+
catalogRoot: semanticRoot(catalog),
|
|
475
|
+
profile,
|
|
476
|
+
profileRoot: semanticRoot(profile),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function inspectIsoBmffFastStart(filePath) {
|
|
481
|
+
const bytes = readRegular(filePath, "MP4 rendition", MAX_BUNDLE_MEMBER_BYTES);
|
|
482
|
+
let offset = 0;
|
|
483
|
+
let moovOffset = -1;
|
|
484
|
+
let mdatOffset = -1;
|
|
485
|
+
let ftypSeen = false;
|
|
486
|
+
while (offset + 8 <= bytes.length) {
|
|
487
|
+
let size = bytes.readUInt32BE(offset);
|
|
488
|
+
const type = bytes.toString("ascii", offset + 4, offset + 8);
|
|
489
|
+
let headerSize = 8;
|
|
490
|
+
if (size === 1) {
|
|
491
|
+
invariant(offset + 16 <= bytes.length, "MP4 extended box header is truncated");
|
|
492
|
+
const extended = bytes.readBigUInt64BE(offset + 8);
|
|
493
|
+
invariant(extended <= BigInt(Number.MAX_SAFE_INTEGER), "MP4 box size is too large");
|
|
494
|
+
size = Number(extended);
|
|
495
|
+
headerSize = 16;
|
|
496
|
+
} else if (size === 0) {
|
|
497
|
+
size = bytes.length - offset;
|
|
498
|
+
}
|
|
499
|
+
invariant(size >= headerSize && offset + size <= bytes.length, "MP4 box layout is invalid");
|
|
500
|
+
if (type === "ftyp") ftypSeen = true;
|
|
501
|
+
if (type === "moov" && moovOffset === -1) moovOffset = offset;
|
|
502
|
+
if (type === "mdat" && mdatOffset === -1) mdatOffset = offset;
|
|
503
|
+
offset += size;
|
|
504
|
+
}
|
|
505
|
+
invariant(offset === bytes.length && ftypSeen, "MP4 top-level boxes are incomplete");
|
|
506
|
+
if (moovOffset === -1 || mdatOffset === -1) return "missing-moov-or-mdat";
|
|
507
|
+
return moovOffset < mdatOffset ? "moov-before-mdat" : "mdat-before-moov";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function rationalNumber(value) {
|
|
511
|
+
const match = /^([0-9]+)\/([0-9]+)$/.exec(String(value || ""));
|
|
512
|
+
if (!match || Number(match[2]) === 0) return 0;
|
|
513
|
+
return Number(match[1]) / Number(match[2]);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function inspectMediaFile(filePath) {
|
|
517
|
+
const result = spawnSync(
|
|
518
|
+
"ffprobe",
|
|
519
|
+
[
|
|
520
|
+
"-v", "error",
|
|
521
|
+
"-show_entries", "format=format_name,duration:stream=codec_type,codec_name,pix_fmt,width,height,avg_frame_rate",
|
|
522
|
+
"-of", "json",
|
|
523
|
+
filePath,
|
|
524
|
+
],
|
|
525
|
+
{ encoding: "utf8", maxBuffer: 1024 * 1024 },
|
|
526
|
+
);
|
|
527
|
+
invariant(!result.error && result.status === 0, `ffprobe failed for ${path.basename(filePath)}`);
|
|
528
|
+
let parsed;
|
|
529
|
+
try {
|
|
530
|
+
parsed = JSON.parse(result.stdout);
|
|
531
|
+
} catch {
|
|
532
|
+
throw new Error(`ffprobe returned invalid JSON for ${path.basename(filePath)}`);
|
|
533
|
+
}
|
|
534
|
+
const streams = Array.isArray(parsed.streams) ? parsed.streams : [];
|
|
535
|
+
const videos = streams.filter((entry) => entry.codec_type === "video");
|
|
536
|
+
const audioStreams = streams.filter((entry) => entry.codec_type === "audio").length;
|
|
537
|
+
invariant(videos.length === 1, `${path.basename(filePath)} must contain exactly one video or image stream`);
|
|
538
|
+
const video = videos[0];
|
|
539
|
+
const formatNames = String(parsed.format?.format_name || "").split(",");
|
|
540
|
+
let container = formatNames.includes("mp4") ? "mp4" : "";
|
|
541
|
+
if (formatNames.includes("webm")) container = "webm";
|
|
542
|
+
if (formatNames.includes("gif") || video.codec_name === "gif") container = "gif";
|
|
543
|
+
if (video.codec_name === "png") container = "png";
|
|
544
|
+
if (video.codec_name === "webp") container = "webp";
|
|
545
|
+
if (video.codec_name === "av1" && formatNames.includes("avif")) container = "avif";
|
|
546
|
+
const duration = Number(parsed.format?.duration || 0);
|
|
547
|
+
return {
|
|
548
|
+
container,
|
|
549
|
+
videoCodec: String(video.codec_name || ""),
|
|
550
|
+
pixelFormat: String(video.pix_fmt || ""),
|
|
551
|
+
width: Number(video.width || 0),
|
|
552
|
+
height: Number(video.height || 0),
|
|
553
|
+
durationMs: Number.isFinite(duration) ? Math.round(duration * 1000) : 0,
|
|
554
|
+
frameRate: rationalNumber(video.avg_frame_rate),
|
|
555
|
+
audioStreams,
|
|
556
|
+
progressiveDownload: container === "mp4" ? inspectIsoBmffFastStart(filePath) : "not-applicable",
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function inspectRendererMedia(values) {
|
|
561
|
+
const renderOutput = path.resolve(required(values, "--render-output"));
|
|
562
|
+
const output = path.resolve(required(values, "--output"));
|
|
563
|
+
const rendererImage = required(values, "--renderer-image");
|
|
564
|
+
invariant(IMAGE_PATTERN.test(rendererImage), "renderer image must use an immutable sha256 coordinate");
|
|
565
|
+
invariant(!fs.existsSync(output), "media inspection output must not already exist");
|
|
566
|
+
const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
|
|
567
|
+
invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
|
|
568
|
+
invariant(manifest.renderer?.image === rendererImage, "renderer manifest image coordinate mismatch");
|
|
569
|
+
const members = Object.keys(manifest.outputs || {})
|
|
570
|
+
.filter((name) => name !== "media-probe.json")
|
|
571
|
+
.sort()
|
|
572
|
+
.map((name) => {
|
|
573
|
+
const target = resolveInside(renderOutput, name, "media inspection member");
|
|
574
|
+
const bytes = readRegular(target, name, MAX_BUNDLE_MEMBER_BYTES);
|
|
575
|
+
return {
|
|
576
|
+
path: name,
|
|
577
|
+
root: sha256(bytes),
|
|
578
|
+
bytes: bytes.length,
|
|
579
|
+
facts: inspectMediaFile(target),
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
const body = {
|
|
583
|
+
schema: "buildchain.auditable-demo-media-inspection/v1",
|
|
584
|
+
rendererImage,
|
|
585
|
+
members,
|
|
586
|
+
};
|
|
587
|
+
writeJson(output, { ...body, inspectionRoot: semanticRoot(body) });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function qualifyMediaFixture(values) {
|
|
591
|
+
const renderOutput = path.resolve(required(values, "--render-output"));
|
|
592
|
+
const output = path.resolve(required(values, "--output"));
|
|
593
|
+
const rendererImage = required(values, "--renderer-image");
|
|
594
|
+
const rendererSourceRepository = required(values, "--renderer-source-repository");
|
|
595
|
+
const rendererSourceRef = required(values, "--renderer-source-ref");
|
|
596
|
+
const rendererSourceSha = required(values, "--renderer-source-sha");
|
|
597
|
+
const mediaProfile = required(values, "--media-profile");
|
|
598
|
+
invariant(IMAGE_PATTERN.test(rendererImage), "renderer image must use an immutable sha256 coordinate");
|
|
599
|
+
invariant(/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/.test(rendererSourceRepository), "renderer source repository is invalid");
|
|
600
|
+
invariant(/^refs\/tags\/[a-zA-Z0-9._-]+$/.test(rendererSourceRef), "renderer source ref must be an exact tag ref");
|
|
601
|
+
invariant(/^[0-9a-f]{40}$/.test(rendererSourceSha), "renderer source SHA must be exact");
|
|
602
|
+
invariant(!fs.existsSync(output), "media fixture evidence output must not already exist");
|
|
603
|
+
const mediaInspection = loadMediaInspection(
|
|
604
|
+
path.resolve(required(values, "--media-inspection")),
|
|
605
|
+
renderOutput,
|
|
606
|
+
rendererImage,
|
|
607
|
+
);
|
|
608
|
+
const verified = verifyRendererOutput(renderOutput, rendererImage, {
|
|
609
|
+
scene: path.join(renderOutput, "scene.json"),
|
|
610
|
+
transcript: path.join(renderOutput, "complete-transcript.txt"),
|
|
611
|
+
projection: path.join(renderOutput, "public-projection.json"),
|
|
612
|
+
}, {
|
|
613
|
+
mediaProfile,
|
|
614
|
+
inspectMedia: mediaInspection.inspectMedia,
|
|
615
|
+
inspectionRoot: mediaInspection.inspectionRoot,
|
|
616
|
+
});
|
|
617
|
+
const body = {
|
|
618
|
+
schema: "buildchain.auditable-demo-media-profile-fixture/v1",
|
|
619
|
+
renderer: {
|
|
620
|
+
image: rendererImage,
|
|
621
|
+
sourceRepository: rendererSourceRepository,
|
|
622
|
+
sourceRef: rendererSourceRef,
|
|
623
|
+
sourceSha: rendererSourceSha,
|
|
624
|
+
},
|
|
625
|
+
inputs: Object.fromEntries(
|
|
626
|
+
["complete-transcript.txt", "public-projection.json", "scene.json"].map((name) => [
|
|
627
|
+
name,
|
|
628
|
+
sha256(readRegular(path.join(renderOutput, name), `fixture ${name}`)),
|
|
629
|
+
]),
|
|
630
|
+
),
|
|
631
|
+
rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
|
|
632
|
+
qualification: verified.qualification,
|
|
633
|
+
};
|
|
634
|
+
writeJson(output, { ...body, evidenceRoot: semanticRoot(body) });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function loadMediaInspection(filePath, renderOutput, rendererImage) {
|
|
638
|
+
const value = readJson(filePath, "media inspection");
|
|
639
|
+
exactKeys(value, ["schema", "rendererImage", "members", "inspectionRoot"], [], "mediaInspection");
|
|
640
|
+
invariant(value.schema === "buildchain.auditable-demo-media-inspection/v1", "unsupported media inspection schema");
|
|
641
|
+
invariant(value.rendererImage === rendererImage, "media inspection renderer image mismatch");
|
|
642
|
+
invariant(Array.isArray(value.members), "mediaInspection.members must be an array");
|
|
643
|
+
const body = {
|
|
644
|
+
schema: value.schema,
|
|
645
|
+
rendererImage: value.rendererImage,
|
|
646
|
+
members: value.members,
|
|
647
|
+
};
|
|
648
|
+
invariant(value.inspectionRoot === semanticRoot(body), "media inspection root mismatch");
|
|
649
|
+
const byPath = new Map();
|
|
650
|
+
for (const [index, entry] of value.members.entries()) {
|
|
651
|
+
exactKeys(entry, ["path", "root", "bytes", "facts"], [], `mediaInspection.members[${index}]`);
|
|
652
|
+
text(entry.path, 1, 256, `mediaInspection.members[${index}].path`);
|
|
653
|
+
invariant(DIGEST_PATTERN.test(entry.root), `mediaInspection.members[${index}].root is invalid`);
|
|
654
|
+
integer(entry.bytes, 1, MAX_BUNDLE_MEMBER_BYTES, `mediaInspection.members[${index}].bytes`);
|
|
655
|
+
exactKeys(
|
|
656
|
+
entry.facts,
|
|
657
|
+
[
|
|
658
|
+
"container",
|
|
659
|
+
"videoCodec",
|
|
660
|
+
"pixelFormat",
|
|
661
|
+
"width",
|
|
662
|
+
"height",
|
|
663
|
+
"durationMs",
|
|
664
|
+
"frameRate",
|
|
665
|
+
"audioStreams",
|
|
666
|
+
"progressiveDownload",
|
|
667
|
+
],
|
|
668
|
+
[],
|
|
669
|
+
`mediaInspection.members[${index}].facts`,
|
|
670
|
+
);
|
|
671
|
+
const facts = {
|
|
672
|
+
container: text(entry.facts.container, 1, 32, `mediaInspection.members[${index}].facts.container`),
|
|
673
|
+
videoCodec: text(entry.facts.videoCodec, 1, 32, `mediaInspection.members[${index}].facts.videoCodec`),
|
|
674
|
+
pixelFormat: text(entry.facts.pixelFormat, 0, 32, `mediaInspection.members[${index}].facts.pixelFormat`),
|
|
675
|
+
width: integer(entry.facts.width, 1, 16384, `mediaInspection.members[${index}].facts.width`),
|
|
676
|
+
height: integer(entry.facts.height, 1, 16384, `mediaInspection.members[${index}].facts.height`),
|
|
677
|
+
durationMs: integer(entry.facts.durationMs, 0, 3_600_000, `mediaInspection.members[${index}].facts.durationMs`),
|
|
678
|
+
frameRate: entry.facts.frameRate,
|
|
679
|
+
audioStreams: integer(entry.facts.audioStreams, 0, 64, `mediaInspection.members[${index}].facts.audioStreams`),
|
|
680
|
+
progressiveDownload: text(
|
|
681
|
+
entry.facts.progressiveDownload,
|
|
682
|
+
1,
|
|
683
|
+
32,
|
|
684
|
+
`mediaInspection.members[${index}].facts.progressiveDownload`,
|
|
685
|
+
),
|
|
686
|
+
};
|
|
687
|
+
invariant(
|
|
688
|
+
Number.isFinite(facts.frameRate) && facts.frameRate >= 0 && facts.frameRate <= 240,
|
|
689
|
+
`mediaInspection.members[${index}].facts.frameRate is out of range`,
|
|
690
|
+
);
|
|
691
|
+
invariant(!byPath.has(entry.path), `duplicate media inspection member: ${entry.path}`);
|
|
692
|
+
const target = resolveInside(renderOutput, entry.path, "media inspection member");
|
|
693
|
+
const bytes = readRegular(target, entry.path, MAX_BUNDLE_MEMBER_BYTES);
|
|
694
|
+
invariant(entry.root === sha256(bytes), `media inspection member root mismatch: ${entry.path}`);
|
|
695
|
+
invariant(entry.bytes === bytes.length, `media inspection member byte count mismatch: ${entry.path}`);
|
|
696
|
+
byPath.set(entry.path, facts);
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
inspectionRoot: value.inspectionRoot,
|
|
700
|
+
inspectMedia: (target) => {
|
|
701
|
+
const facts = byPath.get(path.relative(renderOutput, target).split(path.sep).join("/"));
|
|
702
|
+
invariant(facts, `media inspection facts are missing: ${path.basename(target)}`);
|
|
703
|
+
return facts;
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function constraintsForAdditionalRendition(entry, policy) {
|
|
709
|
+
const common = {
|
|
710
|
+
path: entry.path,
|
|
711
|
+
role: entry.role,
|
|
712
|
+
mimeType: entry.mimeType,
|
|
713
|
+
maximumBytes: policy.maximumBytesByMimeType[entry.mimeType],
|
|
714
|
+
audioStreams: 0,
|
|
715
|
+
};
|
|
716
|
+
if (entry.mimeType === "video/mp4") {
|
|
717
|
+
return { ...common, container: "mp4", videoCodec: "h264", pixelFormat: "yuv420p", progressiveDownload: "moov-before-mdat" };
|
|
718
|
+
}
|
|
719
|
+
if (entry.mimeType === "video/webm") {
|
|
720
|
+
return { ...common, container: "webm", videoCodec: "vp9", pixelFormat: "yuv420p" };
|
|
721
|
+
}
|
|
722
|
+
if (entry.mimeType === "image/webp") return { ...common, container: "webp", videoCodec: "webp" };
|
|
723
|
+
if (entry.mimeType === "image/avif") return { ...common, container: "avif", videoCodec: "av1" };
|
|
724
|
+
throw new Error(`additional rendition MIME type is not allowed: ${entry.mimeType}`);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function qualifyRendererOutput(renderOutput, manifest, scene, profileId, inspectMedia, inspectionRoot = "") {
|
|
728
|
+
const loaded = loadMediaProfile(profileId);
|
|
729
|
+
const { profile, catalog } = loaded;
|
|
730
|
+
const outputs = manifest.outputs || {};
|
|
731
|
+
const rules = profile.renditions.map((entry) => ({ ...entry }));
|
|
732
|
+
const knownPaths = new Set(rules.map((entry) => entry.path));
|
|
733
|
+
const declaration = manifest.webDelivery;
|
|
734
|
+
if (declaration !== undefined) {
|
|
735
|
+
exactKeys(declaration, ["schema", "renditions"], [], "manifest.webDelivery");
|
|
736
|
+
invariant(
|
|
737
|
+
declaration.schema === "build-images.auditable-demo-web-delivery/v1",
|
|
738
|
+
"unsupported renderer web-delivery declaration",
|
|
739
|
+
);
|
|
740
|
+
invariant(Array.isArray(declaration.renditions), "manifest.webDelivery.renditions must be an array");
|
|
741
|
+
for (const [index, entry] of declaration.renditions.entries()) {
|
|
742
|
+
exactKeys(entry, ["path", "role", "mimeType"], [], `manifest.webDelivery.renditions[${index}]`);
|
|
743
|
+
invariant(!knownPaths.has(entry.path), `renderer web-delivery path is already profile-owned: ${entry.path}`);
|
|
744
|
+
const policy = profile.additionalRenditions;
|
|
745
|
+
invariant(policy, `media profile ${profileId} does not admit additional renditions`);
|
|
746
|
+
invariant(policy.allowedRoles.includes(entry.role), `additional rendition role is not allowed: ${entry.role}`);
|
|
747
|
+
invariant(policy.allowedMimeTypes.includes(entry.mimeType), `additional rendition MIME type is not allowed: ${entry.mimeType}`);
|
|
748
|
+
const maximumBytes = policy.maximumBytesByMimeType?.[entry.mimeType];
|
|
749
|
+
integer(maximumBytes, 1, MAX_BUNDLE_MEMBER_BYTES, `media profile ${profileId} additional ${entry.mimeType} budget`);
|
|
750
|
+
rules.push(constraintsForAdditionalRendition(entry, policy));
|
|
751
|
+
knownPaths.add(entry.path);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
for (const name of Object.keys(outputs)) {
|
|
755
|
+
if (name !== "media-probe.json") invariant(knownPaths.has(name), `unbound renderer output: ${name}`);
|
|
756
|
+
}
|
|
757
|
+
const singletonRoles = new Set(profile.singletonRoles || []);
|
|
758
|
+
const observedRoles = new Set();
|
|
759
|
+
const renditions = [];
|
|
760
|
+
for (const rule of rules) {
|
|
761
|
+
const declared = outputs[rule.path];
|
|
762
|
+
invariant(declared && typeof declared === "object", `required renderer output is missing: ${rule.path}`);
|
|
763
|
+
const target = resolveInside(renderOutput, rule.path, "renderer output path");
|
|
764
|
+
const bytes = readRegular(target, rule.path, MAX_BUNDLE_MEMBER_BYTES);
|
|
765
|
+
invariant(declared.root === sha256(bytes), `renderer manifest root mismatch: ${rule.path}`);
|
|
766
|
+
invariant(declared.bytes === bytes.length, `renderer manifest byte count mismatch: ${rule.path}`);
|
|
767
|
+
if (rule.maximumBytes !== undefined) {
|
|
768
|
+
invariant(bytes.length <= rule.maximumBytes, `${rule.path} byte budget exceeded`);
|
|
769
|
+
}
|
|
770
|
+
if (singletonRoles.has(rule.role)) {
|
|
771
|
+
invariant(!observedRoles.has(rule.role), `duplicate singleton role: ${rule.role}`);
|
|
772
|
+
observedRoles.add(rule.role);
|
|
773
|
+
}
|
|
774
|
+
const facts = profile.mode === "web-delivery" ? inspectMedia(target) : {};
|
|
775
|
+
if (profile.mode === "web-delivery") {
|
|
776
|
+
invariant(facts && typeof facts === "object", `${rule.path} inspection is missing`);
|
|
777
|
+
invariant(facts.container === rule.container, `${rule.path} container mismatch`);
|
|
778
|
+
invariant(facts.videoCodec === rule.videoCodec, `${rule.path} video codec mismatch`);
|
|
779
|
+
if (rule.pixelFormat) invariant(facts.pixelFormat === rule.pixelFormat, `${rule.path} pixel format mismatch`);
|
|
780
|
+
invariant(facts.audioStreams === rule.audioStreams, `${rule.path} audio stream policy failed`);
|
|
781
|
+
invariant(facts.width === scene.width && facts.height === scene.height, `${rule.path} dimensions mismatch`);
|
|
782
|
+
if (facts.durationMs > 0) {
|
|
783
|
+
invariant(
|
|
784
|
+
Math.abs(facts.durationMs - scene.durationMs) <= catalog.qualification.durationToleranceMs,
|
|
785
|
+
`${rule.path} duration mismatch`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
if (rule.frameRatePolicy === "scene-exact") {
|
|
789
|
+
invariant(Math.abs(facts.frameRate - scene.fps) < 0.001, `${rule.path} frame rate mismatch`);
|
|
790
|
+
}
|
|
791
|
+
if (rule.progressiveDownload) {
|
|
792
|
+
invariant(
|
|
793
|
+
facts.progressiveDownload === rule.progressiveDownload,
|
|
794
|
+
`${rule.path} progressive download evidence mismatch`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
renditions.push({
|
|
799
|
+
path: rule.path,
|
|
800
|
+
role: rule.role,
|
|
801
|
+
mimeType: rule.mimeType,
|
|
802
|
+
root: declared.root,
|
|
803
|
+
bytes: declared.bytes,
|
|
804
|
+
maximumBytes: rule.maximumBytes || 0,
|
|
805
|
+
...facts,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
const body = {
|
|
809
|
+
schema: "buildchain.auditable-demo-media-qualification/v1",
|
|
810
|
+
profile: {
|
|
811
|
+
id: profileId,
|
|
812
|
+
mode: profile.mode,
|
|
813
|
+
catalogRoot: loaded.catalogRoot,
|
|
814
|
+
profileRoot: loaded.profileRoot,
|
|
815
|
+
},
|
|
816
|
+
inspectionRoot,
|
|
817
|
+
renditions,
|
|
818
|
+
nonClaims: catalog.qualification.nonClaims,
|
|
819
|
+
};
|
|
820
|
+
return { ...body, qualificationRoot: semanticRoot(body) };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, options = {}) {
|
|
396
824
|
invariant(IMAGE_PATTERN.test(expectedImage), "renderer image must use an immutable sha256 coordinate");
|
|
397
|
-
const
|
|
825
|
+
const fixedMembers = [
|
|
398
826
|
"checksums.sha256",
|
|
399
827
|
"complete-transcript.txt",
|
|
400
|
-
"demo.gif",
|
|
401
|
-
"demo.mp4",
|
|
402
|
-
"demo.webm",
|
|
403
828
|
"manifest.json",
|
|
404
|
-
"media-probe.json",
|
|
405
|
-
"poster.png",
|
|
406
829
|
"public-projection.json",
|
|
407
830
|
"scene.json",
|
|
408
831
|
];
|
|
409
|
-
invariant(
|
|
410
|
-
JSON.stringify(listFiles(renderOutput)) === JSON.stringify(expectedMembers),
|
|
411
|
-
"renderer output member set is not exact",
|
|
412
|
-
);
|
|
413
832
|
verifyChecksums(renderOutput);
|
|
414
833
|
const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
|
|
415
834
|
invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
|
|
416
835
|
invariant(manifest.renderer?.image === expectedImage, "renderer manifest image coordinate mismatch");
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
)
|
|
836
|
+
const outputNames = Object.keys(manifest.outputs || {}).sort();
|
|
837
|
+
invariant(outputNames.includes("media-probe.json"), "renderer manifest must declare media-probe.json");
|
|
838
|
+
const expectedMembers = [...new Set([...fixedMembers, ...outputNames])].sort();
|
|
839
|
+
invariant(JSON.stringify(listFiles(renderOutput)) === JSON.stringify(expectedMembers), "renderer output member set is not exact");
|
|
840
|
+
for (const name of outputNames) {
|
|
841
|
+
invariant(!name.includes("\\") && !name.split("/").includes(".."), `renderer output path is invalid: ${name}`);
|
|
842
|
+
const target = resolveInside(renderOutput, name, "renderer manifest output");
|
|
843
|
+
const bytes = readRegular(target, name, MAX_BUNDLE_MEMBER_BYTES);
|
|
844
|
+
const declared = manifest.outputs[name];
|
|
845
|
+
invariant(declared?.root === sha256(bytes), `renderer manifest root mismatch: ${name}`);
|
|
846
|
+
invariant(declared?.bytes === bytes.length, `renderer manifest byte count mismatch: ${name}`);
|
|
847
|
+
}
|
|
422
848
|
for (const [key, filePath] of Object.entries(expectedInputs)) {
|
|
423
849
|
const observed = manifest.inputs?.[key]?.root;
|
|
424
850
|
invariant(observed === sha256(readRegular(filePath, `${key} input`)), `renderer ${key} input root mismatch`);
|
|
425
851
|
}
|
|
426
852
|
const probe = readJson(path.join(renderOutput, "media-probe.json"), "media probe");
|
|
427
853
|
invariant(probe.schema === "build-images.demo-media-probe/v1" && probe.passed === true, "renderer media probe failed");
|
|
428
|
-
|
|
854
|
+
const qualification = qualifyRendererOutput(
|
|
855
|
+
renderOutput,
|
|
856
|
+
manifest,
|
|
857
|
+
readJson(path.join(renderOutput, "scene.json"), "renderer scene"),
|
|
858
|
+
options.mediaProfile || "archive-v1",
|
|
859
|
+
options.inspectMedia || inspectMediaFile,
|
|
860
|
+
options.inspectionRoot || "",
|
|
861
|
+
);
|
|
862
|
+
return { manifest, probe, qualification };
|
|
429
863
|
}
|
|
430
864
|
|
|
431
865
|
function finalizeGate(values) {
|
|
@@ -521,16 +955,32 @@ function finalizeMedia(values) {
|
|
|
521
955
|
const rendererImage = required(values, "--renderer-image");
|
|
522
956
|
const gateRoot = required(values, "--gate-root");
|
|
523
957
|
const sourceSha = required(values, "--source-sha");
|
|
958
|
+
const mediaProfile = values["--media-profile"] || "archive-v1";
|
|
959
|
+
const selectedProfile = loadMediaProfile(mediaProfile).profile;
|
|
960
|
+
const mediaInspectionPath = values["--media-inspection"]
|
|
961
|
+
? path.resolve(values["--media-inspection"])
|
|
962
|
+
: "";
|
|
963
|
+
const mediaInspection = selectedProfile.mode === "web-delivery"
|
|
964
|
+
? loadMediaInspection(
|
|
965
|
+
required({ "--media-inspection": mediaInspectionPath }, "--media-inspection"),
|
|
966
|
+
renderOutput,
|
|
967
|
+
rendererImage,
|
|
968
|
+
)
|
|
969
|
+
: null;
|
|
524
970
|
verifyGate({
|
|
525
971
|
"--bundle": gateBundle,
|
|
526
972
|
"--expected-root": gateRoot,
|
|
527
973
|
"--renderer-image": rendererImage,
|
|
528
974
|
"--source-sha": sourceSha,
|
|
529
975
|
});
|
|
530
|
-
verifyRendererOutput(renderOutput, rendererImage, {
|
|
976
|
+
const verifiedRenderer = verifyRendererOutput(renderOutput, rendererImage, {
|
|
531
977
|
scene: path.join(gateBundle, "scene.json"),
|
|
532
978
|
transcript: path.join(gateBundle, "complete-transcript.txt"),
|
|
533
979
|
projection: path.join(gateBundle, "public-projection.json"),
|
|
980
|
+
}, {
|
|
981
|
+
mediaProfile,
|
|
982
|
+
inspectMedia: mediaInspection?.inspectMedia,
|
|
983
|
+
inspectionRoot: mediaInspection?.inspectionRoot || "",
|
|
534
984
|
});
|
|
535
985
|
ensureEmptyDirectory(output, "media bundle");
|
|
536
986
|
for (const name of listFiles(renderOutput)) {
|
|
@@ -538,19 +988,30 @@ function finalizeMedia(values) {
|
|
|
538
988
|
copyFile(path.join(renderOutput, name), path.join(output, destination));
|
|
539
989
|
}
|
|
540
990
|
copyFile(path.join(gateBundle, "gate-receipt.json"), path.join(output, "gate-receipt.json"));
|
|
541
|
-
|
|
542
|
-
|
|
991
|
+
if (mediaInspectionPath) copyFile(mediaInspectionPath, path.join(output, "media-inspection.json"));
|
|
992
|
+
const commonReceipt = {
|
|
543
993
|
status: "passed",
|
|
544
994
|
sourceSha,
|
|
545
995
|
qualifiedGateRoot: gateRoot,
|
|
546
996
|
rendererImage,
|
|
547
997
|
rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
|
|
548
|
-
}
|
|
998
|
+
};
|
|
999
|
+
const mediaReceipt = selectedProfile.mode === "archive"
|
|
1000
|
+
? { schema: "buildchain.auditable-demo-media/v1", ...commonReceipt }
|
|
1001
|
+
: {
|
|
1002
|
+
schema: "buildchain.auditable-demo-media/v2",
|
|
1003
|
+
...commonReceipt,
|
|
1004
|
+
qualification: verifiedRenderer.qualification,
|
|
1005
|
+
qualificationRoot: verifiedRenderer.qualification.qualificationRoot,
|
|
1006
|
+
};
|
|
1007
|
+
writeJson(path.join(output, "media-receipt.json"), mediaReceipt);
|
|
549
1008
|
const root = writeChecksums(output);
|
|
550
1009
|
const artifactName = `auditable-demo-media-${sourceSha.slice(0, 12)}-${root.slice(7, 23)}`;
|
|
551
1010
|
appendOutputs(values["--github-output"], {
|
|
552
1011
|
"media-root": root,
|
|
553
1012
|
"media-artifact-name": artifactName,
|
|
1013
|
+
"media-profile": mediaProfile,
|
|
1014
|
+
"media-qualification-root": verifiedRenderer.qualification.qualificationRoot,
|
|
554
1015
|
});
|
|
555
1016
|
process.stdout.write(stableJson({ status: "passed", root, artifactName }));
|
|
556
1017
|
}
|
|
@@ -568,6 +1029,10 @@ function main(argv) {
|
|
|
568
1029
|
return verifyGate(values);
|
|
569
1030
|
case "finalize-media":
|
|
570
1031
|
return finalizeMedia(values);
|
|
1032
|
+
case "inspect-media":
|
|
1033
|
+
return inspectRendererMedia(values);
|
|
1034
|
+
case "qualify-media-fixture":
|
|
1035
|
+
return qualifyMediaFixture(values);
|
|
571
1036
|
default:
|
|
572
1037
|
throw new Error(`unknown command: ${command || "<empty>"}`);
|
|
573
1038
|
}
|
|
@@ -586,6 +1051,10 @@ if (invokedDirectly) {
|
|
|
586
1051
|
export {
|
|
587
1052
|
finalizeGate,
|
|
588
1053
|
finalizeMedia,
|
|
1054
|
+
inspectIsoBmffFastStart,
|
|
1055
|
+
inspectMediaFile,
|
|
1056
|
+
inspectRendererMedia,
|
|
1057
|
+
qualifyMediaFixture,
|
|
589
1058
|
prepareSmoke,
|
|
590
1059
|
runAdapter,
|
|
591
1060
|
sha256,
|
|
@@ -9,6 +9,7 @@ const STABLE_PUBLISH_CHANNELS = new Set(["release", "major"]);
|
|
|
9
9
|
const OFFICIAL_REF = /^v(\d+)(?:\.\d+)?(?:-alpha)?$/;
|
|
10
10
|
const EXACT_SHA = /^[0-9a-f]{40}$/i;
|
|
11
11
|
const TRAIN_REF = /^(?:refs\/heads\/)?train\/v(\d+)\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
|
|
12
|
+
const AUTHORITY_REF = /^(?:refs\/heads\/)?authority\/v(\d+)\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
|
|
12
13
|
const SEMVER_TAG = /^refs\/tags\/v?\d+\.\d+\.\d+(?:-([0-9A-Za-z.-]+))?$/;
|
|
13
14
|
|
|
14
15
|
function normalized(value) {
|
|
@@ -36,6 +37,8 @@ function majorFrom(value) {
|
|
|
36
37
|
if (official) return Number(official[1]);
|
|
37
38
|
const train = text.match(TRAIN_REF);
|
|
38
39
|
if (train) return Number(train[1]);
|
|
40
|
+
const authority = text.match(AUTHORITY_REF);
|
|
41
|
+
if (authority) return Number(authority[1]);
|
|
39
42
|
const embedded = text.match(/(?:^|\/)v(\d+)(?:$|[./-])/);
|
|
40
43
|
if (embedded) return Number(embedded[1]);
|
|
41
44
|
const version = text.match(/^(\d+)\.\d+\.\d+/);
|
|
@@ -57,6 +60,9 @@ function classifyRequestedRef(value) {
|
|
|
57
60
|
if (TRAIN_REF.test(value) || TRAIN_REF.test(ref)) {
|
|
58
61
|
return { kind: "override", ref: normalized(value).replace(/^refs\/heads\//, "") };
|
|
59
62
|
}
|
|
63
|
+
if (AUTHORITY_REF.test(value) || AUTHORITY_REF.test(ref)) {
|
|
64
|
+
return { kind: "override", ref: normalized(value).replace(/^refs\/heads\//, "") };
|
|
65
|
+
}
|
|
60
66
|
const official = ref.match(OFFICIAL_REF);
|
|
61
67
|
if (official) {
|
|
62
68
|
return {
|
|
@@ -65,7 +71,7 @@ function classifyRequestedRef(value) {
|
|
|
65
71
|
major: Number(official[1]),
|
|
66
72
|
};
|
|
67
73
|
}
|
|
68
|
-
throw new Error("buildchain-ref must be an official vN/vN.M channel, a train ref, or an exact 40-character SHA");
|
|
74
|
+
throw new Error("buildchain-ref must be an official vN/vN.M channel, a train ref, an authority ref, or an exact 40-character SHA");
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
function selected(channel, major, source, reason) {
|
|
@@ -100,7 +106,7 @@ export function resolveBuildchainChannel({
|
|
|
100
106
|
throw new Error(`buildchain-channel=${channel} conflicts with buildchain-ref=${explicitRef.ref}`);
|
|
101
107
|
}
|
|
102
108
|
if (channel !== "auto" && explicitRef.kind === "override") {
|
|
103
|
-
throw new Error("train and exact-SHA buildchain-ref overrides require buildchain-channel=auto");
|
|
109
|
+
throw new Error("train, authority, and exact-SHA buildchain-ref overrides require buildchain-channel=auto");
|
|
104
110
|
}
|
|
105
111
|
return {
|
|
106
112
|
channel: explicitRef.kind,
|