@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
package/edition/oras.mjs CHANGED
@@ -255,7 +255,7 @@ export function prepareTrustedOras({ env = process.env, trustedDigests, notice =
255
255
  /** The default runner: the trusted oras of the configured data root, under the scrubbed environment. */
256
256
  function defaultRunner(trust = configuredOrasTrust) {
257
257
  let prepared;
258
- return async (args, { stdin } = {}) => {
258
+ return async (args, { stdin, cwd } = {}) => {
259
259
  if (prepared === undefined) {
260
260
  prepared = prepareTrustedOras({ trustedDigests: trust() });
261
261
  process.once('exit', prepared.dispose);
@@ -263,6 +263,7 @@ function defaultRunner(trust = configuredOrasTrust) {
263
263
  try {
264
264
  return await runExecutable(prepared.path(), args, {
265
265
  input: stdin,
266
+ ...(cwd === undefined ? {} : { cwd }),
266
267
  env: orasEnvironment(prepared.home),
267
268
  timeoutMs: ORAS_RUN_TIMEOUT_MS,
268
269
  maxBufferBytes: ORAS_MAX_BUFFER_BYTES,
@@ -364,7 +365,7 @@ export function disposeRegistrySessions() {
364
365
  }
365
366
 
366
367
  /**
367
- * A read-only OCI registry client bound to ONE registry authority and ONE
368
+ * An OCI registry client (reads, and `attach`) bound to ONE registry authority and ONE
368
369
  * resolved credential, doing every operation with the pinned oras.
369
370
  */
370
371
  export class OrasRegistry {
@@ -456,7 +457,7 @@ export class OrasRegistry {
456
457
  }
457
458
 
458
459
  /** One oras invocation under this registry's shared empty config; returns stdout bytes. */
459
- async #oras(subcommand, flags, ref) {
460
+ async #oras(subcommand, flags, ref, { cwd, operands = [] } = {}) {
460
461
  await this.#ensureRealmIsSameOrigin();
461
462
  const { configPath } = registrySession(this.authority);
462
463
  const args = [...subcommand, '--registry-config', configPath];
@@ -465,9 +466,9 @@ export class OrasRegistry {
465
466
  args.push('--username', this.#username, '--password-stdin');
466
467
  stdin = this.#token;
467
468
  }
468
- args.push(...flags, ref);
469
+ args.push(...flags, ref, ...operands);
469
470
  try {
470
- const out = await this.#run(args, { stdin });
471
+ const out = await this.#run(args, { stdin, ...(cwd === undefined ? {} : { cwd }) });
471
472
  return Buffer.isBuffer(out) ? out : Buffer.from(out ?? '');
472
473
  } catch (error) {
473
474
  throw this.#failure(ref, error);
@@ -559,6 +560,47 @@ export class OrasRegistry {
559
560
  return descriptor;
560
561
  }
561
562
 
563
+ /**
564
+ * ATTACH `files` (paths relative to `cwd`, each with its media type) as an artifact of `artifactType` that is a referrer
565
+ * of `subjectDigest`, under the same referrers form discover reads ({@link REFERRERS_DISTRIBUTION_SPEC}). Returns the
566
+ * digest of the referrer manifest oras reports. The only write this client does; the caller states every byte.
567
+ *
568
+ * @param {string} repository
569
+ * @param {string} subjectDigest
570
+ * @param {{ artifactType: string, files: {path: string, mediaType: string}[], annotations?: Record<string,string>, cwd: string }} attachment
571
+ */
572
+ async attach(repository, subjectDigest, { artifactType, files, annotations = {}, cwd }) {
573
+ if (!SHA256_DIGEST_PATTERN.test(subjectDigest)) {
574
+ throw new Error(`"${subjectDigest}" is not a canonical lowercase sha256 digest`);
575
+ }
576
+ if (typeof artifactType !== 'string' || artifactType.length === 0) {
577
+ throw new Error('an attach requires a non-empty artifact type');
578
+ }
579
+ if (!Array.isArray(files) || files.length === 0 || typeof cwd !== 'string' || !isAbsolute(cwd)) {
580
+ throw new Error('an attach requires at least one file and an absolute working directory');
581
+ }
582
+ const ref = `${this.#ref(repository)}@${subjectDigest}`;
583
+ const flags = ['--distribution-spec', REFERRERS_DISTRIBUTION_SPEC, '--artifact-type', artifactType];
584
+ for (const [key, value] of Object.entries(annotations)) {
585
+ flags.push('--annotation', `${key}=${value}`);
586
+ }
587
+ flags.push('--format', 'json');
588
+ const out = await this.#oras(['attach'], flags, ref, {
589
+ cwd,
590
+ operands: files.map((file) => `${file.path}:${file.mediaType}`),
591
+ });
592
+ let document;
593
+ try {
594
+ document = JSON.parse(out.toString('utf8'));
595
+ } catch (error) {
596
+ throw new Error(`${ref} attach did not report JSON: ${error.message}`);
597
+ }
598
+ if (!SHA256_DIGEST_PATTERN.test(document?.digest)) {
599
+ throw new Error(`${ref} attach reported no canonical referrer digest`);
600
+ }
601
+ return document.digest;
602
+ }
603
+
562
604
  /** Every tag of a repository; oras follows the registry's pagination. A failure throws, never an empty list. */
563
605
  async listTags(repository) {
564
606
  const ref = this.#ref(repository);
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ // The edition catalogue statement (E4): "this offering artifact's catalogue layer and lock layer are these digests, on this
3
+ // channel", signed by the vendor's release workflow and attached to the artifact as an `oras attach` referrer.
4
+ //
5
+ // xema edition evidence statement catalogue stage|accept …
6
+ //
7
+ // `stage` reads the artifact manifest by digest, derives the statement from its catalogue layer and lock layer and writes
8
+ // the canonical statement into a NEW staging directory, printing the signing plan; the caller signs it (`cosign
9
+ // attest-blob`); `accept` re-derives the statement from the same flags, refuses staged bytes that differ, verifies the
10
+ // signed bundle with cosign against the declared signer, seals the output (built from the re-derived files, never renamed
11
+ // from the staging) and ATTACHES statement.json and bundle.sigstore.json to the artifact digest as a referrer carrying the
12
+ // signing workflow's commit. A customer's pinned verifier reads exactly that referrer (catalogue-evidence.mjs).
13
+ // This module never signs and never requests an OIDC token.
14
+
15
+ import { existsSync, mkdirSync, rmSync } from 'node:fs';
16
+ import { isAbsolute, resolve } from 'node:path';
17
+
18
+ import {
19
+ CATALOGUE_EVIDENCE_BUNDLE_TITLE,
20
+ CATALOGUE_EVIDENCE_STATEMENT_MEDIA_TYPE,
21
+ CATALOGUE_EVIDENCE_STATEMENT_TITLE,
22
+ CATALOGUE_EVIDENCE_WORKFLOW_SHA_ANNOTATION,
23
+ } from './catalogue-evidence.mjs';
24
+ import { layerTitled, parseCatalogueRef } from './catalogue-fetch.mjs';
25
+ import { CATALOGUE_FILE } from './catalogue.mjs';
26
+ import {
27
+ acceptSignedEvidenceStatement,
28
+ assertPinnedCosignVersion,
29
+ evidenceChannelOfSigner,
30
+ loadPinnedTrustPolicy,
31
+ parseEvidenceCliFlags,
32
+ stageEvidenceStatement,
33
+ } from './distribution-evidence-set.mjs';
34
+ import { EDITION_ARTIFACT_TYPE, EditionLockFile } from './edition-constants.mjs';
35
+ import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot, isEntryModule } from './edition-workspace.mjs';
36
+ import { serializeJson } from './lib.mjs';
37
+ import { EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE, createRegistryClientResolver } from './oci-image-source-evidence.mjs';
38
+ import { githubWorkflowSigner } from './produce-distribution-completeness-evidence.mjs';
39
+ import {
40
+ BUNDLE_MEDIA_TYPE,
41
+ assertGitHubProducerContext,
42
+ createCosignVerifier,
43
+ loadProducerKernelSchemas,
44
+ } from './produce-image-source-evidence.mjs';
45
+ import { assertStagedExactly, discardStaging, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
46
+
47
+ const STATEMENT_TYPE = 'https://in-toto.io/Statement/v1';
48
+ const hexOf = (digest) => digest.slice('sha256:'.length);
49
+
50
+ const SHARED_FLAGS = Object.freeze([
51
+ 'artifact',
52
+ 'offering',
53
+ 'staging',
54
+ 'signer-repository',
55
+ 'signer-ref',
56
+ 'signer-workflow-path',
57
+ 'github-repository',
58
+ 'github-ref',
59
+ 'github-sha',
60
+ 'github-workflow-ref',
61
+ ]);
62
+
63
+ /**
64
+ * The E4 statement of the offering artifact `artifact` (`oci://<repository>@sha256:<d>`), derived from the manifest the
65
+ * registry serves for that digest: its catalogue layer and lock layer, exactly as a customer's `fetchCatalogue` reads them.
66
+ * The channel is the signer's, and must be the repository's.
67
+ */
68
+ export async function deriveEditionCatalogueStatement({ artifact, offeringId, signer, schemas, client }) {
69
+ const { repository, digest, edition } = parseCatalogueRef(artifact);
70
+ const channel = evidenceChannelOfSigner(signer, schemas);
71
+ if (edition.distributionId !== offeringId) {
72
+ throw new Error(`--offering ${offeringId} is not the offering of ${artifact} (its repository is the offering ${edition.distributionId})`);
73
+ }
74
+ if (edition.channel !== channel) {
75
+ throw new Error(`${artifact} is in a ${edition.channel} repository, but the signer signs for the ${channel} channel`);
76
+ }
77
+ const manifest = await client.getManifestByDigest(repository, digest);
78
+ if (manifest.isIndex || manifest.document.artifactType !== EDITION_ARTIFACT_TYPE) {
79
+ throw new Error(`${artifact} is not an edition artifact (artifactType ${JSON.stringify(manifest.document.artifactType)}, expected ${EDITION_ARTIFACT_TYPE})`);
80
+ }
81
+ const catalogueLayer = layerTitled(manifest, CATALOGUE_FILE, artifact);
82
+ const lockLayer = layerTitled(manifest, EditionLockFile.Distribution, artifact);
83
+ const statement = {
84
+ _type: STATEMENT_TYPE,
85
+ subject: [{ name: `xema-edition-${offeringId}`, digest: { sha256: hexOf(digest) } }],
86
+ predicateType: schemas.EDITION_CATALOGUE_PREDICATE_TYPE,
87
+ predicate: {
88
+ schemaVersion: 1,
89
+ offeringId,
90
+ channel,
91
+ catalogueDigest: catalogueLayer.digest,
92
+ lockDigest: hexOf(lockLayer.digest),
93
+ },
94
+ };
95
+ schemas.parseEditionCatalogueStatement(statement);
96
+ return { statement, channel, repository, subjectDigest: digest };
97
+ }
98
+
99
+ function signingOf({ derived, options }) {
100
+ return {
101
+ statement: derived.statement,
102
+ root: options.stagingDirectory,
103
+ statementPath: CATALOGUE_EVIDENCE_STATEMENT_TITLE,
104
+ bundlePath: CATALOGUE_EVIDENCE_BUNDLE_TITLE,
105
+ signer: options.signer,
106
+ certificateWorkflowSha: options.signingWorkflowSha,
107
+ predicateType: options.schemas.EDITION_CATALOGUE_PREDICATE_TYPE,
108
+ };
109
+ }
110
+
111
+ /** Stage the catalogue statement of an artifact for signing. Returns the signing plan and the statement. */
112
+ export async function stageEditionCatalogueEvidence(options) {
113
+ const { stagingDirectory } = options;
114
+ if (typeof stagingDirectory !== 'string' || !isAbsolute(stagingDirectory)) {
115
+ throw new Error('the catalogue staging directory must be absolute');
116
+ }
117
+ if (existsSync(stagingDirectory)) {
118
+ throw new Error(`catalogue staging directory already exists: ${stagingDirectory}`);
119
+ }
120
+ assertGitHubProducerContext({ signer: options.signer, producerWorkflowSha: options.signingWorkflowSha }, options.githubContext);
121
+ const derived = await deriveEditionCatalogueStatement(options);
122
+ mkdirSync(stagingDirectory, { recursive: true, mode: 0o700 });
123
+ try {
124
+ const plan = stageEvidenceStatement(signingOf({ derived, options }));
125
+ return {
126
+ plan: { ...plan, kind: 'edition-catalogue', offeringId: options.offeringId, channel: derived.channel, subjectDigest: derived.subjectDigest },
127
+ statement: derived.statement,
128
+ };
129
+ } catch (error) {
130
+ rmSync(stagingDirectory, { recursive: true, force: true });
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Accept the signed catalogue bundle, seal `outputDirectory` and attach its two files to the artifact digest as an
137
+ * `EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE` referrer. The output is BUILT from the re-derived statement and the
138
+ * canonicalized bundle, never renamed from the staging, which must hold exactly what stage recorded and does not outlive
139
+ * the accept.
140
+ */
141
+ export async function acceptEditionCatalogueEvidence(options) {
142
+ const { outputDirectory, stagingDirectory, readCosignVersion, runCosign, client } = options;
143
+ if (
144
+ typeof outputDirectory !== 'string' ||
145
+ !isAbsolute(outputDirectory) ||
146
+ typeof stagingDirectory !== 'string' ||
147
+ !isAbsolute(stagingDirectory)
148
+ ) {
149
+ throw new Error('the catalogue output and staging directories must be absolute');
150
+ }
151
+ if (typeof readCosignVersion !== 'function' || typeof runCosign !== 'function') {
152
+ throw new Error('cosign seams are required');
153
+ }
154
+ const sealing = reserveSealing(outputDirectory, 'catalogue');
155
+ let derived;
156
+ try {
157
+ assertGitHubProducerContext({ signer: options.signer, producerWorkflowSha: options.signingWorkflowSha }, options.githubContext);
158
+ assertPinnedCosignVersion(readCosignVersion(), options.policy?.cosignVersion);
159
+ derived = await deriveEditionCatalogueStatement(options);
160
+ } catch (error) {
161
+ discardStaging(stagingDirectory);
162
+ throw error;
163
+ }
164
+ const descriptor = sealAccepted({
165
+ stagingDirectory,
166
+ sealing,
167
+ outputDirectory,
168
+ build: () => {
169
+ assertStagedExactly(stagingDirectory, [CATALOGUE_EVIDENCE_STATEMENT_TITLE, CATALOGUE_EVIDENCE_BUNDLE_TITLE]);
170
+ return acceptSignedEvidenceStatement({ ...signingOf({ derived, options }), sealingRoot: sealing, runCosign });
171
+ },
172
+ });
173
+ const referrerDigest = await client.attach(derived.repository, derived.subjectDigest, {
174
+ artifactType: EDITION_CATALOGUE_EVIDENCE_ARTIFACT_TYPE,
175
+ files: [
176
+ { path: descriptor.statementPath, mediaType: CATALOGUE_EVIDENCE_STATEMENT_MEDIA_TYPE },
177
+ { path: descriptor.bundlePath, mediaType: BUNDLE_MEDIA_TYPE },
178
+ ],
179
+ annotations: { [CATALOGUE_EVIDENCE_WORKFLOW_SHA_ANNOTATION]: options.signingWorkflowSha },
180
+ cwd: outputDirectory,
181
+ });
182
+ return {
183
+ outputDirectory,
184
+ statement: derived.statement,
185
+ channel: derived.channel,
186
+ subjectDigest: derived.subjectDigest,
187
+ referrerDigest,
188
+ descriptor,
189
+ };
190
+ }
191
+
192
+ async function main() {
193
+ const [phase, ...args] = configureEditionWorkspaceFromArgv(process.argv.slice(2), [WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
194
+ if (phase !== 'stage' && phase !== 'accept') {
195
+ throw new Error('usage: xema edition evidence statement catalogue <stage|accept> …');
196
+ }
197
+ const flags = parseEvidenceCliFlags(args, {
198
+ single: phase === 'stage' ? SHARED_FLAGS : [...SHARED_FLAGS, 'output', 'cosign'],
199
+ repeated: [],
200
+ });
201
+ const { schemas } = await loadProducerKernelSchemas({ dataRoot: dataRoot() });
202
+ const artifact = flags.require('artifact');
203
+ const githubSha = flags.require('github-sha');
204
+ const { authority } = parseCatalogueRef(artifact);
205
+ const options = {
206
+ artifact,
207
+ offeringId: flags.require('offering'),
208
+ stagingDirectory: resolve(flags.require('staging')),
209
+ schemas,
210
+ policy: loadPinnedTrustPolicy({ dataRoot: dataRoot(), schemas }),
211
+ client: createRegistryClientResolver({ dataRoot: dataRoot() })(authority),
212
+ signer: githubWorkflowSigner({
213
+ repository: flags.require('signer-repository'),
214
+ ref: flags.require('signer-ref'),
215
+ path: flags.require('signer-workflow-path'),
216
+ }),
217
+ githubContext: {
218
+ repository: flags.require('github-repository'),
219
+ ref: flags.require('github-ref'),
220
+ sha: githubSha,
221
+ workflowRef: flags.require('github-workflow-ref'),
222
+ },
223
+ signingWorkflowSha: githubSha,
224
+ };
225
+ if (phase === 'stage') {
226
+ process.stdout.write(serializeJson((await stageEditionCatalogueEvidence(options)).plan));
227
+ return;
228
+ }
229
+ const result = await acceptEditionCatalogueEvidence({
230
+ ...options,
231
+ outputDirectory: resolve(flags.require('output')),
232
+ ...createCosignVerifier(resolve(flags.require('cosign'))),
233
+ });
234
+ process.stdout.write(
235
+ serializeJson({
236
+ schemaVersion: 1,
237
+ kind: 'edition-catalogue',
238
+ offeringId: options.offeringId,
239
+ channel: result.channel,
240
+ subjectDigest: result.subjectDigest,
241
+ referrerDigest: result.referrerDigest,
242
+ evidence: result.descriptor,
243
+ }),
244
+ );
245
+ }
246
+
247
+ if (isEntryModule(import.meta.url)) {
248
+ main().catch((error) => {
249
+ console.error(`[edition-catalogue] ${error.message}`);
250
+ process.exitCode = 1;
251
+ });
252
+ }
@@ -36,6 +36,8 @@ import {
36
36
  } from './edition-workspace.mjs';
37
37
  import { COMMIT_PATTERN, GITHUB_REF_PATTERN, GITHUB_REPOSITORY_PATTERN, SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
38
38
  import {
39
+ KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE,
40
+ KernelContractAuthorityPinError,
39
41
  RUNTIME_IMAGE_SOURCE_INDEX_FILE,
40
42
  loadPinnedDeclaredTrustPolicy,
41
43
  readEvidenceTrustPin,
@@ -50,9 +52,9 @@ import {
50
52
  import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
51
53
  import { serializeJson } from './lib.mjs';
52
54
  import { assertStagedExactly, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
55
+ import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
53
56
 
54
- export const BUNDLE_MEDIA_TYPE =
55
- 'application/vnd.dev.sigstore.bundle.v0.3+json';
57
+ export { BUNDLE_MEDIA_TYPE };
56
58
  const STAGED_STATEMENTS_DIRECTORY = 'statements';
57
59
  const STAGED_BUNDLES_DIRECTORY = 'bundles';
58
60
 
@@ -135,6 +137,13 @@ export async function loadProducerKernelSchemas({
135
137
  }
136
138
  const expected = readRawContractAuthority(root);
137
139
  const pinned = readEvidenceTrustPin({ dataRoot: root, declaration }).kernelContractAuthority;
140
+ if (pinned === null) {
141
+ throw new KernelContractAuthorityPinError(
142
+ KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE,
143
+ `${RUNTIME_IMAGE_SOURCE_INDEX_FILE} at ${root} states kernel contract authority commit ${expected.sourceCommit} ` +
144
+ 'but the evidence-trust pin records none; re-pin with `xema edition repin-evidence-trust`',
145
+ );
146
+ }
138
147
  if (expected.sourceCommit !== pinned.sourceCommit) {
139
148
  throw new Error(
140
149
  `${RUNTIME_IMAGE_SOURCE_INDEX_FILE} at ${root} states kernel contract authority commit ` +
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // release-edition — the customer RELEASE edition's command line: `xema edition release <verb> --definition <file>`.
3
+ //
4
+ // It is `xema edition develop <verb> --definition <file>` on the release channel: the same parser, the same frame
5
+ // and the same verbs (customer-edition-verbs.mjs); only the inputs differ. The id is the definition's, and the
6
+ // catalogue is the one pinned for `release` in catalogue.pin.json beside the definition.
7
+ //
8
+ // freeze-sources --definition <file> --out <file>
9
+ // one `sources/<name>` pin per source the definition declares, each at the source's DECLARED ref (a branch,
10
+ // or a tag taken at its peeled commit); a ref that is both a branch and a tag, or missing, is refused.
11
+ // Written exclusively (wx).
12
+ // target --definition <file> --pins <file> --registry <root> --engine-image <repository>@sha256:<digest>
13
+ // [--source-image <source>=<registry>/<prefix>]...
14
+ // print <root>/<release segment>/xema-edition-<id>:<tag>, the tag being snapshotEditionTag of the CLI
15
+ // version, the engine image, the pins and the source-image map (as on develop).
16
+ // check-existing --definition <file> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
17
+ // [--catalogue-username <name> --catalogue-token-env <VAR>] [--source-image <source>=<registry>/<prefix>]...
18
+ // refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
19
+ // resolve --definition <file> --pins <file> --out <dir> --registry <root> --engine-image <repository>@sha256:<digest>
20
+ // [--source-image <source>=<registry>/<prefix>]... [--catalogue-username <name> --catalogue-token-env <VAR>] [--local]
21
+ // resolve the edition against the pinned release catalogue, seal the customer's own image rows at the digests
22
+ // the registry serves for `<repository>:<contentHash>` (it never builds an image), and write the descriptor and
23
+ // the four locks to <dir>.
24
+ //
25
+ // `resolve` and `check-existing` read the release catalogue only after its signed E4 evidence verified against the
26
+ // CUSTOMER's own evidence trust policy (catalogue-evidence.mjs), and require that policy armed on the release channel.
27
+ // Until the E4 referrer layout lands, no catalogue verifies and both refuse. Takes the roots of the
28
+ // develop customer form (--data-root, --editions-root, --sources-root, --cache-dir per verb), never the kernel.
29
+
30
+ import { customerRootsOf, parseEditionVerbArgs, runCustomerVerb } from './customer-edition-verbs.mjs';
31
+ import { EditionChannel } from './edition-constants.mjs';
32
+ import { configureEditionWorkspaceFromProcess, isEntryModule } from './edition-workspace.mjs';
33
+
34
+ if (isEntryModule(import.meta.url)) {
35
+ const [command] = process.argv.slice(2);
36
+ try {
37
+ configureEditionWorkspaceFromProcess(customerRootsOf(command), { refuseOthers: true });
38
+ } catch (error) {
39
+ console.error(`[release-edition] ${error.message}`);
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ async function run(argv, env) {
45
+ const { command, options } = parseEditionVerbArgs(EditionChannel.Release, argv);
46
+ await runCustomerVerb({ channel: EditionChannel.Release, command, options, env });
47
+ }
48
+
49
+ if (isEntryModule(import.meta.url)) {
50
+ run(process.argv.slice(2), process.env).catch((error) => {
51
+ console.error(`[release-edition] ${error.message}`);
52
+ process.exit(1);
53
+ });
54
+ }
@@ -2,8 +2,9 @@
2
2
  // `xema edition repin-evidence-trust` - write the data root's evidence trust
3
3
  // pin: the digest of the declared policy, the digests of THIS CLI's built
4
4
  // verifier bundle and provenance, the kernel contract authority the data
5
- // root's runtime image source index states, and the oras binaries that may
6
- // read a registry. The result is a data-root change to review; nothing here is
5
+ // root's runtime image source index states (a customer data root has no such
6
+ // index: the pin then records none and pins only what exists), and the oras
7
+ // binaries that may read a registry. The result is a data-root change to review; nothing here is
7
8
  // generated into the library.
8
9
  //
9
10
  // [--trusted-oras-digest sha256:<64 hex>]… the oras binaries to trust. Without
@@ -24,6 +25,7 @@ import {
24
25
  } from './edition-workspace.mjs';
25
26
  import {
26
27
  loadDeclaredTrustPolicy,
28
+ hasRuntimeImageSourceIndex,
27
29
  readEvidenceTrustPin,
28
30
  readStatedKernelContractAuthority,
29
31
  requireOrasDigests,
@@ -62,7 +64,7 @@ export async function repinEvidenceTrust({
62
64
  declaration,
63
65
  policyDigest,
64
66
  verifier,
65
- kernelContractAuthority: readStatedKernelContractAuthority({ dataRoot: root, schemas }),
67
+ kernelContractAuthority: hasRuntimeImageSourceIndex(root) ? readStatedKernelContractAuthority({ dataRoot: root, schemas }) : null,
66
68
  trustedOrasBinaryDigests: orasDigests,
67
69
  });
68
70
  }
@@ -40,7 +40,10 @@ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
40
40
  import { assertBuildLedgerComplete } from './build-ledger-completeness.mjs';
41
41
  import { BUILD_LEDGER_FILE, readBuildLedger } from './build-ledger.mjs';
42
42
  import { CROSS_REPO_BUILD_INPUTS_PATH, crossRepoInputKey, resolveCrossRepoVendoredInputs } from './cross-repo-build-inputs.mjs';
43
- import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-components.mjs';
43
+ import {
44
+ resolveDispatchedRuntimeComponents,
45
+ UndeclaredSupplementalRuntimeComponentError,
46
+ } from './dispatched-runtime-components.mjs';
44
47
  import {
45
48
  assertCanonicalDistributionId,
46
49
  distributionOutputPath,
@@ -681,6 +684,28 @@ export function resolveImageStructure(
681
684
  dispatchers,
682
685
  biomeIndex,
683
686
  )) {
687
+ if (dispatched.supplementalRuntimeComponentId !== undefined) {
688
+ // THE REFUSAL: a dispatched supplemental runtime component must be one the
689
+ // runtime image source index declares as a runtime-component subject.
690
+ // Every image lock is produced here, so this is the one place it is checked.
691
+ const declared = runtimeSourceIndex.supplementalSubjects.find(
692
+ (subject) =>
693
+ subject.kind === enums.RuntimeImageSubjectKind.RuntimeComponent &&
694
+ subject.id === dispatched.supplementalRuntimeComponentId,
695
+ );
696
+ if (declared === undefined) {
697
+ throw new UndeclaredSupplementalRuntimeComponentError(
698
+ dispatched.supplementalRuntimeComponentId,
699
+ dispatched.consumers,
700
+ `distribution "${lock.distributionId}"`,
701
+ );
702
+ }
703
+ addSupplementalSubject(
704
+ declared,
705
+ dispatched.consumers.map((biomeId) => ({ kind: enums.RuntimeImageOwnerKind.Biome, biomeId })),
706
+ );
707
+ continue;
708
+ }
684
709
  const { target } = dispatched;
685
710
  addSubject(subjects, {
686
711
  kind: enums.RuntimeImageSubjectKind.RuntimeComponent,
@@ -717,6 +742,16 @@ export function resolveImageStructure(
717
742
  const boundSubjects = new Set(
718
743
  runtimeSourceIndex.platformServiceBindings.map((binding) => bindingKey(binding)),
719
744
  );
745
+ // A biome's `supplemental:<id>` dispatch edge also carries the subject: the
746
+ // catalogue check reads the whole first-party index, not this roster, so a
747
+ // subject reached only by a dispatch is not "unreachable" in any edition.
748
+ const dispatchedSubjects = new Set(
749
+ biomeIndex
750
+ .filter((entry) => entry.source === undefined)
751
+ .flatMap((entry) => entry.dispatchesRuntimeComponents ?? [])
752
+ .filter((edge) => edge.supplementalRuntimeComponentId !== undefined)
753
+ .map((edge) => `${enums.RuntimeImageSubjectKind.RuntimeComponent}\0${edge.supplementalRuntimeComponentId}`),
754
+ );
720
755
  const launchers = new Map(
721
756
  supplementalSources
722
757
  .filter((source) => source.launchedBy !== undefined)
@@ -733,11 +768,11 @@ export function resolveImageStructure(
733
768
  for (const [key, supplemental] of supplementalSubjects) {
734
769
  const launcher = launchers.get(key);
735
770
  if (launcher === undefined) {
736
- if (!boundSubjects.has(key)) {
771
+ if (!boundSubjects.has(key) && !dispatchedSubjects.has(key)) {
737
772
  throw new Error(
738
- `supplemental subject "${supplemental.kind}:${supplemental.id}" has neither a launchedBy nor a ` +
739
- 'platform-service binding, so no edition could ever carry it — declare what launches it ' +
740
- '(runtimeImageSources.supplementalSources[].launchedBy) or bind it to a platform service',
773
+ `supplemental subject "${supplemental.kind}:${supplemental.id}" has neither a launchedBy, a ` +
774
+ 'platform-service binding nor a biome dispatching it, so no edition could ever carry it — declare what launches it ' +
775
+ '(runtimeImageSources.supplementalSources[].launchedBy), bind it to a platform service or dispatch it from a biome',
741
776
  );
742
777
  }
743
778
  continue;
@@ -0,0 +1,4 @@
1
+ // The one Sigstore bundle media type this plane stores. Its own module because the evidence verifier bundle needs it and
2
+ // must not import a producer entry (every module of a bundle shares one `import.meta.url`, so a producer's entry guard
3
+ // would run inside the verifier).
4
+ export const BUNDLE_MEDIA_TYPE = 'application/vnd.dev.sigstore.bundle.v0.3+json';
@@ -21,6 +21,11 @@
21
21
  // trustedGitBinaryDigests -> same, for the git binary that pins the
22
22
  // graded checkouts.
23
23
  //
24
+ // THE REQUIRED RELEASE ROWS ARE ONE RULE: the edition-catalogue (E4) predicate iff the edition consumes a catalogue
25
+ // (`consumesCatalogue`: a customer edition does, a vendor edition does not), plus the image producer, distribution
26
+ // completeness and delivery entitlement predicates. The same function judges the data root before the catalogue's
27
+ // kernel is adopted (inside the pinned verifier bundle, with its own embedded kernel) and after it (descriptorEvidenceTrust).
28
+ //
24
29
  // The three predicate types are checked separately because a policy can be
25
30
  // non-empty and still be unarmed for a stage: the verifier resolves candidates
26
31
  // per predicate type, so authorizing only the producer identity leaves the
@@ -60,7 +65,7 @@ export function assertArmingChannel(schemas, channel) {
60
65
  * field and what makes verification impossible, so the failure is actionable
61
66
  * without reading the verifier.
62
67
  */
63
- export function assessTrustPolicyArming(policy, schemas, channel) {
68
+ export function assessTrustPolicyArming(policy, schemas, channel, { consumesCatalogue = false } = {}) {
64
69
  assertArmingChannel(schemas, channel);
65
70
  const unmet = [];
66
71
  const stages = [
@@ -68,6 +73,14 @@ export function assessTrustPolicyArming(policy, schemas, channel) {
68
73
  ['distribution-completeness', schemas.DISTRIBUTION_IMAGES_PREDICATE_TYPE],
69
74
  ['delivery-entitlement', schemas.IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE],
70
75
  ];
76
+ if (consumesCatalogue) {
77
+ // A kernel that states no E4 predicate cannot arm a catalogue consumer: an unmet entry, never a skip.
78
+ if (typeof schemas.EDITION_CATALOGUE_PREDICATE_TYPE !== 'string') {
79
+ unmet.push('this kernel states no edition-catalogue predicate (EDITION_CATALOGUE_PREDICATE_TYPE), so a catalogue consumer cannot be armed against it.');
80
+ } else {
81
+ stages.push(['edition-catalogue', schemas.EDITION_CATALOGUE_PREDICATE_TYPE]);
82
+ }
83
+ }
71
84
  const onChannel = policy.policies.filter((candidate) => candidate.channel === channel);
72
85
  const authorized = stages.filter(([, predicateType]) => onChannel.some((candidate) => candidate.predicateTypes.includes(predicateType)));
73
86