@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/release-tail/README.md +8 -0
- package/architecture/ci-lane-change-budget.json +187 -0
- package/architecture/internal-capabilities.json +65 -5
- package/architecture/maintainability-policy.json +27 -21
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -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/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-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +14 -14
- package/dist/site/buildchain-site.json +231 -24
- package/dist/site/capability-registry.json +11 -10
- package/dist/site/cli-registry.json +53 -1
- package/dist/site/kfd-claims.json +279 -14
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -5
- package/dist/site/node-api-registry.json +11569 -9299
- package/dist/site/page-registry.json +214 -15
- package/dist/site/public-surface-audit.json +206 -11
- package/dist/site/publication-authority-registry.json +39 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +15 -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-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +17 -9
- package/dist/site/workflow-registry.json +110 -8
- package/docs/MAP.md +3 -1
- package/docs/cli-reference.md +68 -0
- package/docs/cli.md +8 -0
- package/docs/dev-delivery-warrant.md +22 -1
- package/docs/node-api-reference.md +332 -172
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +18 -3
- package/packages/core/adopter-delivery-passport.js +259 -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-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 +1 -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-state.js +31 -8
- package/packages/core/dev-delivery-warrant.js +8 -3
- package/packages/core/github-governance-authority.js +1 -0
- package/packages/core/index.js +5 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +66 -11
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +36 -9
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -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 +138 -35
- package/packages/core/v4-floating-consumer-release-passport.js +30 -3
- 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/buildchain-cli-help.mjs +10 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +3 -3
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- 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-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +5 -1
- package/scripts/dev-pr-delivery-warrant.mjs +1 -0
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +30 -0
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/resume-from-candidate-run.mjs +556 -29
- package/scripts/site-capability-metadata.mjs +17 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export class V4PublicationRehearsalFault extends Error {
|
|
2
|
+
constructor(code, location, message) {
|
|
3
|
+
super(`${location}: ${message}`);
|
|
4
|
+
this.name = "V4PublicationRehearsalFault";
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.location = location;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function publicationRehearsalFault(code, location, message) {
|
|
11
|
+
throw new V4PublicationRehearsalFault(code, location, message);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function publicationRehearsalExactKeys(value, expected, location) {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
16
|
+
publicationRehearsalFault(
|
|
17
|
+
"invalid-publication-rehearsal-shape",
|
|
18
|
+
location,
|
|
19
|
+
"object required",
|
|
20
|
+
);
|
|
21
|
+
const actual = Object.keys(value).sort();
|
|
22
|
+
const canonical = [...expected].sort();
|
|
23
|
+
if (
|
|
24
|
+
actual.length !== canonical.length ||
|
|
25
|
+
actual.some((key, index) => key !== canonical[index])
|
|
26
|
+
)
|
|
27
|
+
publicationRehearsalFault(
|
|
28
|
+
"invalid-publication-rehearsal-shape",
|
|
29
|
+
location,
|
|
30
|
+
`keys must be exactly ${canonical.join(", ")}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function publicationRehearsalRoot(value, location) {
|
|
35
|
+
if (typeof value !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(value))
|
|
36
|
+
publicationRehearsalFault(
|
|
37
|
+
"invalid-publication-rehearsal-root",
|
|
38
|
+
location,
|
|
39
|
+
"SHA-256 root required",
|
|
40
|
+
);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function publicationRehearsalText(value, location) {
|
|
45
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
46
|
+
publicationRehearsalFault(
|
|
47
|
+
"invalid-publication-rehearsal-text",
|
|
48
|
+
location,
|
|
49
|
+
"non-empty text required",
|
|
50
|
+
);
|
|
51
|
+
return value.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function publicationRehearsalToken(value, location) {
|
|
55
|
+
const normalized = publicationRehearsalText(value, location);
|
|
56
|
+
if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u.test(normalized))
|
|
57
|
+
publicationRehearsalFault(
|
|
58
|
+
"invalid-publication-rehearsal-token",
|
|
59
|
+
location,
|
|
60
|
+
"ASCII token required",
|
|
61
|
+
);
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function publicationRehearsalRelativePath(value, location) {
|
|
66
|
+
const normalized = publicationRehearsalText(value, location);
|
|
67
|
+
if (
|
|
68
|
+
normalized.startsWith("/") ||
|
|
69
|
+
normalized.includes("\\") ||
|
|
70
|
+
normalized.endsWith("/") ||
|
|
71
|
+
normalized.split("/").some((part) => !part || part === "." || part === "..")
|
|
72
|
+
)
|
|
73
|
+
publicationRehearsalFault(
|
|
74
|
+
"invalid-publication-rehearsal-path",
|
|
75
|
+
location,
|
|
76
|
+
"safe POSIX relative file path required",
|
|
77
|
+
);
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function publicationRehearsalByteSorted(
|
|
82
|
+
values,
|
|
83
|
+
location,
|
|
84
|
+
validate = publicationRehearsalToken,
|
|
85
|
+
allowEmpty = false,
|
|
86
|
+
) {
|
|
87
|
+
if (!Array.isArray(values) || (!allowEmpty && values.length === 0))
|
|
88
|
+
publicationRehearsalFault(
|
|
89
|
+
"invalid-publication-rehearsal-shape",
|
|
90
|
+
location,
|
|
91
|
+
"array required",
|
|
92
|
+
);
|
|
93
|
+
const normalized = values.map((value, index) =>
|
|
94
|
+
validate(value, `${location}/${index}`),
|
|
95
|
+
);
|
|
96
|
+
const canonical = [...new Set(normalized)].sort((left, right) =>
|
|
97
|
+
Buffer.from(left).compare(Buffer.from(right)),
|
|
98
|
+
);
|
|
99
|
+
if (JSON.stringify(normalized) !== JSON.stringify(canonical))
|
|
100
|
+
publicationRehearsalFault(
|
|
101
|
+
"unordered-publication-rehearsal-values",
|
|
102
|
+
location,
|
|
103
|
+
"values must be unique and byte-sorted",
|
|
104
|
+
);
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function canonicalObject(value, location, normalize) {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
110
|
+
publicationRehearsalFault(
|
|
111
|
+
"invalid-publication-rehearsal-shape",
|
|
112
|
+
location,
|
|
113
|
+
"object required",
|
|
114
|
+
);
|
|
115
|
+
const keys = Object.keys(value);
|
|
116
|
+
const sorted = [...keys].sort((left, right) =>
|
|
117
|
+
Buffer.from(left).compare(Buffer.from(right)),
|
|
118
|
+
);
|
|
119
|
+
if (JSON.stringify(keys) !== JSON.stringify(sorted))
|
|
120
|
+
publicationRehearsalFault(
|
|
121
|
+
"unordered-publication-rehearsal-values",
|
|
122
|
+
location,
|
|
123
|
+
"object keys must be byte-sorted",
|
|
124
|
+
);
|
|
125
|
+
return Object.fromEntries(
|
|
126
|
+
keys.map((key) => [
|
|
127
|
+
publicationRehearsalToken(key, `${location}/${key}`),
|
|
128
|
+
normalize(value[key], key),
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function validateV4PublicationRehearsalProviderBindings(value) {
|
|
134
|
+
publicationRehearsalExactKeys(
|
|
135
|
+
value,
|
|
136
|
+
["schema", "artifacts", "documents", "evidence"],
|
|
137
|
+
"$/providerBindings",
|
|
138
|
+
);
|
|
139
|
+
if (value.schema !== "kungfu.buildchain.release-tail.provider-bindings/v1")
|
|
140
|
+
publicationRehearsalFault(
|
|
141
|
+
"invalid-publication-rehearsal-provider-bindings",
|
|
142
|
+
"$/providerBindings/schema",
|
|
143
|
+
"unsupported provider bindings schema",
|
|
144
|
+
);
|
|
145
|
+
const artifacts = canonicalObject(
|
|
146
|
+
value.artifacts,
|
|
147
|
+
"$/providerBindings/artifacts",
|
|
148
|
+
(binding, key) => {
|
|
149
|
+
publicationRehearsalExactKeys(
|
|
150
|
+
binding,
|
|
151
|
+
["path", "name"],
|
|
152
|
+
`$/providerBindings/artifacts/${key}`,
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
path: publicationRehearsalRelativePath(
|
|
156
|
+
binding.path,
|
|
157
|
+
`$/providerBindings/artifacts/${key}/path`,
|
|
158
|
+
),
|
|
159
|
+
name: publicationRehearsalText(
|
|
160
|
+
binding.name,
|
|
161
|
+
`$/providerBindings/artifacts/${key}/name`,
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
const documents = canonicalObject(
|
|
167
|
+
value.documents,
|
|
168
|
+
"$/providerBindings/documents",
|
|
169
|
+
(binding, key) => {
|
|
170
|
+
publicationRehearsalExactKeys(
|
|
171
|
+
binding,
|
|
172
|
+
["path", "method"],
|
|
173
|
+
`$/providerBindings/documents/${key}`,
|
|
174
|
+
);
|
|
175
|
+
if (!new Set(["PUT", "POST"]).has(binding.method))
|
|
176
|
+
publicationRehearsalFault(
|
|
177
|
+
"invalid-publication-rehearsal-provider-bindings",
|
|
178
|
+
`$/providerBindings/documents/${key}/method`,
|
|
179
|
+
"PUT or POST required",
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
path: publicationRehearsalRelativePath(
|
|
183
|
+
binding.path,
|
|
184
|
+
`$/providerBindings/documents/${key}/path`,
|
|
185
|
+
),
|
|
186
|
+
method: binding.method,
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
publicationRehearsalExactKeys(
|
|
191
|
+
value.evidence,
|
|
192
|
+
["inputs", "output"],
|
|
193
|
+
"$/providerBindings/evidence",
|
|
194
|
+
);
|
|
195
|
+
return {
|
|
196
|
+
schema: value.schema,
|
|
197
|
+
artifacts,
|
|
198
|
+
documents,
|
|
199
|
+
evidence: {
|
|
200
|
+
inputs: publicationRehearsalByteSorted(
|
|
201
|
+
value.evidence.inputs,
|
|
202
|
+
"$/providerBindings/evidence/inputs",
|
|
203
|
+
publicationRehearsalRelativePath,
|
|
204
|
+
true,
|
|
205
|
+
),
|
|
206
|
+
output: publicationRehearsalRelativePath(
|
|
207
|
+
value.evidence.output,
|
|
208
|
+
"$/providerBindings/evidence/output",
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
executeReleaseTailTransaction,
|
|
7
|
+
releaseTailRoot,
|
|
8
|
+
validateReleaseTailTransaction,
|
|
9
|
+
} from "./release-tail-provider-plane.js";
|
|
10
|
+
import {
|
|
11
|
+
V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT,
|
|
12
|
+
V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
13
|
+
V4_PUBLICATION_REHEARSAL_CORE_VERSION,
|
|
14
|
+
V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
15
|
+
V4_PUBLICATION_REHEARSAL_PROVIDER_POLICY_CONTRACT,
|
|
16
|
+
V4PublicationRehearsalFault,
|
|
17
|
+
assertPublicationRehearsalExactKeys as exactKeys,
|
|
18
|
+
createV4PublicationRehearsalCapsule,
|
|
19
|
+
createV4PublicationRehearsalProviderPolicy,
|
|
20
|
+
publicationRehearsalFault as fault,
|
|
21
|
+
publicationRehearsalRoot,
|
|
22
|
+
validateV4PublicationRehearsalCapsule,
|
|
23
|
+
validateV4PublicationRehearsalProviderBindings,
|
|
24
|
+
} from "./v4-publication-rehearsal-capsule.js";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT,
|
|
28
|
+
V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
29
|
+
V4_PUBLICATION_REHEARSAL_CORE_VERSION,
|
|
30
|
+
V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
31
|
+
V4_PUBLICATION_REHEARSAL_PROVIDER_POLICY_CONTRACT,
|
|
32
|
+
V4PublicationRehearsalFault,
|
|
33
|
+
createV4PublicationRehearsalCapsule,
|
|
34
|
+
createV4PublicationRehearsalProviderPolicy,
|
|
35
|
+
validateV4PublicationRehearsalCapsule,
|
|
36
|
+
validateV4PublicationRehearsalProviderBindings,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
40
|
+
const MODES = new Set(["simulate", "replay", "provider"]);
|
|
41
|
+
|
|
42
|
+
function fileRoot(filePath) {
|
|
43
|
+
const hash = crypto.createHash("sha256");
|
|
44
|
+
const descriptor = fs.openSync(filePath, "r");
|
|
45
|
+
const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
49
|
+
if (count === 0) break;
|
|
50
|
+
hash.update(buffer.subarray(0, count));
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
fs.closeSync(descriptor);
|
|
54
|
+
}
|
|
55
|
+
return `sha256:${hash.digest("hex")}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveCandidateFile(candidateRootPath, relative, location) {
|
|
59
|
+
const base = fs.realpathSync(candidateRootPath);
|
|
60
|
+
const resolved = path.resolve(base, relative);
|
|
61
|
+
if (!resolved.startsWith(`${base}${path.sep}`))
|
|
62
|
+
fault(
|
|
63
|
+
"publication-rehearsal-path-escape",
|
|
64
|
+
location,
|
|
65
|
+
"candidate path escapes the explicit root",
|
|
66
|
+
);
|
|
67
|
+
if (
|
|
68
|
+
!fs.existsSync(resolved) ||
|
|
69
|
+
!fs.statSync(resolved).isFile() ||
|
|
70
|
+
fs.lstatSync(resolved).isSymbolicLink() ||
|
|
71
|
+
!fs.realpathSync(resolved).startsWith(`${base}${path.sep}`)
|
|
72
|
+
)
|
|
73
|
+
fault(
|
|
74
|
+
"publication-rehearsal-file-missing",
|
|
75
|
+
location,
|
|
76
|
+
"regular in-root file required",
|
|
77
|
+
);
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveCandidateOutput(candidateRootPath, relative, location) {
|
|
82
|
+
const base = fs.realpathSync(candidateRootPath);
|
|
83
|
+
const resolved = path.resolve(base, relative);
|
|
84
|
+
if (!resolved.startsWith(`${base}${path.sep}`))
|
|
85
|
+
fault(
|
|
86
|
+
"publication-rehearsal-path-escape",
|
|
87
|
+
location,
|
|
88
|
+
"candidate output escapes the explicit root",
|
|
89
|
+
);
|
|
90
|
+
for (
|
|
91
|
+
let current = path.dirname(resolved);
|
|
92
|
+
current !== base;
|
|
93
|
+
current = path.dirname(current)
|
|
94
|
+
) {
|
|
95
|
+
if (!fs.existsSync(current)) continue;
|
|
96
|
+
if (
|
|
97
|
+
fs.lstatSync(current).isSymbolicLink() ||
|
|
98
|
+
!fs.statSync(current).isDirectory() ||
|
|
99
|
+
!fs.realpathSync(current).startsWith(`${base}${path.sep}`)
|
|
100
|
+
)
|
|
101
|
+
fault(
|
|
102
|
+
"publication-rehearsal-path-escape",
|
|
103
|
+
location,
|
|
104
|
+
"candidate output ancestor is not a regular in-root directory",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
fs.existsSync(resolved) &&
|
|
109
|
+
(fs.lstatSync(resolved).isSymbolicLink() ||
|
|
110
|
+
!fs.statSync(resolved).isFile() ||
|
|
111
|
+
!fs.realpathSync(resolved).startsWith(`${base}${path.sep}`))
|
|
112
|
+
)
|
|
113
|
+
fault(
|
|
114
|
+
"publication-rehearsal-path-escape",
|
|
115
|
+
location,
|
|
116
|
+
"candidate output is not a regular in-root file",
|
|
117
|
+
);
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function verifyV4PublicationRehearsalCapsule({
|
|
122
|
+
capsule,
|
|
123
|
+
candidateRoot: candidateRootPath,
|
|
124
|
+
}) {
|
|
125
|
+
const normalized = validateV4PublicationRehearsalCapsule(capsule);
|
|
126
|
+
if (!path.isAbsolute(candidateRootPath || ""))
|
|
127
|
+
fault(
|
|
128
|
+
"publication-rehearsal-implicit-root-forbidden",
|
|
129
|
+
"$/candidateRoot",
|
|
130
|
+
"explicit absolute candidate root required",
|
|
131
|
+
);
|
|
132
|
+
if (
|
|
133
|
+
!fs.existsSync(candidateRootPath) ||
|
|
134
|
+
!fs.statSync(candidateRootPath).isDirectory()
|
|
135
|
+
)
|
|
136
|
+
fault(
|
|
137
|
+
"publication-rehearsal-candidate-missing",
|
|
138
|
+
"$/candidateRoot",
|
|
139
|
+
"candidate directory required",
|
|
140
|
+
);
|
|
141
|
+
for (const entry of normalized.files) {
|
|
142
|
+
const resolved = resolveCandidateFile(
|
|
143
|
+
candidateRootPath,
|
|
144
|
+
entry.path,
|
|
145
|
+
`$/files/${entry.path}`,
|
|
146
|
+
);
|
|
147
|
+
if (
|
|
148
|
+
fs.statSync(resolved).size !== entry.size ||
|
|
149
|
+
fileRoot(resolved) !== entry.root
|
|
150
|
+
)
|
|
151
|
+
fault(
|
|
152
|
+
"publication-rehearsal-file-tampered",
|
|
153
|
+
`$/files/${entry.path}`,
|
|
154
|
+
"candidate bytes do not match the capsule",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveV4PublicationRehearsalProviderBindings({
|
|
161
|
+
capsule,
|
|
162
|
+
candidateRoot: candidateRootPath,
|
|
163
|
+
providerBindings = capsule?.providerBindings,
|
|
164
|
+
}) {
|
|
165
|
+
const normalized = verifyV4PublicationRehearsalCapsule({
|
|
166
|
+
capsule,
|
|
167
|
+
candidateRoot: candidateRootPath,
|
|
168
|
+
});
|
|
169
|
+
const supplied =
|
|
170
|
+
validateV4PublicationRehearsalProviderBindings(providerBindings);
|
|
171
|
+
if (
|
|
172
|
+
releaseTailRoot(supplied) !== normalized.providerBindingsRoot ||
|
|
173
|
+
JSON.stringify(supplied) !== JSON.stringify(normalized.providerBindings)
|
|
174
|
+
)
|
|
175
|
+
fault(
|
|
176
|
+
"publication-rehearsal-provider-bindings-root-mismatch",
|
|
177
|
+
"$/providerBindings",
|
|
178
|
+
"external provider bindings do not match the capsule",
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
schema: supplied.schema,
|
|
182
|
+
artifacts: Object.fromEntries(
|
|
183
|
+
Object.entries(supplied.artifacts).map(([role, binding]) => [
|
|
184
|
+
role,
|
|
185
|
+
{
|
|
186
|
+
...binding,
|
|
187
|
+
path: resolveCandidateFile(
|
|
188
|
+
candidateRootPath,
|
|
189
|
+
binding.path,
|
|
190
|
+
`$/providerBindings/artifacts/${role}/path`,
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
]),
|
|
194
|
+
),
|
|
195
|
+
documents: Object.fromEntries(
|
|
196
|
+
Object.entries(supplied.documents).map(([capabilityId, binding]) => [
|
|
197
|
+
capabilityId,
|
|
198
|
+
{
|
|
199
|
+
...binding,
|
|
200
|
+
path: resolveCandidateFile(
|
|
201
|
+
candidateRootPath,
|
|
202
|
+
binding.path,
|
|
203
|
+
`$/providerBindings/documents/${capabilityId}/path`,
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
]),
|
|
207
|
+
),
|
|
208
|
+
evidence: {
|
|
209
|
+
inputs: supplied.evidence.inputs.map((inputPath, index) =>
|
|
210
|
+
resolveCandidateFile(
|
|
211
|
+
candidateRootPath,
|
|
212
|
+
inputPath,
|
|
213
|
+
`$/providerBindings/evidence/inputs/${index}`,
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
output: resolveCandidateOutput(
|
|
217
|
+
candidateRootPath,
|
|
218
|
+
supplied.evidence.output,
|
|
219
|
+
"$/providerBindings/evidence/output",
|
|
220
|
+
),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function simulatedAdapters(declaration, transcript) {
|
|
226
|
+
const applied = new Set();
|
|
227
|
+
return Object.fromEntries(
|
|
228
|
+
declaration.capabilities.map((capability) => [
|
|
229
|
+
capability.adapter,
|
|
230
|
+
{
|
|
231
|
+
async readback(effect) {
|
|
232
|
+
const response = applied.has(effect.operationId)
|
|
233
|
+
? {
|
|
234
|
+
outcome: "observed",
|
|
235
|
+
subjectRoot: effect.subjectRoot,
|
|
236
|
+
targetRoot: effect.targetRoot,
|
|
237
|
+
evidenceRoots: [effect.targetRoot],
|
|
238
|
+
providerCode: "simulation-observed",
|
|
239
|
+
}
|
|
240
|
+
: {
|
|
241
|
+
outcome: "absent",
|
|
242
|
+
subjectRoot: "",
|
|
243
|
+
targetRoot: "",
|
|
244
|
+
evidenceRoots: [],
|
|
245
|
+
providerCode: "simulation-absent",
|
|
246
|
+
};
|
|
247
|
+
transcript.push({
|
|
248
|
+
operationId: effect.operationId,
|
|
249
|
+
method: "readback",
|
|
250
|
+
requestRoot: effect.effectRoot,
|
|
251
|
+
response,
|
|
252
|
+
});
|
|
253
|
+
return response;
|
|
254
|
+
},
|
|
255
|
+
async apply(effect) {
|
|
256
|
+
applied.add(effect.operationId);
|
|
257
|
+
transcript.push({
|
|
258
|
+
operationId: effect.operationId,
|
|
259
|
+
method: "apply",
|
|
260
|
+
requestRoot: effect.effectRoot,
|
|
261
|
+
response: { outcome: "simulated", code: "effect-disabled" },
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
]),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function observationAdapters(capsule, transcript, adapters, mode) {
|
|
270
|
+
const records = new Map(
|
|
271
|
+
capsule.expectedObservations.entries.map((entry) => [
|
|
272
|
+
entry.operationId,
|
|
273
|
+
{ ...entry, index: 0 },
|
|
274
|
+
]),
|
|
275
|
+
);
|
|
276
|
+
return Object.fromEntries(
|
|
277
|
+
capsule.declaration.capabilities.map((capability) => {
|
|
278
|
+
const provided = adapters?.[capability.adapter];
|
|
279
|
+
if (
|
|
280
|
+
mode === "provider" &&
|
|
281
|
+
(!provided ||
|
|
282
|
+
typeof provided.readback !== "function" ||
|
|
283
|
+
typeof provided.apply !== "function")
|
|
284
|
+
)
|
|
285
|
+
fault(
|
|
286
|
+
"publication-rehearsal-provider-adapter-missing",
|
|
287
|
+
"$/adapters",
|
|
288
|
+
`adapter missing: ${capability.adapter}`,
|
|
289
|
+
);
|
|
290
|
+
return [
|
|
291
|
+
capability.adapter,
|
|
292
|
+
{
|
|
293
|
+
async readback(effect) {
|
|
294
|
+
const record = records.get(effect.operationId);
|
|
295
|
+
const expected = record?.readbacks[record.index++];
|
|
296
|
+
if (!expected)
|
|
297
|
+
fault(
|
|
298
|
+
"publication-rehearsal-observation-exhausted",
|
|
299
|
+
"$/expectedObservations",
|
|
300
|
+
`readback missing for ${effect.operationId}`,
|
|
301
|
+
);
|
|
302
|
+
const response =
|
|
303
|
+
mode === "replay"
|
|
304
|
+
? structuredClone(expected)
|
|
305
|
+
: await provided.readback(effect);
|
|
306
|
+
if (JSON.stringify(response) !== JSON.stringify(expected))
|
|
307
|
+
fault(
|
|
308
|
+
"publication-rehearsal-provider-readback-mismatch",
|
|
309
|
+
"$/expectedObservations",
|
|
310
|
+
`provider readback drifted for ${effect.operationId}`,
|
|
311
|
+
);
|
|
312
|
+
transcript.push({
|
|
313
|
+
operationId: effect.operationId,
|
|
314
|
+
method: "readback",
|
|
315
|
+
requestRoot: effect.effectRoot,
|
|
316
|
+
response: structuredClone(response),
|
|
317
|
+
});
|
|
318
|
+
return response;
|
|
319
|
+
},
|
|
320
|
+
async apply(effect) {
|
|
321
|
+
const record = records.get(effect.operationId);
|
|
322
|
+
if (!record)
|
|
323
|
+
fault(
|
|
324
|
+
"publication-rehearsal-observation-exhausted",
|
|
325
|
+
"$/expectedObservations",
|
|
326
|
+
`apply missing for ${effect.operationId}`,
|
|
327
|
+
);
|
|
328
|
+
if (mode === "provider") await provided.apply(effect);
|
|
329
|
+
transcript.push({
|
|
330
|
+
operationId: effect.operationId,
|
|
331
|
+
method: "apply",
|
|
332
|
+
requestRoot: effect.effectRoot,
|
|
333
|
+
response: structuredClone(record.apply),
|
|
334
|
+
});
|
|
335
|
+
if (record.apply.outcome !== "applied") {
|
|
336
|
+
const error = new Error(
|
|
337
|
+
"expected provider apply did not succeed",
|
|
338
|
+
);
|
|
339
|
+
error.releaseTailCode = record.apply.code;
|
|
340
|
+
error.releaseTailClass = record.apply.classification;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function createV4PublicationRehearsalAuthority(
|
|
351
|
+
capsule,
|
|
352
|
+
{ authorizationRoot } = {},
|
|
353
|
+
) {
|
|
354
|
+
const normalized = validateV4PublicationRehearsalCapsule(capsule);
|
|
355
|
+
const body = {
|
|
356
|
+
schema: V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT,
|
|
357
|
+
mode: "provider-rehearsal",
|
|
358
|
+
capsuleRoot: normalized.capsuleRoot,
|
|
359
|
+
providerPolicyRoot: normalized.providerPolicy.root,
|
|
360
|
+
providerBindingsRoot: normalized.providerBindingsRoot,
|
|
361
|
+
authorizationRoot: publicationRehearsalRoot(
|
|
362
|
+
authorizationRoot,
|
|
363
|
+
"$/authorizationRoot",
|
|
364
|
+
),
|
|
365
|
+
productionAuthority: false,
|
|
366
|
+
};
|
|
367
|
+
return { ...body, authorityRoot: releaseTailRoot(body) };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function validateAuthority(value, capsule) {
|
|
371
|
+
exactKeys(
|
|
372
|
+
value,
|
|
373
|
+
[
|
|
374
|
+
"schema",
|
|
375
|
+
"mode",
|
|
376
|
+
"capsuleRoot",
|
|
377
|
+
"providerPolicyRoot",
|
|
378
|
+
"providerBindingsRoot",
|
|
379
|
+
"authorizationRoot",
|
|
380
|
+
"productionAuthority",
|
|
381
|
+
"authorityRoot",
|
|
382
|
+
],
|
|
383
|
+
"$/authority",
|
|
384
|
+
);
|
|
385
|
+
const body = { ...value };
|
|
386
|
+
delete body.authorityRoot;
|
|
387
|
+
if (
|
|
388
|
+
value.schema !== V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT ||
|
|
389
|
+
value.mode !== "provider-rehearsal" ||
|
|
390
|
+
value.productionAuthority !== false ||
|
|
391
|
+
value.capsuleRoot !== capsule.capsuleRoot ||
|
|
392
|
+
value.providerPolicyRoot !== capsule.providerPolicy.root ||
|
|
393
|
+
value.providerBindingsRoot !== capsule.providerBindingsRoot ||
|
|
394
|
+
!ROOT.test(value.authorizationRoot) ||
|
|
395
|
+
value.authorityRoot !== releaseTailRoot(body)
|
|
396
|
+
)
|
|
397
|
+
fault(
|
|
398
|
+
"publication-rehearsal-authority-mismatch",
|
|
399
|
+
"$/authority",
|
|
400
|
+
"exact rehearsal-only authority required",
|
|
401
|
+
);
|
|
402
|
+
return structuredClone(value);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function executeV4PublicationRehearsal({
|
|
406
|
+
capsule,
|
|
407
|
+
candidateRoot: candidateRootPath,
|
|
408
|
+
mode = "simulate",
|
|
409
|
+
adapters = {},
|
|
410
|
+
authority = null,
|
|
411
|
+
transaction = null,
|
|
412
|
+
checkpoint,
|
|
413
|
+
} = {}) {
|
|
414
|
+
if (!MODES.has(mode))
|
|
415
|
+
fault(
|
|
416
|
+
"invalid-publication-rehearsal-mode",
|
|
417
|
+
"$/mode",
|
|
418
|
+
"simulate, replay, or provider required",
|
|
419
|
+
);
|
|
420
|
+
const normalized = verifyV4PublicationRehearsalCapsule({
|
|
421
|
+
capsule,
|
|
422
|
+
candidateRoot: candidateRootPath,
|
|
423
|
+
});
|
|
424
|
+
const providerAuthority =
|
|
425
|
+
mode === "provider" ? validateAuthority(authority, normalized) : null;
|
|
426
|
+
const current = transaction
|
|
427
|
+
? structuredClone(transaction)
|
|
428
|
+
: structuredClone(normalized.transaction);
|
|
429
|
+
const validation = validateReleaseTailTransaction(current);
|
|
430
|
+
if (
|
|
431
|
+
!validation.valid ||
|
|
432
|
+
current.transactionRoot !== normalized.transaction.transactionRoot ||
|
|
433
|
+
current.declarationRoot !== normalized.transaction.declarationRoot ||
|
|
434
|
+
current.planRoot !== normalized.transaction.planRoot
|
|
435
|
+
)
|
|
436
|
+
fault(
|
|
437
|
+
"publication-rehearsal-transaction-mismatch",
|
|
438
|
+
"$/transaction",
|
|
439
|
+
"transaction is not bound to this capsule",
|
|
440
|
+
);
|
|
441
|
+
const transcript = [];
|
|
442
|
+
const selectedAdapters =
|
|
443
|
+
mode === "simulate"
|
|
444
|
+
? simulatedAdapters(normalized.declaration, transcript)
|
|
445
|
+
: observationAdapters(normalized, transcript, adapters, mode);
|
|
446
|
+
const result = await executeReleaseTailTransaction(current, {
|
|
447
|
+
adapters: selectedAdapters,
|
|
448
|
+
checkpoint,
|
|
449
|
+
});
|
|
450
|
+
const body = {
|
|
451
|
+
schema: V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
452
|
+
mode,
|
|
453
|
+
truth:
|
|
454
|
+
mode === "provider"
|
|
455
|
+
? "provider-observed"
|
|
456
|
+
: mode === "replay"
|
|
457
|
+
? "recorded-replay"
|
|
458
|
+
: "simulation-only",
|
|
459
|
+
capsuleRoot: normalized.capsuleRoot,
|
|
460
|
+
sourceRoot: normalized.source.root,
|
|
461
|
+
candidateRoot: normalized.candidate.root,
|
|
462
|
+
manifestRoot: normalized.manifest.root,
|
|
463
|
+
configRoot: normalized.config.root,
|
|
464
|
+
providerBindingsRoot: normalized.providerBindingsRoot,
|
|
465
|
+
providerPolicyRoot: normalized.providerPolicy.root,
|
|
466
|
+
expectedObservationRoot: normalized.expectedObservations.root,
|
|
467
|
+
coreVersion: normalized.coreVersion,
|
|
468
|
+
transactionRoot: result.transactionRoot,
|
|
469
|
+
stateRoot: result.stateRoot,
|
|
470
|
+
receiptRoots: result.receipts.map(({ receiptRoot }) => receiptRoot),
|
|
471
|
+
transcript,
|
|
472
|
+
transcriptRoot: releaseTailRoot(transcript),
|
|
473
|
+
authorityRoot: providerAuthority?.authorityRoot || null,
|
|
474
|
+
productionAuthority: false,
|
|
475
|
+
releasePassport: null,
|
|
476
|
+
};
|
|
477
|
+
return {
|
|
478
|
+
transaction: result,
|
|
479
|
+
evidence: { ...body, evidenceRoot: releaseTailRoot(body) },
|
|
480
|
+
};
|
|
481
|
+
}
|