@kungfu-tech/buildchain 2.12.7-alpha.6 → 2.12.7-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.
@@ -4,7 +4,7 @@
4
4
  "status": "passed",
5
5
  "summary": {
6
6
  "cliCommandCount": 79,
7
- "workflowCount": 46,
7
+ "workflowCount": 47,
8
8
  "actionCount": 4,
9
9
  "sitePageCount": 49,
10
10
  "docCommandRefCount": 246,
@@ -609,14 +609,18 @@
609
609
  "reusable": true,
610
610
  "inputs": [
611
611
  "admission-json",
612
+ "authority-workflow-path",
612
613
  "auto-admission",
614
+ "auto-admission-kind",
613
615
  "buildchain-ref",
614
616
  "buildchain-repository",
615
617
  "control-plane-audit-json",
616
618
  "dry-run",
619
+ "evidence-controller-artifact",
617
620
  "evidence-manifest-pattern",
618
621
  "evidence-passport-artifact",
619
622
  "evidence-payload-pattern",
623
+ "evidence-publication-artifact",
620
624
  "evidence-repository",
621
625
  "evidence-run-id",
622
626
  "evidence-summary-artifact",
@@ -627,12 +631,13 @@
627
631
  "publication-target",
628
632
  "publication-version",
629
633
  "publisher-workflow-path",
634
+ "required-status-check",
630
635
  "runner-provenance-json",
631
636
  "source-sha",
632
637
  "target-ref",
633
638
  "used-nonces-json"
634
639
  ],
635
- "inputCount": 23,
640
+ "inputCount": 28,
636
641
  "secrets": [],
637
642
  "secretCount": 0,
638
643
  "outputs": [
@@ -1335,6 +1340,49 @@
1335
1340
  "outputs": [],
1336
1341
  "outputCount": 0
1337
1342
  },
1343
+ {
1344
+ "id": "paper-release-sealed",
1345
+ "path": ".github/workflows/paper-release-sealed.yml",
1346
+ "reusable": true,
1347
+ "inputs": [
1348
+ "artifact-name",
1349
+ "artifact-paths",
1350
+ "artifact-retention-days",
1351
+ "branch-protection-bypass-apps",
1352
+ "branch-protection-bypass-teams",
1353
+ "branch-protection-bypass-users",
1354
+ "buildchain-contract-lock-path",
1355
+ "buildchain-ref",
1356
+ "buildchain-repository",
1357
+ "dry-run",
1358
+ "github-release",
1359
+ "github-release-notes",
1360
+ "github-release-title",
1361
+ "package-name",
1362
+ "publish-dist-tag",
1363
+ "publisher-workflow-path",
1364
+ "release-passport-impact-json",
1365
+ "release-passport-output-dir",
1366
+ "release-passport-product-name",
1367
+ "required-status-check",
1368
+ "target-ref",
1369
+ "target-sha",
1370
+ "toolchain-type",
1371
+ "verify-command",
1372
+ "working-directory"
1373
+ ],
1374
+ "inputCount": 25,
1375
+ "secrets": [],
1376
+ "secretCount": 0,
1377
+ "outputs": [
1378
+ "github-release-url",
1379
+ "package-name",
1380
+ "package-version",
1381
+ "public-release-tag",
1382
+ "release-passport-path"
1383
+ ],
1384
+ "outputCount": 5
1385
+ },
1338
1386
  {
1339
1387
  "id": "paper-release",
1340
1388
  "path": ".github/workflows/paper-release.yml",
@@ -1492,7 +1540,11 @@
1492
1540
  "buildchain-ref",
1493
1541
  "buildchain-repository",
1494
1542
  "node-version",
1543
+ "package-name",
1544
+ "prepare-paper-package",
1545
+ "publish-dist-tag",
1495
1546
  "setup-node",
1547
+ "target-ref",
1496
1548
  "toolchain-command",
1497
1549
  "toolchain-digest",
1498
1550
  "toolchain-image",
@@ -1500,7 +1552,7 @@
1500
1552
  "verify-command",
1501
1553
  "working-directory"
1502
1554
  ],
1503
- "inputCount": 17,
1555
+ "inputCount": 21,
1504
1556
  "secrets": [],
1505
1557
  "secretCount": 0,
1506
1558
  "outputs": [
@@ -1511,13 +1563,17 @@
1511
1563
  "controller-receipt-digest",
1512
1564
  "controller-receipt-json",
1513
1565
  "controller-receipt-status",
1566
+ "package-name",
1567
+ "package-version",
1514
1568
  "publication-artifact-name",
1515
1569
  "publication-manifest-json",
1516
1570
  "publication-manifest-path",
1517
1571
  "publication-passport-path",
1518
- "publication-registry-path"
1572
+ "publication-registry-path",
1573
+ "publish-dist-tag",
1574
+ "runtime-sha"
1519
1575
  ],
1520
- "outputCount": 12
1576
+ "outputCount": 16
1521
1577
  },
