@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/actions/release-tail/README.md +8 -0
- package/actions/v4-release-candidate-promote/README.md +3 -0
- package/architecture/agent-change-map.md +1015 -0
- package/architecture/ci-lane-change-budget.json +256 -0
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +220 -15
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +489 -132
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v3-core-mechanism-inventory.json +4 -3
- package/architecture/v3-v4-capability-closure.md +59 -0
- package/architecture/v3-v4-live-capability-inventory.json +102404 -0
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/buildchain.mjs +7 -1
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +1 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/agent-index.json +4 -0
- package/dist/site/artifact-schemas.json +8 -0
- package/dist/site/buildchain-contract.json +125 -34
- package/dist/site/buildchain-site.json +308 -33
- package/dist/site/capability-registry.json +13 -12
- package/dist/site/cli-registry.json +57 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +438 -24
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +22 -7
- package/dist/site/node-api-registry.json +12843 -9314
- package/dist/site/page-registry.json +288 -21
- package/dist/site/public-surface-audit.json +281 -21
- package/dist/site/publication-authority-registry.json +66 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +20 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +19 -11
- package/dist/site/workflow-registry.json +195 -17
- package/docs/MAP.md +4 -2
- package/docs/cli-reference.md +70 -2
- package/docs/cli.md +8 -0
- package/docs/dev-delivery-warrant.md +34 -10
- package/docs/node-api-reference.md +524 -279
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +152 -0
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +24 -4
- package/packages/core/adopter-delivery-passport.js +295 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/buildchain-publication-authority.js +2 -0
- package/packages/core/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-delivery-native-proof.js +43 -27
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +46 -10
- package/packages/core/dev-delivery-warrant.js +29 -7
- package/packages/core/github-governance-authority.js +5 -4
- package/packages/core/index.js +6 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/paper-scaffold-content.js +401 -0
- package/packages/core/paper.js +12 -378
- package/packages/core/publication-control-plane-audit.js +135 -111
- package/packages/core/publication-rehearsal-projection.js +104 -0
- package/packages/core/publication-rehearsal-runtime.js +190 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +71 -15
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +95 -170
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +139 -35
- package/packages/core/v4-floating-consumer-release-passport.js +154 -8
- package/packages/core/v4-publication-qualification.js +222 -0
- package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +24 -6
- package/scripts/check-maintainability.mjs +145 -4
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +30 -4
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +50 -6
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +45 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-release-candidate-passport.mjs +177 -0
- package/scripts/generate-site-bundle.mjs +36 -0
- package/scripts/maintainability-domain-metrics.mjs +237 -0
- package/scripts/maintainability-governance.mjs +473 -0
- package/scripts/maintainability-metrics.mjs +28 -6
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/promotion-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +68 -3
- package/scripts/resume-from-candidate-run.mjs +595 -65
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +21 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- package/scripts/v4-declarative-promotion-admission.mjs +42 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
v4FloatingConsumerDocumentRoot,
|
|
5
5
|
verifyV4FloatingConsumerPolicyCertification,
|
|
6
6
|
} from "./v4-floating-consumer-evidence.js";
|
|
7
|
-
|
|
7
|
+
import {
|
|
8
|
+
verifyV4RuntimeAuthorizationReceipt,
|
|
9
|
+
verifyV4RuntimeResumeLineage,
|
|
10
|
+
} from "./v4-runtime-ref-resume-authority.js";
|
|
8
11
|
const SHA256_ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
9
|
-
|
|
10
12
|
export function isV4PromotionRouting(value = undefined) {
|
|
11
13
|
return [
|
|
12
14
|
value?.router?.ref,
|
|
@@ -14,7 +16,6 @@ export function isV4PromotionRouting(value = undefined) {
|
|
|
14
16
|
value?.runtime?.requestedRef,
|
|
15
17
|
].some((ref) => /^v4(?:-alpha)?$/u.test(String(ref || "")));
|
|
16
18
|
}
|
|
17
|
-
|
|
18
19
|
function resolveInside(root, relative, label) {
|
|
19
20
|
const resolvedRoot = path.resolve(root);
|
|
20
21
|
const file = path.resolve(resolvedRoot, relative);
|
|
@@ -23,7 +24,6 @@ function resolveInside(root, relative, label) {
|
|
|
23
24
|
}
|
|
24
25
|
return file;
|
|
25
26
|
}
|
|
26
|
-
|
|
27
27
|
function resolveCallerLock(root, relative, label) {
|
|
28
28
|
const file = resolveInside(root, relative, `${label} contract lock`);
|
|
29
29
|
if (!fs.existsSync(file)) {
|
|
@@ -76,11 +76,36 @@ function normalizeCertification(value, expected) {
|
|
|
76
76
|
}
|
|
77
77
|
return { certificationRoot: verification.certificationRoot, certification };
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
export function resolveV4ConsumerPolicyCertificationIdentity({
|
|
80
|
+
release = {},
|
|
81
|
+
routing = {},
|
|
82
|
+
runtimeResume = undefined,
|
|
83
|
+
sourceSha = "",
|
|
84
|
+
} = {}) {
|
|
85
|
+
const certification =
|
|
86
|
+
release?.certification?.certification || release?.certification;
|
|
87
|
+
const certifiedSourceSha = certification?.caller?.sourceSha || "";
|
|
88
|
+
const treeEquivalentSources = [
|
|
89
|
+
release?.builtSourceSha || release?.built_source_sha,
|
|
90
|
+
release?.promotionChannelSha || release?.promotion_channel_sha,
|
|
91
|
+
];
|
|
92
|
+
const buildSha = runtimeResume?.lineage?.attempts?.build?.runtimeSha;
|
|
93
|
+
const certifiedSha = certification?.invocation?.resolvedRuntimeSha || "";
|
|
94
|
+
return {
|
|
95
|
+
sourceSha:
|
|
96
|
+
certifiedSourceSha === sourceSha ||
|
|
97
|
+
(release?.treeEquivalent === true &&
|
|
98
|
+
treeEquivalentSources.includes(certifiedSourceSha))
|
|
99
|
+
? certifiedSourceSha
|
|
100
|
+
: sourceSha,
|
|
101
|
+
runtimeSha: buildSha || certifiedSha || routing?.runtime?.resolvedSha || "",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
80
104
|
export function requireV4ConsumerPolicyCertification({
|
|
81
105
|
value,
|
|
82
106
|
repository,
|
|
83
107
|
sourceSha,
|
|
108
|
+
runtimeSha = "",
|
|
84
109
|
routing,
|
|
85
110
|
cwd,
|
|
86
111
|
certificationRoot,
|
|
@@ -100,7 +125,7 @@ export function requireV4ConsumerPolicyCertification({
|
|
|
100
125
|
return normalizeCertification(value, {
|
|
101
126
|
repository,
|
|
102
127
|
sourceSha,
|
|
103
|
-
runtimeSha: routing?.runtime?.resolvedSha || "",
|
|
128
|
+
runtimeSha: runtimeSha || routing?.runtime?.resolvedSha || "",
|
|
104
129
|
certificationRoot,
|
|
105
130
|
...(v4Routing ? resolveV4FloatingConsumerCallerLocks(value, cwd) : {}),
|
|
106
131
|
});
|
|
@@ -112,15 +137,136 @@ export function releasePassportCertificationVerificationOptions({
|
|
|
112
137
|
routing,
|
|
113
138
|
}) {
|
|
114
139
|
const authority = evidence?.certification?.authority || {};
|
|
140
|
+
const identity = resolveV4ConsumerPolicyCertificationIdentity({
|
|
141
|
+
release: { ...passport?.release, certification: evidence?.certification },
|
|
142
|
+
routing,
|
|
143
|
+
runtimeResume: passport?.v4RuntimeResume,
|
|
144
|
+
sourceSha: passport?.release?.sourceSha || "",
|
|
145
|
+
});
|
|
115
146
|
return {
|
|
116
147
|
certification: evidence?.certification,
|
|
117
148
|
certificationRoot: evidence?.certificationRoot,
|
|
118
149
|
repository: passport?.product?.repository || "",
|
|
119
|
-
sourceSha:
|
|
120
|
-
resolvedRuntimeSha:
|
|
150
|
+
sourceSha: identity.sourceSha,
|
|
151
|
+
resolvedRuntimeSha: identity.runtimeSha,
|
|
121
152
|
stableLockRoot: authority.contractLocks?.stable?.root || "",
|
|
122
153
|
alphaLockRoot: authority.contractLocks?.alpha?.root || "",
|
|
123
154
|
stableLockPath: authority.contractLocks?.stable?.path || "",
|
|
124
155
|
alphaLockPath: authority.contractLocks?.alpha?.path || "",
|
|
125
156
|
};
|
|
126
157
|
}
|
|
158
|
+
export function normalizePromotionRouting(value = undefined) {
|
|
159
|
+
if (value === undefined) return undefined;
|
|
160
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
161
|
+
throw new Error("release.promotionRouting must be a JSON object");
|
|
162
|
+
}
|
|
163
|
+
if (value.contract !== "buildchain.promotion-routing/v1") {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"release.promotionRouting contract must be buildchain.promotion-routing/v1",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
for (const [label, field] of [
|
|
169
|
+
["router.ref", value.router?.ref],
|
|
170
|
+
["router.sha", value.router?.sha],
|
|
171
|
+
["shell.ref", value.shell?.ref],
|
|
172
|
+
["shell.sha", value.shell?.sha],
|
|
173
|
+
["runtime.requestedRef", value.runtime?.requestedRef],
|
|
174
|
+
["runtime.resolvedSha", value.runtime?.resolvedSha],
|
|
175
|
+
["contractLock.path", value.contractLock?.path],
|
|
176
|
+
["contractLock.digest", value.contractLock?.digest],
|
|
177
|
+
["publication.channel", value.publication?.channel],
|
|
178
|
+
["publication.targetRef", value.publication?.targetRef],
|
|
179
|
+
]) {
|
|
180
|
+
if (!String(field || "").trim())
|
|
181
|
+
throw new Error(`release.promotionRouting.${label} is required`);
|
|
182
|
+
}
|
|
183
|
+
for (const [label, sha] of [
|
|
184
|
+
["router.sha", value.router.sha],
|
|
185
|
+
["shell.sha", value.shell.sha],
|
|
186
|
+
["runtime.resolvedSha", value.runtime.resolvedSha],
|
|
187
|
+
]) {
|
|
188
|
+
if (!/^[0-9a-f]{40}$/i.test(String(sha))) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`release.promotionRouting.${label} must be a 40-character Git SHA`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!/^sha256:[0-9a-f]{64}$/i.test(String(value.contractLock.digest))) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"release.promotionRouting.contractLock.digest must be a sha256 digest",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return JSON.parse(JSON.stringify(value));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function normalizePromotionEvidence({
|
|
203
|
+
release = {},
|
|
204
|
+
v4ConsumerPolicyCertification: certification,
|
|
205
|
+
v4ConsumerPolicyCertificationRoot: certificationRoot,
|
|
206
|
+
v4RuntimeResumeEvidence,
|
|
207
|
+
repository,
|
|
208
|
+
sourceSha,
|
|
209
|
+
cwd,
|
|
210
|
+
}) {
|
|
211
|
+
const routing = normalizePromotionRouting(release.promotionRouting);
|
|
212
|
+
const identity = resolveV4ConsumerPolicyCertificationIdentity({
|
|
213
|
+
release: { ...release, certification },
|
|
214
|
+
routing,
|
|
215
|
+
runtimeResume: v4RuntimeResumeEvidence,
|
|
216
|
+
sourceSha,
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
routing,
|
|
220
|
+
consumerPolicy: requireV4ConsumerPolicyCertification({
|
|
221
|
+
value: certification,
|
|
222
|
+
repository,
|
|
223
|
+
sourceSha: identity.sourceSha,
|
|
224
|
+
runtimeSha: identity.runtimeSha,
|
|
225
|
+
routing,
|
|
226
|
+
cwd,
|
|
227
|
+
certificationRoot,
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function normalizeV4RuntimeResumeEvidence(value, expected = {}) {
|
|
233
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
234
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
235
|
+
throw new Error("v4RuntimeResumeEvidence must be a JSON object");
|
|
236
|
+
}
|
|
237
|
+
const authorizationVerification = verifyV4RuntimeAuthorizationReceipt({
|
|
238
|
+
receipt: value.authorization,
|
|
239
|
+
receiptRoot: value.authorizationRoot,
|
|
240
|
+
repository: expected.repository,
|
|
241
|
+
sourceSha: expected.sourceSha,
|
|
242
|
+
runtimeSha: expected.resumeRuntimeSha,
|
|
243
|
+
consumerPolicyReceiptRoot: expected.consumerPolicyReceiptRoot,
|
|
244
|
+
});
|
|
245
|
+
if (!authorizationVerification.ok) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`v4 runtime authorization invalid: ${authorizationVerification.failures.join(", ")}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const lineageVerification = verifyV4RuntimeResumeLineage({
|
|
251
|
+
lineage: value.lineage,
|
|
252
|
+
lineageRoot: value.lineageRoot,
|
|
253
|
+
repository: expected.repository,
|
|
254
|
+
sourceSha: expected.sourceSha,
|
|
255
|
+
resumeRuntimeSha: expected.resumeRuntimeSha,
|
|
256
|
+
consumerPolicyReceiptRoot: expected.consumerPolicyReceiptRoot,
|
|
257
|
+
});
|
|
258
|
+
if (!lineageVerification.ok) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`v4 runtime resume lineage invalid: ${lineageVerification.failures.join(", ")}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (value.lineage?.authorizationRoot !== value.authorizationRoot) {
|
|
264
|
+
throw new Error("v4 runtime resume lineage authorization root mismatch");
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
authorizationRoot: value.authorizationRoot,
|
|
268
|
+
authorization: structuredClone(value.authorization),
|
|
269
|
+
lineageRoot: value.lineageRoot,
|
|
270
|
+
lineage: structuredClone(value.lineage),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const V4_PUBLICATION_QUALIFICATION_SCHEMA =
|
|
4
|
+
"kungfu.buildchain.v4-publication-qualification/v1";
|
|
5
|
+
|
|
6
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
7
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
8
|
+
const FORBIDDEN_EXECUTION_KEYS = new Set([
|
|
9
|
+
"cmd",
|
|
10
|
+
"command",
|
|
11
|
+
"eval",
|
|
12
|
+
"executable",
|
|
13
|
+
"javascript",
|
|
14
|
+
"run",
|
|
15
|
+
"script",
|
|
16
|
+
"shell",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export class V4PublicationQualificationError extends Error {
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "V4PublicationQualificationError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fail(code, message) {
|
|
28
|
+
throw new V4PublicationQualificationError(code, message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function canonical(value) {
|
|
32
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
33
|
+
if (value && typeof value === "object") {
|
|
34
|
+
return `{${Object.keys(value)
|
|
35
|
+
.sort()
|
|
36
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
37
|
+
.join(",")}}`;
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function v4PublicationQualificationRoot(value) {
|
|
43
|
+
return `sha256:${crypto.createHash("sha256").update(canonical(value)).digest("hex")}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assertNoV4ExecutionFields(value, location = "$") {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
value.forEach((entry, index) =>
|
|
49
|
+
assertNoV4ExecutionFields(entry, `${location}[${index}]`),
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!value || typeof value !== "object") return;
|
|
54
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
55
|
+
if (FORBIDDEN_EXECUTION_KEYS.has(key.toLowerCase())) {
|
|
56
|
+
fail(
|
|
57
|
+
"execution-field-forbidden",
|
|
58
|
+
`v4 declarative publication forbids ${location}.${key}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
assertNoV4ExecutionFields(entry, `${location}.${key}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function exactRoot(value, label) {
|
|
66
|
+
const root = String(value || "").toLowerCase();
|
|
67
|
+
if (!ROOT.test(root))
|
|
68
|
+
fail("schema-invalid", `${label} must be a sha256 root`);
|
|
69
|
+
return root;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function exactSha(value, label) {
|
|
73
|
+
const sha = String(value || "").toLowerCase();
|
|
74
|
+
if (!SHA.test(sha))
|
|
75
|
+
fail("schema-invalid", `${label} must be an exact Git SHA`);
|
|
76
|
+
return sha;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizedArtifacts(artifacts) {
|
|
80
|
+
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
81
|
+
fail(
|
|
82
|
+
"schema-invalid",
|
|
83
|
+
"publication qualification requires typed artifacts",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const normalized = artifacts
|
|
87
|
+
.map((artifact) => {
|
|
88
|
+
const value = {
|
|
89
|
+
role: String(artifact?.role || ""),
|
|
90
|
+
platform: String(artifact?.platform || ""),
|
|
91
|
+
artifactRoot: exactRoot(artifact?.artifactRoot, "artifactRoot"),
|
|
92
|
+
manifestRoot: exactRoot(artifact?.manifestRoot, "manifestRoot"),
|
|
93
|
+
};
|
|
94
|
+
if (!value.role || !value.platform)
|
|
95
|
+
fail("schema-invalid", "artifact role and platform are required");
|
|
96
|
+
return value;
|
|
97
|
+
})
|
|
98
|
+
.sort((left, right) =>
|
|
99
|
+
`${left.role}\0${left.platform}`.localeCompare(
|
|
100
|
+
`${right.role}\0${right.platform}`,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
if (
|
|
104
|
+
new Set(normalized.map(({ role, platform }) => `${role}\0${platform}`))
|
|
105
|
+
.size !== normalized.length
|
|
106
|
+
) {
|
|
107
|
+
fail("schema-invalid", "artifact role/platform bindings must be unique");
|
|
108
|
+
}
|
|
109
|
+
return normalized;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createV4PublicationQualificationReceipt({
|
|
113
|
+
repository,
|
|
114
|
+
candidateRoot,
|
|
115
|
+
sourceSha,
|
|
116
|
+
sourceRoot,
|
|
117
|
+
policyDigest,
|
|
118
|
+
artifacts,
|
|
119
|
+
issuedAt,
|
|
120
|
+
expiresAt,
|
|
121
|
+
}) {
|
|
122
|
+
const body = {
|
|
123
|
+
schema: V4_PUBLICATION_QUALIFICATION_SCHEMA,
|
|
124
|
+
repository: String(repository || ""),
|
|
125
|
+
candidateRoot: exactRoot(candidateRoot, "candidateRoot"),
|
|
126
|
+
sourceSha: exactSha(sourceSha, "sourceSha"),
|
|
127
|
+
sourceRoot: exactRoot(sourceRoot, "sourceRoot"),
|
|
128
|
+
policyDigest: exactRoot(policyDigest, "policyDigest"),
|
|
129
|
+
artifacts: normalizedArtifacts(artifacts),
|
|
130
|
+
issuedAt: String(issuedAt || ""),
|
|
131
|
+
expiresAt: String(expiresAt || ""),
|
|
132
|
+
};
|
|
133
|
+
if (!/^[^/\s]+\/[^/\s]+$/u.test(body.repository))
|
|
134
|
+
fail("schema-invalid", "repository must be owner/repo");
|
|
135
|
+
const issued = Date.parse(body.issuedAt);
|
|
136
|
+
const expires = Date.parse(body.expiresAt);
|
|
137
|
+
if (
|
|
138
|
+
!Number.isFinite(issued) ||
|
|
139
|
+
!Number.isFinite(expires) ||
|
|
140
|
+
expires <= issued
|
|
141
|
+
)
|
|
142
|
+
fail("schema-invalid", "qualification freshness interval is invalid");
|
|
143
|
+
assertNoV4ExecutionFields(body);
|
|
144
|
+
const artifactRoot = v4PublicationQualificationRoot(body.artifacts);
|
|
145
|
+
const receipt = { ...body, artifactRoot };
|
|
146
|
+
return {
|
|
147
|
+
...receipt,
|
|
148
|
+
receiptRoot: v4PublicationQualificationRoot(receipt),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateV4PublicationQualificationReceipt(
|
|
153
|
+
receipt,
|
|
154
|
+
{
|
|
155
|
+
repository,
|
|
156
|
+
candidateRoot,
|
|
157
|
+
sourceSha,
|
|
158
|
+
sourceRoot,
|
|
159
|
+
artifactRoot,
|
|
160
|
+
policyDigest,
|
|
161
|
+
evaluatedAt = new Date().toISOString(),
|
|
162
|
+
} = {},
|
|
163
|
+
) {
|
|
164
|
+
if (!receipt)
|
|
165
|
+
fail("qualification-missing", "qualification receipt is required");
|
|
166
|
+
assertNoV4ExecutionFields(receipt);
|
|
167
|
+
const recreated = createV4PublicationQualificationReceipt(receipt);
|
|
168
|
+
if (
|
|
169
|
+
receipt.artifactRoot !== recreated.artifactRoot ||
|
|
170
|
+
receipt.receiptRoot !== recreated.receiptRoot
|
|
171
|
+
) {
|
|
172
|
+
fail(
|
|
173
|
+
"qualification-tampered",
|
|
174
|
+
"qualification receipt root does not verify",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (repository && receipt.repository !== repository)
|
|
178
|
+
fail("repository-mismatch", "qualification repository does not match");
|
|
179
|
+
if (candidateRoot && receipt.candidateRoot !== candidateRoot)
|
|
180
|
+
fail("candidate-mismatch", "qualification candidate root does not match");
|
|
181
|
+
if (sourceSha && receipt.sourceSha !== sourceSha)
|
|
182
|
+
fail("source-mismatch", "qualification source SHA does not match");
|
|
183
|
+
if (sourceRoot && receipt.sourceRoot !== sourceRoot)
|
|
184
|
+
fail("source-mismatch", "qualification source root does not match");
|
|
185
|
+
if (artifactRoot && receipt.artifactRoot !== artifactRoot)
|
|
186
|
+
fail("artifact-mismatch", "qualification artifact root does not match");
|
|
187
|
+
if (policyDigest && receipt.policyDigest !== policyDigest)
|
|
188
|
+
fail("policy-mismatch", "qualification policy digest does not match");
|
|
189
|
+
const evaluated = Date.parse(evaluatedAt);
|
|
190
|
+
if (!Number.isFinite(evaluated))
|
|
191
|
+
fail("schema-invalid", "evaluatedAt is invalid");
|
|
192
|
+
if (
|
|
193
|
+
evaluated < Date.parse(receipt.issuedAt) ||
|
|
194
|
+
evaluated >= Date.parse(receipt.expiresAt)
|
|
195
|
+
)
|
|
196
|
+
fail(
|
|
197
|
+
"qualification-stale",
|
|
198
|
+
"qualification receipt is outside its freshness window",
|
|
199
|
+
);
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
ok: true,
|
|
202
|
+
receiptRoot: receipt.receiptRoot,
|
|
203
|
+
artifactRoot: receipt.artifactRoot,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function assertV4DeclarativePromotionInputs(inputs) {
|
|
208
|
+
const forbidden = Object.entries(inputs || {}).filter(
|
|
209
|
+
([name, value]) =>
|
|
210
|
+
/(command|cmd|script|shell|run)$/iu.test(name) &&
|
|
211
|
+
String(value || "").trim() !== "",
|
|
212
|
+
);
|
|
213
|
+
if (forbidden.length > 0) {
|
|
214
|
+
fail(
|
|
215
|
+
"legacy-command-input-forbidden",
|
|
216
|
+
`v4 declarative publication rejects: ${forbidden
|
|
217
|
+
.map(([name]) => name)
|
|
218
|
+
.sort()
|
|
219
|
+
.join(", ")}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|