@kungfu-tech/buildchain 3.0.2-alpha.0 → 3.0.2-alpha.10
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 +4 -2
- package/actions/github-artifact-attestation/README.md +10 -0
- package/actions/promote-buildchain-ref/README.md +7 -0
- package/bin/buildchain.mjs +5 -0
- package/bin/internal/trust-release-cli.mjs +74 -3
- 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/artifact-schemas.json +5 -1
- package/dist/site/buildchain-contract.json +133 -34
- package/dist/site/buildchain-site.json +159 -40
- package/dist/site/capability-registry.json +14 -13
- package/dist/site/cli-registry.json +12 -0
- package/dist/site/controller-registry.json +72 -4
- package/dist/site/kfd-claims.json +338 -20
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +24 -9
- package/dist/site/node-api-registry.json +89 -11
- package/dist/site/page-registry.json +135 -25
- package/dist/site/public-surface-audit.json +203 -21
- package/dist/site/publication-authority-registry.json +72 -2
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-model.json +2 -1
- package/dist/site/release-passport-check-manifest.json +1 -0
- package/dist/site/release-provenance.json +6 -0
- package/dist/site/schemas/release-passport-v1.schema.json +6 -0
- package/dist/site/site-manifest.json +21 -13
- package/dist/site/workflow-registry.json +167 -13
- package/docs/MAP.md +5 -1
- package/docs/auditable-demo.md +55 -3
- package/docs/binary-distribution.md +7 -0
- package/docs/cli.md +9 -0
- package/docs/dev-alpha-candidate-patrol.md +111 -0
- package/docs/github-artifact-attestation.md +219 -0
- package/docs/release-governance.md +32 -0
- package/docs/release-passport.md +13 -0
- package/docs/reusable-build-surface.md +83 -61
- package/docs/runtime-train-validation.md +21 -0
- package/docs/versioning.md +1 -0
- package/package.json +8 -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 +14 -0
- package/packages/core/buildchain-kfd-claims.js +5 -0
- package/packages/core/buildchain-publication-authority.js +3 -0
- package/packages/core/cache-evidence.js +288 -0
- package/packages/core/channel-candidate.js +186 -0
- package/packages/core/detached-artifact-signature.js +121 -0
- package/packages/core/diagnostics.js +276 -10
- package/packages/core/github-artifact-attestation.js +642 -0
- package/packages/core/github-governance-authority.js +77 -16
- package/packages/core/index.js +62 -0
- package/packages/core/publication-authority.js +1 -1
- package/packages/core/release-passport-contract.js +2 -0
- package/packages/core/release-passport.js +60 -3
- package/scripts/auditable-demo.mjs +520 -28
- package/scripts/build-contract-core.mjs +31 -0
- package/scripts/buildchain-channel-router.mjs +8 -2
- package/scripts/check-inventory.mjs +9 -0
- package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +864 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
- package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
- package/scripts/generate-channel-promotion-workflow.mjs +6 -0
- package/scripts/generate-site-bundle.mjs +20 -0
- package/scripts/import-artifact-signing-results.mjs +76 -0
- package/scripts/inspect-artifact-signing-requests.mjs +101 -0
- package/scripts/locked-source-checkout.mjs +48 -0
- package/scripts/materialize-artifact-signing-request.mjs +66 -0
- package/scripts/merge-artifact-signing-results.mjs +76 -0
- package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
- package/scripts/reconcile-github-governance.mjs +158 -25
- package/scripts/release-candidate-resolver.mjs +12 -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/shifu-gate-profile.mjs +26 -20
- package/scripts/sign-detached-artifact-requests.mjs +237 -0
- package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
- package/scripts/verify-artifact-signing-results.mjs +99 -0
|
@@ -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(
|
|
@@ -164,7 +173,18 @@ function validateScene(value) {
|
|
|
164
173
|
for (const key of ["background", "accent"]) {
|
|
165
174
|
if (value[key] !== undefined) invariant(/^#[0-9a-fA-F]{6}$/.test(value[key]), `scene.${key} is invalid`);
|
|
166
175
|
}
|
|
167
|
-
return
|
|
176
|
+
return {
|
|
177
|
+
schema: value.schema,
|
|
178
|
+
id: value.id,
|
|
179
|
+
width: value.width,
|
|
180
|
+
height: value.height,
|
|
181
|
+
fps: value.fps,
|
|
182
|
+
durationMs: value.durationMs,
|
|
183
|
+
title: value.title,
|
|
184
|
+
commandLabel: value.commandLabel ?? "",
|
|
185
|
+
background: (value.background ?? "#10151f").toLowerCase(),
|
|
186
|
+
accent: (value.accent ?? "#67e8a5").toLowerCase(),
|
|
187
|
+
};
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
function validateProjection(value, scene, transcriptLineCount) {
|
|
@@ -190,7 +210,17 @@ function validateProjection(value, scene, transcriptLineCount) {
|
|
|
190
210
|
}
|
|
191
211
|
if (cue.annotation !== undefined) text(cue.annotation, 0, 200, `projection.cues[${index}].annotation`);
|
|
192
212
|
}
|
|
193
|
-
return
|
|
213
|
+
return {
|
|
214
|
+
schema: value.schema,
|
|
215
|
+
evidenceClass: value.evidenceClass,
|
|
216
|
+
claimBoundary: value.claimBoundary,
|
|
217
|
+
cues: value.cues.map((cue) => ({
|
|
218
|
+
startMs: cue.startMs,
|
|
219
|
+
endMs: cue.endMs,
|
|
220
|
+
transcriptLines: cue.transcriptLines,
|
|
221
|
+
annotation: cue.annotation ?? "",
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
194
224
|
}
|
|
195
225
|
|
|
196
226
|
function validateSourceCoordinate(value) {
|
|
@@ -371,40 +401,465 @@ function prepareSmoke(values) {
|
|
|
371
401
|
writeJson(path.join(output, "public-projection.json"), projection);
|
|
372
402
|
}
|
|
373
403
|
|
|
374
|
-
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 = {}) {
|
|
375
824
|
invariant(IMAGE_PATTERN.test(expectedImage), "renderer image must use an immutable sha256 coordinate");
|
|
376
|
-
const
|
|
825
|
+
const fixedMembers = [
|
|
377
826
|
"checksums.sha256",
|
|
378
827
|
"complete-transcript.txt",
|
|
379
|
-
"demo.gif",
|
|
380
|
-
"demo.mp4",
|
|
381
|
-
"demo.webm",
|
|
382
828
|
"manifest.json",
|
|
383
|
-
"media-probe.json",
|
|
384
|
-
"poster.png",
|
|
385
829
|
"public-projection.json",
|
|
386
830
|
"scene.json",
|
|
387
831
|
];
|
|
388
|
-
invariant(
|
|
389
|
-
JSON.stringify(listFiles(renderOutput)) === JSON.stringify(expectedMembers),
|
|
390
|
-
"renderer output member set is not exact",
|
|
391
|
-
);
|
|
392
832
|
verifyChecksums(renderOutput);
|
|
393
833
|
const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
|
|
394
834
|
invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
|
|
395
835
|
invariant(manifest.renderer?.image === expectedImage, "renderer manifest image coordinate mismatch");
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
)
|
|
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
|
+
}
|
|
401
848
|
for (const [key, filePath] of Object.entries(expectedInputs)) {
|
|
402
849
|
const observed = manifest.inputs?.[key]?.root;
|
|
403
850
|
invariant(observed === sha256(readRegular(filePath, `${key} input`)), `renderer ${key} input root mismatch`);
|
|
404
851
|
}
|
|
405
852
|
const probe = readJson(path.join(renderOutput, "media-probe.json"), "media probe");
|
|
406
853
|
invariant(probe.schema === "build-images.demo-media-probe/v1" && probe.passed === true, "renderer media probe failed");
|
|
407
|
-
|
|
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 };
|
|
408
863
|
}
|
|
409
864
|
|
|
410
865
|
function finalizeGate(values) {
|
|
@@ -427,7 +882,9 @@ function finalizeGate(values) {
|
|
|
427
882
|
const sourceCoordinate = validateSourceCoordinate(readJson(sourceCoordinatePath, "source artifact coordinate"));
|
|
428
883
|
invariant(sourceCoordinate.sourceSha === sourceSha, "source artifact coordinate SHA mismatch");
|
|
429
884
|
ensureEmptyDirectory(output, "gate bundle");
|
|
430
|
-
|
|
885
|
+
fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
|
|
886
|
+
writeJson(path.join(output, "scene.json"), normalized.scene);
|
|
887
|
+
writeJson(path.join(output, "public-projection.json"), normalized.projection);
|
|
431
888
|
copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
|
|
432
889
|
copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
|
|
433
890
|
for (const name of listFiles(smokeOutput)) {
|
|
@@ -454,9 +911,9 @@ function finalizeGate(values) {
|
|
|
454
911
|
smokeManifestRoot: sha256(readRegular(path.join(smokeOutput, "manifest.json"), "smoke manifest")),
|
|
455
912
|
},
|
|
456
913
|
qualifiedInputs: {
|
|
457
|
-
transcript: sha256(readRegular(path.join(
|
|
458
|
-
projection: sha256(readRegular(path.join(
|
|
459
|
-
scene: sha256(readRegular(path.join(
|
|
914
|
+
transcript: sha256(readRegular(path.join(output, "complete-transcript.txt"), "transcript")),
|
|
915
|
+
projection: sha256(readRegular(path.join(output, "public-projection.json"), "projection")),
|
|
916
|
+
scene: sha256(readRegular(path.join(output, "scene.json"), "scene")),
|
|
460
917
|
evidenceClass: normalized.projection.evidenceClass,
|
|
461
918
|
claimBoundary: normalized.projection.claimBoundary,
|
|
462
919
|
},
|
|
@@ -498,16 +955,32 @@ function finalizeMedia(values) {
|
|
|
498
955
|
const rendererImage = required(values, "--renderer-image");
|
|
499
956
|
const gateRoot = required(values, "--gate-root");
|
|
500
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;
|
|
501
970
|
verifyGate({
|
|
502
971
|
"--bundle": gateBundle,
|
|
503
972
|
"--expected-root": gateRoot,
|
|
504
973
|
"--renderer-image": rendererImage,
|
|
505
974
|
"--source-sha": sourceSha,
|
|
506
975
|
});
|
|
507
|
-
verifyRendererOutput(renderOutput, rendererImage, {
|
|
976
|
+
const verifiedRenderer = verifyRendererOutput(renderOutput, rendererImage, {
|
|
508
977
|
scene: path.join(gateBundle, "scene.json"),
|
|
509
978
|
transcript: path.join(gateBundle, "complete-transcript.txt"),
|
|
510
979
|
projection: path.join(gateBundle, "public-projection.json"),
|
|
980
|
+
}, {
|
|
981
|
+
mediaProfile,
|
|
982
|
+
inspectMedia: mediaInspection?.inspectMedia,
|
|
983
|
+
inspectionRoot: mediaInspection?.inspectionRoot || "",
|
|
511
984
|
});
|
|
512
985
|
ensureEmptyDirectory(output, "media bundle");
|
|
513
986
|
for (const name of listFiles(renderOutput)) {
|
|
@@ -515,19 +988,30 @@ function finalizeMedia(values) {
|
|
|
515
988
|
copyFile(path.join(renderOutput, name), path.join(output, destination));
|
|
516
989
|
}
|
|
517
990
|
copyFile(path.join(gateBundle, "gate-receipt.json"), path.join(output, "gate-receipt.json"));
|
|
518
|
-
|
|
519
|
-
|
|
991
|
+
if (mediaInspectionPath) copyFile(mediaInspectionPath, path.join(output, "media-inspection.json"));
|
|
992
|
+
const commonReceipt = {
|
|
520
993
|
status: "passed",
|
|
521
994
|
sourceSha,
|
|
522
995
|
qualifiedGateRoot: gateRoot,
|
|
523
996
|
rendererImage,
|
|
524
997
|
rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
|
|
525
|
-
}
|
|
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);
|
|
526
1008
|
const root = writeChecksums(output);
|
|
527
1009
|
const artifactName = `auditable-demo-media-${sourceSha.slice(0, 12)}-${root.slice(7, 23)}`;
|
|
528
1010
|
appendOutputs(values["--github-output"], {
|
|
529
1011
|
"media-root": root,
|
|
530
1012
|
"media-artifact-name": artifactName,
|
|
1013
|
+
"media-profile": mediaProfile,
|
|
1014
|
+
"media-qualification-root": verifiedRenderer.qualification.qualificationRoot,
|
|
531
1015
|
});
|
|
532
1016
|
process.stdout.write(stableJson({ status: "passed", root, artifactName }));
|
|
533
1017
|
}
|
|
@@ -545,6 +1029,10 @@ function main(argv) {
|
|
|
545
1029
|
return verifyGate(values);
|
|
546
1030
|
case "finalize-media":
|
|
547
1031
|
return finalizeMedia(values);
|
|
1032
|
+
case "inspect-media":
|
|
1033
|
+
return inspectRendererMedia(values);
|
|
1034
|
+
case "qualify-media-fixture":
|
|
1035
|
+
return qualifyMediaFixture(values);
|
|
548
1036
|
default:
|
|
549
1037
|
throw new Error(`unknown command: ${command || "<empty>"}`);
|
|
550
1038
|
}
|
|
@@ -563,6 +1051,10 @@ if (invokedDirectly) {
|
|
|
563
1051
|
export {
|
|
564
1052
|
finalizeGate,
|
|
565
1053
|
finalizeMedia,
|
|
1054
|
+
inspectIsoBmffFastStart,
|
|
1055
|
+
inspectMediaFile,
|
|
1056
|
+
inspectRendererMedia,
|
|
1057
|
+
qualifyMediaFixture,
|
|
566
1058
|
prepareSmoke,
|
|
567
1059
|
runAdapter,
|
|
568
1060
|
sha256,
|