@kungfu-tech/buildchain 2.12.7-alpha.2 → 2.12.7-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/actions/promote-buildchain-ref/README.md +5 -1
  2. package/dist/site/buildchain-contract.json +53 -23
  3. package/dist/site/buildchain-site.json +16 -16
  4. package/dist/site/capability-registry.json +1 -1
  5. package/dist/site/controller-registry.json +27 -3
  6. package/dist/site/kfd-claims.json +63 -9
  7. package/dist/site/kfd-upstream-aggregate.json +1 -1
  8. package/dist/site/manual-registry.json +2 -2
  9. package/dist/site/node-api-registry.json +6 -6
  10. package/dist/site/page-registry.json +10 -10
  11. package/dist/site/public-surface-audit.json +74 -10
  12. package/dist/site/publication-authority-registry.json +25 -1
  13. package/dist/site/publication-registry.json +4 -4
  14. package/dist/site/site-manifest.json +6 -6
  15. package/dist/site/workflow-registry.json +74 -7
  16. package/docs/publication-artifacts.md +30 -20
  17. package/docs/publication-authority.md +21 -9
  18. package/docs/release-candidate.md +19 -0
  19. package/docs/shifu-gate-profiles.md +6 -0
  20. package/package.json +1 -1
  21. package/packages/core/buildchain-publication-authority.js +1 -0
  22. package/packages/core/controller-evidence.js +1 -1
  23. package/packages/core/index.js +7 -0
  24. package/packages/core/publication-artifact-candidate.js +128 -0
  25. package/packages/core/publication-authority.js +41 -2
  26. package/packages/core/publication-control-plane-audit.js +3 -1
  27. package/scripts/assemble-publication-artifact-admission.mjs +190 -0
  28. package/scripts/assemble-self-publication-admission.mjs +19 -9
  29. package/scripts/audit-publication-control-plane.mjs +5 -2
  30. package/scripts/check-inventory.mjs +3 -0
  31. package/scripts/locked-source-checkout.mjs +22 -2
  32. package/scripts/publication-artifact-candidate.mjs +122 -0
  33. package/scripts/workflow-friction-report.mjs +13 -2
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-publication-release-registry",
4
- "generatedAt": "2026-07-14T16:40:36.563Z",
5
- "publishedAt": "2026-07-14T16:40:36.563Z",
4
+ "generatedAt": "2026-07-15T12:26:57.218Z",
5
+ "publishedAt": "2026-07-15T12:26:57.218Z",
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": "31f70f81cbdd542d9473651cf13cd6e8eb33cd74",
22
+ "sourceRevision": "97973413b7ba208df42a3ead28d2b80067a3ecc8",
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.2",
35
+ "version": "2.12.7-alpha.21",
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-14T16:40:36.563Z",
5
- "publishedAt": "2026-07-14T16:40:36.563Z",
4
+ "generatedAt": "2026-07-15T12:26:57.218Z",
5
+ "publishedAt": "2026-07-15T12:26:57.218Z",
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": "31f70f81cbdd542d9473651cf13cd6e8eb33cd74",
22
+ "sourceRevision": "97973413b7ba208df42a3ead28d2b80067a3ecc8",
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.2",
40
+ "version": "2.12.7-alpha.21",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -85,7 +85,7 @@
85
85
  "path": "docs/publication-authority.md",
86
86
  "plane": "verify",
87
87
  "exists": true,
88
- "digest": "sha256:fc08e6657a1b586b6b8b5618ca4f49dfaa85955687f5bba8e2e69e7513fe6c71"
88
+ "digest": "sha256:05bc22c7e5885e591bc121fc43f588e5d49ccf0a8de6a642811c1d0d03dcee1e"
89
89
  },