1522
1578
  {
1523
1579
  "id": "release-candidate-promote",
@@ -3331,8 +3387,8 @@
3331
3387
  "workflowRegistryPath": "dist/site/workflow-registry.json",
3332
3388
  "pageRegistryPath": "dist/site/page-registry.json",
3333
3389
  "cliRegistryDigest": "a6daf4822365bf576425777230246046ebca67b3f8dcfea61fc7a61773776185",
3334
- "workflowRegistryDigest": "2b36c3c532082a835a2054ce565b4b5ca39ef1c09b8188a080d5dc4a5909218b",
3335
- "pageRegistryDigest": "dc526c6f9aefa92aa566a9664374eb4f177f4b3ae5b9480eb94abbbf3a257a30"
3390
+ "workflowRegistryDigest": "a61b85b07185e02e340c27d50f77a6710968db87dd42d1b1f4dd81260779a0ed",
3391
+ "pageRegistryDigest": "3a4779e3d85d0a175e33c7d2a8bee30ede3931cc721a7d8c422ab321f0dec723"
3336
3392
  },
3337
3393
  "comparison": {
3338
3394
  "missingCliRegistry": [],
@@ -306,6 +306,22 @@
306
306
  "runnerPolicy": "unqualified",
307
307
  "notes": ""
308
308
  },
309
+ {
310
+ "workflowPath": ".github/workflows/paper-release-sealed.yml",
311
+ "authorityClass": "product-publication",
312
+ "publicationCapable": true,
313
+ "capabilityIds": [
314
+ "github-release",
315
+ "npm-publish"
316
+ ],
317
+ "credentialMode": "trusted-publishing",
318
+ "publisherWorkflowMode": "caller-bound",
319
+ "publisherWorkflowPath": ".github/workflows/paper-release-sealed.yml",
320
+ "environment": "none",
321
+ "environmentMode": "fixed",
322
+ "runnerPolicy": "qualified-measured",
323
+ "notes": ""
324
+ },
309
325
  {
310
326
  "workflowPath": ".github/workflows/paper-release.yml",
311
327
  "authorityClass": "product-publication",
@@ -655,6 +671,14 @@
655
671
  ],
656
672
  "classified": true
657
673
  },
674
+ {
675
+ "path": ".github/workflows/paper-release-sealed.yml",
676
+ "signals": [
677
+ "write-permission",
678
+ "oidc"
679
+ ],
680
+ "classified": true
681
+ },
658
682
  {
659
683
  "path": ".github/workflows/paper-release.yml",
660
684
  "signals": [
@@ -750,5 +774,5 @@
750
774
  "classified": false
751
775
  }
752
776
  ],
753
- "registryDigest": "a606a7da7188a8eee970d63b15f859b99024c246fb698d74e04fd548d2552c55"
777
+ "registryDigest": "760215fb39e6c85890c925e924c538a766d78abd6319794c8799fb47c23dec3d"
754
778
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-publication-release-registry",
4
- "generatedAt": "2026-07-14T23:50:10.932Z",
5
- "publishedAt": "2026-07-14T23:50:10.932Z",
4
+ "generatedAt": "2026-07-15T01:31:56.311Z",
5
+ "publishedAt": "2026-07-15T01:31:56.311Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "24fef92c4300b69ece36dcfaa9c1453ed6fb588e",
22
+ "sourceRevision": "a2f11bfd2bae6539f0196ef8fd9571130a809918",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "2.12.7-alpha.6",
35
+ "version": "2.12.7-alpha.8",
36
36
  "versionSource": "package.json#version"
37
37
  },
38
38
  "sourceKind": "package-site-bundle",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-07-14T23:50:10.932Z",
