@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.
Files changed (219) hide show
  1. package/LICENSE +175 -0
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +47 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/edition.d.ts +4 -0
  7. package/dist/commands/edition.d.ts.map +1 -0
  8. package/dist/commands/edition.js +58 -0
  9. package/dist/commands/edition.js.map +1 -0
  10. package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
  11. package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
  12. package/dist/lib/exit-codes.d.ts +6 -0
  13. package/dist/lib/exit-codes.d.ts.map +1 -0
  14. package/dist/lib/exit-codes.js +21 -0
  15. package/dist/lib/exit-codes.js.map +1 -0
  16. package/dist/runtime/edition/run-entry.d.ts +10 -0
  17. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  18. package/dist/runtime/edition/run-entry.js +49 -0
  19. package/dist/runtime/edition/run-entry.js.map +1 -0
  20. package/dist/runtime/edition/verb-map.d.ts +70 -0
  21. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  22. package/dist/runtime/edition/verb-map.js +166 -0
  23. package/dist/runtime/edition/verb-map.js.map +1 -0
  24. package/edition/README.md +291 -0
  25. package/edition/adapter-bundled-versions.mjs +128 -0
  26. package/edition/adapter-host-versions.mjs +76 -0
  27. package/edition/assemble-producer-input.mjs +1423 -0
  28. package/edition/assemble-third-party-input.mjs +348 -0
  29. package/edition/authored-source-check.mjs +141 -0
  30. package/edition/authority-binding.mjs +23 -0
  31. package/edition/authorization-base.mjs +357 -0
  32. package/edition/biome-content.mjs +327 -0
  33. package/edition/build-biome-index.mjs +180 -0
  34. package/edition/build-ledger-completeness.mjs +67 -0
  35. package/edition/build-ledger.mjs +107 -0
  36. package/edition/build-platform-service-index.mjs +292 -0
  37. package/edition/build-runtime-image-source-index.mjs +1197 -0
  38. package/edition/built-verifier.mjs +55 -0
  39. package/edition/candidate-edition.mjs +735 -0
  40. package/edition/canonical-evidence-json.mjs +82 -0
  41. package/edition/canonical-repository-url.mjs +61 -0
  42. package/edition/catalogue-edition.mjs +49 -0
  43. package/edition/catalogue-evidence.mjs +228 -0
  44. package/edition/catalogue-fetch.mjs +124 -0
  45. package/edition/catalogue-lock-field.mjs +41 -0
  46. package/edition/catalogue-pin.mjs +71 -0
  47. package/edition/catalogue.mjs +237 -0
  48. package/edition/chart-set-source-commit.mjs +97 -0
  49. package/edition/check-committed-image-evidence.mjs +412 -0
  50. package/edition/checkout-indexed-source-repositories.mjs +283 -0
  51. package/edition/collected-refusals.mjs +59 -0
  52. package/edition/comment-blanker.mjs +75 -0
  53. package/edition/connector-source-authority.mjs +60 -0
  54. package/edition/contract-authority.mjs +122 -0
  55. package/edition/cosign-certificate-constraints.mjs +50 -0
  56. package/edition/cross-repo-build-inputs.mjs +158 -0
  57. package/edition/customer-definition.mjs +72 -0
  58. package/edition/customer-edition-verbs.mjs +610 -0
  59. package/edition/customer-flags.mjs +15 -0
  60. package/edition/definition-checkout.mjs +96 -0
  61. package/edition/develop-build-record.mjs +122 -0
  62. package/edition/develop-catalogue-free-resolve.mjs +282 -0
  63. package/edition/develop-catalogue-free-vendor.mjs +118 -0
  64. package/edition/develop-channel-evidence.mjs +23 -0
  65. package/edition/develop-channel-record.mjs +49 -0
  66. package/edition/develop-channel-satellites.mjs +65 -0
  67. package/edition/develop-chart-roster.mjs +96 -0
  68. package/edition/develop-chart-set.mjs +147 -0
  69. package/edition/develop-current-resolution.mjs +29 -0
  70. package/edition/develop-edition-invariants.mjs +142 -0
  71. package/edition/develop-edition-resolve.mjs +250 -0
  72. package/edition/develop-edition.mjs +332 -0
  73. package/edition/develop-engine-image.mjs +97 -0
  74. package/edition/develop-freeze.mjs +304 -0
  75. package/edition/develop-git-bundles.mjs +296 -0
  76. package/edition/develop-graph.mjs +34 -0
  77. package/edition/develop-image-coordinate.mjs +85 -0
  78. package/edition/develop-image-probe.mjs +122 -0
  79. package/edition/develop-image-record.mjs +260 -0
  80. package/edition/develop-resolve-report.mjs +159 -0
  81. package/edition/develop-select-records.mjs +90 -0
  82. package/edition/develop-selection-line.mjs +29 -0
  83. package/edition/develop-selection-pins.mjs +36 -0
  84. package/edition/develop-snapshot-inputs.mjs +112 -0
  85. package/edition/develop-store-root.mjs +20 -0
  86. package/edition/dispatched-runtime-components.mjs +172 -0
  87. package/edition/dist-check.mjs +1174 -0
  88. package/edition/distribution-evidence-set.mjs +1064 -0
  89. package/edition/distribution-paths.mjs +191 -0
  90. package/edition/edition-artifact.mjs +529 -0
  91. package/edition/edition-biome-manifests.mjs +85 -0
  92. package/edition/edition-ceiling.mjs +182 -0
  93. package/edition/edition-channel.mjs +27 -0
  94. package/edition/edition-chart-set.mjs +104 -0
  95. package/edition/edition-config.mjs +374 -0
  96. package/edition/edition-constants.mjs +110 -0
  97. package/edition/edition-contract-parse.mjs +179 -0
  98. package/edition/edition-contract.json +100 -0
  99. package/edition/edition-image-seal.mjs +115 -0
  100. package/edition/edition-kind.mjs +10 -0
  101. package/edition/edition-list.mjs +35 -0
  102. package/edition/edition-locks.mjs +78 -0
  103. package/edition/edition-pin-verbs.mjs +183 -0
  104. package/edition/edition-pin.mjs +17 -0
  105. package/edition/edition-pinned-inputs.mjs +116 -0
  106. package/edition/edition-protocol.mjs +285 -0
  107. package/edition/edition-publisher.mjs +21 -0
  108. package/edition/edition-seal.mjs +257 -0
  109. package/edition/edition-source-pins.mjs +211 -0
  110. package/edition/edition-unpin.mjs +16 -0
  111. package/edition/edition-workspace.mjs +205 -0
  112. package/edition/evidence-checkout.mjs +439 -0
  113. package/edition/evidence-grammars.mjs +18 -0
  114. package/edition/evidence-input-mode.mjs +136 -0
  115. package/edition/evidence-trust-pin.mjs +392 -0
  116. package/edition/evidence-trust-policy.mjs +374 -0
  117. package/edition/external-source-walk.mjs +486 -0
  118. package/edition/first-party-source-ids.mjs +62 -0
  119. package/edition/fleet-coordinate-finality.mjs +363 -0
  120. package/edition/fulcio-certificate-extensions.mjs +278 -0
  121. package/edition/git-json.mjs +54 -0
  122. package/edition/git-source-auth.mjs +131 -0
  123. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  124. package/edition/image-evidence-replay-state.mjs +242 -0
  125. package/edition/image-evidence.mjs +1328 -0
  126. package/edition/image-lock-service-rows.mjs +26 -0
  127. package/edition/image-subject-identity.mjs +19 -0
  128. package/edition/images-from-record.mjs +216 -0
  129. package/edition/indexes-at-pins.mjs +13 -0
  130. package/edition/infra-images-lock.mjs +105 -0
  131. package/edition/kernel-capability-domain.mjs +30 -0
  132. package/edition/kernel-contract-authority.mjs +526 -0
  133. package/edition/kernel-contract-source-path-set.mjs +96 -0
  134. package/edition/kernel-package.mjs +237 -0
  135. package/edition/kernel-pin-source.mjs +63 -0
  136. package/edition/kernel-protocol-choice.mjs +239 -0
  137. package/edition/kernel-signer-kind.mjs +23 -0
  138. package/edition/lib.mjs +3322 -0
  139. package/edition/locked-bundle-signer.mjs +297 -0
  140. package/edition/map-with-concurrency.mjs +19 -0
  141. package/edition/no-follow-file.mjs +94 -0
  142. package/edition/oci-evidence-verbs.mjs +421 -0
  143. package/edition/oci-image-source-evidence.mjs +377 -0
  144. package/edition/operation-grant-admission.mjs +285 -0
  145. package/edition/operation-requirements.mjs +282 -0
  146. package/edition/oras-process.mjs +35 -0
  147. package/edition/oras.mjs +664 -0
  148. package/edition/pin-source.mjs +242 -0
  149. package/edition/pinned-cosign.mjs +86 -0
  150. package/edition/plan-image-evidence-refresh.mjs +428 -0
  151. package/edition/platform-lock-readability.mjs +903 -0
  152. package/edition/platform-service-class.mjs +85 -0
  153. package/edition/platform-service-ownership.mjs +91 -0
  154. package/edition/platform-service-source-authority.mjs +105 -0
  155. package/edition/positive-closure.mjs +162 -0
  156. package/edition/previous-image-lock.mjs +25 -0
  157. package/edition/produce-distribution-completeness-evidence.mjs +317 -0
  158. package/edition/produce-edition-catalogue-evidence.mjs +252 -0
  159. package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
  160. package/edition/produce-image-source-evidence.mjs +788 -0
  161. package/edition/producer-evidence-bundle.mjs +201 -0
  162. package/edition/producer-evidence-index-file.mjs +4 -0
  163. package/edition/record-refused.mjs +11 -0
  164. package/edition/registry-client-factory.mjs +21 -0
  165. package/edition/registry-credentials.mjs +225 -0
  166. package/edition/release-catalogue.mjs +48 -0
  167. package/edition/release-channel-guard.mjs +108 -0
  168. package/edition/release-edition-artifact.mjs +152 -0
  169. package/edition/release-edition.mjs +54 -0
  170. package/edition/remedy-command.mjs +89 -0
  171. package/edition/repin-evidence-trust.mjs +109 -0
  172. package/edition/required-resource-provider.mjs +58 -0
  173. package/edition/resolve-chart-set.mjs +376 -0
  174. package/edition/resolve-customer-edition.mjs +374 -0
  175. package/edition/resolve-distribution.mjs +2642 -0
  176. package/edition/resolve-edition.mjs +192 -0
  177. package/edition/resolve-images.mjs +2344 -0
  178. package/edition/resolve-infra-images.mjs +88 -0
  179. package/edition/seal-image-digests.mjs +954 -0
  180. package/edition/sealed-distribution-lock.mjs +22 -0
  181. package/edition/sealed-evidence-accept.mjs +82 -0
  182. package/edition/shallow-checkout.mjs +18 -0
  183. package/edition/sigstore-bundle-media-type.mjs +4 -0
  184. package/edition/source-authority.mjs +713 -0
  185. package/edition/source-closure.mjs +115 -0
  186. package/edition/store-publisher-trust.mjs +40 -0
  187. package/edition/strict-json.mjs +121 -0
  188. package/edition/string-order.mjs +5 -0
  189. package/edition/third-party-admission.mjs +118 -0
  190. package/edition/third-party-bundle-registry.mjs +349 -0
  191. package/edition/third-party-certificate-extensions.mjs +58 -0
  192. package/edition/third-party-image-pins.mjs +41 -0
  193. package/edition/third-party-pins-trusted.mjs +24 -0
  194. package/edition/third-party-producer-evidence.mjs +76 -0
  195. package/edition/third-party-source-check.mjs +28 -0
  196. package/edition/third-party-source-context.mjs +41 -0
  197. package/edition/third-party-source-evidence.mjs +326 -0
  198. package/edition/third-party-source-flags.mjs +148 -0
  199. package/edition/third-party-source-pins.mjs +270 -0
  200. package/edition/third-party-source-uniqueness.mjs +54 -0
  201. package/edition/third-party-source-walk.mjs +118 -0
  202. package/edition/third-party-source-writer.mjs +228 -0
  203. package/edition/third-party-subjects.mjs +128 -0
  204. package/edition/trust-policy-arming-verb.mjs +60 -0
  205. package/edition/trust-policy-arming.mjs +144 -0
  206. package/edition/trust-policy-image-ownership.mjs +85 -0
  207. package/edition/trusted-executable.mjs +166 -0
  208. package/edition/verifier-binding.mjs +45 -0
  209. package/edition/verifier-digest-pin.mjs +17 -0
  210. package/edition/verifier-files.mjs +8 -0
  211. package/edition/verifier-kernel-schemas.mjs +22 -0
  212. package/edition/verifier-provenance.mjs +64 -0
  213. package/edition/verify-catalogue-evidence.mjs +247 -0
  214. package/edition/verify-image-evidence.mjs +493 -0
  215. package/edition/verify-image-platforms.mjs +523 -0
  216. package/edition/why.mjs +56 -0
  217. package/edition/workspace-sections.mjs +287 -0
  218. package/package.json +83 -4
  219. package/README.md +0 -3
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ // The distribution completeness statement and the evidence set it seals.
3
+ //
4
+ // xema-edition evidence statement completeness stage … (the bytes to sign)
5
+ // xema-edition evidence assemble-set … (evidence-set.json)
6
+ //
7
+ // `stage` resolves the edition's producer evidence into a NEW staging
8
+ // directory, writes the canonical completeness statement there and prints the
9
+ // signing plan. The caller signs it (`cosign attest-blob`). `assemble-set`
10
+ // re-derives everything from the same flags, refuses staged bytes that differ,
11
+ // verifies the signed bundle with cosign against the declared signer, writes
12
+ // `evidence-set.json` and seals the evidence root (built from the re-derived
13
+ // files, never renamed from the staging).
14
+ // This module never signs and never requests an OIDC token.
15
+
16
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
17
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
18
+
19
+ import {
20
+ acceptSignedEvidenceStatement,
21
+ assertPinnedCosignVersion,
22
+ buildDistributionImageEvidenceSet,
23
+ buildDistributionImagesStatement,
24
+ evidenceChannelOfSigner,
25
+ loadPinnedTrustPolicy,
26
+ parseEvidenceCliFlags,
27
+ resolveProducerEvidence,
28
+ stageEvidenceStatement,
29
+ stageProducerEvidence,
30
+ stagedProducerFiles,
31
+ } from './distribution-evidence-set.mjs';
32
+ import {
33
+ WorkspaceRoot,
34
+ configureEditionWorkspaceFromArgv,
35
+ dataRoot,
36
+ editionsRoot,
37
+ isEntryModule,
38
+ } from './edition-workspace.mjs';
39
+ import { canonicalJsonBytes } from './image-evidence.mjs';
40
+ import { oidcCertificateSignerKind } from './kernel-signer-kind.mjs';
41
+ import { serializeJson } from './lib.mjs';
42
+ import {
43
+ assertGitHubProducerContext,
44
+ createCosignVerifier,
45
+ loadProducerKernelSchemas,
46
+ } from './produce-image-source-evidence.mjs';
47
+ import { assertStagedExactly, discardStaging, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
48
+
49
+ const EVIDENCE_SET_FILE = 'evidence-set.json';
50
+ const COMPLETENESS_STATEMENT_PATH = 'completeness/statement.json';
51
+ const COMPLETENESS_BUNDLE_PATH = 'completeness/bundle.sigstore.json';
52
+
53
+ /** The keyless signer a GitHub workflow is: its certificate identity and issuer. */
54
+ export function githubWorkflowSigner({ repository, ref, path }) {
55
+ return {
56
+ kind: oidcCertificateSignerKind(),
57
+ certificateIdentity: `https://github.com/${repository}/${path}@${ref}`,
58
+ oidcIssuer: 'https://token.actions.githubusercontent.com',
59
+ githubWorkflow: { repository, ref, path },
60
+ };
61
+ }
62
+
63
+ /** The producer evidence and the completeness statement these inputs resolve to; deterministic. */
64
+ async function deriveCompleteness(options, { outputRoot }) {
65
+ // The channel the signer signs for is the channel the completeness statement states.
66
+ const channel = evidenceChannelOfSigner(options.signer, options.schemas);
67
+ const resolution = {
68
+ distributionId: options.distributionId,
69
+ dataRoot: options.dataRoot,
70
+ editionsRoot: options.editionsRoot,
71
+ schemas: options.schemas,
72
+ expectedContractAuthority: options.expectedContractAuthority,
73
+ registryClientFactory: options.registryClientFactory,
74
+ };
75
+ const staged = outputRoot === null
76
+ ? await resolveProducerEvidence(resolution)
77
+ : await stageProducerEvidence({ ...resolution, outputRoot });
78
+ const completenessStatement = buildDistributionImagesStatement({
79
+ distributionLock: staged.distributionLock,
80
+ imageLock: staged.imageLock,
81
+ images: staged.images,
82
+ channel,
83
+ releaseSequence: options.releaseSequence,
84
+ releaseSourceCommit: options.releaseSourceCommit,
85
+ runId: options.runId,
86
+ runAttempt: options.runAttempt,
87
+ issuedAt: options.issuedAt,
88
+ expiresAt: options.expiresAt,
89
+ maxTtlSeconds: options.policy.maxCompletenessTtlSeconds,
90
+ schemas: options.schemas,
91
+ });
92
+ return { staged, completenessStatement };
93
+ }
94
+
95
+ function signingArguments(options, statement) {
96
+ return {
97
+ statement,
98
+ root: options.stagingDirectory,
99
+ statementPath: COMPLETENESS_STATEMENT_PATH,
100
+ bundlePath: COMPLETENESS_BUNDLE_PATH,
101
+ signer: options.signer,
102
+ certificateWorkflowSha: options.releaseSourceCommit,
103
+ predicateType: options.schemas.DISTRIBUTION_IMAGES_PREDICATE_TYPE,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Stage the producer evidence of one edition and its completeness statement
109
+ * for signing. Returns the signing plan and the statement.
110
+ */
111
+ export async function stageDistributionCompletenessEvidence(options) {
112
+ const { stagingDirectory } = options;
113
+ if (typeof stagingDirectory !== 'string' || !isAbsolute(stagingDirectory)) {
114
+ throw new Error('the staging directory must be an absolute path');
115
+ }
116
+ if (existsSync(stagingDirectory)) {
117
+ throw new Error(`evidence staging directory already exists: ${stagingDirectory}`);
118
+ }
119
+ assertGitHubProducerContext(
120
+ { signer: options.signer, producerWorkflowSha: options.releaseSourceCommit },
121
+ options.githubContext,
122
+ );
123
+ mkdirSync(stagingDirectory, { recursive: true, mode: 0o700 });
124
+ try {
125
+ const { completenessStatement } = await deriveCompleteness(options, { outputRoot: options.stagingDirectory });
126
+ const plan = stageEvidenceStatement(signingArguments(options, completenessStatement));
127
+ return {
128
+ plan: { ...plan, kind: 'completeness', distributionId: options.distributionId },
129
+ completenessStatement,
130
+ };
131
+ } catch (error) {
132
+ rmSync(stagingDirectory, { recursive: true, force: true });
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Accept the signed completeness bundle of a staged directory, write
139
+ * `evidence-set.json` and seal `evidenceRoot`.
140
+ *
141
+ * `evidenceRoot` is BUILT, never renamed from the staging: the producer
142
+ * evidence is re-resolved from the registry and must be byte-identical to what
143
+ * was staged, the completeness statement is re-derived and the signed bundle
144
+ * canonicalized and verified, and only those derived files are written. The
145
+ * staging must hold exactly what stage recorded and does not outlive the
146
+ * accept (see `sealed-evidence-accept.mjs`).
147
+ */
148
+ export async function assembleDistributionEvidenceSet(options) {
149
+ const { evidenceRoot, stagingDirectory, readCosignVersion, runCosign } = options;
150
+ if (
151
+ typeof evidenceRoot !== 'string' ||
152
+ !isAbsolute(evidenceRoot) ||
153
+ typeof stagingDirectory !== 'string' ||
154
+ !isAbsolute(stagingDirectory)
155
+ ) {
156
+ throw new Error('the evidence root and the staging directory must be absolute paths');
157
+ }
158
+ if (typeof readCosignVersion !== 'function' || typeof runCosign !== 'function') {
159
+ throw new Error('cosign seams are required');
160
+ }
161
+ const sealing = reserveSealing(evidenceRoot, 'evidence');
162
+ let derived;
163
+ try {
164
+ assertGitHubProducerContext(
165
+ { signer: options.signer, producerWorkflowSha: options.releaseSourceCommit },
166
+ options.githubContext,
167
+ );
168
+ assertPinnedCosignVersion(readCosignVersion(), options.policy?.cosignVersion);
169
+ derived = await deriveCompleteness(options, { outputRoot: null });
170
+ } catch (error) {
171
+ discardStaging(stagingDirectory);
172
+ throw error;
173
+ }
174
+ const { staged, completenessStatement } = derived;
175
+ return sealAccepted({
176
+ stagingDirectory,
177
+ sealing,
178
+ outputDirectory: evidenceRoot,
179
+ build: () => {
180
+ assertStagedExactly(stagingDirectory, [
181
+ ...staged.producers.flatMap((producer) => stagedProducerFiles(producer).map(([path]) => path)),
182
+ COMPLETENESS_STATEMENT_PATH,
183
+ COMPLETENESS_BUNDLE_PATH,
184
+ ]);
185
+ for (const producer of staged.producers) {
186
+ for (const [path, bytes] of stagedProducerFiles(producer)) {
187
+ if (!readFileSync(join(stagingDirectory, path)).equals(bytes)) {
188
+ throw new Error(`staged ${path} is not the producer evidence this edition resolves to`);
189
+ }
190
+ mkdirSync(dirname(join(sealing, path)), { recursive: true, mode: 0o755 });
191
+ writeFileSync(join(sealing, path), bytes, { flag: 'wx', mode: 0o644 });
192
+ }
193
+ }
194
+ const completeness = acceptSignedEvidenceStatement({
195
+ ...signingArguments(options, completenessStatement),
196
+ sealingRoot: sealing,
197
+ runCosign,
198
+ });
199
+ const evidenceSet = buildDistributionImageEvidenceSet({
200
+ distributionId: options.distributionId,
201
+ producerEvidence: staged.producerEvidence,
202
+ completeness,
203
+ schemas: options.schemas,
204
+ });
205
+ writeFileSync(join(sealing, EVIDENCE_SET_FILE), canonicalJsonBytes(evidenceSet), {
206
+ flag: 'wx',
207
+ mode: 0o644,
208
+ });
209
+ return {
210
+ evidenceRoot,
211
+ evidenceSet,
212
+ completeness,
213
+ completenessStatement,
214
+ completenessStatementDigest: completeness.statementDigest,
215
+ };
216
+ },
217
+ });
218
+ }
219
+
220
+ function requireInteger(raw, label, { positive }) {
221
+ const value = Number(raw);
222
+ if (!/^\d+$/.test(String(raw)) || !Number.isSafeInteger(value) || (positive && value <= 0)) {
223
+ throw new Error(`--${label} must be a ${positive ? 'positive' : 'non-negative'} integer`);
224
+ }
225
+ return value;
226
+ }
227
+
228
+ const SHARED_FLAGS = Object.freeze([
229
+ 'distribution',
230
+ 'staging',
231
+ 'release-sequence',
232
+ 'release-source-commit',
233
+ 'run-id',
234
+ 'run-attempt',
235
+ 'issued-at',
236
+ 'expires-at',
237
+ 'signer-repository',
238
+ 'signer-ref',
239
+ 'signer-workflow-path',
240
+ 'github-repository',
241
+ 'github-ref',
242
+ 'github-sha',
243
+ 'github-workflow-ref',
244
+ ]);
245
+
246
+ async function main() {
247
+ const [phase, ...args] = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
248
+ WorkspaceRoot.Data,
249
+ WorkspaceRoot.Editions,
250
+ WorkspaceRoot.Kernel,
251
+ ]);
252
+ if (phase !== 'stage' && phase !== 'accept') {
253
+ throw new Error(
254
+ 'usage: xema-edition evidence statement completeness stage … | xema-edition evidence assemble-set …',
255
+ );
256
+ }
257
+ const flags = parseEvidenceCliFlags(args, {
258
+ single: phase === 'stage' ? SHARED_FLAGS : [...SHARED_FLAGS, 'evidence-root', 'cosign'],
259
+ });
260
+ const { schemas, contractAuthority } = await loadProducerKernelSchemas({ dataRoot: dataRoot() });
261
+ const options = {
262
+ distributionId: flags.require('distribution'),
263
+ dataRoot: dataRoot(),
264
+ editionsRoot: editionsRoot(),
265
+ stagingDirectory: resolve(flags.require('staging')),
266
+ schemas,
267
+ expectedContractAuthority: contractAuthority,
268
+ policy: loadPinnedTrustPolicy({ dataRoot: dataRoot(), schemas }),
269
+ signer: githubWorkflowSigner({
270
+ repository: flags.require('signer-repository'),
271
+ ref: flags.require('signer-ref'),
272
+ path: flags.require('signer-workflow-path'),
273
+ }),
274
+ githubContext: {
275
+ repository: flags.require('github-repository'),
276
+ ref: flags.require('github-ref'),
277
+ sha: flags.require('github-sha'),
278
+ workflowRef: flags.require('github-workflow-ref'),
279
+ },
280
+ releaseSequence: requireInteger(flags.require('release-sequence'), 'release-sequence', {
281
+ positive: false,
282
+ }),
283
+ releaseSourceCommit: flags.require('release-source-commit'),
284
+ runId: flags.require('run-id'),
285
+ runAttempt: requireInteger(flags.require('run-attempt'), 'run-attempt', { positive: false }),
286
+ issuedAt: flags.require('issued-at'),
287
+ expiresAt: flags.require('expires-at'),
288
+ };
289
+ if (phase === 'stage') {
290
+ const { plan } = await stageDistributionCompletenessEvidence(options);
291
+ process.stdout.write(serializeJson(plan));
292
+ return;
293
+ }
294
+ const result = await assembleDistributionEvidenceSet({
295
+ ...options,
296
+ evidenceRoot: resolve(flags.require('evidence-root')),
297
+ ...createCosignVerifier(resolve(flags.require('cosign'))),
298
+ });
299
+ process.stdout.write(
300
+ serializeJson({
301
+ schemaVersion: 1,
302
+ kind: 'evidence-set',
303
+ distributionId: options.distributionId,
304
+ evidenceRoot: result.evidenceRoot,
305
+ evidenceSetPath: EVIDENCE_SET_FILE,
306
+ producerBundles: result.evidenceSet.producerEvidence.length,
307
+ completenessStatementDigest: result.completenessStatementDigest,
308
+ }),
309
+ );
310
+ }
311
+
312
+ if (isEntryModule(import.meta.url)) {
313
+ main().catch((error) => {
314
+ console.error(`[distribution-completeness-evidence] ${error.message}`);
315
+ process.exitCode = 1;
316
+ });
317
+ }
@@ -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
+ }