90
90
  {
91
91
  "id": "release-candidate",
@@ -93,7 +93,7 @@
93
93
  "path": "docs/release-candidate.md",
94
94
  "plane": "verify",
95
95
  "exists": true,
96
- "digest": "sha256:841d6f3281ba6dfe80527e4f149807bdbd05d41f9e94afe1c446fcd6d29e307c"
96
+ "digest": "sha256:6a20c6e3e483327d5ccbe70271f7fb4752731bd0523eeddb96f27b2b6ebf1cf8"
97
97
  },
98
98
  {
99
99
  "id": "release-propagation",
@@ -215,14 +215,19 @@
215
215
  "reusable": true,
216
216
  "inputs": [
217
217
  "admission-json",
218
+ "authority-workflow-path",
218
219
  "auto-admission",
220
+ "auto-admission-kind",
221
+ "auto-no-gate",
219
222
  "buildchain-ref",
220
223
  "buildchain-repository",
221
224
  "control-plane-audit-json",
222
225
  "dry-run",
226
+ "evidence-controller-artifact",
223
227
  "evidence-manifest-pattern",
224
228
  "evidence-passport-artifact",
225
229
  "evidence-payload-pattern",
230
+ "evidence-publication-artifact",
226
231
  "evidence-repository",
227
232
  "evidence-run-id",
228
233
  "evidence-summary-artifact",
@@ -231,13 +236,15 @@
231
236
  "package-name",
232
237
  "product",
233
238
  "publication-target",
239
+ "publication-version",
234
240
  "publisher-workflow-path",
241
+ "required-status-check",
235
242
  "runner-provenance-json",
236
243
  "source-sha",
237
244
  "target-ref",
238
245
  "used-nonces-json"
239
246
  ],
240
- "inputCount": 22,
247
+ "inputCount": 29,
241
248
  "secrets": [],
242
249
  "secretCount": 0,
243
250
  "outputs": [
@@ -1024,6 +1031,54 @@
1024
1031
  "capabilityGroup": "api-cli-reference",
1025
1032
  "status": "active"
1026
1033
  },
1034
+ {
1035
+ "id": "paper-release-sealed",
1036
+ "path": ".github/workflows/paper-release-sealed.yml",
1037
+ "reusable": true,
1038
+ "inputs": [
1039
+ "artifact-name",
1040
+ "artifact-paths",
1041
+ "artifact-retention-days",
1042
+ "branch-protection-bypass-apps",
1043
+ "branch-protection-bypass-teams",
1044
+ "branch-protection-bypass-users",
1045
+ "buildchain-contract-lock-path",
1046
+ "buildchain-ref",
1047
+ "buildchain-repository",
1048
+ "dry-run",
1049
+ "github-release",
1050
+ "github-release-notes",
1051
+ "github-release-title",
1052
+ "package-name",
1053
+ "publish-dist-tag",
1054
+ "publisher-workflow-path",
1055
+ "release-passport-impact-json",
1056
+ "release-passport-output-dir",
1057
+ "release-passport-product-name",
1058
+ "required-status-check",
1059
+ "target-ref",
1060
+ "target-sha",
1061
+ "toolchain-type",
1062
+ "verify-command",
1063
+ "working-directory"
1064
+ ],
1065
+ "inputCount": 25,
1066
+ "secrets": [
1067
+ "BUILDCHAIN_PROMOTION_TOKEN"
1068
+ ],
1069
+ "secretCount": 1,
1070
+ "outputs": [
1071
+ "github-release-url",
1072
+ "package-name",
1073
+ "package-version",
1074
+ "public-release-tag",
1075
+ "release-passport-path"
1076
+ ],
1077
+ "outputCount": 5,
1078
+ "surface": "repository-workflow",
1079
+ "capabilityGroup": "api-cli-reference",
1080
+ "status": "active"
1081
+ },
1027
1082
  {
1028
1083
  "id": "paper-release",
1029
1084
  "path": ".github/workflows/paper-release.yml",
@@ -1193,7 +1248,11 @@
1193
1248
  "buildchain-ref",
1194
1249
  "buildchain-repository",
1195
1250
  "node-version",
1251
+ "package-name",
1252
+ "prepare-paper-package",
1253
+ "publish-dist-tag",
1196
1254
  "setup-node",
1255
+ "target-ref",
1197
1256
  "toolchain-command",
1198
1257
  "toolchain-digest",
1199
1258
  "toolchain-image",
@@ -1201,7 +1260,7 @@
1201
1260
  "verify-command",
1202
1261
  "working-directory"
1203
1262
  ],
1204
- "inputCount": 17,
1263
+ "inputCount": 21,
1205
1264
  "secrets": [],
1206
1265
  "secretCount": 0,
1207
1266
  "outputs": [
@@ -1212,13 +1271,17 @@
1212
1271
  "controller-receipt-digest",
1213
1272
  "controller-receipt-json",
1214
1273
  "controller-receipt-status",
1274
+ "package-name",
1275
+ "package-version",
1215
1276
  "publication-artifact-name",
1216
1277
  "publication-manifest-json",
1217
1278
  "publication-manifest-path",
1218
1279
  "publication-passport-path",
1219
- "publication-registry-path"
1280
+ "publication-registry-path",
1281
+ "publish-dist-tag",
1282
+ "runtime-sha"
1220
1283
  ],
1221
- "outputCount": 12,
1284
+ "outputCount": 16,
1222
1285
  "surface": "repository-workflow",
1223
1286
  "capabilityGroup": "reusable-build",
1224
1287
  "status": "active"
@@ -1247,6 +1310,7 @@
1247
1310
  "package-manager",
1248
1311
  "publication-admission-json",
1249
1312
  "publication-auto-admission",
1313
+ "publication-auto-no-gate",
1250
1314
  "publication-control-plane-audit-json",
1251
1315
  "publication-expected-json",
1252
1316
  "publication-gate-aggregate-json",
@@ -1284,16 +1348,17 @@
1284
1348
  "target-sha",
1285
1349
  "trusted-publishing"
1286
1350
  ],
