@xemahq/xema 0.40.3 → 0.41.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.
Files changed (46) hide show
  1. package/dist/commands/edition.d.ts.map +1 -1
  2. package/dist/commands/edition.js +1 -0
  3. package/dist/commands/edition.js.map +1 -1
  4. package/dist/edition/verify-image-evidence.bundle.mjs +289 -19
  5. package/dist/edition/verify-image-evidence.bundle.provenance.json +71 -47
  6. package/dist/runtime/edition/verb-map.d.ts +3 -1
  7. package/dist/runtime/edition/verb-map.d.ts.map +1 -1
  8. package/dist/runtime/edition/verb-map.js +4 -0
  9. package/dist/runtime/edition/verb-map.js.map +1 -1
  10. package/edition/README.md +5 -0
  11. package/edition/catalogue-evidence.mjs +225 -0
  12. package/edition/catalogue-fetch.mjs +40 -7
  13. package/edition/customer-edition-verbs.mjs +477 -0
  14. package/edition/customer-flags.mjs +2 -0
  15. package/edition/develop-coverage-readers.mjs +212 -0
  16. package/edition/develop-coverage-run.mjs +48 -0
  17. package/edition/develop-coverage-verdict.mjs +153 -0
  18. package/edition/develop-coverage.mjs +120 -0
  19. package/edition/develop-edition-resolve.mjs +18 -7
  20. package/edition/develop-edition.mjs +47 -220
  21. package/edition/develop-freeze.mjs +50 -13
  22. package/edition/develop-image-probe.mjs +7 -7
  23. package/edition/dispatched-runtime-components.mjs +90 -1
  24. package/edition/edition-artifact.mjs +31 -14
  25. package/edition/edition-constants.mjs +9 -0
  26. package/edition/edition-contract.json +2 -1
  27. package/edition/{develop-seal.mjs → edition-image-seal.mjs} +36 -17
  28. package/edition/edition-workspace.mjs +4 -3
  29. package/edition/evidence-trust-pin.mjs +28 -8
  30. package/edition/evidence-trust-policy.mjs +51 -7
  31. package/edition/image-evidence.mjs +2 -2
  32. package/edition/lib.mjs +20 -11
  33. package/edition/oci-image-source-evidence.mjs +3 -0
  34. package/edition/oras-process.mjs +3 -3
  35. package/edition/oras.mjs +47 -5
  36. package/edition/produce-edition-catalogue-evidence.mjs +252 -0
  37. package/edition/produce-image-source-evidence.mjs +11 -2
  38. package/edition/release-edition.mjs +54 -0
  39. package/edition/repin-evidence-trust.mjs +5 -3
  40. package/edition/resolve-images.mjs +40 -5
  41. package/edition/sigstore-bundle-media-type.mjs +4 -0
  42. package/edition/trust-policy-arming.mjs +14 -1
  43. package/edition/verify-catalogue-evidence.mjs +245 -0
  44. package/edition/verify-image-evidence.mjs +15 -1
  45. package/package.json +2 -2
  46. package/edition/develop-customer-edition.mjs +0 -149
