@xemahq/edition-tooling 0.0.0-stage → 0.1.0

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,296 @@
1
+ // develop-git-bundles — the OCI bundle of a git-sourced biome, in a DEVELOP edition only (XEMA-2456): a vendor develop
2
+ // resolve, or a customer develop resolve (`--definition`) whose data root declares a pinned evidence trust policy
3
+ // (`customerDevelopGitBundleVerifier`). Each is verified under the trust policy of ITS OWN data root.
4
+ //
5
+ // A release edition never carries an OCI bundle on a git-sourced row: the manifest's `bundleSource` names an artifact
6
+ // the release lane builds, not one built from the pinned commit (resolve-distribution.mjs strips it, the release refuses
7
+ // it). A resolve without a verifier (a customer edition whose data root declares no policy, or any customer release)
8
+ // refuses it on a required row (`assertNoUnverifiedRequiredGitBundles`). A develop edition freezes a git source at its develop head F, and the source's own develop lane
9
+ // publishes the bundle the manifest names, signed keyless by its develop workflow. This module proves that bundle is
10
+ // the one F describes, and only then does the resolve keep it on the row, by DIGEST, with its develop bundle signer:
11
+ //
12
+ // 1. the bundle: the manifest's own `bundleSource.ociRef` at F (`<repository>:<version>`; the kernel holds the tag to
13
+ // the manifest version) resolved to its digest at the registry. The lock records `<repository>@sha256:<hex>`, never
14
+ // the tag, so the edition names immutable bytes;
15
+ // 2. the signer: the ONE `develop` row of the source's repository in the data root's trust policy
16
+ // (`bundleSignerRowFor`). cosign verifies the digest's signature under that row's identity, issuer, workflow
17
+ // repository and ref, and states the ONE commit P its certificate was issued at (`verifiedBundleSignerCommit`);
18
+ // `verifyBundleOnRegistry` then holds the signature AND the SLSA provenance to exactly the row and P. P is never
19
+ // read from bytes cosign has not verified;
20
+ // 3. the content binding of P to F. A version that is already published is never re-attested by the publisher
21
+ // (`xema biome publish` "already current"), so P is the commit that last published the version, and F may be a
22
+ // later commit of the same version. Two checks, refused on either, each naming P, F and the mismatching pair:
23
+ // (a) the kernel's `computeBiomeSourceManifestHash` of the bundle's own `xema-biome.json` equals the
24
+ // `sourceManifestHash` of the biome at F (the hash the seal and verify hold to the git source at F);
25
+ // (b) the git tree id of the biome directory at P equals its tree id at F (the catalogue's `sourceTreeIds`
26
+ // notion), so the bundle was built from the same biome source F holds.
27
+ // THE GAP, develop only: build inputs outside the biome directory (shared packages, the lockfile, the publisher's
28
+ // toolchain) are not bound. A change there between P and F reaches no check here. A release never relies on this:
29
+ // it carries a third-party bundle only through an `oci` source with a third-party pin, whose evidence is bound to
30
+ // the pinned commit itself.
31
+ //
32
+ // That verification is the OFFICIAL lane's (a customer develop resolve, and the catalogue-free resolve's vendor step).
33
+ // The channel lane every deployment we run resolves with (`createDevelopGitBundleDigestReader`) takes the bundle by
34
+ // step 1 only: the digest the manifest's tag serves, whose own xema-biome.json must name the biome. No cosign, no
35
+ // signer commit, no content binding.
36
+ //
37
+ // Every failure is a `DevelopGitBundleError` naming the biome; all subjects are verified before one is refused.
38
+
39
+ import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
40
+ import { RefusalError } from './collected-refusals.mjs';
41
+ import { EditionChannel } from './edition-channel.mjs';
42
+ import { editionWorkspaceConfig } from './edition-config.mjs';
43
+ import { dataRoot as configuredDataRoot, editionsRoot } from './edition-workspace.mjs';
44
+ import { loadPinnedDeclaredTrustPolicy } from './evidence-trust-pin.mjs';
45
+ import { sourceTreeIdAt } from './external-source-walk.mjs';
46
+ import { prepareTrustedCosignBinary } from './image-evidence.mjs';
47
+ import { BundleSignerRefusedError, DevelopGitBundleError } from './locked-bundle-signer.mjs';
48
+ import { createRegistryClientResolver } from './oci-image-source-evidence.mjs';
49
+ import { bundleDigestRef, fetchBundleManifest, verifiedBundleSignerCommit, verifyBundleOnRegistry } from './third-party-bundle-registry.mjs';
50
+ import { bundleSignerRowFor } from './third-party-source-evidence.mjs';
51
+ import { OCI_BUNDLE_SOURCE_KIND } from './third-party-subjects.mjs';
52
+
53
+ /** The key a verified bundle is looked up by: the source name and the biome id of its index entry. */
54
+ export const developGitBundleKey = (sourceName, biomeId) => `${sourceName}\u0000${biomeId}`;
55
+
56
+ /**
57
+ * The rows a develop resolve must verify a bundle for: each git-sourced biome of `lock` (a row with a `source`) whose
58
+ * index entry declares a biome-level OCI `bundleSource`.
59
+ *
60
+ * @returns {{biomeId: string, source: {name: string, url: string, commit: string}, entry: object}[]}
61
+ */
62
+ export function developGitBundleSubjects({ lock, index }) {
63
+ const entries = new Map(index.filter((entry) => entry.source !== undefined).map((entry) => [developGitBundleKey(entry.source, entry.id), entry]));
64
+ return lock.biomes.flatMap((biome) => {
65
+ if (biome.source === undefined) {return [];}
66
+ const entry = entries.get(developGitBundleKey(biome.source.name, biome.id));
67
+ return entry?.bundleSource?.kind === OCI_BUNDLE_SOURCE_KIND ? [{ biomeId: biome.id, source: biome.source, entry }] : [];
68
+ });
69
+ }
70
+
71
+ const subjectLabel = ({ biomeId, source }) => `biome "${biomeId}" (git source "${source.name}" at ${source.commit})`;
72
+
73
+ /**
74
+ * A resolve with no develop bundle verifier (a customer edition, either channel) drops a git-sourced row's OCI
75
+ * `bundleSource` (resolve-distribution.mjs, at the copy point). For a row the edition installs as `required`, that
76
+ * drop is never silent: biome-host would get a required biome with no bundle to fetch. Every such row is refused at
77
+ * once, naming the declared bundle and what is missing (XEMA-2456).
78
+ *
79
+ * @param {object} options
80
+ * @param {object} options.lock the resolved lock (its rows carry `installPolicy`)
81
+ * @param {object[]} options.index the index it was resolved from (it still holds the manifest's bundleSource)
82
+ * @param {string} options.required the kernel's `BiomeInstallPolicy.Required`
83
+ */
84
+ export function assertNoUnverifiedRequiredGitBundles({ lock, index, required }) {
85
+ const requiredRows = new Set(lock.biomes.filter((biome) => biome.installPolicy === required).map((biome) => biome.id));
86
+ const refused = developGitBundleSubjects({ lock, index })
87
+ .filter((subject) => requiredRows.has(subject.biomeId))
88
+ .map(
89
+ (subject) =>
90
+ new DevelopGitBundleError(
91
+ `${subjectLabel(subject)} is installPolicy required and its manifest declares an OCI bundleSource (${subject.entry.bundleSource.ociRef}), ` +
92
+ 'but this resolve verifies no git-sourced bundle, so its lock row would carry no bundleSource and no bundleSigner and biome-host could not install it. ' +
93
+ 'A develop resolve verifies it when its data root declares a pinned evidenceTrustPolicy (edition-workspace.json) holding the ONE develop ' +
94
+ `bundle-signer row of the source's repository, and it is given --cosign; a release carries such a biome only as an \`oci\` source with a third-party source pin`,
95
+ ),
96
+ );
97
+ if (refused.length === 1) {throw refused[0];}
98
+ if (refused.length > 1) {throw new BundleSignerRefusedError(refused);}
99
+ }
100
+
101
+ /** The biome directory relative to its source repository: the index path is `<source name>/<directory>`. */
102
+ function repositoryPathOf({ source, entry }) {
103
+ const prefix = `${source.name}/`;
104
+ if (!entry.path.startsWith(prefix)) {
105
+ throw new Error(`${subjectLabel({ biomeId: entry.id, source })}: index path ${JSON.stringify(entry.path)} is not under the source "${source.name}"`);
106
+ }
107
+ return entry.path.slice(prefix.length);
108
+ }
109
+
110
+ /** Verify every subject, then refuse them all at once: one refusal is rethrown unchanged, several are one error. */
111
+ async function verifyEach(subjects, verifyOne) {
112
+ const verified = new Map();
113
+ const refused = [];
114
+ for (const subject of subjects) {
115
+ try {
116
+ verified.set(developGitBundleKey(subject.source.name, subject.biomeId), await verifyOne(subject));
117
+ } catch (error) {
118
+ // A refusal, or a plain Error of the registry, cosign or git: this subject is refused. A defect (TypeError, ...) is not.
119
+ const refusal = error instanceof RefusalError || error?.constructor === Error;
120
+ if (!refusal) {throw error;}
121
+ refused.push(error instanceof DevelopGitBundleError ? error : new DevelopGitBundleError(`${subjectLabel(subject)}: ${error.message}`));
122
+ }
123
+ }
124
+ if (refused.length === 1) {throw refused[0];}
125
+ if (refused.length > 1) {throw new BundleSignerRefusedError(refused);}
126
+ return verified;
127
+ }
128
+
129
+ /** The bundle's own xema-biome.json, which must name the biome the row is for. */
130
+ async function bundleManifestOfBiome({ clientFor, ociRef, biomeId, label }) {
131
+ const { manifest } = await fetchBundleManifest({ clientFor, ociRef });
132
+ if (manifest?.xema?.id !== biomeId) {
133
+ throw new Error(`${label}: ${ociRef} declares xema.id ${JSON.stringify(manifest?.xema?.id)}, not "${biomeId}"`);
134
+ }
135
+ return manifest;
136
+ }
137
+
138
+ /**
139
+ * One subject on the channel lane: the digest the manifest's `bundleSource.ociRef` tag serves (step 1), its manifest
140
+ * naming the biome. Nothing is verified beyond that.
141
+ *
142
+ * @returns {Promise<{bundleSource: object}>} the bundle source the lock row carries: the declared one, by digest
143
+ */
144
+ export async function developGitBundleByDigest({ subject, clientFor }) {
145
+ const { biomeId, entry } = subject;
146
+ const ociRef = await bundleDigestRef({ clientFor, ociRef: entry.bundleSource.ociRef });
147
+ await bundleManifestOfBiome({ clientFor, ociRef, biomeId, label: subjectLabel(subject) });
148
+ return { bundleSource: { ...entry.bundleSource, ociRef } };
149
+ }
150
+
151
+ /**
152
+ * The reader a channel-lane develop resolve hands `prepareDistributionWithIndex` (`inputs.developGitBundles`): each
153
+ * git-sourced OCI bundle by the digest its tag serves (`developGitBundleByDigest`). No trust policy, no cosign.
154
+ *
155
+ * @param {object} options
156
+ * @param {string} options.registryRoot the root whose image-registry-credentials.json the registry is read with
157
+ */
158
+ export function createDevelopGitBundleDigestReader({ registryRoot, clientFor }) {
159
+ return Object.freeze({
160
+ async verify({ subjects }) {
161
+ if (subjects.length === 0) {return new Map();}
162
+ const registry = clientFor ?? createRegistryClientResolver({ dataRoot: registryRoot });
163
+ return verifyEach(subjects, (subject) => developGitBundleByDigest({ subject, clientFor: registry }));
164
+ },
165
+ });
166
+ }
167
+
168
+ /**
169
+ * One subject, verified (steps 1 to 3 of this file's header). `context` is `{policy, policyRoot, schemas, trustedCosign}`.
170
+ *
171
+ * @returns {Promise<{bundleSource: object}>} the bundle source the lock row carries: the declared one, by digest
172
+ */
173
+ export async function verifyDevelopGitBundle({
174
+ subject,
175
+ checkout,
176
+ context,
177
+ clientFor,
178
+ signerCommit = verifiedBundleSignerCommit,
179
+ verifyOnRegistry = verifyBundleOnRegistry,
180
+ treeIdAt = sourceTreeIdAt,
181
+ env = process.env,
182
+ }) {
183
+ const { biomeId, source, entry } = subject;
184
+ const label = subjectLabel(subject);
185
+ if (checkout === undefined) {
186
+ throw new Error(`${label}: the source walk holds no checkout of "${source.name}"`);
187
+ }
188
+ const repository = repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(source.url, `${label} source url`), `${label} source url`);
189
+ const row = bundleSignerRowFor({ policy: context.policy, repository, channel: EditionChannel.Develop, schemas: context.schemas, label: `${label} develop bundle signer` });
190
+
191
+ // 1 + 2: the digest the manifest's tag serves; the commit P cosign verified the develop signature at; signature and
192
+ // provenance held to the row at P.
193
+ const ociRef = await bundleDigestRef({ clientFor, ociRef: entry.bundleSource.ociRef });
194
+ const cosign = { trustedCosign: context.trustedCosign, policy: context.policy, policyRoot: context.policyRoot, row, ociRef, env };
195
+ const attestedCommit = signerCommit(cosign);
196
+ verifyOnRegistry({ ...cosign, workflowSha: attestedCommit });
197
+
198
+ // 3: the content binding of P to F (see the header for the gap it leaves: build inputs outside the biome directory).
199
+ const binding = `the bundle ${ociRef} attested at ${attestedCommit} (P) against the source at ${source.commit} (F)`;
200
+ const manifest = await bundleManifestOfBiome({ clientFor, ociRef, biomeId, label });
201
+ const bundleManifestHash = await context.schemas.computeBiomeSourceManifestHash(manifest);
202
+ if (bundleManifestHash !== entry.sourceManifestHash) {
203
+ throw new DevelopGitBundleError(
204
+ `${label}: ${binding}: the bundle's xema-biome.json hashes to sourceManifestHash ${bundleManifestHash}, but the manifest at F hashes to ${entry.sourceManifestHash}`,
205
+ );
206
+ }
207
+ const path = repositoryPathOf(subject);
208
+ const treeAtAttested = treeIdAt({ checkout, commit: attestedCommit, path, env });
209
+ const treeAtFrozen = treeIdAt({ checkout, commit: source.commit, path, env });
210
+ if (treeAtAttested !== treeAtFrozen) {
211
+ throw new DevelopGitBundleError(
212
+ `${label}: ${binding}: the biome directory ${path} is tree ${treeAtAttested} at P but tree ${treeAtFrozen} at F; publish a new version of the biome from F`,
213
+ );
214
+ }
215
+ return { bundleSource: { ...entry.bundleSource, ociRef } };
216
+ }
217
+
218
+ /**
219
+ * The verifier a vendor develop resolve hands `prepareDistributionWithIndex` (`inputs.developGitBundles`). Nothing is
220
+ * read (no trust policy, no cosign) unless a subject exists.
221
+ *
222
+ * @param {object} options
223
+ * @param {string|undefined} options.cosignBinary the cosign the trust policy pins by digest (`--cosign`)
224
+ * @param {string} options.dataRoot the root whose pinned trust policy holds the develop bundle-signer rows
225
+ * @param {string} [options.registryRoot] the root whose image-registry-credentials.json the registry is read with (default: dataRoot)
226
+ */
227
+ export function createDevelopGitBundleVerifier({ cosignBinary, dataRoot, registryRoot = dataRoot, env = process.env, clientFor, treeIdAt }) {
228
+ return Object.freeze({
229
+ async verify({ subjects, checkouts, schemas }) {
230
+ if (subjects.length === 0) {return new Map();}
231
+ if (cosignBinary === undefined) {
232
+ throw new DevelopGitBundleError(
233
+ `${subjects.map(subjectLabel).join(', ')} declare an OCI bundle; verifying it needs a cosign binary (--cosign), and none was given`,
234
+ );
235
+ }
236
+ const policy = loadPinnedDeclaredTrustPolicy({ dataRoot, declaration: editionWorkspaceConfig().evidenceTrustPolicy, schemas });
237
+ const trustedCosign = prepareTrustedCosignBinary({ cosignBinary, policy });
238
+ const context = { policy, policyRoot: dataRoot, schemas, trustedCosign };
239
+ const registry = clientFor ?? createRegistryClientResolver({ dataRoot: registryRoot });
240
+ try {
241
+ return await verifyEach(subjects, (subject) =>
242
+ verifyDevelopGitBundle({ subject, checkout: checkouts.get(subject.source.name), context, clientFor: registry, env, ...(treeIdAt === undefined ? {} : { treeIdAt }) }),
243
+ );
244
+ } finally {
245
+ trustedCosign.dispose();
246
+ }
247
+ },
248
+ });
249
+ }
250
+
251
+ /**
252
+ * The verifier a CUSTOMER resolve (`--definition`) hands `resolveCustomerDistribution`, or undefined. Only a develop
253
+ * resolve whose configured data root DECLARES an evidence trust policy gets one: the bundles are verified under that
254
+ * customer policy, held to its pin (`loadPinnedDeclaredTrustPolicy`, read only when a subject exists), never the
255
+ * vendor's. The registry is read with the editions root's credentials, as the customer's own images are. Without a
256
+ * declared policy (or on release) there is none, and a required git-sourced row declaring an OCI bundle is refused
257
+ * (`assertNoUnverifiedRequiredGitBundles`).
258
+ */
259
+ export function customerDevelopGitBundleVerifier({ channel, cosignBinary, env = process.env, clientFor, treeIdAt }) {
260
+ if (channel !== EditionChannel.Develop) {return undefined;}
261
+ if (editionWorkspaceConfig().evidenceTrustPolicy.policy === null) {return undefined;}
262
+ return createDevelopGitBundleVerifier({
263
+ cosignBinary,
264
+ dataRoot: configuredDataRoot(),
265
+ registryRoot: editionsRoot(),
266
+ env,
267
+ ...(clientFor === undefined ? {} : { clientFor }),
268
+ ...(treeIdAt === undefined ? {} : { treeIdAt }),
269
+ });
270
+ }
271
+
272
+ /**
273
+ * The OFFLINE verifier of a candidate re-seal: the bundle the pinned develop edition already verified for the SAME
274
+ * source commit, carried forward unchanged, with the signer it was verified under (`pinnedBundleSigner`): the resolve
275
+ * refuses the row when the current trust policy selects any other signer (a rotated develop row). A subject at another commit, or one the pinned edition carried no bundle
276
+ * for, is refused: verifying a new bundle reads the registry, which an offline re-seal does not.
277
+ *
278
+ * @param {object} pinnedLock the pinned edition's parsed distribution lock
279
+ */
280
+ export function carriedDevelopGitBundles(pinnedLock) {
281
+ return Object.freeze({
282
+ async verify({ subjects }) {
283
+ return verifyEach(subjects, async (subject) => {
284
+ const row = pinnedLock.biomes.find(
285
+ (biome) => biome.id === subject.biomeId && biome.source?.name === subject.source.name && biome.source.commit === subject.source.commit,
286
+ );
287
+ if (row?.bundleSource?.kind !== OCI_BUNDLE_SOURCE_KIND || row.bundleSigner === undefined) {
288
+ throw new DevelopGitBundleError(
289
+ `${subjectLabel(subject)}: the pinned edition carries no verified bundle at this commit, and an offline re-seal cannot verify one; resolve the develop edition instead`,
290
+ );
291
+ }
292
+ return { bundleSource: row.bundleSource, pinnedBundleSigner: row.bundleSigner };
293
+ });
294
+ },
295
+ });
296
+ }
@@ -0,0 +1,34 @@
1
+ // develop-graph — which repositories a DEVELOP candidate is built from.
2
+ //
3
+ // The develop graph is every first-party repository the data root declares
4
+ // (`firstPartySources` in edition-workspace.json), each at its declared
5
+ // `repositoryUrl`. It is never read from the release pins
6
+ // (`runtime-image-source-index.json#repositoryPins`): develop and release are
7
+ // separate lifecycles, and a repository is first-party on develop before any
8
+ // release pins it. freeze-graph, the snapshot checkout, the snapshot pins'
9
+ // validation and the snapshot's index all take their members from here.
10
+
11
+ import { firstPartySources } from './first-party-source-ids.mjs';
12
+ import { compareStrings } from './lib.mjs';
13
+
14
+ /**
15
+ * The develop graph: `{ repo, repositoryUrl }` per declared first-party
16
+ * repository, sorted by repo handle, plus `currentResolution` (its ticket) on a
17
+ * member the data root declares one for (develop-current-resolution.mjs), and `developImageRegistry` (its develop
18
+ * store root, where its build records live) on a member that declares one.
19
+ *
20
+ * @returns {readonly { repo: string, repositoryUrl: string, currentResolution?: string, developImageRegistry?: string }[]}
21
+ */
22
+ export function developGraphMembers() {
23
+ return Object.freeze(
24
+ Object.entries(firstPartySources('the develop graph'))
25
+ .map(([repo, { repositoryUrl, currentResolution, developImageRegistry }]) =>
26
+ Object.freeze({
27
+ repo,
28
+ repositoryUrl,
29
+ ...(currentResolution === undefined ? {} : { currentResolution }),
30
+ ...(developImageRegistry === undefined ? {} : { developImageRegistry }),
31
+ }))
32
+ .sort((a, b) => compareStrings(a.repo, b.repo)),
33
+ );
34
+ }
@@ -0,0 +1,85 @@
1
+ // develop-image-coordinate — the ONE mapping from a release image repository to
2
+ // the develop repository its producer pushes the same build to.
3
+ //
4
+ // <host>/<owner>/<name...> -> <host>/<owner>/dev/<name...>
5
+ //
6
+ // Every first-party source declares its develop store (`developImageRegistry`, edition-workspace.json): the root
7
+ // replaces `<host>/<owner>` in every develop ref and in the build-record repository,
8
+ //
9
+ // <host>/<owner>/<name...> -> <developImageRegistry>/dev/<name...>
10
+ // build records of <name> -> <developImageRegistry>/build-records/<name>
11
+ //
12
+ // A producer's develop lane pushes `<devRepository>:<contentHash>` (contract C1)
13
+ // and nothing else reads or writes that namespace, so a release coordinate can
14
+ // never be satisfied by a develop build or the other way round.
15
+
16
+ import { DEVELOP_IMAGE_NAMESPACE_SEGMENT } from './edition-constants.mjs';
17
+ import { splitImageRef } from './registry-credentials.mjs';
18
+
19
+ const DEVELOP_NAMESPACE_SEGMENT = DEVELOP_IMAGE_NAMESPACE_SEGMENT;
20
+
21
+ const OWNER_SEGMENT_INDEX = 0;
22
+ const NAMESPACE_SEGMENT_INDEX = 1;
23
+ const MINIMUM_REPOSITORY_SEGMENTS = 2;
24
+
25
+ /** The `{ authority, owner, name }` of a release ref `<host>/<owner>/<name...>`; refuses what is not a bare release repository. */
26
+ function releaseCoordinate(releaseImageRef) {
27
+ const { authority, repository } = splitImageRef(releaseImageRef);
28
+ if (repository.includes('@') || repository.includes(':')) {
29
+ throw new Error(
30
+ `image reference "${releaseImageRef}" carries a tag or digest; a develop coordinate maps a bare repository`,
31
+ );
32
+ }
33
+ const segments = repository.split('/');
34
+ if (segments.length < MINIMUM_REPOSITORY_SEGMENTS) {
35
+ throw new Error(
36
+ `image reference "${releaseImageRef}" has no <owner>/<name> repository path; it cannot be mapped to a develop repository`,
37
+ );
38
+ }
39
+ if (segments[NAMESPACE_SEGMENT_INDEX] === DEVELOP_NAMESPACE_SEGMENT) {
40
+ throw new Error(`image reference "${releaseImageRef}" is already a develop repository`);
41
+ }
42
+ const [owner, ...name] = segments;
43
+ return { authority, owner, name: name.join('/') };
44
+ }
45
+
46
+ /** `developImageRef('registry.example.com/acme/identity-api') === 'registry.example.com/acme/dev/identity-api'`. */
47
+ export function developImageRef(releaseImageRef) {
48
+ const { authority, owner, name } = releaseCoordinate(releaseImageRef);
49
+ return `${authority}/${owner}/${DEVELOP_NAMESPACE_SEGMENT}/${name}`;
50
+ }
51
+
52
+ /** Whether a repository reference lies in the develop namespace: a `dev` segment after the store root and before the name. */
53
+ export function isDevelopImageRef(imageRef) {
54
+ let repository;
55
+ try {
56
+ ({ repository } = splitImageRef(imageRef));
57
+ } catch {
58
+ return false;
59
+ }
60
+ const segments = repository.split('/');
61
+ // structural: the lane segment sits after the store root (whatever its depth) and before the image name
62
+ return (
63
+ segments.length > MINIMUM_REPOSITORY_SEGMENTS &&
64
+ segments[OWNER_SEGMENT_INDEX].length > 0 &&
65
+ segments.slice(NAMESPACE_SEGMENT_INDEX, -1).includes(DEVELOP_NAMESPACE_SEGMENT)
66
+ );
67
+ }
68
+
69
+ /**
70
+ * The develop image ref of a subject: `<developImageRegistry>/dev/<name...>` for a subject of a declared first-party
71
+ * source, else (a subject of no declared source, such as a customer's own image) the derivation from its release ref.
72
+ *
73
+ * @param {{repo?: string, imageRef: string}} subject
74
+ * @param {Record<string, {developImageRegistry: string}>} declaredSources `firstPartySources`
75
+ */
76
+ export function developImageRefFor(subject, declaredSources) {
77
+ const root = developRootOf(subject.repo, declaredSources);
78
+ if (root === undefined) {return developImageRef(subject.imageRef);}
79
+ return `${root}/${DEVELOP_NAMESPACE_SEGMENT}/${releaseCoordinate(subject.imageRef).name}`;
80
+ }
81
+
82
+ /** The develop store root of the declared first-party source `repo`; undefined when `repo` is no declared source. */
83
+ export function developRootOf(repo, declaredSources) {
84
+ return Object.hasOwn(declaredSources, repo) ? declaredSources[repo].developImageRegistry : undefined;
85
+ }
@@ -0,0 +1,122 @@
1
+ // develop-image-probe — learn EARLY which develop images are not built yet.
2
+ //
3
+ // The develop seal verifies every subject's platforms, and only after it has
4
+ // read all of them can it say which coordinates are absent. A develop edition
5
+ // whose producer CI has not finished therefore failed minutes after the
6
+ // content hashes were known. This probe asks the registry one cheap question
7
+ // per subject — does `<developImageRef>:<contentHash>` resolve to a
8
+ // hash-verified manifest — as soon as that subject's hash exists, in parallel
9
+ // with the hashing of the rest, and `settle()` refuses with ONE error naming
10
+ // every missing coordinate. It verifies existence only: the platform check, the
11
+ // digest decision and the size bounds stay in the seal, which still runs. `settle()` also answers the digest each
12
+ // present coordinate resolved to (`<kind>:<id>` -> digest), for a caller that records it (develop image-record).
13
+
14
+ import { RefusalError } from './collected-refusals.mjs';
15
+ import { developImageRefFor } from './develop-image-coordinate.mjs';
16
+ import { editionsRoot } from './edition-workspace.mjs';
17
+ import { loadRegistryCredentialDeclarations, splitImageRef } from './registry-credentials.mjs';
18
+ import { isNotPublishedYet } from './seal-image-digests.mjs';
19
+ import { registryClientsFor } from './verify-image-platforms.mjs';
20
+
21
+ const DEFAULT_PROBE_CONCURRENCY = 6;
22
+
23
+ /** One subject's line in the "not built yet" refusal of `channel` (develop unless named). */
24
+ export function notBuiltYetMessage(subject, channel = 'develop') {
25
+ return (
26
+ `${subject.repo}@${subject.sourceCommit} ${channel} image ` +
27
+ `${subject.imageRef}:${subject.contentHash} not built yet`
28
+ );
29
+ }
30
+
31
+ /** The ONE refusal naming every subject of `channel` (develop unless named) whose coordinate is absent. */
32
+ export function notBuiltYetError(distributionId, missingSubjects, channel = 'develop') {
33
+ return new RefusalError(
34
+ `${missingSubjects.length} ${channel} image(s) of "${distributionId}" are not built yet ` +
35
+ "(the producer's CI is not green or still running):\n " +
36
+ missingSubjects.map((subject) => notBuiltYetMessage(subject, channel)).join('\n '),
37
+ );
38
+ }
39
+
40
+ /**
41
+ * @param {object} request
42
+ * @param {string} request.distributionId the edition (named in the refusal)
43
+ * @param {object} request.declaredSources `firstPartySources`: roots the develop ref of each subject
44
+ * @returns {{ observe(subject: object): void, settle(): Promise<Map<string, string>> }}
45
+ * `observe` takes a subject with `repo`, `sourceCommit`, `imageRef` (release ref) and `contentHash`
46
+ * and starts its probe; `settle` waits for all of them and throws when any is absent or unreadable.
47
+ */
48
+ export function createDevelopImageProbe({
49
+ distributionId,
50
+ declaredSources,
51
+ distributionsRoot = editionsRoot(),
52
+ env = process.env,
53
+ fetchImpl = globalThis.fetch,
54
+ run = null,
55
+ concurrency = DEFAULT_PROBE_CONCURRENCY,
56
+ progress = null,
57
+ }) {
58
+ const declarations = loadRegistryCredentialDeclarations(distributionsRoot);
59
+ const clients = new Map();
60
+ const outcomes = [];
61
+ const waiting = [];
62
+ let active = 0;
63
+
64
+ function clientFor(subject) {
65
+ const { authority } = splitImageRef(subject.imageRef);
66
+ if (!clients.has(authority)) {
67
+ for (const [key, client] of registryClientsFor([subject], { declarations, env, fetchImpl, run })) {
68
+ clients.set(key, client);
69
+ }
70
+ }
71
+ return clients.get(authority);
72
+ }
73
+
74
+ async function probeOne(subject) {
75
+ const startedAt = Date.now();
76
+ const { repository } = splitImageRef(subject.imageRef);
77
+ try {
78
+ const { digest } = await clientFor(subject).getManifestByTag(repository, subject.contentHash);
79
+ progress?.(`[probe] ${subject.kind}:${subject.id} present ${Date.now() - startedAt}ms`);
80
+ return { subject, digest };
81
+ } catch (error) {
82
+ progress?.(`[probe] ${subject.kind}:${subject.id} ${isNotPublishedYet(error) ? 'ABSENT' : 'FAILED'} ${Date.now() - startedAt}ms`);
83
+ return { subject, error };
84
+ }
85
+ }
86
+
87
+ function next() {
88
+ const start = waiting.shift();
89
+ if (start !== undefined) {start();}
90
+ }
91
+
92
+ function schedule(subject) {
93
+ return new Promise((resolve) => {
94
+ const start = () => {
95
+ active += 1;
96
+ probeOne(subject).then((outcome) => {
97
+ active -= 1;
98
+ next();
99
+ resolve(outcome);
100
+ });
101
+ };
102
+ if (active < concurrency) {start();} else {waiting.push(start);}
103
+ });
104
+ }
105
+
106
+ return {
107
+ observe(subject) {
108
+ const developSubject = { ...subject, imageRef: developImageRefFor(subject, declaredSources) };
109
+ // Credentials resolve here, on the first subject of each registry, so a missing token stops the run now.
110
+ clientFor(developSubject);
111
+ outcomes.push(schedule(developSubject));
112
+ },
113
+ async settle() {
114
+ const settled = await Promise.all(outcomes);
115
+ const aborting = settled.find(({ error }) => error !== undefined && !isNotPublishedYet(error));
116
+ if (aborting) {throw aborting.error;}
117
+ const missing = settled.filter(({ error }) => error !== undefined).map(({ subject }) => subject);
118
+ if (missing.length > 0) {throw notBuiltYetError(distributionId, missing);}
119
+ return new Map(settled.map(({ subject, digest }) => [`${subject.kind}:${subject.id}`, digest]));
120
+ },
121
+ };
122
+ }