1287
- "inputCount": 55,
1351
+ "inputCount": 56,
1288
1352
  "secrets": [
1289
1353
  "BUILDCHAIN_ISSUE_APP_ID",
1290
1354
  "BUILDCHAIN_ISSUE_APP_PRIVATE_KEY",
1291
1355
  "BUILDCHAIN_ISSUE_TOKEN",
1356
+ "BUILDCHAIN_PROMOTION_TOKEN",
1292
1357
  "buildchain-issue-app-id",
1293
1358
  "buildchain-issue-app-private-key",
1294
1359
  "buildchain-issue-token"
1295
1360
  ],
1296
- "secretCount": 6,
1361
+ "secretCount": 7,
1297
1362
  "outputs": [
1298
1363
  "built-source-sha",
1299
1364
  "controller-plan-artifact",
@@ -1486,9 +1551,11 @@
1486
1551
  "branch-protection-bypass-teams",
1487
1552
  "branch-protection-bypass-users",
1488
1553
  "dry-run",
1554
+ "expected-publication-version",
1489
1555
  "generated-ref-update-token",
1490
1556
  "generated-status-check-token",
1491
1557
  "github-release",
1558
+ "github-release-artifact-paths",
1492
1559
  "github-release-notes",
1493
1560
  "github-release-title",
1494
1561
  "promote-only-release-candidate",
@@ -1534,7 +1601,7 @@
1534
1601
  "transaction-state-path",
1535
1602
  "verification-command"
1536
1603
  ],
1537
- "inputCount": 52,
1604
+ "inputCount": 54,
1538
1605
  "capabilityGroup": "release-passport-trust",
1539
1606
  "status": "active"
1540
1607
  },