@@ -0,0 +1,245 @@
1
+ // verify-catalogue-evidence — the `catalogue-evidence` mode of the evidence verifier bundle.
2
+ //
3
+ // <bundle> catalogue-evidence arming --channel <release|develop> --data-root <dir>
4
+ // <bundle> catalogue-evidence verify --channel <c> --data-root <dir> --evidence-root <abs dir>
5
+ // --statement-digest <sha256:..> --bundle-digest <sha256:..> --workflow-sha <40hex> --offering-id <id>
6
+ // --subject-digest <sha256:..> --catalogue-layer-digest <sha256:..> --lock-layer-digest <sha256:..>
7
+ // --cosign <abs path>
8
+ //
9
+ // A customer verifies the E4 statement of a release catalogue BEFORE the catalogue's kernel is adopted, so there is no
10
+ // kernel package to load schemas from yet. The only kernel-dependent parts of the judgement are the policy schema
11
+ // (parseSigstoreOfflineTrustPolicy) and the predicate-type/channel constants; before adoption they come from the pinned
12
+ // bundle's own embedded kernel, through `loadVerifierKernelSchemas` and nowhere else. This mode never takes `--kernel`.
13
+ //
14
+ // The anchors are the customer's pinned policy and the pinned verifier itself (the bundle refuses to run unless its own
15
+ // bytes are the ones the data-root pin records). There is NO git grading in this mode: it reads no checkout of an editions
16
+ // root, only the declared policy and pin of the data root, both of which the pin binds by digest.
17
+ //
18
+ // ONE arming rule (trust-policy-arming.mjs): the policy must be ARMED on the channel with the E4 row required
19
+ // (`consumesCatalogue: true`); `arming` stops there. `verify` then holds the statement to that policy and to the exact
20
+ // manifest, catalogue layer and lock layer the caller read, with the kernel's own `editionCatalogueRefusal`.
21
+
22
+ import { editionWorkspaceConfig, resetEditionWorkspaceConfig } from './edition-config.mjs';
23
+ import { EDITION_CONTRACT } from './edition-constants.mjs';
24
+ import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot } from './edition-workspace.mjs';
25
+ import {
26
+ assertPinnedPolicy,
27
+ assertPinnedVerifier,
28
+ loadDeclaredTrustPolicy,
29
+ readEvidenceTrustPin,
30
+ verifierDigests,
31
+ } from './evidence-trust-pin.mjs';
32
+ import { prepareTrustedCosignBinary, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
33
+ import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
34
+ import { TrustPolicyArmingState, assessTrustPolicyArming } from './trust-policy-arming.mjs';
35
+ import { assertSanitizedOperatorEnvironment, readRunningVerifier } from './verifier-binding.mjs';
36
+ import { loadVerifierKernelSchemas } from './verifier-kernel-schemas.mjs';
37
+
38
+ /** The first argument that selects this mode of the bundle. */
39
+ export const CATALOGUE_EVIDENCE_MODE = 'catalogue-evidence';
40
+ export const CATALOGUE_EVIDENCE_USAGE =
41
+ 'usage: catalogue-evidence arming --channel <release|develop> --data-root <dir> | ' +
42
+ 'catalogue-evidence verify --channel <c> --data-root <dir> --evidence-root <abs dir> --statement-digest <sha256:..> ' +
43
+ '--bundle-digest <sha256:..> --workflow-sha <40-hex> --offering-id <id> --subject-digest <sha256:..> ' +
44
+ '--catalogue-layer-digest <sha256:..> --lock-layer-digest <sha256:..> --cosign <abs path>';
45
+
46
+ export const CATALOGUE_EVIDENCE_STATEMENT_FILE = 'statement.json';
47
+ export const CATALOGUE_EVIDENCE_BUNDLE_FILE = 'bundle.sigstore.json';
48
+ /** The code of the refusal that the data root's policy is not armed (also the code of the library's error). */
49
+ export const CATALOGUE_EVIDENCE_UNARMED_CODE = EDITION_CONTRACT.evidenceTrust.refusal.code;
50
+
51
+ export const CatalogueEvidenceRefusalCode = Object.freeze({
52
+ SubjectMismatch: 'catalogue_evidence_subject_mismatch',
53
+ OfferingMismatch: 'catalogue_evidence_offering_mismatch',
54
+ LockDigestMismatch: 'catalogue_evidence_lock_digest_mismatch',
55
+ });
56
+
57
+ const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
58
+ const COMMIT = /^[a-f0-9]{40}$/;
59
+ const ARMING_FLAGS = Object.freeze(['--channel']);
60
+ const VERIFY_FLAGS = Object.freeze([
61
+ '--channel',
62
+ '--evidence-root',
63
+ '--statement-digest',
64
+ '--bundle-digest',
65
+ '--workflow-sha',
66
+ '--offering-id',
67
+ '--subject-digest',
68
+ '--catalogue-layer-digest',
69
+ '--lock-layer-digest',
70
+ '--cosign',
71
+ ]);
72
+ const ABSOLUTE_FLAGS = Object.freeze(['--evidence-root', '--cosign']);
73
+ const DIGEST_FLAGS = Object.freeze(['--statement-digest', '--bundle-digest', '--subject-digest', '--catalogue-layer-digest', '--lock-layer-digest']);
74
+
75
+ /** A bad command line: exit 2. */
76
+ export class CatalogueEvidenceUsageError extends Error {
77
+ constructor(message) {
78
+ super(message);
79
+ this.name = 'CatalogueEvidenceUsageError';
80
+ }
81
+ }
82
+
83
+ /** The data root's policy is not armed (or declared) for a catalogue consumer on the channel. Carries the stable code. */
84
+ export class CatalogueEvidenceUnarmedError extends Error {
85
+ constructor(message) {
86
+ super(`${CATALOGUE_EVIDENCE_UNARMED_CODE}: ${message}`);
87
+ this.name = 'EvidenceTrustRequiredError';
88
+ this.code = CATALOGUE_EVIDENCE_UNARMED_CODE;
89
+ }
90
+ }
91
+
92
+ /** The statement does not bind what the caller read; `code` is the refusal's stable name. */
93
+ export class CatalogueEvidenceRefusal extends Error {
94
+ constructor(code, message) {
95
+ super(`${code}: ${message}`);
96
+ this.name = 'CatalogueEvidenceRefusal';
97
+ this.code = code;
98
+ }
99
+ }
100
+
101
+ const hexOf = (digest) => digest.slice('sha256:'.length);
102
+
103
+ function parseFlags(args, allowed) {
104
+ const flags = new Map();
105
+ for (let index = 0; index < args.length; index += 2) {
106
+ const flag = args[index];
107
+ const value = args[index + 1];
108
+ if (typeof flag !== 'string' || !allowed.includes(flag)) {
109
+ throw new CatalogueEvidenceUsageError(`unknown or unexpected argument ${JSON.stringify(flag)}`);
110
+ }
111
+ if (value === undefined || value.startsWith('--')) {throw new CatalogueEvidenceUsageError(`${flag} requires a value`);}
112
+ if (flags.has(flag)) {throw new CatalogueEvidenceUsageError(`${flag} was given twice`);}
113
+ flags.set(flag, value);
114
+ }
115
+ for (const flag of allowed) {
116
+ if (!flags.has(flag)) {throw new CatalogueEvidenceUsageError(`${flag} is required`);}
117
+ }
118
+ for (const flag of ABSOLUTE_FLAGS.filter((candidate) => flags.has(candidate))) {
119
+ if (!flags.get(flag).startsWith('/')) {throw new CatalogueEvidenceUsageError(`${flag} must be an absolute path`);}
120
+ }
121
+ for (const flag of DIGEST_FLAGS.filter((candidate) => flags.has(candidate))) {
122
+ if (!SHA256_DIGEST.test(flags.get(flag))) {throw new CatalogueEvidenceUsageError(`${flag} must be sha256:<64 lowercase hex>`);}
123
+ }
124
+ if (flags.has('--workflow-sha') && !COMMIT.test(flags.get('--workflow-sha'))) {
125
+ throw new CatalogueEvidenceUsageError('--workflow-sha must be a 40-character lowercase hexadecimal git commit SHA');
126
+ }
127
+ return flags;
128
+ }
129
+
130
+ /**
131
+ * The anchors, then the ONE arming rule: the sanitized launch, the running verifier, the declared policy held to the
132
+ * data-root pin of both the policy and THIS verifier, and the policy ARMED on `channel` with the E4 row required.
133
+ */
134
+ async function armedAnchors({ channel, env, bundlePath }) {
135
+ assertSanitizedOperatorEnvironment(env);
136
+ const running = readRunningVerifier(bundlePath);
137
+ const { schemas } = await loadVerifierKernelSchemas();
138
+ resetEditionWorkspaceConfig();
139
+ const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
140
+ if (declaration.policy === null) {
141
+ throw new CatalogueEvidenceUnarmedError(`${dataRoot()} declares no evidenceTrustPolicy in edition-workspace.json`);
142
+ }
143
+ const { policy, policyDigest } = loadDeclaredTrustPolicy({ dataRoot: dataRoot(), declaration, schemas });
144
+ const pin = readEvidenceTrustPin({ dataRoot: dataRoot(), declaration });
145
+ assertPinnedPolicy({ pin, policyDigest });
146
+ assertPinnedVerifier({ pin, actual: verifierDigests(running) });
147
+ const { state, unmet } = assessTrustPolicyArming(policy, schemas, channel, { consumesCatalogue: true });
148
+ if (state !== TrustPolicyArmingState.ARMED) {
149
+ throw new CatalogueEvidenceUnarmedError(
150
+ `the declared policy is ${state.toUpperCase()} on the ${channel} channel for a catalogue consumer: ${unmet.join('; ')}`,
151
+ );
152
+ }
153
+ return { schemas, policy };
154
+ }
155
+
156
+ /**
157
+ * Run the mode. `argv` is the bundle's arguments (`['catalogue-evidence', '<phase>', ...]`). Resolves to the JSON line to
158
+ * print on success; rejects with a named error otherwise.
159
+ */
160
+ export async function runCatalogueEvidence({ argv, env, bundlePath }) {
161
+ const [mode, phase, ...tail] = argv;
162
+ if (mode !== CATALOGUE_EVIDENCE_MODE || (phase !== 'arming' && phase !== 'verify')) {
163
+ throw new CatalogueEvidenceUsageError(CATALOGUE_EVIDENCE_USAGE);
164
+ }
165
+ // `--kernel` is no root of this mode: the schemas are the bundle's own.
166
+ let rest;
167
+ try {
168
+ rest = configureEditionWorkspaceFromArgv(tail, [WorkspaceRoot.Data], { refuseOthers: true });
169
+ } catch (error) {
170
+ throw new CatalogueEvidenceUsageError(error.message);
171
+ }
172
+ const flags = parseFlags(rest, phase === 'arming' ? ARMING_FLAGS : VERIFY_FLAGS);
173
+ const channel = flags.get('--channel');
174
+ const { schemas, policy } = await armedAnchors({ channel, env, bundlePath });
175
+ if (phase === 'arming') {
176
+ return `${JSON.stringify({ channel, state: TrustPolicyArmingState.ARMED })}\n`;
177
+ }
178
+ const offeringId = flags.get('--offering-id');
179
+ const descriptor = {
180
+ statementPath: CATALOGUE_EVIDENCE_STATEMENT_FILE,
181
+ statementDigest: flags.get('--statement-digest'),
182
+ bundlePath: CATALOGUE_EVIDENCE_BUNDLE_FILE,
183
+ bundleDigest: flags.get('--bundle-digest'),
184
+ bundleMediaType: BUNDLE_MEDIA_TYPE,
185
+ };
186
+ const trustedCosign = prepareTrustedCosignBinary({ cosignBinary: flags.get('--cosign'), policy });
187
+ let statement;
188
+ try {
189
+ ({ statement } = verifySigstoreEvidenceDescriptor({
190
+ evidenceRoot: flags.get('--evidence-root'),
191
+ descriptor,
192
+ policy,
193
+ predicateType: schemas.EDITION_CATALOGUE_PREDICATE_TYPE,
194
+ channel,
195
+ expectedWorkflowSha: flags.get('--workflow-sha'),
196
+ parseStatement: schemas.parseEditionCatalogueStatement,
197
+ policyRoot: dataRoot(),
198
+ trustedCosign,
199
+ }));
200
+ } finally {
201
+ trustedCosign.dispose();
202
+ }
203
+ const [subject] = statement.subject;
204
+ const expectedSubject = `xema-edition-${offeringId}`;
205
+ if (subject.digest.sha256 !== hexOf(flags.get('--subject-digest')) || subject.name !== expectedSubject) {
206
+ throw new CatalogueEvidenceRefusal(
207
+ CatalogueEvidenceRefusalCode.SubjectMismatch,
208
+ `the statement is over ${subject.name}@sha256:${subject.digest.sha256}, not ${expectedSubject}@${flags.get('--subject-digest')}`,
209
+ );
210
+ }
211
+ const refusal = schemas.editionCatalogueRefusal(statement.predicate, {
212
+ channel,
213
+ catalogueLayerDigest: flags.get('--catalogue-layer-digest'),
214
+ });
215
+ if (refusal !== undefined) {
216
+ throw new CatalogueEvidenceRefusal(
217
+ refusal,
218
+ `the statement states channel ${statement.predicate.channel} and catalogue ${statement.predicate.catalogueDigest}; ` +
219
+ `this catalogue is on ${channel} with layer ${flags.get('--catalogue-layer-digest')}`,
220
+ );
221
+ }
222
+ if (statement.predicate.offeringId !== offeringId) {
223
+ throw new CatalogueEvidenceRefusal(
224
+ CatalogueEvidenceRefusalCode.OfferingMismatch,
225
+ `the statement is for offering ${statement.predicate.offeringId}, not ${offeringId}`,
226
+ );
227
+ }
228
+ if (statement.predicate.lockDigest !== hexOf(flags.get('--lock-layer-digest'))) {
229
+ throw new CatalogueEvidenceRefusal(
230
+ CatalogueEvidenceRefusalCode.LockDigestMismatch,
231
+ `the statement states lock digest ${statement.predicate.lockDigest}, not that of the lock layer ${flags.get('--lock-layer-digest')}`,
232
+ );
233
+ }
234
+ return `${JSON.stringify({ verified: true, offeringId, channel, statementDigest: descriptor.statementDigest })}\n`;
235
+ }
236
+
237
+ /** Run the mode as the bundle's entry: print its line, or the named refusal on stderr with exit 1 (usage: 2). */
238
+ export async function runCatalogueEvidenceEntry({ argv, env, bundlePath }) {
239
+ try {
240
+ process.stdout.write(await runCatalogueEvidence({ argv, env, bundlePath }));
241
+ } catch (error) {
242
+ process.stderr.write(`catalogue-evidence: ${error.name}: ${error.message}\n`);
243
+ process.exitCode = error instanceof CatalogueEvidenceUsageError ? 2 : 1;
244
+ }
245
+ }
@@ -12,6 +12,9 @@
12
12
  // * the data root and the editions root are clean git checkouts at an exact
13
13
  // HEAD with every file read from them bound to its HEAD blob.
14
14
  //
15
+ // A second mode, `catalogue-evidence arming|verify` (verify-catalogue-evidence.mjs), judges a release catalogue's E4
16
+ // statement for a customer before the catalogue's kernel is adopted; it is selected by the first argument.
17
+ //
15
18
  // usage: verify-image-evidence <distributionId> --evidence-root <dir>
16
19
  // --descriptor <absolute-path> --state <absolute-path> --cosign <absolute-path> --git <absolute-path>
17
20
  // [--evidence-set <relative-path>] --data-root <dir> --editions-root <dir>
@@ -65,6 +68,10 @@ import {
65
68
  readRunningVerifier,
66
69
  } from './verifier-binding.mjs';
67
70
  import { loadVerifierKernelSchemas } from './verifier-kernel-schemas.mjs';
71
+ import {
72
+ CATALOGUE_EVIDENCE_MODE,
73
+ runCatalogueEvidenceEntry,
74
+ } from './verify-catalogue-evidence.mjs';
68
75
 
69
76
  const USAGE =
70
77
  'usage: verify-image-evidence <distributionId> --evidence-root <dir> --descriptor <absolute-path> --state <absolute-path> ' +
@@ -445,7 +452,14 @@ export async function verifyEvidence({ argv, env, bundlePath }) {
445
452
  }
446
453
  }