5
- "publishedAt": "2026-07-14T23:50:10.932Z",
4
+ "generatedAt": "2026-07-15T01:31:56.311Z",
5
+ "publishedAt": "2026-07-15T01:31:56.311Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "24fef92c4300b69ece36dcfaa9c1453ed6fb588e",
22
+ "sourceRevision": "a2f11bfd2bae6539f0196ef8fd9571130a809918",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "2.12.7-alpha.6",
40
+ "version": "2.12.7-alpha.8",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -215,14 +215,18 @@
215
215
  "reusable": true,
216
216
  "inputs": [
217
217
  "admission-json",
218
+ "authority-workflow-path",
218
219
  "auto-admission",
220
+ "auto-admission-kind",
219
221
  "buildchain-ref",
220
222
  "buildchain-repository",
221
223
  "control-plane-audit-json",
222
224
  "dry-run",
225
+ "evidence-controller-artifact",
223
226
  "evidence-manifest-pattern",
224
227
  "evidence-passport-artifact",
225
228
  "evidence-payload-pattern",
229
+ "evidence-publication-artifact",
226
230
  "evidence-repository",
227
231
  "evidence-run-id",
228
232
  "evidence-summary-artifact",
@@ -233,12 +237,13 @@
233
237
  "publication-target",
234
238
  "publication-version",
235
239
  "publisher-workflow-path",
240
+ "required-status-check",
236
241
  "runner-provenance-json",
237
242
  "source-sha",
238
243
  "target-ref",
239
244
  "used-nonces-json"
240
245
  ],
241
- "inputCount": 23,
246
+ "inputCount": 28,
242
247
  "secrets": [],
243
248
  "secretCount": 0,
244
249
  "outputs": [
@@ -1025,6 +1030,52 @@
1025
1030
  "capabilityGroup": "api-cli-reference",
1026
1031
  "status": "active"
1027
1032
  },
1033
+ {
1034
+ "id": "paper-release-sealed",
1035
+ "path": ".github/workflows/paper-release-sealed.yml",
1036
+ "reusable": true,
1037
+ "inputs": [
1038
+ "artifact-name",
1039
+ "artifact-paths",
1040
+ "artifact-retention-days",
1041
+ "branch-protection-bypass-apps",
1042
+ "branch-protection-bypass-teams",
1043
+ "branch-protection-bypass-users",
1044
+ "buildchain-contract-lock-path",
1045
+ "buildchain-ref",
1046
+ "buildchain-repository",
1047
+ "dry-run",
1048
+ "github-release",
1049
+ "github-release-notes",
1050
+ "github-release-title",
1051
+ "package-name",
1052
+ "publish-dist-tag",
1053
+ "publisher-workflow-path",
1054
+ "release-passport-impact-json",
1055
+ "release-passport-output-dir",
1056
+ "release-passport-product-name",
1057
+ "required-status-check",
1058
+ "target-ref",
1059
+ "target-sha",
1060
+ "toolchain-type",
1061
+ "verify-command",
1062
+ "working-directory"
1063
+ ],
1064
+ "inputCount": 25,
1065
+ "secrets": [],
1066
+ "secretCount": 0,
1067
+ "outputs": [
1068
+ "github-release-url",
1069
+ "package-name",
1070
+ "package-version",
1071
+ "public-release-tag",
1072
+ "release-passport-path"
1073
+ ],
1074
+ "outputCount": 5,
1075
+ "surface": "repository-workflow",
1076
+ "capabilityGroup": "api-cli-reference",
1077
+ "status": "active"
1078
+ },
1028
1079
  {
1029
1080
  "id": "paper-release",
1030
1081
  "path": ".github/workflows/paper-release.yml",
@@ -1194,7 +1245,11 @@
1194
1245
  "buildchain-ref",
1195
1246
  "buildchain-repository",
1196
1247
  "node-version",
1248
+ "package-name",
1249
+ "prepare-paper-package",
1250
+ "publish-dist-tag",
1197
1251
  "setup-node",
1252
+ "target-ref",
1198
1253
  "toolchain-command",
1199
1254
  "toolchain-digest",
1200
1255
  "toolchain-image",
@@ -1202,7 +1257,7 @@
1202
1257
  "verify-command",
1203
1258
  "working-directory"
1204
1259
  ],
1205
- "inputCount": 17,
1260
+ "inputCount": 21,
1206
1261
  "secrets": [],
