@kungfu-tech/buildchain 2.12.9-alpha.1 → 2.13.0-alpha.1
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/actions/promote-buildchain-ref/README.md +13 -0
- package/actions/run-lifecycle/README.md +5 -0
- package/bin/buildchain.mjs +62 -0
- package/dist/site/buildchain-contract.json +50 -25
- package/dist/site/buildchain-site.json +82 -41
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +24 -0
- package/dist/site/controller-registry.json +24 -4
- package/dist/site/kfd-claims.json +78 -16
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -10
- package/dist/site/node-api-registry.json +20 -7
- package/dist/site/page-registry.json +68 -27
- package/dist/site/public-surface-audit.json +65 -15
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +14 -14
- package/dist/site/workflow-registry.json +25 -9
- package/docs/MAP.md +1 -0
- package/docs/artifact-verification-envelope.md +113 -0
- package/docs/cli.md +22 -0
- package/docs/install.md +1 -3
- package/docs/publication-authority.md +26 -0
- package/docs/publish-transaction.md +1 -1
- package/docs/release-candidate.md +24 -0
- package/docs/release-governance.md +12 -6
- package/docs/release-propagation.md +3 -0
- package/docs/reusable-build-surface.md +13 -0
- package/docs/versioning.md +1 -0
- package/package.json +2 -1
- package/packages/core/README.md +13 -0
- package/packages/core/artifact-passport.js +6 -1
- package/packages/core/artifact-verification-envelope.js +386 -0
- package/packages/core/buildchain-config.js +6 -2
- package/packages/core/controller-evidence.js +2 -3
- package/packages/core/index.js +16 -0
- package/packages/core/publication-authority.js +281 -12
- package/packages/core/release-line-bootstrap.js +24 -0
- package/scripts/assemble-publication-artifact-admission.mjs +10 -0
- package/scripts/assemble-self-publication-admission.mjs +10 -1
- package/scripts/check-inventory.mjs +5 -0
- package/scripts/generate-site-bundle.mjs +4 -0
- package/scripts/release-candidate-resolver.mjs +52 -29
- package/scripts/run-lifecycle-core.mjs +50 -6
|
@@ -136,6 +136,32 @@ publication capability.
|
|
|
136
136
|
Evidence publication is a separate authority class and never grants product
|
|
137
137
|
publication.
|
|
138
138
|
|
|
139
|
+
## Consumer qualification handoff
|
|
140
|
+
|
|
141
|
+
Consumers may explicitly opt in to a final, consumer-owned qualification
|
|
142
|
+
predicate. Buildchain then transports the complete Gate aggregate alongside the
|
|
143
|
+
sealed capability instead of reducing it to a summary. The capability binds the
|
|
144
|
+
authority registry, consumer Gate registry and policy, exact source and runtime,
|
|
145
|
+
controller, runner, control-plane, artifact, provider target, channel, version,
|
|
146
|
+
freshness window, nonce, and exact predicate command digest.
|
|
147
|
+
|
|
148
|
+
The predicate runs in a separate job with read-only source access, no inherited
|
|
149
|
+
secrets, no OIDC permission, and no provider write permission. It receives only
|
|
150
|
+
paths to `capability.json` and `gate-aggregate.json` plus the exact predicate id
|
|
151
|
+
and digest. The consumer writes a
|
|
152
|
+
`kungfu-buildchain-consumer-publication-decision`; Buildchain seals that result
|
|
153
|
+
as a deterministic
|
|
154
|
+
`kungfu-buildchain-publication-qualification-receipt`. Buildchain does not parse
|
|
155
|
+
product-specific Gate meanings.
|
|
156
|
+
|
|
157
|
+
The provider action revalidates the capability, full aggregate, receipt,
|
|
158
|
+
predicate identity, freshness, nonce, source, version, channel, target, and all
|
|
159
|
+
sealed digests immediately before the publish transaction. Missing, denied,
|
|
160
|
+
stale, replayed, substituted, or drifted receipts fail before provider mutation.
|
|
161
|
+
Direct calls to the provider action cannot bypass the receipt when
|
|
162
|
+
`require-publication-qualification` is enabled. Consumers that do not opt in
|
|
163
|
+
retain the existing publication contract.
|
|
164
|
+
|
|
139
165
|
## API and CLI
|
|
140
166
|
|
|
141
167
|
Use `@kungfu-tech/buildchain/publication-authority` or run:
|
|
@@ -210,7 +210,7 @@ Buildchain distinguishes two npm release modes:
|
|
|
210
210
|
|
|
211
211
|
| Mode | Use | Auth | npm operation |
|
|
212
212
|
| --- | --- | --- | --- |
|
|
213
|
-
| `publish-final-version` | normal alpha or stable publication | `trusted-publishing` | `npm publish --tag <alpha|
|
|
213
|
+
| `publish-final-version` | normal alpha or stable publication | `trusted-publishing` | `npm publish --tag <alpha|latest>` |
|
|
214
214
|
| `promote-existing-version` | same-version alpha-to-latest recovery | `npm-token` | `npm dist-tag add <pkg>@<version> latest` |
|
|
215
215
|
|
|
216
216
|
The normal libnode path is `publish-final-version`: publish an alpha package set
|
|
@@ -82,6 +82,11 @@ other package as `role: platform`. Consumer workflows therefore stay
|
|
|
82
82
|
declarative and do not need their own artifact download or publish-evidence
|
|
83
83
|
generation scripts.
|
|
84
84
|
|
|
85
|
+
Because a channel merge can trigger promotion before its PR-stage matrix has
|
|
86
|
+
finished uploading evidence, the resolver waits up to ten minutes for the exact
|
|
87
|
+
merged PR's successful workflow run and paired artifacts. Polling remains bound
|
|
88
|
+
to the PR/head identity; timeout or a sibling run still fails closed.
|
|
89
|
+
|
|
85
90
|
By default, the wrapper forwards GitHub Release publication to the underlying
|
|
86
91
|
`promote-buildchain-ref` semver model. Once the release transaction is complete,
|
|
87
92
|
the action creates or updates the public GitHub Release, applies
|
|
@@ -135,3 +140,22 @@ consumer with a Shifu Gate registry supplies `publication-gate-aggregate-json`
|
|
|
135
140
|
instead. Buildchain still requires caller-owned RC evidence, an exact authority
|
|
136
141
|
runtime and source SHA, a repository-local publisher workflow, matching npm
|
|
137
142
|
target/package identity, and a qualifying control-plane audit.
|
|
143
|
+
|
|
144
|
+
A consumer that owns additional product qualification semantics can opt in to
|
|
145
|
+
the sealed handoff without teaching Buildchain those semantics:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
publication-consumer-predicate-id: kungfu.release-admission/v1
|
|
149
|
+
publication-consumer-qualification-command: node scripts/qualify-release.mjs
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The command reads `BUILDCHAIN_PUBLICATION_CAPABILITY_PATH` and
|
|
153
|
+
`BUILDCHAIN_PUBLICATION_GATE_AGGREGATE_PATH`, evaluates the complete aggregate,
|
|
154
|
+
and writes a decision JSON document to
|
|
155
|
+
`BUILDCHAIN_PUBLICATION_QUALIFICATION_RESULT_PATH`. It also receives
|
|
156
|
+
`BUILDCHAIN_PUBLICATION_PREDICATE_ID` and
|
|
157
|
+
`BUILDCHAIN_PUBLICATION_PREDICATE_DIGEST`. The separate qualification job has
|
|
158
|
+
no write or OIDC permission and does not inherit publication secrets. A
|
|
159
|
+
successful deterministic receipt is rechecked by the provider action
|
|
160
|
+
immediately before mutation. Omitting both inputs preserves the existing
|
|
161
|
+
consumer contract; supplying only one fails closed.
|
|
@@ -94,6 +94,13 @@ The implementation is intentionally stricter than a local release script:
|
|
|
94
94
|
release source tree against that tested alpha tree;
|
|
95
95
|
- generated version-state commits are verified before refs move.
|
|
96
96
|
|
|
97
|
+
Exact publication planning installs the checked-out promotion source's declared
|
|
98
|
+
dependencies before version-state verification. This keeps the pre-authority
|
|
99
|
+
version plan on the same package-manager boundary as the later promotion job,
|
|
100
|
+
including repositories whose verification commands import production packages.
|
|
101
|
+
The planning pass may materialize and verify declared derived files locally,
|
|
102
|
+
but dry-run never creates Git blobs, trees, commits, refs, or tags.
|
|
103
|
+
|
|
97
104
|
Promotion intents are serialized globally per caller repository with
|
|
98
105
|
`cancel-in-progress: false`. A queued intent re-reads its protected target ref
|
|
99
106
|
before checkout, dependency installation, release-candidate resolution, or any
|
|
@@ -162,11 +169,6 @@ Buildchain then:
|
|
|
162
169
|
7. Moves `vX.Y-alpha` to the same generated alpha commit.
|
|
163
170
|
8. Moves `vX-alpha` only when `X.Y` is the highest minor in major `X` with a published alpha; older minor alpha work records a skip and cannot move the major channel backwards.
|
|
164
171
|
|
|
165
|
-
The npm channel follows the same ownership rule. The highest alpha minor publishes
|
|
166
|
-
with dist-tag `alpha`; maintenance alphas on an older minor publish with the
|
|
167
|
-
line-specific dist-tag `vX.Y-alpha` so they cannot roll the global `alpha`
|
|
168
|
-
channel backward. Exact prerelease versions remain installable directly.
|
|
169
|
-
|
|
170
172
|
This keeps the test channel self-describing. If a consumer checks out
|
|
171
173
|
`v2.0-alpha` or `v2-alpha`, the manifests and exact alpha tag agree. The major
|
|
172
174
|
alpha ref removes routine consumer edits when Buildchain opens a newer minor,
|
|
@@ -588,7 +590,11 @@ Buildchain's own promotion workflow reads `BUILDCHAIN_PROMOTION_BYPASS_APPS`,
|
|
|
588
590
|
`BUILDCHAIN_PROMOTION_BYPASS_TEAMS` repository variables so the declared bypass
|
|
589
591
|
identity can match the actual `BUILDCHAIN_PROMOTION_TOKEN` actor, but consumers
|
|
590
592
|
do not need to duplicate that actor manually when the token identity is
|
|
591
|
-
discoverable.
|
|
593
|
+
discoverable. Buildchain's release-line bootstrap applies the same declared
|
|
594
|
+
allowances when it creates new `dev`, `alpha`, and `release` branch protection,
|
|
595
|
+
and fails closed before changing protection when no promotion authority is
|
|
596
|
+
declared. This keeps a new minor line publishable without weakening its normal
|
|
597
|
+
human review gate.
|
|
592
598
|
|
|
593
599
|
## What This Guarantees
|
|
594
600
|
|
|
@@ -194,6 +194,9 @@ downstream repository, writes the exact lock, and opens or updates a PR. It does
|
|
|
194
194
|
not publish the downstream release directly. The downstream repository keeps its
|
|
195
195
|
normal Buildchain governance: the PR updates source-of-truth facts, then
|
|
196
196
|
downstream alpha or release publication runs through its own protected channel.
|
|
197
|
+
The workflow stages the declared lock path before checking the index, so the
|
|
198
|
+
first propagation creates a PR even when the lock did not previously exist; a
|
|
199
|
+
byte-identical rerun is an explicit successful no-op.
|
|
197
200
|
For unreleased runtime validation, keep the caller's reusable workflow reference
|
|
198
201
|
on `@v2` and pass a temporary train ref through `buildchain-ref`.
|
|
199
202
|
|
|
@@ -903,6 +903,18 @@ with:
|
|
|
903
903
|
verify-command: ctest --test-dir build --output-on-failure
|
|
904
904
|
```
|
|
905
905
|
|
|
906
|
+
Every native and container matrix job is bounded by
|
|
907
|
+
`lifecycle-timeout-minutes`, which defaults to 120 minutes. The same input is
|
|
908
|
+
the fallback deadline for each install, build, and verify action, so a hung
|
|
909
|
+
command fails with the lifecycle name and matrix platform before it can occupy
|
|
910
|
+
a self-hosted runner indefinitely. A stage-level `timeout_minutes` in
|
|
911
|
+
`buildchain.toml` remains the more specific override for that stage.
|
|
912
|
+
|
|
913
|
+
```yaml
|
|
914
|
+
with:
|
|
915
|
+
lifecycle-timeout-minutes: 90
|
|
916
|
+
```
|
|
917
|
+
|
|
906
918
|
The reusable build workflow samples the build lifecycle by default and carries
|
|
907
919
|
the generated summary into the final verify diagnostics. Callers can override
|
|
908
920
|
the sidecar path or disable sampling:
|
|
@@ -932,6 +944,7 @@ For custom workflows, use the action directly:
|
|
|
932
944
|
with:
|
|
933
945
|
stage: build
|
|
934
946
|
required: "true"
|
|
947
|
+
timeout-minutes: "90"
|
|
935
948
|
artifact-name: libnode-linux-x64-${{ github.sha }}
|
|
936
949
|
artifact-paths: |
|
|
937
950
|
dist
|
package/docs/versioning.md
CHANGED
|
@@ -61,6 +61,7 @@ repository changes are patch".
|
|
|
61
61
|
|
|
62
62
|
| Date | Action | Line | Faces | Class | Rationale | PR |
|
|
63
63
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
64
|
+
| 2026-07-15 | open-minor | `v2.13` | artifact-verification-envelope, package-subpaths | additive | The public envelope seals exact artifact, provenance, identity, lifecycle, revocation, and existing KFD assessment roots into one consumer-ready KFX admission input, with a dedicated Node API export and fail-closed verifier. | |
|
|
64
65
|
| 2026-07-11 | open-minor | `v2.12` | channel-build-router, channel-selection-protocol | additive | The public `build.yml` reusable workflow lets consumers declare one build job while Buildchain selects generic major alpha for development/prerelease intent and stable major for production release intent, with explicit overrides, separate locks, and fail-closed ambiguity handling. | |
|
|
65
66
|
| 2026-07-08 | open-minor | `v2.9` | build-facts-contract | additive | Build Facts add a public CLI command family, Node API export, config protocol, module/product fact contracts, release-passport evidence section, and Kungfu legacy buildinfo projection from the same source facts. | |
|
|
66
67
|
| 2026-07-06 | open-minor | `v2.8` | kfd-1-contract-world-release-gate, kfd-2-release-trust-passport-audit, kfd-3-collaboration-interface-trust-proof, publish-source-lock-enforcement, required-check-protection | additive | KFD release gates add KFD-1 self contract verification, KFD-2 public release trust claim audit, KFD-3 collaboration-interface trust proofs, publish-side source-lock enforcement for promote-only wrappers, and protected channel required checks repaired to bind GitHub Actions check runs instead of legacy commit status contexts. | |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0-alpha.1",
|
|
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",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"./build-facts": "./packages/core/build-facts.js",
|
|
17
17
|
"./core": "./packages/core/index.js",
|
|
18
18
|
"./artifact-passport": "./packages/core/artifact-passport.js",
|
|
19
|
+
"./artifact-verification-envelope": "./packages/core/artifact-verification-envelope.js",
|
|
19
20
|
"./buildchain-contract": "./packages/core/buildchain-contract.js",
|
|
20
21
|
"./controller-evidence": "./packages/core/controller-evidence.js",
|
|
21
22
|
"./diagnostics": "./packages/core/diagnostics.js",
|
package/packages/core/README.md
CHANGED
|
@@ -16,6 +16,8 @@ Current shared surfaces:
|
|
|
16
16
|
`@kungfu-tech/buildchain/build-facts`;
|
|
17
17
|
- release passport creation and verification through
|
|
18
18
|
`@kungfu-tech/buildchain/release-passport`.
|
|
19
|
+
- sealed exact-root and KFD assessment inputs for KFX admission through
|
|
20
|
+
`@kungfu-tech/buildchain/artifact-verification-envelope`.
|
|
19
21
|
- managed KFD / Release Passport badge bundle facts and README marker blocks
|
|
20
22
|
through `@kungfu-tech/buildchain/badges`.
|
|
21
23
|
- publication artifact manifests, source bundles, and publication artifact
|
|
@@ -66,6 +68,17 @@ const fact = collectModuleBuildFacts({ moduleId: "native-core" });
|
|
|
66
68
|
writeBuildFacts({ fact, output: ".buildchain/facts/native-core.json" });
|
|
67
69
|
```
|
|
68
70
|
|
|
71
|
+
KFX producers can seal one passing artifact verification with exact roots,
|
|
72
|
+
identity, lifecycle, revocation, and an existing ADR-0052 assessment. Consumers
|
|
73
|
+
verify or project the same envelope instead of reconstructing bindings:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import {
|
|
77
|
+
projectArtifactVerificationEnvelopeToKfx,
|
|
78
|
+
verifyArtifactVerificationEnvelope,
|
|
79
|
+
} from "@kungfu-tech/buildchain/artifact-verification-envelope";
|
|
80
|
+
```
|
|
81
|
+
|
|
69
82
|
Publication repositories can produce site-consumable paper/report facts without
|
|
70
83
|
becoming web-surface repositories. When `[publication.archive]` is configured,
|
|
71
84
|
the same API also maintains the append-only publication registry used by site
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import https from "node:https";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { sealArtifactVerificationReport } from "./artifact-verification-envelope.js";
|
|
6
7
|
import { readJsonFromLocation, sha256File, verifyReleasePassport } from "./release-passport.js";
|
|
7
8
|
|
|
8
9
|
export const ARTIFACT_VERIFICATION_CONTRACT = "kungfu-buildchain-artifact-verification";
|
|
@@ -708,6 +709,7 @@ export async function verifyArtifactPassport({
|
|
|
708
709
|
subjectDigest = "",
|
|
709
710
|
subjectKind = "",
|
|
710
711
|
npmRegistryBaseUrl = "",
|
|
712
|
+
verificationEnvelope = undefined,
|
|
711
713
|
} = {}) {
|
|
712
714
|
const resolvedSubject = await resolveArtifactSubject(subject, { cwd, subjectDigest, subjectKind, npmRegistryBaseUrl });
|
|
713
715
|
const issues = [];
|
|
@@ -789,7 +791,7 @@ export async function verifyArtifactPassport({
|
|
|
789
791
|
}
|
|
790
792
|
const ok = passportReport.ok && Boolean(match);
|
|
791
793
|
const outcome = ok ? "pass" : "fail";
|
|
792
|
-
|
|
794
|
+
const report = {
|
|
793
795
|
schemaVersion: 1,
|
|
794
796
|
contract: ARTIFACT_VERIFICATION_CONTRACT,
|
|
795
797
|
outcome,
|
|
@@ -817,6 +819,9 @@ export async function verifyArtifactPassport({
|
|
|
817
819
|
: undefined,
|
|
818
820
|
issues,
|
|
819
821
|
};
|
|
822
|
+
return verificationEnvelope
|
|
823
|
+
? sealArtifactVerificationReport({ report, ...verificationEnvelope })
|
|
824
|
+
: report;
|
|
820
825
|
}
|
|
821
826
|
|
|
822
827
|
export async function explainArtifactPassport(options = {}) {
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT =
|
|
4
|
+
"kungfu-buildchain-artifact-verification-envelope";
|
|
5
|
+
export const ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT =
|
|
6
|
+
"kungfu-buildchain-artifact-verification-envelope-check";
|
|
7
|
+
export const KFX_ADMISSION_INPUTS_CONTRACT =
|
|
8
|
+
"kungfu-buildchain-kfx-admission-inputs";
|
|
9
|
+
|
|
10
|
+
const ARTIFACT_VERIFICATION_CONTRACT =
|
|
11
|
+
"kungfu-buildchain-artifact-verification";
|
|
12
|
+
const KFX_TRUST_INPUTS_SCHEMA = "kungfu.kfx-trust-inputs/v1";
|
|
13
|
+
const KFD_ASSESSMENT_SCHEMA = "kungfu.trust.assessment/v1";
|
|
14
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
const ROOT_FIELDS = Object.freeze([
|
|
16
|
+
"packageRoot",
|
|
17
|
+
"sourceRoot",
|
|
18
|
+
"dependencyRoot",
|
|
19
|
+
"buildPlanRoot",
|
|
20
|
+
"toolchainRoot",
|
|
21
|
+
"artifactRoot",
|
|
22
|
+
"qualificationRoot",
|
|
23
|
+
"verifierRoot",
|
|
24
|
+
]);
|
|
25
|
+
const BINDING_FIELDS = Object.freeze([
|
|
26
|
+
"schema",
|
|
27
|
+
...ROOT_FIELDS,
|
|
28
|
+
"issuer",
|
|
29
|
+
"publisher",
|
|
30
|
+
"contractVersion",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function stableJson(value) {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
36
|
+
}
|
|
37
|
+
if (value && typeof value === "object") {
|
|
38
|
+
return `{${Object.keys(value)
|
|
39
|
+
.sort()
|
|
40
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
41
|
+
.join(",")}}`;
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function jsonClone(value) {
|
|
47
|
+
return JSON.parse(JSON.stringify(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function artifactVerificationEnvelopeDigest(value) {
|
|
51
|
+
return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function issue(code, message, details = {}) {
|
|
55
|
+
return { level: "error", code, message, details };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isObject(value) {
|
|
59
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRoot(value) {
|
|
63
|
+
return SHA256_PATTERN.test(String(value || ""));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function nonEmptyString(value, label) {
|
|
67
|
+
const normalized = String(value || "").trim();
|
|
68
|
+
if (!normalized) {
|
|
69
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function nonNegativeInteger(value, label) {
|
|
75
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
76
|
+
throw new Error(`${label} must be a non-negative safe integer`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeBindings(value = {}) {
|
|
82
|
+
if (!isObject(value)) {
|
|
83
|
+
throw new Error("bindings must be an object");
|
|
84
|
+
}
|
|
85
|
+
const supplied = Object.keys(value).sort();
|
|
86
|
+
const expected = [...BINDING_FIELDS].sort();
|
|
87
|
+
if (stableJson(supplied) !== stableJson(expected)) {
|
|
88
|
+
throw new Error(`bindings must contain exactly: ${BINDING_FIELDS.join(", ")}`);
|
|
89
|
+
}
|
|
90
|
+
if (value.schema !== KFX_TRUST_INPUTS_SCHEMA) {
|
|
91
|
+
throw new Error(`bindings.schema must be ${KFX_TRUST_INPUTS_SCHEMA}`);
|
|
92
|
+
}
|
|
93
|
+
const normalized = { schema: KFX_TRUST_INPUTS_SCHEMA };
|
|
94
|
+
for (const field of ROOT_FIELDS) {
|
|
95
|
+
const root = String(value[field] || "");
|
|
96
|
+
if (!isRoot(root)) {
|
|
97
|
+
throw new Error(`bindings.${field} must be a canonical lowercase sha256 root`);
|
|
98
|
+
}
|
|
99
|
+
normalized[field] = root;
|
|
100
|
+
}
|
|
101
|
+
normalized.issuer = nonEmptyString(value.issuer, "bindings.issuer");
|
|
102
|
+
normalized.publisher = nonEmptyString(value.publisher, "bindings.publisher");
|
|
103
|
+
normalized.contractVersion = nonEmptyString(value.contractVersion, "bindings.contractVersion");
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeRevocation(value = {}, { issuedAt } = {}) {
|
|
108
|
+
if (!isObject(value)) {
|
|
109
|
+
throw new Error("revocation must be an object");
|
|
110
|
+
}
|
|
111
|
+
const revoked = value.revoked === true;
|
|
112
|
+
const status = nonEmptyString(value.status || (revoked ? "revoked" : "active"), "revocation.status");
|
|
113
|
+
if (!new Set(["active", "revoked"]).has(status) || revoked !== (status === "revoked")) {
|
|
114
|
+
throw new Error("revocation.status must agree with revocation.revoked");
|
|
115
|
+
}
|
|
116
|
+
const checkedAt = nonNegativeInteger(value.checkedAt ?? issuedAt, "revocation.checkedAt");
|
|
117
|
+
const root = String(value.root || "");
|
|
118
|
+
if (!isRoot(root)) {
|
|
119
|
+
throw new Error("revocation.root must be a canonical lowercase sha256 root");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
status,
|
|
123
|
+
revoked,
|
|
124
|
+
checkedAt,
|
|
125
|
+
source: nonEmptyString(value.source, "revocation.source"),
|
|
126
|
+
root,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function envelopeRoot(value) {
|
|
131
|
+
const basis = jsonClone(value);
|
|
132
|
+
if (isObject(basis.envelope)) {
|
|
133
|
+
delete basis.envelope.root;
|
|
134
|
+
}
|
|
135
|
+
return artifactVerificationEnvelopeDigest(basis);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function recomputeKfdReportRoot(report) {
|
|
139
|
+
const basis = jsonClone(report);
|
|
140
|
+
delete basis.report_hash;
|
|
141
|
+
return artifactVerificationEnvelopeDigest(basis);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function verifyArtifactVerificationEnvelope({
|
|
145
|
+
envelope,
|
|
146
|
+
assessmentTime = Math.floor(Date.now() / 1000),
|
|
147
|
+
expectedEnvelopeRoot = "",
|
|
148
|
+
expectedIssuer = "",
|
|
149
|
+
expectedPublisher = "",
|
|
150
|
+
expectedContractVersion = "",
|
|
151
|
+
} = {}) {
|
|
152
|
+
const issues = [];
|
|
153
|
+
const at = Number.isSafeInteger(assessmentTime) && assessmentTime >= 0
|
|
154
|
+
? assessmentTime
|
|
155
|
+
: -1;
|
|
156
|
+
if (!isObject(envelope)) {
|
|
157
|
+
issues.push(issue("envelope.object", "artifact verification envelope must be an object"));
|
|
158
|
+
return {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
contract: ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
|
|
161
|
+
ok: false,
|
|
162
|
+
outcome: "fail",
|
|
163
|
+
assessmentTime,
|
|
164
|
+
issues,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const metadata = isObject(envelope.envelope) ? envelope.envelope : {};
|
|
169
|
+
if (
|
|
170
|
+
metadata.contract !== ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT ||
|
|
171
|
+
metadata.schemaVersion !== 1 ||
|
|
172
|
+
metadata.canonicalization !== "buildchain-stable-json/v1"
|
|
173
|
+
) {
|
|
174
|
+
issues.push(issue("envelope.contract", "artifact verification envelope contract metadata is invalid"));
|
|
175
|
+
}
|
|
176
|
+
const computedEnvelopeRoot = envelopeRoot(envelope);
|
|
177
|
+
if (!isRoot(metadata.root) || metadata.root !== computedEnvelopeRoot) {
|
|
178
|
+
issues.push(issue("envelope.root", "artifact verification envelope root does not match canonical content", {
|
|
179
|
+
expected: computedEnvelopeRoot,
|
|
180
|
+
actual: metadata.root || "",
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
if (expectedEnvelopeRoot && metadata.root !== expectedEnvelopeRoot) {
|
|
184
|
+
issues.push(issue("envelope.root.expected", "artifact verification envelope root does not match the pinned root"));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const verificationOk =
|
|
188
|
+
envelope.contract === ARTIFACT_VERIFICATION_CONTRACT &&
|
|
189
|
+
envelope.schemaVersion === 1 &&
|
|
190
|
+
envelope.outcome === "pass" &&
|
|
191
|
+
envelope.ok === true &&
|
|
192
|
+
envelope.trust === "pass" &&
|
|
193
|
+
envelope.passport?.verification?.ok === true &&
|
|
194
|
+
envelope.passport?.verification?.trust === "pass";
|
|
195
|
+
if (!verificationOk) {
|
|
196
|
+
issues.push(issue("verification.invalid", "base artifact verification must be a passing v1 report"));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let bindings;
|
|
200
|
+
try {
|
|
201
|
+
bindings = normalizeBindings(envelope.bindings);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
issues.push(issue("bindings.invalid", error.message));
|
|
204
|
+
}
|
|
205
|
+
if (bindings) {
|
|
206
|
+
const subjectRoot = String(envelope.subject?.digest || "");
|
|
207
|
+
const matchedRoot = String(envelope.match?.artifact?.digest || "");
|
|
208
|
+
if (
|
|
209
|
+
!isRoot(subjectRoot) ||
|
|
210
|
+
subjectRoot !== matchedRoot ||
|
|
211
|
+
bindings.artifactRoot !== subjectRoot
|
|
212
|
+
) {
|
|
213
|
+
issues.push(issue("bindings.artifactRoot", "artifact root must match the exact verified subject and passport artifact"));
|
|
214
|
+
}
|
|
215
|
+
if (expectedIssuer && bindings.issuer !== expectedIssuer) {
|
|
216
|
+
issues.push(issue("bindings.issuer", "issuer does not match the expected authority"));
|
|
217
|
+
}
|
|
218
|
+
if (expectedPublisher && bindings.publisher !== expectedPublisher) {
|
|
219
|
+
issues.push(issue("bindings.publisher", "publisher does not match the expected identity"));
|
|
220
|
+
}
|
|
221
|
+
if (expectedContractVersion && bindings.contractVersion !== expectedContractVersion) {
|
|
222
|
+
issues.push(issue("bindings.contractVersion", "verification contract version does not match the expected version"));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const issuedAt = envelope.issuedAt;
|
|
227
|
+
const expiresAt = envelope.expiresAt;
|
|
228
|
+
if (
|
|
229
|
+
at < 0 ||
|
|
230
|
+
!Number.isSafeInteger(issuedAt) ||
|
|
231
|
+
issuedAt < 0 ||
|
|
232
|
+
!Number.isSafeInteger(expiresAt) ||
|
|
233
|
+
expiresAt <= issuedAt ||
|
|
234
|
+
at < issuedAt ||
|
|
235
|
+
at >= expiresAt
|
|
236
|
+
) {
|
|
237
|
+
issues.push(issue("lifecycle.invalid", "envelope must be active at assessmentTime with valid issuedAt/expiresAt bounds"));
|
|
238
|
+
}
|
|
239
|
+
const revocation = envelope.revocation;
|
|
240
|
+
if (
|
|
241
|
+
!isObject(revocation) ||
|
|
242
|
+
!isRoot(revocation.root) ||
|
|
243
|
+
!Number.isSafeInteger(revocation.checkedAt) ||
|
|
244
|
+
revocation.checkedAt < issuedAt ||
|
|
245
|
+
(at >= 0 && revocation.checkedAt > at) ||
|
|
246
|
+
!["active", "revoked"].includes(revocation.status) ||
|
|
247
|
+
revocation.revoked !== (revocation.status === "revoked") ||
|
|
248
|
+
envelope.revoked !== revocation.revoked
|
|
249
|
+
) {
|
|
250
|
+
issues.push(issue("lifecycle.revocation", "revocation binding is invalid or inconsistent"));
|
|
251
|
+
}
|
|
252
|
+
if (envelope.revoked === true) {
|
|
253
|
+
issues.push(issue("lifecycle.revoked", "artifact verification envelope has been revoked"));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const assessment = envelope.kfdAssessment;
|
|
257
|
+
const report = isObject(assessment?.report) ? assessment.report : {};
|
|
258
|
+
if (
|
|
259
|
+
!isObject(assessment) ||
|
|
260
|
+
assessment.schema !== KFD_ASSESSMENT_SCHEMA ||
|
|
261
|
+
assessment.state !== "fresh" ||
|
|
262
|
+
report.state !== "fresh"
|
|
263
|
+
) {
|
|
264
|
+
issues.push(issue("kfd.assessment.state", "KFD assessment must be a fresh ADR-0052 assessment"));
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
!isRoot(assessment?.assessment_key) ||
|
|
268
|
+
report.assessment_key !== assessment?.assessment_key
|
|
269
|
+
) {
|
|
270
|
+
issues.push(issue("kfd.assessment.key", "KFD assessment key must be canonical and match the report"));
|
|
271
|
+
}
|
|
272
|
+
const computedReportRoot = isObject(report) ? recomputeKfdReportRoot(report) : "";
|
|
273
|
+
if (!isRoot(report.report_hash) || report.report_hash !== computedReportRoot) {
|
|
274
|
+
issues.push(issue("kfd.report.root", "KFD report hash does not match canonical report content", {
|
|
275
|
+
expected: computedReportRoot,
|
|
276
|
+
actual: report.report_hash || "",
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
if (
|
|
280
|
+
!String(report.purpose || "").trim() ||
|
|
281
|
+
!isRoot(report.query_proof_root) ||
|
|
282
|
+
!isRoot(report.contract_world?.root) ||
|
|
283
|
+
!isRoot(report.policy?.root) ||
|
|
284
|
+
!Array.isArray(report.fact_surfaces) ||
|
|
285
|
+
report.fact_surfaces.length === 0 ||
|
|
286
|
+
report.fact_surfaces.some((surface) => !isRoot(surface?.root))
|
|
287
|
+
) {
|
|
288
|
+
issues.push(issue("kfd.report.bindings", "KFD report must bind purpose, query proof, contract world, policy, and fact-surface roots"));
|
|
289
|
+
}
|
|
290
|
+
if (bindings && bindings.qualificationRoot !== report.report_hash) {
|
|
291
|
+
issues.push(issue("bindings.qualificationRoot", "qualification root must equal the KFD report hash"));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const ok = issues.length === 0;
|
|
295
|
+
return {
|
|
296
|
+
schemaVersion: 1,
|
|
297
|
+
contract: ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
|
|
298
|
+
ok,
|
|
299
|
+
outcome: ok ? "pass" : "fail",
|
|
300
|
+
assessmentTime,
|
|
301
|
+
envelopeRoot: metadata.root || "",
|
|
302
|
+
bindings: bindings || undefined,
|
|
303
|
+
kfdAssessment: isObject(assessment)
|
|
304
|
+
? {
|
|
305
|
+
state: assessment.state || "",
|
|
306
|
+
assessmentKey: assessment.assessment_key || "",
|
|
307
|
+
reportRoot: report.report_hash || "",
|
|
308
|
+
}
|
|
309
|
+
: undefined,
|
|
310
|
+
issues,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function sealArtifactVerificationReport({
|
|
315
|
+
report,
|
|
316
|
+
bindings,
|
|
317
|
+
kfdAssessment,
|
|
318
|
+
issuedAt,
|
|
319
|
+
expiresAt,
|
|
320
|
+
revocation,
|
|
321
|
+
} = {}) {
|
|
322
|
+
if (!isObject(report)) {
|
|
323
|
+
throw new Error("report must be an artifact verification object");
|
|
324
|
+
}
|
|
325
|
+
const normalizedIssuedAt = nonNegativeInteger(issuedAt, "issuedAt");
|
|
326
|
+
const normalizedExpiresAt = nonNegativeInteger(expiresAt, "expiresAt");
|
|
327
|
+
if (normalizedExpiresAt <= normalizedIssuedAt) {
|
|
328
|
+
throw new Error("expiresAt must be greater than issuedAt");
|
|
329
|
+
}
|
|
330
|
+
const normalizedRevocation = normalizeRevocation(revocation, {
|
|
331
|
+
issuedAt: normalizedIssuedAt,
|
|
332
|
+
});
|
|
333
|
+
const result = {
|
|
334
|
+
...jsonClone(report),
|
|
335
|
+
issuedAt: normalizedIssuedAt,
|
|
336
|
+
expiresAt: normalizedExpiresAt,
|
|
337
|
+
revoked: normalizedRevocation.revoked,
|
|
338
|
+
revocation: normalizedRevocation,
|
|
339
|
+
bindings: normalizeBindings(bindings),
|
|
340
|
+
kfdAssessment: jsonClone(kfdAssessment),
|
|
341
|
+
envelope: {
|
|
342
|
+
schemaVersion: 1,
|
|
343
|
+
contract: ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT,
|
|
344
|
+
canonicalization: "buildchain-stable-json/v1",
|
|
345
|
+
root: "",
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
result.envelope.root = envelopeRoot(result);
|
|
349
|
+
const check = verifyArtifactVerificationEnvelope({
|
|
350
|
+
envelope: result,
|
|
351
|
+
assessmentTime: normalizedRevocation.checkedAt,
|
|
352
|
+
});
|
|
353
|
+
if (!check.ok) {
|
|
354
|
+
throw new Error(`artifact verification envelope is invalid: ${check.issues.map((entry) => entry.code).join(", ")}`);
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function projectArtifactVerificationEnvelopeToKfx({
|
|
360
|
+
envelope,
|
|
361
|
+
assessmentTime = Math.floor(Date.now() / 1000),
|
|
362
|
+
expectedEnvelopeRoot = "",
|
|
363
|
+
expectedIssuer = "",
|
|
364
|
+
expectedPublisher = "",
|
|
365
|
+
expectedContractVersion = "",
|
|
366
|
+
} = {}) {
|
|
367
|
+
const check = verifyArtifactVerificationEnvelope({
|
|
368
|
+
envelope,
|
|
369
|
+
assessmentTime,
|
|
370
|
+
expectedEnvelopeRoot,
|
|
371
|
+
expectedIssuer,
|
|
372
|
+
expectedPublisher,
|
|
373
|
+
expectedContractVersion,
|
|
374
|
+
});
|
|
375
|
+
if (!check.ok) {
|
|
376
|
+
throw new Error(`artifact verification envelope failed closed: ${check.issues.map((entry) => entry.code).join(", ")}`);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
schemaVersion: 1,
|
|
380
|
+
contract: KFX_ADMISSION_INPUTS_CONTRACT,
|
|
381
|
+
envelopeRoot: envelope.envelope.root,
|
|
382
|
+
attestation: jsonClone(envelope),
|
|
383
|
+
trustInputs: jsonClone(envelope.bindings),
|
|
384
|
+
kfdAssessment: jsonClone(envelope.kfdAssessment),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
@@ -1181,7 +1181,7 @@ export function getPublishContract(loadedConfig) {
|
|
|
1181
1181
|
return loadedConfig?.config?.publish;
|
|
1182
1182
|
}
|
|
1183
1183
|
|
|
1184
|
-
export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, stage, env: extraEnv }) {
|
|
1184
|
+
export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, stage, env: extraEnv, timeoutMinutes }) {
|
|
1185
1185
|
const lifecycle = loadedConfig?.config?.lifecycle || {};
|
|
1186
1186
|
const selected = stage || getLifecycleStage(loadedConfig, name);
|
|
1187
1187
|
if (!selected) {
|
|
@@ -1193,7 +1193,11 @@ export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, sta
|
|
|
1193
1193
|
...(selected.env || {}),
|
|
1194
1194
|
...(extraEnv || {}),
|
|
1195
1195
|
};
|
|
1196
|
-
const
|
|
1196
|
+
const effectiveTimeoutMinutes = selected.timeoutMinutes ?? timeoutMinutes;
|
|
1197
|
+
if (effectiveTimeoutMinutes !== undefined && (!Number.isFinite(effectiveTimeoutMinutes) || effectiveTimeoutMinutes <= 0)) {
|
|
1198
|
+
throw new Error("lifecycle timeoutMinutes must be a positive number");
|
|
1199
|
+
}
|
|
1200
|
+
const timeout = effectiveTimeoutMinutes ? effectiveTimeoutMinutes * 60_000 : undefined;
|
|
1197
1201
|
const execOptions = {
|
|
1198
1202
|
cwd,
|
|
1199
1203
|
env,
|
|
@@ -344,7 +344,6 @@ export function createControllerReceipt({ plan, stages = [], evidence = [], reas
|
|
|
344
344
|
const issues = status === "passed"
|
|
345
345
|
? missingEvidence.map((kind) => `required controller evidence is missing: ${kind}`)
|
|
346
346
|
: [];
|
|
347
|
-
const qualifying = status === "passed" && plan.qualifying === true && issues.length === 0;
|
|
348
347
|
const receipt = {
|
|
349
348
|
schemaVersion: 1,
|
|
350
349
|
contract: BUILDCHAIN_CONTROLLER_EVIDENCE_CONTRACT,
|
|
@@ -354,11 +353,11 @@ export function createControllerReceipt({ plan, stages = [], evidence = [], reas
|
|
|
354
353
|
runtime: { ...plan.runtime },
|
|
355
354
|
planDigest: plan.digest,
|
|
356
355
|
status,
|
|
357
|
-
qualifying,
|
|
356
|
+
qualifying: status === "passed" && plan.qualifying === true && issues.length === 0,
|
|
358
357
|
stages: normalizedStages,
|
|
359
358
|
evidence: normalizedEvidence,
|
|
360
359
|
issues,
|
|
361
|
-
...(reason
|
|
360
|
+
...(reason ? {
|
|
362
361
|
reason: {
|
|
363
362
|
code: nonEmptyString(reason.code, "reason.code"),
|
|
364
363
|
summary: nonEmptyString(reason.summary, "reason.summary"),
|