@@ -182,40 +182,42 @@ 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
199
+ secrets:
200
+ BUILDCHAIN_PROMOTION_TOKEN: ${{ secrets.BUILDCHAIN_PROMOTION_TOKEN }}
200
201
  ```
201
202
 
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.
203
+ The sealed preset does not use a long-lived token for npm publication. It may
204
+ accept an optional `BUILDCHAIN_PROMOTION_TOKEN` only for machine-generated
205
+ version-state updates on protected channel branches; npm publication remains
206
+ bound to GitHub OIDC trusted publishing. The preset builds and packages the
207
+ paper in a read-only job, then a credential-free authority job downloads that
208
+ exact candidate, audits the external control plane, and seals a capability over
209
+ the source tree, Buildchain runtime, controller receipt, PDF, and npm package
210
+ bytes. Only the final job receives write and OIDC permissions; it downloads the
211
+ admitted candidate, recomputes the capability binding, and publishes without
212
+ executing consumer build commands. npm binds the OIDC identity to the consumer
213
+ workflow named by `publisher-workflow-path`.
210
214
 
211
215
  The preset:
212
216
 
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;
217
+ - resolves the floating Buildchain runtime once and binds the exact SHA into the
218
+ publication candidate and authority capability;
217
219
  - builds the PDF through the declared pinned LaTeX Docker toolchain or custom
218
- command;
220
+ command in a read-only job;
219
221
  - verifies the paper repository;
220
222
  - writes the publication manifest, publication passport, optional archive
221
223
  registry, and source bundle;
@@ -226,14 +228,22 @@ The preset:
226
228
  - creates a `publish-gate/<alpha|release>/.../<version>` source lock for the
227
229
  channel commit and requires `promote-buildchain-ref` to verify that lock
228
230
  before any publish side effect;
229
- - publishes the package through npm Trusted Publishing;
231
+ - verifies the complete candidate again after authority and publishes the
232
+ package through npm Trusted Publishing without rebuilding it;
230
233
  - writes Buildchain release/passport evidence; and
231
- - creates or updates the exact-version GitHub Release by default.
234
+ - creates or updates the exact-version GitHub Release by default, uploading
235
+ every file declared by `publication.primary_artifact` and
236
+ `publication.artifact_paths` alongside the release evidence.
232
237
 
233
238
  Consumers can opt out of the GitHub Release with `github-release: false`, but
234
239
  the default is on so downstream release propagation can observe
235
240
  `release.published` without hand-written `gh release` steps.
236
241
 
242
+ Declared publication artifacts are resolved from the generated publication
243
+ manifest rather than repeated in consumer workflow YAML. Publication fails
244
+ before upload if a declared artifact is missing or if its basename would
245
+ collide with another GitHub Release asset.
246
+
237
247
  For npm Trusted Publishing, register the consumer workflow file that calls this
238
248
  preset, for example `.github/workflows/paper-release.yml`, against the declared
239
249
  package in npm. The trusted publisher is the consumer repository and workflow
@@ -8,11 +8,11 @@ confidence: high
8
8
  sensitivity: public
9
9
  evidence_grade: B
10
10
  review_state: unreviewed
11
- last_reviewed: 2026-07-14
11
+ last_reviewed: 2026-07-15
12
12
  ai_provenance:
13
13
  model_family: GPT-5
14
14
  product: Codex
15
- generated_at: 2026-07-14
15
+ generated_at: 2026-07-15
16
16
  limits: Live provider configuration must be re-audited; no credential values are represented.
17
17
  ---
18
18
 
@@ -109,17 +109,29 @@ artifacts. The denial explicitly records that npm Trusted Publishing and OIDC
109
109
  were not evaluated, so downstream diagnostics cannot misclassify an admission
110
110
  assembly failure as an npm authentication failure.
111
111
 
112
- Buildchain's own `workflow_run` promotion lane may assemble those inputs only
113
- for `kungfu-systems/buildchain`. It downloads the exact prior RC passport,
112
+ An explicitly opted-in managed `workflow_run` promotion lane may assemble those
113
+ inputs from evidence owned by its caller repository. It downloads the exact prior RC passport,
114
114
  summary, referenced controller receipt, manifests, and product payloads; proves
115
115
  the admitted channel commit has the same Git tree as the RC; performs the live
116
116
  read-only control-plane audit; records the GitHub-hosted job as ephemeral runner
117
- provenance; and creates an explicit Buildchain-owned no-Gate decision. The
117
+ provenance; and consumes either a caller-supplied Gate aggregate or an explicit
118
+ caller no-Gate decision. The
118
119
  independent verifier then recomputes every receipt and payload digest exactly as
119
- it does for externally supplied admission. The self-assembly mode rejects other
120
- repositories, unknown refs, non-exact source SHAs, and any caller other than
121
- `.github/workflows/buildchain-ref-promotion.yml`. Manual apply and external
122
- consumer workflows still require their own explicit admission inputs.
120
+ it does for externally supplied admission. Automatic assembly keeps Buildchain
121
+ as the canonical authority repository, requires evidence to belong to the
122
+ caller, binds a repository-local publisher workflow, and rejects unknown refs,
123
+ non-exact source SHAs, package/target mismatches, or an undeclared Gate policy.
124
+ Manual apply and consumers that do not opt in still require their own explicit
125
+ admission inputs.
126
+
127
+ Before authority verification, the reusable promotion controller runs the same
128
+ release transaction selector in read-only mode. That plan supplies one exact
129
+ publication version and tag to the admission verifier, the publish-gate source
130
+ lock, and the real promotion action. The verifier rejects a capability for a
131
+ different version, and the promotion action rechecks the planned version before
132
+ any publish transaction side effect. Release-candidate fixture versions are
133
+ artifact evidence only; they never name Buildchain's own source-lock or
134
+ publication capability.
123
135
 
124
136
  Evidence publication is a separate authority class and never grants product
125
137
  publication.
@@ -116,3 +116,22 @@ KFD-1 source/artifact contract surfaces, audits KFD-2 public release claims, and
116
116
  compares KFD-3 declared shipped public surfaces with artifact-exposed public
117
117
  surfaces. The release passport records the results under `kfd-1`, `kfd-2`, and
118
118
  the KFD-provided `kfd-3` section.
119
+
120
+ Managed consumers may also ask the promotion wrapper to assemble sealed
121
+ publication evidence from the exact release candidate instead of producing
122
+ short-lived admission JSON in repository-specific workflow code:
123
+
124
+ ```yaml
125
+ publication-auto-admission: true
126
+ publication-auto-no-gate: true
127
+ publication-publisher-workflow-path: .github/workflows/buildchain-ref-promotion.yml
128
+ publication-product: Example Product
129
+ publication-target: npm:@example/product
130
+ publication-package-name: "@example/product"
131
+ ```
132
+
133
+ `publication-auto-no-gate` is an explicit consumer decision, not a default. A
134
+ consumer with a Shifu Gate registry supplies `publication-gate-aggregate-json`
135
+ instead. Buildchain still requires caller-owned RC evidence, an exact authority
136
+ runtime and source SHA, a repository-local publisher workflow, matching npm
137
+ target/package identity, and a qualifying control-plane audit.
@@ -78,6 +78,12 @@ Every matrix job checks out the exact source SHA planned by Buildchain, invokes
78
78
  then invokes `shifu gate receipt validate`. Buildchain uploads the original
79
79
  receipt and validation result even when the run fails.
80
80
 
81
+ Before invoking the project-owned command, the workflow adds the runner
82
+ account's `~/.local/bin` directory to `PATH` on Windows, Linux, and macOS. This
83
+ keeps user-scoped tools such as `uv` available to strict Shifu cache profiles
84
+ without assuming an administrator-managed system installation. The consumer or
85
+ runner owner remains responsible for provisioning the declared tools.
86
+
81
87
  The fixed `Gate profile / aggregate` job fails closed for missing receipts,
82
88
  invalid or stale Shifu validation, dirty or mismatched source SHA, registry or
83
89
  plan drift, missing required results, required failures/skips, or gate action
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.12.7-alpha.2",
3
+ "version": "2.12.7-alpha.21",
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"],
@@ -303,7 +303,7 @@ function normalizeEvidence(entries = []) {
303
303
  function receiptStatus(plan, stages) {
304
304
  if (stages.some((stage) => stage.status === "failed")) return "failed";
305
305
  if (stages.length > 0 && stages.every((stage) => stage.status === "skipped")) return "skipped";
306
- if (stages.some((stage) => stage.status === "cancelled" || stage.status === "missing")) return "partial";
306
+ if (stages.some((stage) => stage.status === "cancelled" || (stage.required && stage.status === "missing"))) return "partial";
307
307
  const required = new Set(plan.expected.stages.filter((stage) => stage.required).map((stage) => stage.id));
308
308
  if (stages.filter((stage) => required.has(stage.id)).every((stage) => stage.status === "passed")) return "passed";
309
309
  return "partial";
@@ -153,6 +153,13 @@ export {
153
153
  verifyPublicationAdmission,
154
154
  } from "./publication-authority.js";
155
155
 
156
+ export {
157
+ PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
158
+ createPublicationArtifactCandidate,
159
+ publicationArtifactCandidateDigest,
160
+ resolvePublicationCandidateFile,
161
+ } from "./publication-artifact-candidate.js";
162
+
156
163
  export {
157
164
  buildchainPublicationAuthorityDescriptors,
158
165
  createBuildchainPublicationAuthorityRegistry,
@@ -0,0 +1,128 @@
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
+ export function resolvePublicationCandidateFile(files = [], candidatePath) {
24
+ const normalizedPath = requiredString(candidatePath, "candidatePath").replaceAll("\\", "/");
25
+ if (normalizedPath.startsWith("/") || normalizedPath.split("/").includes("..")) {
26
+ throw new Error(`publication artifact candidate contains an unsafe path: ${normalizedPath}`);
27
+ }
28
+ const matches = files.filter((entry) => entry.path === normalizedPath);
29
+ if (matches.length !== 1) {
30
+ throw new Error(`expected exactly one publication candidate file at ${normalizedPath}, found ${matches.length}`);
31
+ }
32
+ return matches[0].path;
33
+ }
34
+
35
+ function requiredString(value, label) {
36
+ const normalized = String(value || "").trim();
37
+ if (!normalized) throw new Error(`${label} must be a non-empty string`);
38
+ return normalized;
39
+ }
40
+
41
+ function normalizeDigest(value, label) {
42
+ const normalized = requiredString(value, label).replace(/^sha256:/, "").toLowerCase();
43
+ if (!/^[0-9a-f]{64}$/.test(normalized)) throw new Error(`${label} must be a sha256 digest`);
44
+ return normalized;
45
+ }
46
+
47
+ function normalizeGitSha(value, label) {
48
+ const normalized = requiredString(value, label).toLowerCase();
49
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(normalized)) {
50
+ throw new Error(`${label} must be a 40- or 64-character Git SHA`);
51
+ }
52
+ return normalized;
53
+ }
54
+
55
+ export function createPublicationArtifactCandidate({
56
+ repository,
57
+ sourceSha,
58
+ sourceTreeSha,
59
+ runtimeSha,
60
+ manifest,
61
+ passport,
62
+ controllerReceipt,
63
+ files = [],
64
+ } = {}) {
65
+ const normalizedRepository = requiredString(repository, "repository");
66
+ const normalizedSourceSha = normalizeGitSha(sourceSha, "sourceSha");
67
+ const normalizedSourceTreeSha = normalizeGitSha(sourceTreeSha, "sourceTreeSha");
68
+ const normalizedRuntimeSha = normalizeGitSha(runtimeSha, "runtimeSha");
69
+ if (manifest?.contract !== MANIFEST_CONTRACT) throw new Error("publication artifact manifest contract mismatch");
70
+ if (passport?.contract !== PASSPORT_CONTRACT || passport.status !== "passed") {
71
+ throw new Error("publication artifact passport is not qualifying");
72
+ }
73
+ if (manifest.source?.sha !== normalizedSourceSha || passport.source?.sha !== normalizedSourceSha) {
74
+ throw new Error("publication artifact source SHA mismatch");
75
+ }
76
+ if (manifest.source?.treeSha !== normalizedSourceTreeSha || passport.source?.treeSha !== normalizedSourceTreeSha) {
77
+ throw new Error("publication artifact source tree mismatch");
78
+ }
79
+ const manifestDigest = crypto.createHash("sha256").update(JSON.stringify(manifest, null, 2)).digest("hex");
80
+ if (normalizeDigest(passport.manifestDigest, "passport.manifestDigest") !== manifestDigest) {
81
+ throw new Error("publication artifact passport manifest digest mismatch");
82
+ }
83
+ const controllerValidation = validateControllerReceipt(controllerReceipt, {
84
+ expectedSourceSha: normalizedSourceSha,
85
+ expectedRuntimeSha: normalizedRuntimeSha,
86
+ });
87
+ if (!controllerValidation.ok || !controllerValidation.qualifying) {
88
+ throw new Error(`publication artifact controller receipt did not qualify: ${controllerValidation.issues.join("; ")}`);
89
+ }
90
+ const evidenceKinds = new Set((controllerReceipt.evidence || []).map((entry) => entry.kind));
91
+ for (const kind of ["publication-manifest", "publication-passport"]) {
92
+ if (!evidenceKinds.has(kind)) throw new Error(`publication artifact controller receipt is missing ${kind} evidence`);
93
+ }
94
+ const normalizedFiles = files.map((entry, index) => {
95
+ const filePath = requiredString(entry.path, `files[${index}].path`).replaceAll("\\", "/");
96
+ if (filePath.startsWith("/") || filePath.split("/").includes("..")) {
97
+ throw new Error(`publication artifact candidate contains an unsafe path: ${filePath}`);
98
+ }
99
+ const size = Number(entry.size ?? entry.bytes);
100
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error(`files[${index}].size must be a non-negative safe integer`);
101
+ return { path: filePath, size, sha256: normalizeDigest(entry.sha256, `files[${index}].sha256`) };
102
+ }).sort((left, right) => left.path.localeCompare(right.path));
103
+ if (new Set(normalizedFiles.map((entry) => entry.path)).size !== normalizedFiles.length) {
104
+ throw new Error("publication artifact candidate file paths must be unique");
105
+ }
106
+ const byPath = new Map(normalizedFiles.map((entry) => [entry.path, entry]));
107
+ for (const [index, artifact] of (manifest.artifacts || []).entries()) {
108
+ const artifactPath = requiredString(artifact.path, `manifest.artifacts[${index}].path`);
109
+ const actual = byPath.get(artifactPath);
110
+ if (!actual) throw new Error(`publication artifact candidate is missing declared artifact: ${artifactPath}`);
111
+ if (actual.size !== Number(artifact.bytes) || actual.sha256 !== normalizeDigest(artifact.sha256, `manifest.artifacts[${index}].sha256`)) {
112
+ throw new Error(`publication artifact candidate bytes do not match manifest: ${artifactPath}`);
113
+ }
114
+ }
115
+ const payload = {
116
+ schemaVersion: 1,
117
+ contract: PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
118
+ repository: normalizedRepository,
119
+ sourceSha: normalizedSourceSha,
120
+ sourceTreeSha: normalizedSourceTreeSha,
121
+ runtimeSha: normalizedRuntimeSha,
122
+ manifestDigest,
123
+ passportDigest: publicationArtifactCandidateDigest(passport),
124
+ controllerReceiptDigest: normalizeDigest(controllerReceipt.digest, "controllerReceipt.digest"),
125
+ files: normalizedFiles,
126
+ };
127
+ return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
128
+ }