1207
1262
  "secretCount": 0,
1208
1263
  "outputs": [
@@ -1213,13 +1268,17 @@
1213
1268
  "controller-receipt-digest",
1214
1269
  "controller-receipt-json",
1215
1270
  "controller-receipt-status",
1271
+ "package-name",
1272
+ "package-version",
1216
1273
  "publication-artifact-name",
1217
1274
  "publication-manifest-json",
1218
1275
  "publication-manifest-path",
1219
1276
  "publication-passport-path",
1220
- "publication-registry-path"
1277
+ "publication-registry-path",
1278
+ "publish-dist-tag",
1279
+ "runtime-sha"
1221
1280
  ],
1222
- "outputCount": 12,
1281
+ "outputCount": 16,
1223
1282
  "surface": "repository-workflow",
1224
1283
  "capabilityGroup": "reusable-build",
1225
1284
  "status": "active"
@@ -182,40 +182,37 @@ on:
182
182
 
183
183
  jobs:
184
184
  paper-release:
185
- uses: kungfu-systems/buildchain/.github/workflows/paper-release.yml@v2
185
+ uses: kungfu-systems/buildchain/.github/workflows/paper-release-sealed.yml@v2
186
186
  permissions:
187
+ actions: read
187
188
  checks: write
188
189
  contents: write
189
190
  id-token: write
190
191
  issues: write
191
192
  with:
192
193
  buildchain-ref: ${{ inputs.buildchain-ref || '' }}
193
- publication-admission-json: ${{ needs.authority.outputs.admission-json }}
194
- publication-runner-provenance-json: ${{ needs.authority.outputs.runner-provenance-json }}
195
- publication-control-plane-audit-json: ${{ needs.authority.outputs.control-plane-audit-json }}
196
- publication-expected-json: ${{ needs.authority.outputs.expected-json }}
194
+ publisher-workflow-path: .github/workflows/paper-release.yml
197
195
  toolchain-type: config
198
196
  verify-command: make check
197
+ artifact-paths: _build/paper-name.pdf
199
198
  buildchain-contract-lock-path: .buildchain/contract-lock.json
