@xemahq/edition-tooling 0.0.0-stage → 0.1.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/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- package/README.md +0 -3
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
// customer-edition-verbs — the verbs of a CUSTOMER edition, for either channel.
|
|
2
|
+
//
|
|
3
|
+
// xema-edition develop <verb> --definition <file> ... channel develop
|
|
4
|
+
// xema-edition release <verb> --definition <file> ... channel release
|
|
5
|
+
//
|
|
6
|
+
// ONE parser, ONE frame and ONE set of verbs: the channel is an input, never a mode. A customer
|
|
7
|
+
// definition is resolved against the catalogue pinned for the channel in catalogue.pin.json beside
|
|
8
|
+
// it, frozen at the customer's OWN repository pins. The pins file holds one `sources/<name>` commit
|
|
9
|
+
// per source the definition declares and nothing else (the vendor repositories are the catalogue's,
|
|
10
|
+
// not the customer's). The digest of the catalogue artifact the edition was resolved against is
|
|
11
|
+
// frozen into the descriptor's provenance pins as `catalogue`; the artifact tag (snapshotEditionTag)
|
|
12
|
+
// covers it. The vendor image rows arrive sealed from the catalogue; only the customer's own image
|
|
13
|
+
// rows are sealed, in the repositories of the channel (edition-image-seal.mjs).
|
|
14
|
+
|
|
15
|
+
import { writeFileSync } from 'node:fs';
|
|
16
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { sealLock } from './biome-content.mjs';
|
|
19
|
+
import { assertCatalogueConsumerArmed } from './catalogue-evidence.mjs';
|
|
20
|
+
import { fetchCatalogue, parseCatalogueRef } from './catalogue-fetch.mjs';
|
|
21
|
+
import { customerLockCatalogueField } from './catalogue-lock-field.mjs';
|
|
22
|
+
import { readCataloguePin } from './catalogue-pin.mjs';
|
|
23
|
+
import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, COSIGN_FLAG, DEFINITION_AT_FLAG, DEFINITION_FLAG, OFFERING_FLAG, OFFERING_PREVIOUS_LOCK_FLAG, OFFERING_PREVIOUS_LOCK_SHA256_FLAG, SOURCE_IMAGE_FLAG } from './customer-flags.mjs';
|
|
24
|
+
import { freezeCustomerSources, lsRemoteRef } from './develop-freeze.mjs';
|
|
25
|
+
import { customerDevelopGitBundleVerifier } from './develop-git-bundles.mjs';
|
|
26
|
+
import { isDevelopImageRef } from './develop-image-coordinate.mjs';
|
|
27
|
+
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
28
|
+
import { LOCAL_WORKFLOW, assertEngineImage, assertExistingArtifactMatches, snapshotEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
29
|
+
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
30
|
+
import { CATALOGUE_PIN_KEY, CustomerPinForm, DEFINITION_PIN_KEY, EditionChannel, EditionKind, EditionLockFile, customerPinFormAdmitted } from './edition-constants.mjs';
|
|
31
|
+
import { channelImageRef, sealImageSubjects } from './edition-image-seal.mjs';
|
|
32
|
+
import { WorkspaceRoot } from './edition-workspace.mjs';
|
|
33
|
+
import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
|
|
34
|
+
import { holdSource } from './git-source-auth.mjs';
|
|
35
|
+
import { canonicalJson, sha256Digest } from './image-evidence.mjs';
|
|
36
|
+
import { adoptCatalogueKernel } from './kernel-package.mjs';
|
|
37
|
+
import { loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
38
|
+
import { GitSourcedBundle } from './locked-bundle-signer.mjs';
|
|
39
|
+
import { isSourcePinKey, sortedSnapshotPins, sourcePinKey } from './pin-source.mjs';
|
|
40
|
+
import { assertSealedImageLock } from './release-edition-artifact.mjs';
|
|
41
|
+
import { assertSourceImagesCoverDefinition, parseSourceImages, resolveCustomerEdition } from './resolve-customer-edition.mjs';
|
|
42
|
+
import { externalSourceTrees, imageRefForSubject, resolvePreviousSeal, sourceTreeLocator } from './resolve-images.mjs';
|
|
43
|
+
import { readStrictJsonFile } from './strict-json.mjs';
|
|
44
|
+
|
|
45
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
46
|
+
|
|
47
|
+
export class CustomerEditionPinsError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'CustomerEditionPinsError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A customer's own image rows belong to no first-party source, so none roots their develop ref. */
|
|
55
|
+
const CUSTOMER_DECLARED_SOURCES = Object.freeze({});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The digest of a customer definition: sha256 of its canonical JSON (keys sorted). It covers everything the definition
|
|
59
|
+
* decides that no other pin fixes: the vendor biomes it selects, its image registry mirror, its Store publisher anchor
|
|
60
|
+
* keys and its declared sources.
|
|
61
|
+
*/
|
|
62
|
+
export function customerDefinitionDigest(rawDefinition) {
|
|
63
|
+
return sha256Digest(Buffer.from(canonicalJson(rawDefinition), 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The pins of a customer edition of `channel`: `sources/<name>` commits for exactly
|
|
68
|
+
* the definition's declared sources, plus `catalogue` = the digest of the
|
|
69
|
+
* catalogue artifact and `definition` = the digest of the definition (customerDefinitionDigest).
|
|
70
|
+
* A `catalogue` or `definition` entry in the file must equal that digest.
|
|
71
|
+
*/
|
|
72
|
+
export function customerEditionPins({ channel, pins, declaredSourceNames, catalogueDigest, definitionDigest, vendorPins }) {
|
|
73
|
+
const catalogueFree = vendorPins !== undefined;
|
|
74
|
+
if (catalogueFree && !customerPinFormAdmitted(CustomerPinForm.CatalogueFree, EditionKind.Resolved, channel)) {
|
|
75
|
+
throw new CustomerEditionPinsError(`a ${channel} customer edition is never resolved catalogue-free`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof definitionDigest !== 'string') {throw new CustomerEditionPinsError('a customer edition pins the digest of its definition');}
|
|
78
|
+
if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
|
|
79
|
+
throw new CustomerEditionPinsError('--pins must be an object mapping sources/<name> -> 40-hex commit');
|
|
80
|
+
}
|
|
81
|
+
const own = {};
|
|
82
|
+
for (const [key, value] of Object.entries(pins)) {
|
|
83
|
+
if (key === CATALOGUE_PIN_KEY) {
|
|
84
|
+
if (catalogueFree) {throw new CustomerEditionPinsError(`--pins names "${CATALOGUE_PIN_KEY}", but a catalogue-free edition pins no catalogue: the catalogue's digest goes into the distribution lock only`);}
|
|
85
|
+
if (value !== catalogueDigest) {
|
|
86
|
+
throw new CustomerEditionPinsError(`--pins "${CATALOGUE_PIN_KEY}" is ${JSON.stringify(value)}, but the pinned ${channel} catalogue is ${catalogueDigest}`);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (key === DEFINITION_PIN_KEY) {
|
|
91
|
+
if (value !== definitionDigest) {
|
|
92
|
+
throw new CustomerEditionPinsError(`--pins "${DEFINITION_PIN_KEY}" is ${JSON.stringify(value)}, but the definition is ${definitionDigest}`);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!isSourcePinKey(key)) {
|
|
97
|
+
throw new CustomerEditionPinsError(`--pins "${key}" is not one of the customer's own repositories: a customer ${channel} pins only sources/<name> (the vendor's pins are the catalogue's)`);
|
|
98
|
+
}
|
|
99
|
+
if (typeof value !== 'string' || !SHA_PATTERN.test(value)) {
|
|
100
|
+
throw new CustomerEditionPinsError(`--pins "${key}" is not a 40-hex commit SHA: ${JSON.stringify(value)}`);
|
|
101
|
+
}
|
|
102
|
+
own[key] = value;
|
|
103
|
+
}
|
|
104
|
+
const declared = new Set(declaredSourceNames.map(sourcePinKey));
|
|
105
|
+
for (const key of declared) {
|
|
106
|
+
if (!(key in own)) {throw new CustomerEditionPinsError(`--pins is missing "${key}": the definition declares that source`);}
|
|
107
|
+
}
|
|
108
|
+
for (const key of Object.keys(own)) {
|
|
109
|
+
if (!declared.has(key)) {throw new CustomerEditionPinsError(`--pins names "${key}", but the definition declares no such source`);}
|
|
110
|
+
}
|
|
111
|
+
if (!catalogueFree) {return sortedSnapshotPins({ ...own, [CATALOGUE_PIN_KEY]: catalogueDigest, [DEFINITION_PIN_KEY]: definitionDigest });}
|
|
112
|
+
for (const key of Object.keys(vendorPins)) {
|
|
113
|
+
if (key === CATALOGUE_PIN_KEY || key === DEFINITION_PIN_KEY || key in own) {
|
|
114
|
+
throw new CustomerEditionPinsError(`the vendor pins of a catalogue-free edition name "${key}", which is the customer's own`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return sortedSnapshotPins({ ...vendorPins, ...own, [DEFINITION_PIN_KEY]: definitionDigest });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The source names a raw definition declares (the pins are checked before the definition is composed). */
|
|
121
|
+
export function declaredSourceNamesOf(rawDefinition) {
|
|
122
|
+
const sources = rawDefinition?.sources;
|
|
123
|
+
return Array.isArray(sources) ? sources.map((source) => source?.name) : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve and seal the customer edition of `channel`; returns the four locks and the frozen provenance pins. The pin form
|
|
128
|
+
* decides the lock's catalogue field: catalogue-free (`vendorPins`) records `catalogueContentDigest`, the catalogue form
|
|
129
|
+
* `catalogueDigest`. `previousSeal(id)` answers the customer's own previous seal (resolvePreviousSeal; absent: a first seal).
|
|
130
|
+
*/
|
|
131
|
+
export async function resolveCustomerChannelEdition({ channel, rawDefinition, catalogue, catalogueDigest, catalogueContentDigest, pins, vendorPins, previousSeal, sourceImages, env = process.env, sealOptions = {}, adoptKernel, materialize, developGitBundles }) {
|
|
132
|
+
const catalogueField = customerLockCatalogueField({ catalogueFree: vendorPins !== undefined, catalogueDigest, catalogueContentDigest }, { what: `a ${channel} customer resolve`, ErrorType: CustomerEditionPinsError });
|
|
133
|
+
const frozen = customerEditionPins({ channel, pins, declaredSourceNames: declaredSourceNamesOf(rawDefinition), catalogueDigest, definitionDigest: customerDefinitionDigest(rawDefinition), ...(vendorPins === undefined ? {} : { vendorPins }) });
|
|
134
|
+
const resolved = await resolveCustomerEdition({
|
|
135
|
+
rawDefinition,
|
|
136
|
+
catalogue,
|
|
137
|
+
channel,
|
|
138
|
+
sourceImages,
|
|
139
|
+
pins: frozen,
|
|
140
|
+
[catalogueField.key]: catalogueField.digest,
|
|
141
|
+
...(previousSeal === undefined ? {} : { previousSeal }),
|
|
142
|
+
...(adoptKernel === undefined ? {} : { adoptKernel }),
|
|
143
|
+
...(materialize === undefined ? {} : { materialize }),
|
|
144
|
+
...(developGitBundles === undefined ? {} : { developGitBundles }),
|
|
145
|
+
sealOwn: ({ distributionId, subjects }) => sealImageSubjects({ channel, distributionId, subjects, declaredSources: CUSTOMER_DECLARED_SOURCES, env, ...sealOptions }),
|
|
146
|
+
});
|
|
147
|
+
// The customer edition is sealed here: every biomeContentHash is recomputed through the
|
|
148
|
+
// same code as the vendor's, vendor biomes on the CATALOGUE's authority (biome-content.mjs).
|
|
149
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
150
|
+
const distributionLock = await sealLock({
|
|
151
|
+
schemas,
|
|
152
|
+
unsealed: resolved.distributionLock,
|
|
153
|
+
imageLock: resolved.imageLock,
|
|
154
|
+
locate: sourceTreeLocator(externalSourceTrees(resolved.distributionLock)),
|
|
155
|
+
vendoredCommit: (repo) => {
|
|
156
|
+
throw new Error(`customer biome source "${repo}" is first-party to no checkout; vendor biomes are taken from the catalogue`);
|
|
157
|
+
},
|
|
158
|
+
catalogue,
|
|
159
|
+
[catalogueField.key]: catalogueField.digest,
|
|
160
|
+
storePublisherAnchorKeys: resolved.storePublisherAnchorKeys,
|
|
161
|
+
// A row the develop verifier kept carries its bundle and develop signer; the seal admits exactly those.
|
|
162
|
+
gitSourcedBundle: developGitBundles === undefined ? GitSourcedBundle.Refused : GitSourcedBundle.DevelopVerified,
|
|
163
|
+
});
|
|
164
|
+
return { ...resolved, distributionLock, pins: frozen };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* What a verb of a customer edition of `channel` derives from its definition file and its `--pins` file with no
|
|
169
|
+
* network: the id (the definition's), the pinned catalogue reference and digest, and the frozen
|
|
170
|
+
* provenance pins (the customer's `sources/<name>` commits plus that digest).
|
|
171
|
+
*/
|
|
172
|
+
export function customerEditionFrame({ channel, definitionFile, pinsFile }) {
|
|
173
|
+
const rawDefinition = readStrictJsonFile(definitionFile);
|
|
174
|
+
assertCanonicalDistributionId(rawDefinition?.id);
|
|
175
|
+
const catalogueRef = readCataloguePin({ definitionFile, channel });
|
|
176
|
+
const { digest } = parseCatalogueRef(catalogueRef);
|
|
177
|
+
const definitionDigest = customerDefinitionDigest(rawDefinition);
|
|
178
|
+
const pins = customerEditionPins({ channel, pins: readStrictJsonFile(pinsFile), declaredSourceNames: declaredSourceNamesOf(rawDefinition), catalogueDigest: digest, definitionDigest });
|
|
179
|
+
return { id: rawDefinition.id, rawDefinition, catalogueRef, catalogueDigest: digest, definitionDigest, pins };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
183
|
+
// The command line: one grammar for both channels.
|
|
184
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/** The roots a vendor develop verb takes (the catalogue-free resolve's vendor step takes the same). */
|
|
187
|
+
export const DEVELOP_VENDOR_ROOTS = Object.freeze([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache]);
|
|
188
|
+
|
|
189
|
+
/** The roots of develop `select-records`: no kernel, since it runs before any member pin exists and parses records with the CLI's own contracts. */
|
|
190
|
+
export const SELECT_RECORDS_ROOTS = Object.freeze(DEVELOP_VENDOR_ROOTS.filter((root) => root !== WorkspaceRoot.Kernel));
|
|
191
|
+
|
|
192
|
+
/** The roots a verb takes in its customer form. The kernel is never one: the catalogue's kernel is downloaded from npm into the cache root. */
|
|
193
|
+
export const CUSTOMER_ROOTS = Object.freeze({
|
|
194
|
+
resolve: [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Cache],
|
|
195
|
+
target: [],
|
|
196
|
+
'check-existing': [WorkspaceRoot.Data, WorkspaceRoot.Cache],
|
|
197
|
+
'freeze-sources': [WorkspaceRoot.Data],
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/** The roots of a customer verb; an unknown verb is refused naming the verbs there are. */
|
|
201
|
+
export function customerRootsOf(command) {
|
|
202
|
+
const roots = CUSTOMER_ROOTS[command];
|
|
203
|
+
if (roots === undefined) {
|
|
204
|
+
throw new Error(`unknown command ${JSON.stringify(command)}; expected ${Object.keys(CUSTOMER_ROOTS).join(' | ')}`);
|
|
205
|
+
}
|
|
206
|
+
return roots;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Per verb: the vendor form (`positional` id, `values`, `switches`; develop only) and, where the verb has one, the
|
|
210
|
+
// customer form: the flags only it takes, the further flags it requires, and the vendor flags it refuses.
|
|
211
|
+
const CREDENTIAL_FLAGS = Object.freeze([CATALOGUE_USERNAME_FLAG, CATALOGUE_TOKEN_ENV_FLAG]);
|
|
212
|
+
const REPEATABLE = Object.freeze([SOURCE_IMAGE_FLAG]);
|
|
213
|
+
// Every resolve is handed its engine by digest, except the catalogue-free form, whose vendor step finds and proves it
|
|
214
|
+
// (develop-catalogue-free-vendor.mjs) from --engine-repository and --engine-signer.
|
|
215
|
+
const ENGINE_IMAGE_FLAG = '--engine-image';
|
|
216
|
+
const ENGINE_REPOSITORY_FLAG = '--engine-repository';
|
|
217
|
+
const ENGINE_SIGNER_FLAG = '--engine-signer';
|
|
218
|
+
export const IMAGES_FROM_FLAG = '--images-from';
|
|
219
|
+
export const SELECTION_FLAG = '--selection';
|
|
220
|
+
export const CHANNEL_FLAG = '--channel';
|
|
221
|
+
export const PREVIOUS_LOCK_FLAG = '--previous-lock';
|
|
222
|
+
export const PREVIOUS_LOCK_SHA256_FLAG = '--previous-lock-sha256';
|
|
223
|
+
export const FIRST_SEAL_FLAG = '--first-seal';
|
|
224
|
+
/** Removed from the vendor resolve (the channel lane verifies no engine, bundle or previous seal): refused by name. */
|
|
225
|
+
const CHANNEL_LANE_REMOVAL = `the vendor develop resolve takes ${ENGINE_IMAGE_FLAG} <repository>@sha256:<digest>, ${IMAGES_FROM_FLAG}, ${CHANNEL_FLAG} and ${SELECTION_FLAG}, and verifies no signature`;
|
|
226
|
+
|
|
227
|
+
const COMMANDS = Object.freeze({
|
|
228
|
+
resolve: {
|
|
229
|
+
positional: true,
|
|
230
|
+
// The vendor form is the channel lane: the engine by digest, every image digest from the build record (--images-from,
|
|
231
|
+
// images-from-record.mjs) whose satellites are checked against the --channel records; no cosign. The previous seal
|
|
232
|
+
// whose composed artifacts carry their generation forward is exactly one of --previous-lock with --previous-lock-sha256
|
|
233
|
+
// (that edition's images.lock layer, taken by the caller) or --first-seal.
|
|
234
|
+
// --selection is the select-records file the run's image record came from: the satellite check reads the records it pinned.
|
|
235
|
+
values: ['--pins', '--out', '--registry', ENGINE_IMAGE_FLAG, IMAGES_FROM_FLAG, CHANNEL_FLAG, SELECTION_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG],
|
|
236
|
+
switches: ['--local', FIRST_SEAL_FLAG],
|
|
237
|
+
required: ['--pins', '--out', '--registry', ENGINE_IMAGE_FLAG, IMAGES_FROM_FLAG, CHANNEL_FLAG, SELECTION_FLAG],
|
|
238
|
+
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG, CHANNEL_FLAG, SELECTION_FLAG], cosign: true, developCosign: true },
|
|
239
|
+
// The customer's own previous seal (customerPreviousSeal): --previous-lock with --previous-lock-sha256, or --first-seal, on develop only.
|
|
240
|
+
customerPreviousSeal: [PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG, FIRST_SEAL_FLAG],
|
|
241
|
+
// The catalogue-free customer form (develop only, develop-catalogue-free-resolve.mjs): the vendor offering is resolved in
|
|
242
|
+
// the same call on the verified lane, so it names both definitions as triples, the vendor step's engine flags and the
|
|
243
|
+
// offering's frozen previous seal; --previous-lock/--first-seal are the customer's own.
|
|
244
|
+
catalogueFree: {
|
|
245
|
+
markers: [DEFINITION_AT_FLAG, OFFERING_FLAG],
|
|
246
|
+
values: [DEFINITION_AT_FLAG, OFFERING_FLAG, SOURCE_IMAGE_FLAG, ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, OFFERING_PREVIOUS_LOCK_FLAG, OFFERING_PREVIOUS_LOCK_SHA256_FLAG],
|
|
247
|
+
required: [DEFINITION_AT_FLAG, OFFERING_FLAG, '--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, OFFERING_PREVIOUS_LOCK_FLAG, OFFERING_PREVIOUS_LOCK_SHA256_FLAG],
|
|
248
|
+
refused: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, ENGINE_IMAGE_FLAG, IMAGES_FROM_FLAG, CHANNEL_FLAG, SELECTION_FLAG],
|
|
249
|
+
},
|
|
250
|
+
// developCosign: on a customer develop resolve, --cosign verifies the git-sourced OCI bundles under the data root's pinned policy (develop-git-bundles.mjs).
|
|
251
|
+
removedFromVendor: {
|
|
252
|
+
[ENGINE_REPOSITORY_FLAG]: CHANNEL_LANE_REMOVAL,
|
|
253
|
+
[ENGINE_SIGNER_FLAG]: CHANNEL_LANE_REMOVAL,
|
|
254
|
+
[COSIGN_FLAG]: CHANNEL_LANE_REMOVAL,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
target: {
|
|
258
|
+
positional: true,
|
|
259
|
+
values: ['--pins', '--registry', '--engine-image', IMAGES_FROM_FLAG],
|
|
260
|
+
switches: [],
|
|
261
|
+
required: ['--pins', '--registry', '--engine-image'],
|
|
262
|
+
customer: { values: [DEFINITION_FLAG, SOURCE_IMAGE_FLAG], required: ['--pins', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG] },
|
|
263
|
+
},
|
|
264
|
+
'freeze-graph': {
|
|
265
|
+
positional: false,
|
|
266
|
+
values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--member-pins', '--out'],
|
|
267
|
+
switches: [],
|
|
268
|
+
required: ['--data-sha', '--deploy-sha', '--out'],
|
|
269
|
+
},
|
|
270
|
+
'freeze-sources': {
|
|
271
|
+
positional: true,
|
|
272
|
+
values: ['--pins', '--overrides', '--out'],
|
|
273
|
+
switches: [],
|
|
274
|
+
required: ['--pins', '--out'],
|
|
275
|
+
customer: { values: [DEFINITION_FLAG], required: ['--out'], refused: ['--pins', '--overrides'] },
|
|
276
|
+
},
|
|
277
|
+
'select-records': {
|
|
278
|
+
positional: false,
|
|
279
|
+
// --registry is still accepted for one release (the workflow passes it) and is unused: the records live under each member's developImageRegistry
|
|
280
|
+
values: ['--branch', '--registry', '--out', CHANNEL_FLAG],
|
|
281
|
+
switches: [],
|
|
282
|
+
// the records are read through the channel tag their producer's CI moves (develop-select-records.mjs); no cosign
|
|
283
|
+
required: ['--branch', '--out', CHANNEL_FLAG],
|
|
284
|
+
},
|
|
285
|
+
'image-record': {
|
|
286
|
+
positional: true,
|
|
287
|
+
values: ['--selection', '--pins', '--out', CHANNEL_FLAG, '--registry'],
|
|
288
|
+
switches: [],
|
|
289
|
+
required: ['--selection', '--pins', '--out', CHANNEL_FLAG],
|
|
290
|
+
},
|
|
291
|
+
'check-overrides': { positional: false, values: ['--overrides'], switches: [], required: ['--overrides'] },
|
|
292
|
+
'choose-kernel': { positional: false, values: ['--kernel-pin', '--out'], switches: [], required: ['--out'] },
|
|
293
|
+
'check-existing': {
|
|
294
|
+
positional: true,
|
|
295
|
+
values: ['--pins', '--artifact', '--engine-image'],
|
|
296
|
+
switches: [],
|
|
297
|
+
required: ['--pins', '--artifact', '--engine-image'],
|
|
298
|
+
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG], required: ['--pins', '--artifact', '--engine-image'], refused: [], cosign: true },
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The verbs of a channel. Develop has the vendor form of every verb as well as the customer form; a release edition
|
|
304
|
+
* is a customer edition only (the vendor release is `xema-edition artifact`), so release has the customer verbs.
|
|
305
|
+
*/
|
|
306
|
+
function commandsOf(channel) {
|
|
307
|
+
if (channel === EditionChannel.Develop) {return COMMANDS;}
|
|
308
|
+
if (channel === EditionChannel.Release) {
|
|
309
|
+
return Object.fromEntries(Object.entries(COMMANDS).filter(([, spec]) => spec.customer !== undefined));
|
|
310
|
+
}
|
|
311
|
+
throw new Error(`unknown edition channel ${JSON.stringify(channel)}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The catalogue-free customer form of a develop verb: no positional id (it is the definition's), no catalogue flags. */
|
|
315
|
+
function catalogueFreeArgs(command, spec, options, positional) {
|
|
316
|
+
for (const refused of spec.refused) {
|
|
317
|
+
if (options[refused] !== undefined) {
|
|
318
|
+
throw new Error(`a catalogue-free ${command} takes no ${refused}: it names the definition and the offering as ${DEFINITION_AT_FLAG} and ${OFFERING_FLAG}, pins no catalogue, and its vendor step finds its engine itself`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (positional.length !== 0) {throw new Error(`a catalogue-free ${command} takes no <distribution-id>: the id is the definition's`);}
|
|
322
|
+
for (const required of spec.required) {
|
|
323
|
+
if (options[required] === undefined) {throw new Error(`a catalogue-free ${command} requires ${required}`);}
|
|
324
|
+
}
|
|
325
|
+
if (!isAbsolute(options[COSIGN_FLAG])) {throw new Error(`${COSIGN_FLAG} must be an absolute path, got ${JSON.stringify(options[COSIGN_FLAG])}`);}
|
|
326
|
+
return { command, id: undefined, options, customer: false, catalogueFree: true };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Parse `<command> [<id>] --flag value ...` of `channel` against the closed vocabulary of the command. */
|
|
330
|
+
export function parseEditionVerbArgs(channel, argv) {
|
|
331
|
+
const commands = commandsOf(channel);
|
|
332
|
+
const [command, ...rest] = argv;
|
|
333
|
+
const spec = commands[command];
|
|
334
|
+
if (spec === undefined) {
|
|
335
|
+
throw new Error(`unknown command ${JSON.stringify(command)}; expected ${Object.keys(commands).join(' | ')}`);
|
|
336
|
+
}
|
|
337
|
+
// The flags only the customer form takes: a flag the vendor form takes too (`--engine-image` of a develop resolve) does not make it one.
|
|
338
|
+
const customerValues = (spec.customer?.values ?? []).filter((flag) => !spec.values.includes(flag));
|
|
339
|
+
const catalogueFreeValues = channel === EditionChannel.Develop ? (spec.catalogueFree?.values ?? []) : [];
|
|
340
|
+
const options = {};
|
|
341
|
+
const positional = [];
|
|
342
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
343
|
+
const arg = rest[i];
|
|
344
|
+
if (spec.switches.includes(arg)) {
|
|
345
|
+
options[arg] = true;
|
|
346
|
+
} else if (spec.values.includes(arg) || customerValues.includes(arg) || catalogueFreeValues.includes(arg)) {
|
|
347
|
+
const value = rest[i + 1];
|
|
348
|
+
if (value === undefined || (value.startsWith('--') && value.length > 2)) {
|
|
349
|
+
throw new Error(`${arg} requires a value`);
|
|
350
|
+
}
|
|
351
|
+
if (REPEATABLE.includes(arg)) {
|
|
352
|
+
options[arg] = [...(options[arg] ?? []), value];
|
|
353
|
+
} else if (options[arg] !== undefined) {
|
|
354
|
+
throw new Error(`${arg} given twice`);
|
|
355
|
+
} else {
|
|
356
|
+
options[arg] = value;
|
|
357
|
+
}
|
|
358
|
+
i += 1;
|
|
359
|
+
} else if (arg.startsWith('--')) {
|
|
360
|
+
throw new Error(`unknown argument ${arg} for ${command}`);
|
|
361
|
+
} else {
|
|
362
|
+
positional.push(arg);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (catalogueFreeValues.length > 0 && spec.catalogueFree.markers.some((flag) => options[flag] !== undefined)) {
|
|
366
|
+
return catalogueFreeArgs(command, spec.catalogueFree, options, positional);
|
|
367
|
+
}
|
|
368
|
+
// A removed vendor flag with no other sign of a customer form (no --definition, no customer-only flag) is a stale vendor call.
|
|
369
|
+
const removedFlags = channel === EditionChannel.Develop ? Object.keys(spec.removedFromVendor ?? {}) : [];
|
|
370
|
+
if (!customerValues.some((flag) => !removedFlags.includes(flag) && options[flag] !== undefined)) {
|
|
371
|
+
for (const flag of removedFlags) {
|
|
372
|
+
if (options[flag] !== undefined) {throw new Error(`${flag} was removed from a vendor ${command}: ${spec.removedFromVendor[flag]}`);}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const flag of catalogueFreeValues.filter((value) => !customerValues.includes(value) && !spec.values.includes(value))) {
|
|
376
|
+
if (options[flag] !== undefined) {throw new Error(`${flag} belongs to the catalogue-free form of ${command} (${spec.catalogueFree.markers.join(' and ')})`);}
|
|
377
|
+
}
|
|
378
|
+
const customer = spec.customer !== undefined && customerValues.some((flag) => options[flag] !== undefined);
|
|
379
|
+
// `--cosign` of a customer form verifies a release catalogue, and on a develop resolve the git-sourced OCI bundles
|
|
380
|
+
// (`developCosign`); no vendor verb takes one.
|
|
381
|
+
if (customer && spec.customer?.cosign === true && options[COSIGN_FLAG] !== undefined && channel !== EditionChannel.Release && spec.customer.developCosign !== true) {
|
|
382
|
+
throw new Error(`${COSIGN_FLAG} verifies a release catalogue's evidence; a ${channel} ${command} takes none`);
|
|
383
|
+
}
|
|
384
|
+
if (channel === EditionChannel.Release && options[DEFINITION_FLAG] === undefined) {
|
|
385
|
+
throw new Error(`a ${channel} ${command} requires ${DEFINITION_FLAG}`);
|
|
386
|
+
}
|
|
387
|
+
if (customer) {
|
|
388
|
+
if (options[DEFINITION_FLAG] === undefined) {throw new Error(`a customer ${command} requires ${DEFINITION_FLAG}`);}
|
|
389
|
+
for (const refused of spec.customer.refused) {
|
|
390
|
+
if (options[refused] !== undefined) {throw new Error(`a customer ${command} takes no ${refused}`);}
|
|
391
|
+
}
|
|
392
|
+
// The customer's own previous seal is a develop resolve's (customerPreviousSeal); a release customer resolve takes none.
|
|
393
|
+
for (const flag of spec.customerPreviousSeal ?? []) {
|
|
394
|
+
if (channel !== EditionChannel.Develop && options[flag] !== undefined) {throw new Error(`a ${channel} customer ${command} takes no ${flag}`);}
|
|
395
|
+
}
|
|
396
|
+
if (positional.length !== 0) {throw new Error(`a customer ${command} takes no <distribution-id>: the id is the definition's`);}
|
|
397
|
+
if (spec.customer.cosign === true && channel === EditionChannel.Release && options[COSIGN_FLAG] === undefined) {
|
|
398
|
+
throw new Error(`a release ${command} requires ${COSIGN_FLAG} <absolute path of the cosign binary>`);
|
|
399
|
+
}
|
|
400
|
+
if (spec.customer.cosign === true && options[COSIGN_FLAG] !== undefined && !isAbsolute(options[COSIGN_FLAG])) {
|
|
401
|
+
throw new Error(`${COSIGN_FLAG} must be an absolute path, got ${JSON.stringify(options[COSIGN_FLAG])}`);
|
|
402
|
+
}
|
|
403
|
+
} else if (spec.positional ? positional.length !== 1 : positional.length !== 0) {
|
|
404
|
+
throw new Error(`${command} takes ${spec.positional ? 'exactly one <distribution-id>' : 'no positional argument'}`);
|
|
405
|
+
}
|
|
406
|
+
for (const required of customer ? spec.customer.required : spec.required) {
|
|
407
|
+
if (options[required] === undefined) {throw new Error(`${command} requires ${required}`);}
|
|
408
|
+
}
|
|
409
|
+
if (spec.positional && !customer) {assertCanonicalDistributionId(positional[0]);}
|
|
410
|
+
if (options['--engine-image'] !== undefined) {assertEngineImage(options['--engine-image']);}
|
|
411
|
+
return { command, id: positional[0], options, customer };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
415
|
+
// The verbs.
|
|
416
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
/** The ref a channel freezes a source at: develop the `develop` branch of every repository, release the ref the source declares. */
|
|
419
|
+
const FREEZE_REF = Object.freeze({
|
|
420
|
+
[EditionChannel.Develop]: () => 'develop',
|
|
421
|
+
[EditionChannel.Release]: (source) => source.ref,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
/** The head lookup and the hold of one source, for the freeze of `channel`. */
|
|
425
|
+
export function headResolver(channel, env) {
|
|
426
|
+
const workspace = editionWorkspaceConfig();
|
|
427
|
+
return {
|
|
428
|
+
resolveHead: (source) => lsRemoteRef(source, FREEZE_REF[channel](source), { channel, workspace, env }),
|
|
429
|
+
holdSource: (source) => holdSource(source, workspace, env),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The pinned catalogue of a customer definition on `channel`, fetched by its digest. A release edition is resolved only
|
|
435
|
+
* under the CUSTOMER's own evidence trust policy (its data root's, never the vendor's): before any registry call the policy
|
|
436
|
+
* must be declared, the built verifier must be the pinned one, and the policy ARMED on the release channel with the E4 row
|
|
437
|
+
* (`assertCatalogueConsumerArmed`, the one arming rule judged by the pinned bundle with its own kernel). The catalogue's E4
|
|
438
|
+
* evidence is then verified before the catalogue is parsed (`fetchCatalogue`).
|
|
439
|
+
*/
|
|
440
|
+
async function fetchPinnedCatalogue(channel, options, env, fetch = fetchCatalogue, { assertArmed = assertCatalogueConsumerArmed, locateVerifier } = {}) {
|
|
441
|
+
if (channel === EditionChannel.Release) {assertArmed({ channel, ...(locateVerifier === undefined ? {} : { locateVerifier }) });}
|
|
442
|
+
return fetch({
|
|
443
|
+
ref: readCataloguePin({ definitionFile: options[DEFINITION_FLAG], channel }),
|
|
444
|
+
channel,
|
|
445
|
+
username: options[CATALOGUE_USERNAME_FLAG],
|
|
446
|
+
tokenEnv: options[CATALOGUE_TOKEN_ENV_FLAG],
|
|
447
|
+
env,
|
|
448
|
+
cosign: options[COSIGN_FLAG],
|
|
449
|
+
...(locateVerifier === undefined ? {} : { locateVerifier }),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The customer's own previous seal of a develop resolve, as `previousSeal(id)` (resolveCustomerChannelEdition): exactly
|
|
455
|
+
* one of `--previous-lock <path> --previous-lock-sha256 <hex>` (the customer edition's own last images lock, its bytes
|
|
456
|
+
* held to the sha256 and its id to the edition's) or `--first-seal`. No signer form: a resolve reads no "newest", so the
|
|
457
|
+
* same frozen inputs resolve the same bytes. A release customer resolve takes none (refused at the parse).
|
|
458
|
+
*/
|
|
459
|
+
export function customerPreviousSeal(channel, options) {
|
|
460
|
+
if (channel !== EditionChannel.Develop) {return undefined;}
|
|
461
|
+
return (distributionId) =>
|
|
462
|
+
resolvePreviousSeal({
|
|
463
|
+
distributionId,
|
|
464
|
+
previousLockPath: options[PREVIOUS_LOCK_FLAG],
|
|
465
|
+
previousLockSha256: options[PREVIOUS_LOCK_SHA256_FLAG],
|
|
466
|
+
firstSeal: Boolean(options[FIRST_SEAL_FLAG]),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** A customer resolve: the artifact (descriptor and four locks, no catalogue: it is not an offering) goes to --out. */
|
|
471
|
+
async function resolveCustomer(channel, options, env) {
|
|
472
|
+
const { catalogue, digest } = await fetchPinnedCatalogue(channel, options, env);
|
|
473
|
+
const rawDefinition = readStrictJsonFile(options[DEFINITION_FLAG]);
|
|
474
|
+
const sourceImages = customerSourceImages(options, rawDefinition);
|
|
475
|
+
// The catalogue's kernel is adopted inside the resolve, before any kernel schema is loaded; the evidence trust
|
|
476
|
+
// state loads kernel schemas, so it is read after.
|
|
477
|
+
const { id, pins, ...resolved } = await resolveCustomerChannelEdition({
|
|
478
|
+
channel,
|
|
479
|
+
rawDefinition,
|
|
480
|
+
catalogue,
|
|
481
|
+
catalogueDigest: digest,
|
|
482
|
+
pins: readStrictJsonFile(options['--pins']),
|
|
483
|
+
previousSeal: customerPreviousSeal(channel, options),
|
|
484
|
+
sourceImages,
|
|
485
|
+
env,
|
|
486
|
+
// Develop, under a declared customer policy: the git-sourced OCI bundles are verified with --cosign (none: refused when a subject exists).
|
|
487
|
+
developGitBundles: customerDevelopGitBundleVerifier({ channel, cosignBinary: options[COSIGN_FLAG], env }),
|
|
488
|
+
});
|
|
489
|
+
const evidenceTrustPolicy = await descriptorEvidenceTrust({ kind: EditionKind.Resolved, channel, consumesCatalogue: true });
|
|
490
|
+
const target = snapshotEditionTarget(channel, id, pins, { registry: options['--registry'], engineImage: options['--engine-image'], sourceImages });
|
|
491
|
+
// A release edition is admitted only sealed: every image names the digest the registry serves. (A develop edition is not held to it today.)
|
|
492
|
+
if (channel === EditionChannel.Release) {assertSealedImageLock(resolved.imageLock, `the images lock of "${id}"`);}
|
|
493
|
+
writeEditionArtifact({
|
|
494
|
+
outDir: resolve(options['--out']),
|
|
495
|
+
channel,
|
|
496
|
+
distributionId: id,
|
|
497
|
+
pins,
|
|
498
|
+
workflow: options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env),
|
|
499
|
+
evidenceTrustPolicy,
|
|
500
|
+
engineImage: options['--engine-image'],
|
|
501
|
+
distributionLock: resolved.distributionLock,
|
|
502
|
+
imageLock: resolved.imageLock,
|
|
503
|
+
authorizationLock: resolved.authorizationLock,
|
|
504
|
+
chartSetLock: resolved.chartSetLock,
|
|
505
|
+
});
|
|
506
|
+
console.log(target);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** A customer check-existing: the pinned catalogue is fetched to take its kernel; the digest fetched is the digest the artifact is compared with. */
|
|
510
|
+
export async function checkExistingCustomer(channel, options, env, { adopt = adoptCatalogueKernel, fetch = fetchCatalogue, locateVerifier, assertArmed } = {}) {
|
|
511
|
+
const frame = customerEditionFrame({ channel, definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
512
|
+
const sourceImages = customerSourceImages(options, frame.rawDefinition);
|
|
513
|
+
const { catalogue, digest } = await fetchPinnedCatalogue(channel, options, env, fetch, { assertArmed, locateVerifier });
|
|
514
|
+
if (digest !== frame.catalogueDigest) {
|
|
515
|
+
throw new Error(`the fetched catalogue is ${digest}, not the pinned ${frame.catalogueDigest}`);
|
|
516
|
+
}
|
|
517
|
+
await adopt(catalogue);
|
|
518
|
+
assertExistingArtifactMatches({
|
|
519
|
+
artifactDir: resolve(options['--artifact']),
|
|
520
|
+
distributionId: frame.id,
|
|
521
|
+
channel,
|
|
522
|
+
evidenceTrustPolicy: await descriptorEvidenceTrust({ kind: EditionKind.Resolved, channel, locateVerifier, consumesCatalogue: true }),
|
|
523
|
+
pins: frame.pins,
|
|
524
|
+
engineImage: options['--engine-image'],
|
|
525
|
+
catalogueDigest: frame.catalogueDigest,
|
|
526
|
+
definitionDigest: frame.definitionDigest,
|
|
527
|
+
});
|
|
528
|
+
assertOwnImageRowsMatch({ channel, artifactDir: resolve(options['--artifact']), sourceImages });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The parsed --source-image map of a customer verb, covering exactly the definition's declared sources. Every entry
|
|
533
|
+
* names RELEASE repositories on either channel: a develop edition maps them to the develop namespace itself
|
|
534
|
+
* (developImageRef), and a release edition seals them as they are, so an entry already in the develop namespace is
|
|
535
|
+
* refused here (a release edition would otherwise seal develop images).
|
|
536
|
+
*/
|
|
537
|
+
export function customerSourceImages(options, rawDefinition) {
|
|
538
|
+
const sourceImages = parseSourceImages(options[SOURCE_IMAGE_FLAG] ?? []);
|
|
539
|
+
assertSourceImagesCoverDefinition(rawDefinition, sourceImages);
|
|
540
|
+
for (const [name, { imageRegistry, imageNamePrefix }] of sourceImages) {
|
|
541
|
+
if (isDevelopImageRef(imageRefForSubject('probe', imageRegistry, imageNamePrefix))) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`--source-image ${name}=${imageRegistry}/${imageNamePrefix} names the develop image namespace; ` +
|
|
544
|
+
'a --source-image names the release repositories (a develop edition maps them to develop itself)',
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return sourceImages;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The own image rows of an existing customer artifact of `channel` are the rows this `--source-image` map composes:
|
|
553
|
+
* every subject owned by a biome drawn from a declared source is `<registry>/<prefix><subject>` mapped to its
|
|
554
|
+
* repository on the channel. The tag covers the map, but a tag is mutable registry state, so the pulled bytes are
|
|
555
|
+
* compared too. Throws naming the first row that differs.
|
|
556
|
+
*/
|
|
557
|
+
export function assertOwnImageRowsMatch({ channel, artifactDir, sourceImages }) {
|
|
558
|
+
const lock = readStrictJsonFile(join(artifactDir, EditionLockFile.Distribution));
|
|
559
|
+
const imageLock = readStrictJsonFile(join(artifactDir, EditionLockFile.Images));
|
|
560
|
+
const sourceOf = new Map((lock.biomes ?? []).filter((biome) => biome.source !== undefined).map((biome) => [biome.id, biome.source.name]));
|
|
561
|
+
for (const subject of imageLock.subjects ?? []) {
|
|
562
|
+
const biomeId = (subject.owners ?? []).find((owner) => sourceOf.has(owner.biomeId))?.biomeId;
|
|
563
|
+
if (biomeId === undefined) {continue;}
|
|
564
|
+
const name = sourceOf.get(biomeId);
|
|
565
|
+
const image = sourceImages.get(name);
|
|
566
|
+
if (image === undefined) {
|
|
567
|
+
throw new Error(`the existing artifact draws biome "${biomeId}" from source "${name}", which has no --source-image; refusing to reuse it`);
|
|
568
|
+
}
|
|
569
|
+
const expected = channelImageRef(channel, { imageRef: imageRefForSubject(subject.id, image.imageRegistry, image.imageNamePrefix) }, CUSTOMER_DECLARED_SOURCES);
|
|
570
|
+
if (subject.imageRef !== expected) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`the existing artifact's own image row ${subject.kind}:${subject.id} is ${subject.imageRef}, not ${expected} ` +
|
|
573
|
+
'(the --source-image map differs); refusing to reuse it',
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Write a file exclusively (wx): a second run over the same path is refused. */
|
|
580
|
+
export function writeOnce(path, value) {
|
|
581
|
+
writeFileSync(path, serializeJson(value), { flag: 'wx' });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Run one verb in its customer form on `channel`. */
|
|
585
|
+
export async function runCustomerVerb({ channel, command, options, env }) {
|
|
586
|
+
switch (command) {
|
|
587
|
+
case 'target': {
|
|
588
|
+
const frame = customerEditionFrame({ channel, definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
589
|
+
const sourceImages = customerSourceImages(options, frame.rawDefinition);
|
|
590
|
+
console.log(snapshotEditionTarget(channel, frame.id, frame.pins, { registry: options['--registry'], engineImage: options['--engine-image'], sourceImages }));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
case 'resolve':
|
|
594
|
+
return resolveCustomer(channel, options, env);
|
|
595
|
+
case 'check-existing':
|
|
596
|
+
return checkExistingCustomer(channel, options, env);
|
|
597
|
+
case 'freeze-sources':
|
|
598
|
+
writeOnce(
|
|
599
|
+
options['--out'],
|
|
600
|
+
freezeCustomerSources({
|
|
601
|
+
definition: readStrictJsonFile(options[DEFINITION_FLAG]),
|
|
602
|
+
...headResolver(channel, env),
|
|
603
|
+
sourceRepositoryUrlPrefix: editionWorkspaceConfig().sourceRepositoryUrlPrefix,
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
return;
|
|
607
|
+
default:
|
|
608
|
+
throw new Error(`${command} has no customer form`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The flags that make a resolve a CUSTOMER resolve (against a catalogue).
|
|
2
|
+
export const DEFINITION_FLAG = '--definition';
|
|
3
|
+
export const CHANNEL_FLAG = '--channel';
|
|
4
|
+
export const SOURCE_IMAGE_FLAG = '--source-image';
|
|
5
|
+
export const CATALOGUE_USERNAME_FLAG = '--catalogue-username';
|
|
6
|
+
export const CATALOGUE_TOKEN_ENV_FLAG = '--catalogue-token-env';
|
|
7
|
+
/** The absolute path of the cosign binary that verifies a release catalogue's E4 evidence (release only). */
|
|
8
|
+
export const COSIGN_FLAG = '--cosign';
|
|
9
|
+
/** A definition named as `<owner>/<repo>,<path>,<dataRoot>` at a pinned commit; `--definition` is a file. */
|
|
10
|
+
export const DEFINITION_AT_FLAG = '--definition-at';
|
|
11
|
+
/** The vendor offering a catalogue-free resolve resolves in the same call, as `<owner>/<repo>,<path>,<dataRoot>` of the vendor data checkout. */
|
|
12
|
+
export const OFFERING_FLAG = '--offering';
|
|
13
|
+
/** The offering's own previous seal a catalogue-free resolve's vendor step carries forward: the frozen offering images lock, and its sha256. */
|
|
14
|
+
export const OFFERING_PREVIOUS_LOCK_FLAG = '--offering-previous-lock';
|
|
15
|
+
export const OFFERING_PREVIOUS_LOCK_SHA256_FLAG = '--offering-previous-lock-sha256';
|