447
454
 
448
- if (isEntryModule(import.meta.url)) {
455
+ if (isEntryModule(import.meta.url) && process.argv[2] === CATALOGUE_EVIDENCE_MODE) {
456
+ // The customer's pre-adoption mode: the E4 statement of a catalogue, judged with this bundle's own embedded kernel.
457
+ await runCatalogueEvidenceEntry({
458
+ argv: process.argv.slice(2),
459
+ env: process.env,
460
+ bundlePath: process.argv[1],
461
+ });
462
+ } else if (isEntryModule(import.meta.url)) {
449
463
  await verifyEvidence({
450
464
  argv: process.argv.slice(2),
451
465
  env: process.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.40.3",
3
+ "version": "0.41.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:55a458c54ff157ee053b4f074068c355fb35d28f54a01b0a2662bf14b07b8323"
95
+ "sourceDigest": "sha256:7e8af916b8cd5c8de331d7e274a7022071e6b6ac0d9d54042c2e55461ad46bab"
96
96
  }
97
97
  }
@@ -1,149 +0,0 @@
1
- // develop-customer-edition — a DEVELOP customer edition: a customer definition
2
- // resolved against a catalogue, frozen at the customer's OWN repository pins.
3
- //
4
- // The pins file holds one `sources/<name>` commit per source the definition
5
- // declares and nothing else (the vendor repositories are the catalogue's, not
6
- // the customer's). The digest of the catalogue artifact the edition was
7
- // resolved against is frozen into the descriptor's provenance pins as
8
- // `catalogue`; the artifact tag (developEditionTag) covers it. The vendor image
9
- // rows arrive sealed from the catalogue; only the customer's own image rows are
10
- // sealed against the develop image repositories.
11
-
12
- import { join } from 'node:path';
13
-
14
- import { sealLock } from './biome-content.mjs';
15
- import { parseCatalogueRef } from './catalogue-fetch.mjs';
16
- import { readCataloguePin } from './catalogue-pin.mjs';
17
- import { developImageRef } from './develop-image-coordinate.mjs';
18
- import { sealDevelopImageSubjects } from './develop-seal.mjs';
19
- import { assertCanonicalDistributionId } from './distribution-paths.mjs';
20
- import { CATALOGUE_PIN_KEY, EditionChannel, EditionLockFile } from './edition-constants.mjs';
21
- import { loadKernelDistributionSchemas } from './lib.mjs';
22
- import { isSourcePinKey, sortedSnapshotPins, sourcePinKey } from './pin-source.mjs';
23
- import { resolveCustomerEdition } from './resolve-customer-edition.mjs';
24
- import { externalSourceTrees, imageRefForSubject, sourceTreeLocator } from './resolve-images.mjs';
25
- import { readStrictJsonFile } from './strict-json.mjs';
26
-
27
- const SHA_PATTERN = /^[0-9a-f]{40}$/;
28
-
29
- export class CustomerDevelopPinsError extends Error {
30
- constructor(message) {
31
- super(message);
32
- this.name = 'CustomerDevelopPinsError';
33
- }
34
- }
35
-
36
- /**
37
- * The pins of a develop customer edition: `sources/<name>` commits for exactly
38
- * the definition's declared sources, plus `catalogue` = the digest of the
39
- * catalogue artifact. A `catalogue` entry in the file must equal that digest.
40
- */
41
- export function customerDevelopPins({ pins, declaredSourceNames, catalogueDigest }) {
42
- if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
43
- throw new CustomerDevelopPinsError('--pins must be an object mapping sources/<name> -> 40-hex commit');
44
- }
45
- const own = {};
46
- for (const [key, value] of Object.entries(pins)) {
47
- if (key === CATALOGUE_PIN_KEY) {
48
- if (value !== catalogueDigest) {
49
- throw new CustomerDevelopPinsError(`--pins "${CATALOGUE_PIN_KEY}" is ${JSON.stringify(value)}, but the pinned develop catalogue is ${catalogueDigest}`);
50
- }
51
- continue;
52
- }
53
- if (!isSourcePinKey(key)) {
54
- throw new CustomerDevelopPinsError(`--pins "${key}" is not one of the customer's own repositories: a customer develop pins only sources/<name> (the vendor's pins are the catalogue's)`);
55
- }
56
- if (typeof value !== 'string' || !SHA_PATTERN.test(value)) {
57
- throw new CustomerDevelopPinsError(`--pins "${key}" is not a 40-hex commit SHA: ${JSON.stringify(value)}`);
58
- }
59
- own[key] = value;
60
- }
61
- const declared = new Set(declaredSourceNames.map(sourcePinKey));
62
- for (const key of declared) {
63
- if (!(key in own)) {throw new CustomerDevelopPinsError(`--pins is missing "${key}": the definition declares that source`);}
64
- }
65
- for (const key of Object.keys(own)) {
66
- if (!declared.has(key)) {throw new CustomerDevelopPinsError(`--pins names "${key}", but the definition declares no such source`);}
67
- }
68
- return sortedSnapshotPins({ ...own, [CATALOGUE_PIN_KEY]: catalogueDigest });
69
- }
70
-
71
- /** The source names a raw definition declares (the pins are checked before the definition is composed). */
72
- export function declaredSourceNamesOf(rawDefinition) {
73
- const sources = rawDefinition?.sources;
74
- return Array.isArray(sources) ? sources.map((source) => source?.name) : [];
75
- }
76
-
77
- /** Resolve and seal the develop customer edition; returns the four locks and the frozen provenance pins. */
78
- export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue, catalogueDigest, pins, sourceImages, env = process.env, sealOptions = {}, adoptKernel, materialize }) {
79
- const frozen = customerDevelopPins({ pins, declaredSourceNames: declaredSourceNamesOf(rawDefinition), catalogueDigest });
80
- const resolved = await resolveCustomerEdition({
81
- rawDefinition,
82
- catalogue,
83
- channel: EditionChannel.Develop,
84
- sourceImages,
85
- pins: frozen,
86
- catalogueDigest,
87
- ...(adoptKernel === undefined ? {} : { adoptKernel }),
88
- ...(materialize === undefined ? {} : { materialize }),
89
- sealOwn: ({ distributionId, subjects }) => sealDevelopImageSubjects({ distributionId, subjects, env, ...sealOptions }),
90
- });
91
- // The customer edition is sealed here: every biomeContentHash is recomputed through the
92
- // same code as the vendor's, vendor biomes on the CATALOGUE's authority (biome-content.mjs).
93
- const schemas = await loadKernelDistributionSchemas();
94
- const distributionLock = await sealLock({
95
- schemas,
96
- unsealed: resolved.distributionLock,
97
- imageLock: resolved.imageLock,
98
- locate: sourceTreeLocator(externalSourceTrees(resolved.distributionLock)),
99
- vendoredCommit: (repo) => {
100
- throw new Error(`customer biome source "${repo}" is first-party to no checkout; vendor biomes are taken from the catalogue`);
101
- },
102
- catalogue,
103
- catalogueDigest,
104
- storePublisherAnchorKeys: resolved.storePublisherAnchorKeys,
105
- });
106
- return { ...resolved, distributionLock, pins: frozen };
107
- }
108
-
109
- /**
110
- * What a develop verb of a customer edition derives from its definition file and its `--pins` file with no
111
- * network: the id (the definition's), the pinned develop catalogue reference and digest, and the frozen
112
- * provenance pins (the customer's `sources/<name>` commits plus that digest).
113
- */
114
- export function customerDevelopFrame({ definitionFile, pinsFile }) {
115
- const rawDefinition = readStrictJsonFile(definitionFile);
116
- assertCanonicalDistributionId(rawDefinition?.id);
117
- const catalogueRef = readCataloguePin({ definitionFile, channel: EditionChannel.Develop });
118
- const { digest } = parseCatalogueRef(catalogueRef);
119
- const pins = customerDevelopPins({ pins: readStrictJsonFile(pinsFile), declaredSourceNames: declaredSourceNamesOf(rawDefinition), catalogueDigest: digest });
120
- return { id: rawDefinition.id, rawDefinition, catalogueRef, catalogueDigest: digest, pins };
121
- }
122
-
123
- /**
124
- * The own image rows of an existing customer develop artifact are the rows this `--source-image` map composes:
125
- * every subject owned by a biome drawn from a declared source is `<registry>/<prefix><subject>` mapped to its
126
- * develop repository. The tag covers the map, but a tag is mutable registry state, so the pulled bytes are
127
- * compared too. Throws naming the first row that differs.
128
- */
129
- export function assertOwnImageRowsMatch({ artifactDir, sourceImages }) {
130
- const lock = readStrictJsonFile(join(artifactDir, EditionLockFile.Distribution));
131
- const imageLock = readStrictJsonFile(join(artifactDir, EditionLockFile.Images));
132
- const sourceOf = new Map((lock.biomes ?? []).filter((biome) => biome.source !== undefined).map((biome) => [biome.id, biome.source.name]));
133
- for (const subject of imageLock.subjects ?? []) {
134
- const biomeId = (subject.owners ?? []).find((owner) => sourceOf.has(owner.biomeId))?.biomeId;
135
- if (biomeId === undefined) {continue;}
136
- const name = sourceOf.get(biomeId);
137
- const image = sourceImages.get(name);
138
- if (image === undefined) {
139
- throw new Error(`the existing artifact draws biome "${biomeId}" from source "${name}", which has no --source-image; refusing to reuse it`);
140
- }
141
- const expected = developImageRef(imageRefForSubject(subject.id, image.imageRegistry, image.imageNamePrefix));
142
- if (subject.imageRef !== expected) {
143
- throw new Error(
144
- `the existing artifact's own image row ${subject.kind}:${subject.id} is ${subject.imageRef}, not ${expected} ` +
145
- '(the --source-image map differs); refusing to reuse it',
146
- );
147
- }
148
- }
149
- }