@kungfu-tech/buildchain 2.12.7-alpha.6 → 2.12.7-alpha.7

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",
@@ -632,7 +636,7 @@
632
636
  "target-ref",
633
637
  "used-nonces-json"
634
638
  ],
635
- "inputCount": 23,
639
+ "inputCount": 27,
636
640
  "secrets": [],
637
641
  "secretCount": 0,
638
642
  "outputs": [
@@ -1335,6 +1339,49 @@
1335
1339
  "outputs": [],
1336
1340
  "outputCount": 0
1337
1341
  },
1342
+ {
1343
+ "id": "paper-release-sealed",
1344
+ "path": ".github/workflows/paper-release-sealed.yml",
1345
+ "reusable": true,
1346
+ "inputs": [
1347
+ "artifact-name",
1348
+ "artifact-paths",
1349
+ "artifact-retention-days",
1350
+ "branch-protection-bypass-apps",
1351
+ "branch-protection-bypass-teams",
1352
+ "branch-protection-bypass-users",
1353
+ "buildchain-contract-lock-path",
1354
+ "buildchain-ref",
1355
+ "buildchain-repository",
1356
+ "dry-run",
1357
+ "github-release",
1358
+ "github-release-notes",
1359
+ "github-release-title",
1360
+ "package-name",
1361
+ "publish-dist-tag",
1362
+ "publisher-workflow-path",
1363
+ "release-passport-impact-json",
1364
+ "release-passport-output-dir",
1365
+ "release-passport-product-name",
1366
+ "required-status-check",
1367
+ "target-ref",
1368
+ "target-sha",
1369
+ "toolchain-type",
1370
+ "verify-command",
1371
+ "working-directory"
1372
+ ],
1373
+ "inputCount": 25,
1374
+ "secrets": [],
1375
+ "secretCount": 0,
1376
+ "outputs": [
1377
+ "github-release-url",
1378
+ "package-name",
1379
+ "package-version",
1380
+ "public-release-tag",
1381
+ "release-passport-path"
1382
+ ],
1383
+ "outputCount": 5
1384
+ },
1338
1385
  {
1339
1386
  "id": "paper-release",
1340
1387
  "path": ".github/workflows/paper-release.yml",
@@ -1492,7 +1539,11 @@
1492
1539
  "buildchain-ref",
1493
1540
  "buildchain-repository",
1494
1541
  "node-version",
1542
+ "package-name",
1543
+ "prepare-paper-package",
1544
+ "publish-dist-tag",
1495
1545
  "setup-node",
1546
+ "target-ref",
1496
1547
  "toolchain-command",
1497
1548
  "toolchain-digest",
1498
1549
  "toolchain-image",
@@ -1500,7 +1551,7 @@
1500
1551
  "verify-command",
1501
1552
  "working-directory"
1502
1553
  ],
1503
- "inputCount": 17,
1554
+ "inputCount": 21,
1504
1555
  "secrets": [],
1505
1556
  "secretCount": 0,
1506
1557
  "outputs": [
@@ -1511,13 +1562,17 @@
1511
1562
  "controller-receipt-digest",
1512
1563
  "controller-receipt-json",
1513
1564
  "controller-receipt-status",
1565
+ "package-name",
1566
+ "package-version",
1514
1567
  "publication-artifact-name",
1515
1568
  "publication-manifest-json",
1516
1569
  "publication-manifest-path",
1517
1570
  "publication-passport-path",
1518
- "publication-registry-path"
1571
+ "publication-registry-path",
1572
+ "publish-dist-tag",
1573
+ "runtime-sha"
1519
1574
  ],
1520
- "outputCount": 12
1575
+ "outputCount": 16
1521
1576
  },
1522
1577
  {
1523
1578
  "id": "release-candidate-promote",
@@ -3331,8 +3386,8 @@
3331
3386
  "workflowRegistryPath": "dist/site/workflow-registry.json",
3332
3387
  "pageRegistryPath": "dist/site/page-registry.json",
3333
3388
  "cliRegistryDigest": "a6daf4822365bf576425777230246046ebca67b3f8dcfea61fc7a61773776185",
3334
- "workflowRegistryDigest": "2b36c3c532082a835a2054ce565b4b5ca39ef1c09b8188a080d5dc4a5909218b",
3335
- "pageRegistryDigest": "dc526c6f9aefa92aa566a9664374eb4f177f4b3ae5b9480eb94abbbf3a257a30"
3389
+ "workflowRegistryDigest": "89b7907718c8b4a7b22c3d36a971510a918f113b245bb7fe1d8982dbaf707c66",
3390
+ "pageRegistryDigest": "3a4779e3d85d0a175e33c7d2a8bee30ede3931cc721a7d8c422ab321f0dec723"
3336
3391
  },