200
199
  ```
201
200
 
202
- The publication job does not accept a long-lived promotion token. The caller
203
- must first produce a fresh sealed admission, runner provenance, external
204
- control-plane audit, and exact expected bindings. The credential-free verifier
205
- job checks those receipts; only then can the publication job use its short-lived
206
- `github.token` and caller-bound OIDC trusted publisher identity. npm binds that
207
- identity to the consumer workflow filename; an npm Environment restriction is
208
- optional and must be represented explicitly when configured. The workflow fails
209
- before the publication build when the target branch protection cannot be read.
201
+ The sealed preset does not accept a long-lived promotion token. It builds and
202
+ packages the paper in a read-only job, then a credential-free authority job
203
+ downloads that exact candidate, audits the external control plane, and seals a
204
+ capability over the source tree, Buildchain runtime, controller receipt, PDF,
205
+ and npm package bytes. Only the final job receives write and OIDC permissions;
206
+ it downloads the admitted candidate, recomputes the capability binding, and
207
+ publishes without executing consumer build commands. npm binds the OIDC identity
208
+ to the consumer workflow named by `publisher-workflow-path`.
210
209
 
211
210
  The preset:
212
211
 
213
- - resolves the same floating Buildchain runtime and contract lock as the build
214
- workflow;
215
- - verifies that the declared promotion authority can read the protected target
216
- channel before starting the publication build;
212
+ - resolves the floating Buildchain runtime once and binds the exact SHA into the
213
+ publication candidate and authority capability;
217
214
  - builds the PDF through the declared pinned LaTeX Docker toolchain or custom
218
- command;
215
+ command in a read-only job;
219
216
  - verifies the paper repository;
220
217
  - writes the publication manifest, publication passport, optional archive
221
218
  registry, and source bundle;
@@ -226,7 +223,8 @@ The preset:
226
223
  - creates a `publish-gate/<alpha|release>/.../<version>` source lock for the
227
224
  channel commit and requires `promote-buildchain-ref` to verify that lock
228
225
  before any publish side effect;
229
- - publishes the package through npm Trusted Publishing;
226
+ - verifies the complete candidate again after authority and publishes the
227
+ package through npm Trusted Publishing without rebuilding it;
230
228
  - writes Buildchain release/passport evidence; and
231
229
  - creates or updates the exact-version GitHub Release by default, uploading
232
230
  every file declared by `publication.primary_artifact` and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.12.7-alpha.6",
3
+ "version": "2.12.7-alpha.8",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -27,6 +27,7 @@ const DESCRIPTORS = Object.freeze([
27
27
  [".github/workflows/buildchain-stable-candidate-patrol.yml", "governance-write"],
28
28
  [".github/workflows/dev-pr-auto-merge.yml", "governance-write"],
29
29
  [".github/workflows/npm-publish.yml", "dry-run-only"],
30
+ [".github/workflows/paper-release-sealed.yml", "product-publication", true, ["npm-publish", "github-release"], "trusted-publishing", "none", "fixed", "caller-bound"],
30
31
  [".github/workflows/paper-release.yml", "product-publication", true, ["npm-publish", "github-release"], "trusted-publishing", "none", "fixed", "caller-bound"],
31
32
  [".github/workflows/patrol-daily.yml", "governance-write"],
32
33
  [".github/workflows/patrol-monthly.yml", "governance-write"],
@@ -153,6 +153,12 @@ export {
153
153
  verifyPublicationAdmission,
154
154
  } from "./publication-authority.js";
155
155
 
156
+ export {
157
+ PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
158
+ createPublicationArtifactCandidate,
159
+ publicationArtifactCandidateDigest,
160
+ } from "./publication-artifact-candidate.js";
161
+
156
162
  export {
157
163
  buildchainPublicationAuthorityDescriptors,
158
164
  createBuildchainPublicationAuthorityRegistry,
@@ -0,0 +1,116 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { validateControllerReceipt } from "./controller-evidence.js";
4
+
5
+ export const PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT =
6
+ "kungfu-buildchain-publication-artifact-candidate";
7
+
8
+ const MANIFEST_CONTRACT = "kungfu-buildchain-publication-artifact-manifest";
9
+ const PASSPORT_CONTRACT = "kungfu-buildchain-publication-artifact-passport";
10
+
11
+ function stableJson(value) {
12
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
13
+ if (value && typeof value === "object") {
14
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
15
+ }
16
+ return JSON.stringify(value);
17
+ }
18
+
19
+ export function publicationArtifactCandidateDigest(value) {
20
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
21
+ }
22
+
23
+ function requiredString(value, label) {
24
+ const normalized = String(value || "").trim();
25
+ if (!normalized) throw new Error(`${label} must be a non-empty string`);
26
+ return normalized;
27
+ }
28
+
29
+ function normalizeDigest(value, label) {
30
+ const normalized = requiredString(value, label).replace(/^sha256:/, "").toLowerCase();
31
+ if (!/^[0-9a-f]{64}$/.test(normalized)) throw new Error(`${label} must be a sha256 digest`);
32
+ return normalized;
33
+ }
34
+
35
+ function normalizeGitSha(value, label) {
36
+ const normalized = requiredString(value, label).toLowerCase();
37
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(normalized)) {
38
+ throw new Error(`${label} must be a 40- or 64-character Git SHA`);
39
+ }
40
+ return normalized;
41
+ }
42
+
43
+ export function createPublicationArtifactCandidate({
44
+ repository,
45
+ sourceSha,
46
+ sourceTreeSha,
47
+ runtimeSha,
48
+ manifest,
49
+ passport,
50
+ controllerReceipt,
51
+ files = [],
52
+ } = {}) {
53
+ const normalizedRepository = requiredString(repository, "repository");
54
+ const normalizedSourceSha = normalizeGitSha(sourceSha, "sourceSha");
55
+ const normalizedSourceTreeSha = normalizeGitSha(sourceTreeSha, "sourceTreeSha");
56
+ const normalizedRuntimeSha = normalizeGitSha(runtimeSha, "runtimeSha");
57
+ if (manifest?.contract !== MANIFEST_CONTRACT) throw new Error("publication artifact manifest contract mismatch");
58
+ if (passport?.contract !== PASSPORT_CONTRACT || passport.status !== "passed") {
59
+ throw new Error("publication artifact passport is not qualifying");
60
+ }
61
+ if (manifest.source?.sha !== normalizedSourceSha || passport.source?.sha !== normalizedSourceSha) {
62
+ throw new Error("publication artifact source SHA mismatch");
63
+ }
64
+ if (manifest.source?.treeSha !== normalizedSourceTreeSha || passport.source?.treeSha !== normalizedSourceTreeSha) {
65
+ throw new Error("publication artifact source tree mismatch");
66
+ }
67
+ const manifestDigest = crypto.createHash("sha256").update(JSON.stringify(manifest, null, 2)).digest("hex");
68
+ if (normalizeDigest(passport.manifestDigest, "passport.manifestDigest") !== manifestDigest) {
69
+ throw new Error("publication artifact passport manifest digest mismatch");
70
+ }
71
+ const controllerValidation = validateControllerReceipt(controllerReceipt, {
72
+ expectedSourceSha: normalizedSourceSha,
73
+ expectedRuntimeSha: normalizedRuntimeSha,
74
+ });
75
+ if (!controllerValidation.ok || !controllerValidation.qualifying) {
76
+ throw new Error(`publication artifact controller receipt did not qualify: ${controllerValidation.issues.join("; ")}`);
77
+ }
78
+ const evidenceKinds = new Set((controllerReceipt.evidence || []).map((entry) => entry.kind));
79
+ for (const kind of ["publication-manifest", "publication-passport"]) {
80
+ if (!evidenceKinds.has(kind)) throw new Error(`publication artifact controller receipt is missing ${kind} evidence`);
81
+ }
82
+ const normalizedFiles = files.map((entry, index) => {
83
+ const filePath = requiredString(entry.path, `files[${index}].path`).replaceAll("\\", "/");
84
+ if (filePath.startsWith("/") || filePath.split("/").includes("..")) {
85
+ throw new Error(`publication artifact candidate contains an unsafe path: ${filePath}`);
86
+ }
87
+ const size = Number(entry.size ?? entry.bytes);
88
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error(`files[${index}].size must be a non-negative safe integer`);
89
+ return { path: filePath, size, sha256: normalizeDigest(entry.sha256, `files[${index}].sha256`) };
90
+ }).sort((left, right) => left.path.localeCompare(right.path));
91
+ if (new Set(normalizedFiles.map((entry) => entry.path)).size !== normalizedFiles.length) {
92
+ throw new Error("publication artifact candidate file paths must be unique");
93
+ }
94
+ const byPath = new Map(normalizedFiles.map((entry) => [entry.path, entry]));
95
+ for (const [index, artifact] of (manifest.artifacts || []).entries()) {
96
+ const artifactPath = requiredString(artifact.path, `manifest.artifacts[${index}].path`);
97
+ const actual = byPath.get(artifactPath);
98
+ if (!actual) throw new Error(`publication artifact candidate is missing declared artifact: ${artifactPath}`);
99
+ if (actual.size !== Number(artifact.bytes) || actual.sha256 !== normalizeDigest(artifact.sha256, `manifest.artifacts[${index}].sha256`)) {
100
+ throw new Error(`publication artifact candidate bytes do not match manifest: ${artifactPath}`);
101
+ }
102
+ }
103
+ const payload = {
104
+ schemaVersion: 1,
105
+ contract: PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
106
+ repository: normalizedRepository,
107
+ sourceSha: normalizedSourceSha,
108
+ sourceTreeSha: normalizedSourceTreeSha,
109
+ runtimeSha: normalizedRuntimeSha,
110
+ manifestDigest,
111
+ passportDigest: publicationArtifactCandidateDigest(passport),
112
+ controllerReceiptDigest: normalizeDigest(controllerReceipt.digest, "controllerReceipt.digest"),
113
+ files: normalizedFiles,
114
+ };
115
+ return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
116
+ }
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
3
  import { validateControllerReceipt } from "./controller-evidence.js";
4
+ import { createPublicationArtifactCandidate } from "./publication-artifact-candidate.js";
4
5
  import { sha256Json, validateReleaseCandidatePassport } from "./release-candidate.js";
5
6
 
6
7
  export const PUBLICATION_AUTHORITY_REGISTRY_CONTRACT =
@@ -381,6 +382,7 @@ export function createPublicationArtifactManifestSet({
381
382
  }
382
383
 
383
384
  function validateGateAggregate(gateAggregate, { admission, passport }) {
385
+ const passportSourceSha = passport.source?.headSha || passport.source?.sha;
384
386
  if (gateAggregate?.contract === "buildchain.shifu-gate-aggregate/v1") {
385
387
  const { digest, ...payload } = gateAggregate;
386
388
  const actualDigest = sha256Json(payload);
@@ -390,7 +392,7 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
390
392
  if (gateAggregate.status !== "pass" || gateAggregate.qualifying !== true) {
391
393
  throw new Error("gate aggregate is not qualifying");
392
394
  }
393
- if (![admission.sourceSha, passport.source?.headSha].includes(gateAggregate.sourceSha)) {
395
+ if (![admission.sourceSha, passportSourceSha].includes(gateAggregate.sourceSha)) {
394
396
  throw new Error("gate aggregate source SHA mismatch");
395
397
  }
396
398
  normalizeDigest(gateAggregate.registry?.digest, "gateAggregate.registry.digest");
@@ -408,7 +410,7 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
408
410
  if (gateAggregate.required !== false) {
409
411
  throw new Error("a required Gate policy must supply a qualifying Shifu Gate aggregate");
410
412
  }
411
- if (![admission.sourceSha, passport.source?.headSha].includes(gateAggregate.sourceSha)) {
413
+ if (![admission.sourceSha, passportSourceSha].includes(gateAggregate.sourceSha)) {
412
414
  throw new Error("publication Gate decision source SHA mismatch");
413
415
  }
414
416
  const normalizedPolicy = {
@@ -428,6 +430,43 @@ function validatePublicationEvidence(publicationEvidence, admission) {
428
430
  if (!publicationEvidence || typeof publicationEvidence !== "object" || Array.isArray(publicationEvidence)) {
429
431
  throw new Error("independent publication evidence is required");
430
432
  }
433
+ if (publicationEvidence.publicationArtifactCandidate) {
434
+ const candidate = createPublicationArtifactCandidate(publicationEvidence.publicationArtifactCandidate);
435
+ const passport = publicationEvidence.publicationArtifactCandidate.passport;
436
+ const controllerReceipt = publicationEvidence.publicationArtifactCandidate.controllerReceipt;
437
+ const controllerReceiptDigest = normalizeDigest(controllerReceipt.digest, "controllerReceipt.digest");
438
+ if (controllerReceiptDigest !== normalizeDigest(admission.controllerReceiptDigest, "admission.controllerReceiptDigest")) {
439
+ throw new Error("controller receipt evidence binding mismatch");
440
+ }
441
+ const contractDigest = normalizeDigest(controllerReceipt.runtime?.contractDigest, "controllerReceipt.runtime.contractDigest");
442
+ if (contractDigest !== normalizeDigest(admission.contractDigest, "admission.contractDigest")) {
443
+ throw new Error("runtime contract evidence binding mismatch");
444
+ }
445
+ const gate = validateGateAggregate(publicationEvidence.gateAggregate, { admission, passport });
446
+ if (gate.gateAggregateDigest !== normalizeDigest(admission.gateAggregateDigest, "admission.gateAggregateDigest")) {
447
+ throw new Error("Gate aggregate evidence binding mismatch");
448
+ }
449
+ if (gate.policyDigest !== normalizeDigest(admission.policyDigest, "admission.policyDigest")) {
450
+ throw new Error("Gate policy evidence binding mismatch");
451
+ }
452
+ if (candidate.candidateDigest !== normalizeDigest(admission.artifactDigest, "admission.artifactDigest")) {
453
+ throw new Error("publication artifact candidate evidence binding mismatch");
454
+ }
455
+ return {
456
+ sourceTreeSha: candidate.sourceTreeSha,
457
+ controllerReceiptDigest,
458
+ contractDigest,
459
+ gateAggregateDigest: gate.gateAggregateDigest,
460
+ policyDigest: gate.policyDigest,
461
+ artifactDigest: candidate.candidateDigest,
462
+ evidenceDigest: publicationAuthorityDigest({
463
+ sourceTreeSha: candidate.sourceTreeSha,
464
+ controllerReceiptDigest,
465
+ gateAggregateDigest: gate.gateAggregateDigest,
466
+ artifactDigest: candidate.candidateDigest,
467
+ }),
468
+ };
469
+ }
431
470
  const passport = publicationEvidence.releaseCandidatePassport;
432
471
  const buildSummary = publicationEvidence.buildSummary;
433
472
  const passportValidation = validateReleaseCandidatePassport({