3337
3392
  "comparison": {
3338
3393
  "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:01:29.454Z",
5
+ "publishedAt": "2026-07-15T01:01:29.454Z",
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": "f0f922906c80f4096d2ef9871a4ad831be12f46e",
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.7",
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:01:29.454Z",
5
+ "publishedAt": "2026-07-15T01:01:29.454Z",
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": "f0f922906c80f4096d2ef9871a4ad831be12f46e",
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.7",
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",
@@ -238,7 +242,7 @@
238
242
  "target-ref",
239
243
  "used-nonces-json"
240
244
  ],
241
- "inputCount": 23,
245
+ "inputCount": 27,
242
246
  "secrets": [],
243
247
  "secretCount": 0,
244
248
  "outputs": [
@@ -1025,6 +1029,52 @@
1025
1029
  "capabilityGroup": "api-cli-reference",
1026
1030
  "status": "active"
1027
1031
  },
1032
+ {
1033
+ "id": "paper-release-sealed",
1034
+ "path": ".github/workflows/paper-release-sealed.yml",
1035
+ "reusable": true,
1036
+ "inputs": [
1037
+ "artifact-name",
1038
+ "artifact-paths",
1039
+ "artifact-retention-days",
1040
+ "branch-protection-bypass-apps",
1041
+ "branch-protection-bypass-teams",
1042
+ "branch-protection-bypass-users",
1043
+ "buildchain-contract-lock-path",
1044
+ "buildchain-ref",
1045
+ "buildchain-repository",
1046
+ "dry-run",
1047
+ "github-release",
1048
+ "github-release-notes",
1049
+ "github-release-title",
1050
+ "package-name",
1051
+ "publish-dist-tag",
1052
+ "publisher-workflow-path",
1053
+ "release-passport-impact-json",
1054
+ "release-passport-output-dir",
1055
+ "release-passport-product-name",
1056
+ "required-status-check",
1057
+ "target-ref",
1058
+ "target-sha",
1059
+ "toolchain-type",
1060
+ "verify-command",
1061
+ "working-directory"
1062
+ ],
1063
+ "inputCount": 25,
1064
+ "secrets": [],
1065
+ "secretCount": 0,
1066
+ "outputs": [
1067
+ "github-release-url",
1068
+ "package-name",
1069
+ "package-version",
1070
+ "public-release-tag",
1071
+ "release-passport-path"
1072
+ ],
1073
+ "outputCount": 5,
1074
+ "surface": "repository-workflow",
1075
+ "capabilityGroup": "api-cli-reference",
1076
+ "status": "active"
1077
+ },
1028
1078
  {
1029
1079
  "id": "paper-release",
1030
1080
  "path": ".github/workflows/paper-release.yml",
@@ -1194,7 +1244,11 @@
1194
1244
  "buildchain-ref",
1195
1245
  "buildchain-repository",
1196
1246
  "node-version",
1247
+ "package-name",
1248
+ "prepare-paper-package",
1249
+ "publish-dist-tag",
1197
1250
  "setup-node",
1251
+ "target-ref",
1198
1252
  "toolchain-command",
1199
1253
  "toolchain-digest",
1200
1254
  "toolchain-image",
@@ -1202,7 +1256,7 @@
1202
1256
  "verify-command",
1203
1257
  "working-directory"
1204
1258
  ],
1205
- "inputCount": 17,
1259
+ "inputCount": 21,
1206
1260
  "secrets": [],
1207
1261
  "secretCount": 0,
1208
1262
  "outputs": [
@@ -1213,13 +1267,17 @@
1213
1267
  "controller-receipt-digest",
1214
1268
  "controller-receipt-json",
1215
1269
  "controller-receipt-status",
1270
+ "package-name",
1271
+ "package-version",
1216
1272
  "publication-artifact-name",
1217
1273
  "publication-manifest-json",
1218
1274
  "publication-manifest-path",
1219
1275
  "publication-passport-path",
1220
- "publication-registry-path"
1276
+ "publication-registry-path",
1277
+ "publish-dist-tag",
1278
+ "runtime-sha"
1221
1279
  ],
1222
- "outputCount": 12,
1280
+ "outputCount": 16,
1223
1281
  "surface": "repository-workflow",
1224
1282
  "capabilityGroup": "reusable-build",
1225
1283
  "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.7",
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({