@xemahq/xema 0.26.0 → 0.28.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 (230) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/distribution-assets.manifest.json +1 -2
  3. package/dist/cli.js +2 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/dev.js +1 -0
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/down.d.ts.map +1 -1
  8. package/dist/commands/down.js +7 -1
  9. package/dist/commands/down.js.map +1 -1
  10. package/dist/commands/edition.d.ts +3 -0
  11. package/dist/commands/edition.d.ts.map +1 -0
  12. package/dist/commands/edition.js +46 -0
  13. package/dist/commands/edition.js.map +1 -0
  14. package/dist/commands/init.js +2 -2
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/serve.js +1 -0
  17. package/dist/commands/serve.js.map +1 -1
  18. package/dist/commands/up.js +1 -0
  19. package/dist/commands/up.js.map +1 -1
  20. package/dist/dev/env-defaults.d.ts.map +1 -1
  21. package/dist/dev/env-defaults.js +5 -2
  22. package/dist/dev/env-defaults.js.map +1 -1
  23. package/dist/dev/infra-preflight.d.ts +3 -2
  24. package/dist/dev/infra-preflight.d.ts.map +1 -1
  25. package/dist/dev/infra-preflight.js +18 -8
  26. package/dist/dev/infra-preflight.js.map +1 -1
  27. package/dist/edition/verify-image-evidence.bundle.mjs +54283 -0
  28. package/dist/edition/verify-image-evidence.bundle.provenance.json +2877 -0
  29. package/dist/runtime/config/schema.d.ts +1 -0
  30. package/dist/runtime/config/schema.d.ts.map +1 -1
  31. package/dist/runtime/custody/local-custody.d.ts +12 -0
  32. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  33. package/dist/runtime/custody/local-custody.js +22 -0
  34. package/dist/runtime/custody/local-custody.js.map +1 -0
  35. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  36. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  37. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  38. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  39. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  40. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  41. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  42. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  43. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  44. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  45. package/dist/runtime/custody/openbao-http-client.js +154 -0
  46. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  47. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  48. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  49. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  50. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  51. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  52. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  53. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  54. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  55. package/dist/runtime/edition/run-entry.d.ts +9 -0
  56. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  57. package/dist/runtime/edition/run-entry.js +36 -0
  58. package/dist/runtime/edition/run-entry.js.map +1 -0
  59. package/dist/runtime/edition/verb-map.d.ts +60 -0
  60. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  61. package/dist/runtime/edition/verb-map.js +146 -0
  62. package/dist/runtime/edition/verb-map.js.map +1 -0
  63. package/dist/runtime/env-seeder.d.ts +2 -0
  64. package/dist/runtime/env-seeder.d.ts.map +1 -1
  65. package/dist/runtime/env-seeder.js +18 -3
  66. package/dist/runtime/env-seeder.js.map +1 -1
  67. package/dist/runtime/launch.d.ts +2 -1
  68. package/dist/runtime/launch.d.ts.map +1 -1
  69. package/dist/runtime/launch.js +31 -7
  70. package/dist/runtime/launch.js.map +1 -1
  71. package/dist/runtime/paths.d.ts +1 -2
  72. package/dist/runtime/paths.d.ts.map +1 -1
  73. package/dist/runtime/paths.js +0 -10
  74. package/dist/runtime/paths.js.map +1 -1
  75. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  76. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  77. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  78. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  79. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  80. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  81. package/dist/runtime/runner/chart-preflight.js +30 -69
  82. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  83. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  84. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  85. package/dist/runtime/runner/chart-pull.js +96 -0
  86. package/dist/runtime/runner/chart-pull.js.map +1 -0
  87. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  88. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  89. package/dist/runtime/runner/chart-resolver.js +16 -62
  90. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  91. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  92. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  93. package/dist/runtime/runner/chart-set-lock.js +55 -99
  94. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  95. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  96. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  97. package/dist/runtime/runner/helm-cli.js +9 -7
  98. package/dist/runtime/runner/helm-cli.js.map +1 -1
  99. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  100. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  101. package/dist/runtime/runner/helm-runner.js +18 -16
  102. package/dist/runtime/runner/helm-runner.js.map +1 -1
  103. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  104. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  105. package/dist/runtime/sources/kinds/git.js +8 -5
  106. package/dist/runtime/sources/kinds/git.js.map +1 -1
  107. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  108. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  109. package/dist/runtime/sources/manifest-walk.js +8 -3
  110. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  111. package/dist/runtime/sources/schema.d.ts +3 -0
  112. package/dist/runtime/sources/schema.d.ts.map +1 -1
  113. package/dist/runtime/sources/schema.js +6 -1
  114. package/dist/runtime/sources/schema.js.map +1 -1
  115. package/dist/runtime/supervisor.d.ts +2 -0
  116. package/dist/runtime/supervisor.d.ts.map +1 -1
  117. package/dist/runtime/supervisor.js +1 -0
  118. package/dist/runtime/supervisor.js.map +1 -1
  119. package/edition/README.md +313 -0
  120. package/edition/assemble-producer-input.mjs +1251 -0
  121. package/edition/authorization-base.mjs +347 -0
  122. package/edition/biome-content.mjs +268 -0
  123. package/edition/build-biome-index.mjs +175 -0
  124. package/edition/build-platform-service-index.mjs +288 -0
  125. package/edition/build-runtime-image-source-index.mjs +821 -0
  126. package/edition/built-verifier.mjs +55 -0
  127. package/edition/catalogue-edition.mjs +49 -0
  128. package/edition/catalogue-fetch.mjs +64 -0
  129. package/edition/catalogue.mjs +135 -0
  130. package/edition/chart-set-source-commit.mjs +92 -0
  131. package/edition/check-committed-image-evidence.mjs +383 -0
  132. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  133. package/edition/comment-blanker.mjs +75 -0
  134. package/edition/connector-source-authority.mjs +60 -0
  135. package/edition/customer-definition.mjs +183 -0
  136. package/edition/customer-flags.mjs +7 -0
  137. package/edition/develop-customer-edition.mjs +99 -0
  138. package/edition/develop-edition-resolve.mjs +65 -0
  139. package/edition/develop-edition.mjs +274 -0
  140. package/edition/develop-freeze.mjs +173 -0
  141. package/edition/develop-image-coordinate.mjs +54 -0
  142. package/edition/develop-seal.mjs +77 -0
  143. package/edition/develop-snapshot-inputs.mjs +104 -0
  144. package/edition/dist-check.mjs +986 -0
  145. package/edition/distribution-evidence-set.mjs +987 -0
  146. package/edition/distribution-paths.mjs +191 -0
  147. package/edition/edition-artifact.mjs +284 -0
  148. package/edition/edition-biome-manifests.mjs +85 -0
  149. package/edition/edition-channel.mjs +6 -0
  150. package/edition/edition-chart-set.mjs +93 -0
  151. package/edition/edition-config.mjs +205 -0
  152. package/edition/edition-constants.mjs +57 -0
  153. package/edition/edition-contract.json +30 -0
  154. package/edition/edition-list.mjs +35 -0
  155. package/edition/edition-locks.mjs +69 -0
  156. package/edition/edition-protocol.mjs +228 -0
  157. package/edition/edition-workspace.mjs +187 -0
  158. package/edition/evidence-checkout.mjs +439 -0
  159. package/edition/evidence-grammars.mjs +16 -0
  160. package/edition/evidence-trust-pin.mjs +304 -0
  161. package/edition/evidence-trust-policy.mjs +294 -0
  162. package/edition/external-source-walk.mjs +463 -0
  163. package/edition/first-party-source-ids.mjs +59 -0
  164. package/edition/fleet-coordinate-finality.mjs +350 -0
  165. package/edition/git-source-auth.mjs +52 -0
  166. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  167. package/edition/image-evidence-replay-state.mjs +242 -0
  168. package/edition/image-evidence.mjs +1233 -0
  169. package/edition/kernel-contract-authority.mjs +443 -0
  170. package/edition/lib.mjs +3021 -0
  171. package/edition/no-follow-file.mjs +94 -0
  172. package/edition/oci-evidence-verbs.mjs +392 -0
  173. package/edition/oci-image-source-evidence.mjs +239 -0
  174. package/edition/oras.mjs +648 -0
  175. package/edition/pin-source.mjs +227 -0
  176. package/edition/plan-image-evidence-refresh.mjs +408 -0
  177. package/edition/platform-lock-readability.mjs +892 -0
  178. package/edition/platform-service-class.mjs +18 -0
  179. package/edition/platform-service-source-authority.mjs +105 -0
  180. package/edition/positive-closure.mjs +162 -0
  181. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  182. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  183. package/edition/produce-image-source-evidence.mjs +765 -0
  184. package/edition/producer-evidence-bundle.mjs +157 -0
  185. package/edition/registry-credentials.mjs +193 -0
  186. package/edition/release-catalogue.mjs +39 -0
  187. package/edition/release-channel-guard.mjs +109 -0
  188. package/edition/release-edition-artifact.mjs +157 -0
  189. package/edition/remedy-command.mjs +89 -0
  190. package/edition/repin-evidence-trust.mjs +101 -0
  191. package/edition/required-resource-provider.mjs +58 -0
  192. package/edition/resolve-chart-set.mjs +415 -0
  193. package/edition/resolve-customer-edition.mjs +288 -0
  194. package/edition/resolve-distribution.mjs +2318 -0
  195. package/edition/resolve-edition.mjs +186 -0
  196. package/edition/resolve-images.mjs +1606 -0
  197. package/edition/seal-image-digests.mjs +855 -0
  198. package/edition/sealed-evidence-accept.mjs +82 -0
  199. package/edition/source-authority.mjs +725 -0
  200. package/edition/source-closure.mjs +116 -0
  201. package/edition/strict-json.mjs +121 -0
  202. package/edition/string-order.mjs +5 -0
  203. package/edition/trust-policy-arming.mjs +120 -0
  204. package/edition/trusted-executable.mjs +166 -0
  205. package/edition/verifier-binding.mjs +45 -0
  206. package/edition/verifier-files.mjs +8 -0
  207. package/edition/verifier-kernel-schemas.mjs +22 -0
  208. package/edition/verifier-provenance.mjs +64 -0
  209. package/edition/verify-image-evidence.mjs +458 -0
  210. package/edition/verify-image-platforms.mjs +495 -0
  211. package/edition/why.mjs +56 -0
  212. package/edition/workspace-sections.mjs +195 -0
  213. package/examples/06-biome-sources.yaml +1 -0
  214. package/package.json +14 -6
  215. package/dist/commands/dist.d.ts +0 -16
  216. package/dist/commands/dist.d.ts.map +0 -1
  217. package/dist/commands/dist.js +0 -182
  218. package/dist/commands/dist.js.map +0 -1
  219. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  220. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  221. package/dist/runtime/distribution/available-biome.js +0 -156
  222. package/dist/runtime/distribution/available-biome.js.map +0 -1
  223. package/dist/runtime/distribution/compose.d.ts +0 -23
  224. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  225. package/dist/runtime/distribution/compose.js +0 -48
  226. package/dist/runtime/distribution/compose.js.map +0 -1
  227. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  228. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  229. package/dist/runtime/runner/chart-trust.js +0 -270
  230. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,495 @@
1
+ #!/usr/bin/env node
2
+ // verify-image-platforms — delivery-time platform verification (plan decision D4).
3
+ //
4
+ // node repos-infra/xema-distributions/verify-image-platforms.mjs <distributionId>
5
+ //
6
+ // `images.lock.json` DECLARES `requiredPlatforms` per subject. Resolve time
7
+ // cannot assert those platforms exist: arm64 lands ASYNCHRONOUSLY in a separate
8
+ // `release-arm64.yaml` triggered by `workflow_run` after the amd64 build
9
+ // succeeds, and only then does `docker buildx imagetools create` merge it into a
10
+ // multi-arch index on the canonical `:<contentHash>` tag. So the manifest a lock
11
+ // points at legitimately changes shape minutes-to-hours after the lock resolved.
12
+ //
13
+ // DELIVERY is therefore the only place the claim can be checked, and it is
14
+ // checked against the REAL registry, fail-closed. A distribution whose arm64
15
+ // supplement has not landed is simply not deliverable yet. That is what makes
16
+ // standing the arm64 lane down (`SKIP_SUPPLEMENTARY_ARCHES`) SAFE: it surfaces
17
+ // as a loudly-undeliverable distribution rather than a silently amd64-only one.
18
+ //
19
+ // This is NOT a substitute for the offline signed-evidence verifier, and the
20
+ // offline verifier is not a substitute for this. The offline verifier proves the
21
+ // PRODUCER SIGNED a claim about a platform set; this proves the REGISTRY IS
22
+ // SERVING those platforms at the coordinate the lock names. Neither implies the
23
+ // other: evidence can be signed for an image the arm64 merge never reached, and
24
+ // a registry can serve an index nobody attested.
25
+ //
26
+ // What is proven, per required platform, is a fully content-addressed chain:
27
+ //
28
+ // tag -> index bytes (hash-checked against Docker-Content-Digest)
29
+ // -> child manifest fetched BY DIGEST (bytes must hash to that digest)
30
+ // -> image config fetched BY DIGEST (bytes must hash to that digest)
31
+ // -> the config's own os/architecture/variant
32
+ //
33
+ // The last hop is load-bearing, not belt-and-braces: D3 merges a separately
34
+ // built `:<contentHash>-arm64` tag into the index, so a mislabelled or
35
+ // mis-sourced merge is the realistic failure. An index descriptor that CLAIMS
36
+ // linux/arm64 while the image config underneath says amd64 is exactly the bug
37
+ // that would otherwise ship an unbootable appliance. We do not take the
38
+ // descriptor's word for it.
39
+
40
+ import { readFileSync } from 'node:fs';
41
+ import { join } from 'node:path';
42
+
43
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
44
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule } from './edition-workspace.mjs';
45
+ import {
46
+ loadKernelDistributionSchemas,
47
+ validateImmutableImageLock,
48
+ } from './lib.mjs';
49
+ import { OrasRegistry } from './oras.mjs';
50
+ import {
51
+ REGISTRY_CREDENTIALS_FILE,
52
+ loadRegistryCredentialDeclarations,
53
+ resolveRegistryCredential,
54
+ splitImageRef,
55
+ } from './registry-credentials.mjs';
56
+ import { assertReleaseContent, assertReleaseEditionTree } from './release-channel-guard.mjs';
57
+
58
+ if (isEntryModule(import.meta.url)) {
59
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Kernel]);
60
+ }
61
+
62
+ /**
63
+ * BuildKit publishes SBOM/provenance attestations as extra children of the
64
+ * image index, carrying the placeholder platform `unknown/unknown` and the
65
+ * `vnd.docker.reference.type=attestation-manifest` annotation. They are not
66
+ * runnable platforms and must never be counted as one — nor may they be
67
+ * silently ignored under any OTHER shape, which is why an unrecognised child is
68
+ * a hard failure rather than a skip.
69
+ */
70
+ const ATTESTATION_REFERENCE_TYPE_ANNOTATION = 'vnd.docker.reference.type';
71
+ const ATTESTATION_REFERENCE_TYPE = 'attestation-manifest';
72
+ const UNKNOWN_PLATFORM_TOKEN = 'unknown';
73
+
74
+ const DEFAULT_CONCURRENCY = 6;
75
+
76
+ /**
77
+ * `linux/arm64` and `linux/arm64/v8` are the SAME platform: v8 is arm64's
78
+ * implied variant, and containerd's platform matcher normalizes it away. The
79
+ * three sources compared here disagree about spelling it — a lock declares
80
+ * `linux/arm64`, an index descriptor written by buildx often carries
81
+ * `variant: "v8"`, and the image config may carry either — so without this the
82
+ * verifier would report a missing arm64 that is plainly present.
83
+ *
84
+ * This is the ONLY normalization. It is exact and closed: any other variant
85
+ * difference (e.g. `arm/v7` vs `arm/v6`) stays a real mismatch and fails.
86
+ */
87
+ const IMPLIED_VARIANTS = new Map([['arm64', 'v8']]);
88
+
89
+ function normalizedVariant(architecture, variant) {
90
+ if (!variant) {return undefined;}
91
+ return IMPLIED_VARIANTS.get(architecture) === variant ? undefined : variant;
92
+ }
93
+
94
+ /**
95
+ * One spelling of a platform identity, used for the lock's declaration, the
96
+ * index descriptor and the image config alike — the comparison is only sound if
97
+ * all three are normalized the same way.
98
+ */
99
+ export function platformIdentity(platform) {
100
+ const variant = normalizedVariant(platform.architecture, platform.variant);
101
+ return variant
102
+ ? `${platform.os}/${platform.architecture}/${variant}`
103
+ : `${platform.os}/${platform.architecture}`;
104
+ }
105
+
106
+ function isAttestationChild(child) {
107
+ if (
108
+ child.annotations?.[ATTESTATION_REFERENCE_TYPE_ANNOTATION] ===
109
+ ATTESTATION_REFERENCE_TYPE
110
+ ) {
111
+ return true;
112
+ }
113
+ return (
114
+ child.platform?.os === UNKNOWN_PLATFORM_TOKEN &&
115
+ child.platform?.architecture === UNKNOWN_PLATFORM_TOKEN
116
+ );
117
+ }
118
+
119
+ function assertChildDescriptor(child, index, label) {
120
+ if (typeof child !== 'object' || child === null || Array.isArray(child)) {
121
+ throw new Error(`${label} manifests[${index}] is not an object`);
122
+ }
123
+ if (typeof child.digest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(child.digest)) {
124
+ throw new Error(
125
+ `${label} manifests[${index}] has no canonical sha256 digest`,
126
+ );
127
+ }
128
+ if (
129
+ typeof child.platform !== 'object' ||
130
+ child.platform === null ||
131
+ typeof child.platform.os !== 'string' ||
132
+ typeof child.platform.architecture !== 'string' ||
133
+ (child.platform.variant !== undefined &&
134
+ typeof child.platform.variant !== 'string')
135
+ ) {
136
+ throw new Error(
137
+ `${label} manifests[${index}] (${child.digest}) declares no usable platform — ` +
138
+ 'a child that is neither a real platform nor a BuildKit attestation makes the index unreadable',
139
+ );
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Read the image config a manifest points at and return the platform IT
145
+ * declares. The config blob is fetched by digest and hash-verified, so this is
146
+ * the authoritative platform of the actual image content.
147
+ */
148
+ async function readManifestConfigPlatform(client, repository, manifest, label) {
149
+ const config = manifest.document.config;
150
+ if (
151
+ typeof config !== 'object' ||
152
+ config === null ||
153
+ typeof config.digest !== 'string'
154
+ ) {
155
+ throw new Error(`${label} has no image config descriptor`);
156
+ }
157
+ const bytes = await client.getBlob(repository, config.digest);
158
+ let document;
159
+ try {
160
+ document = JSON.parse(bytes.toString('utf8'));
161
+ } catch (error) {
162
+ throw new Error(`${label} image config is not valid JSON: ${error.message}`);
163
+ }
164
+ if (
165
+ typeof document !== 'object' ||
166
+ document === null ||
167
+ typeof document.os !== 'string' ||
168
+ typeof document.architecture !== 'string' ||
169
+ (document.variant !== undefined && typeof document.variant !== 'string')
170
+ ) {
171
+ throw new Error(`${label} image config declares no os/architecture`);
172
+ }
173
+ return {
174
+ os: document.os,
175
+ architecture: document.architecture,
176
+ ...(document.variant === undefined ? {} : { variant: document.variant }),
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Resolve the set of platforms a published coordinate ACTUALLY serves.
182
+ * Every returned platform is proven by a hash-verified image config, never by
183
+ * an index descriptor's self-declaration alone.
184
+ */
185
+ export async function resolvePublishedPlatforms({ client, repository, tag, coordinate }) {
186
+ const top = await client.getManifestByTag(repository, tag);
187
+ const platforms = [];
188
+
189
+ if (!top.isIndex) {
190
+ // A single-arch manifest. Legitimate before the arm64 supplement merges;
191
+ // never sufficient for a multi-platform requirement.
192
+ const platform = await readManifestConfigPlatform(
193
+ client,
194
+ repository,
195
+ top,
196
+ `${coordinate} manifest ${top.digest}`,
197
+ );
198
+ platforms.push({
199
+ platform,
200
+ manifestDigest: top.digest,
201
+ declaredBy: 'image-config',
202
+ });
203
+ return { indexDigest: top.digest, isIndex: false, platforms };
204
+ }
205
+
206
+ const children = top.document.manifests;
207
+ if (!Array.isArray(children) || children.length === 0) {
208
+ throw new Error(`${coordinate} index ${top.digest} declares no child manifests`);
209
+ }
210
+ const label = `${coordinate} index ${top.digest}`;
211
+ const runnable = [];
212
+ for (const [index, child] of children.entries()) {
213
+ assertChildDescriptor(child, index, label);
214
+ if (isAttestationChild(child)) {continue;}
215
+ if (
216
+ child.platform.os === UNKNOWN_PLATFORM_TOKEN ||
217
+ child.platform.architecture === UNKNOWN_PLATFORM_TOKEN
218
+ ) {
219
+ throw new Error(
220
+ `${label} manifests[${index}] (${child.digest}) is half-unknown ` +
221
+ `("${platformIdentity(child.platform)}") and carries no attestation annotation`,
222
+ );
223
+ }
224
+ runnable.push(child);
225
+ }
226
+ if (runnable.length === 0) {
227
+ throw new Error(
228
+ `${label} contains only attestation manifests and no runnable platform`,
229
+ );
230
+ }
231
+
232
+ for (const child of runnable) {
233
+ const childManifest = await client.getManifestByDigest(repository, child.digest);
234
+ if (childManifest.isIndex) {
235
+ throw new Error(
236
+ `${label} child ${child.digest} is itself an index; nested indexes are refused`,
237
+ );
238
+ }
239
+ const actual = await readManifestConfigPlatform(
240
+ client,
241
+ repository,
242
+ childManifest,
243
+ `${coordinate} child ${child.digest}`,
244
+ );
245
+ const declared = platformIdentity(child.platform);
246
+ const proven = platformIdentity(actual);
247
+ if (declared !== proven) {
248
+ throw new Error(
249
+ `${label} child ${child.digest} is described as "${declared}" but its image ` +
250
+ `config declares "${proven}" — the index misdescribes its own content`,
251
+ );
252
+ }
253
+ platforms.push({
254
+ platform: actual,
255
+ manifestDigest: child.digest,
256
+ declaredBy: 'image-config',
257
+ });
258
+ }
259
+
260
+ const seen = new Set();
261
+ for (const entry of platforms) {
262
+ const identity = platformIdentity(entry.platform);
263
+ if (seen.has(identity)) {
264
+ throw new Error(`${label} serves duplicate platform "${identity}"`);
265
+ }
266
+ seen.add(identity);
267
+ }
268
+ return { indexDigest: top.digest, isIndex: true, platforms };
269
+ }
270
+
271
+ function missingPlatformMessage({ distributionId, subject, coordinate, missing, present }) {
272
+ return (
273
+ `distribution "${distributionId}" is NOT deliverable: subject ` +
274
+ `${subject.kind}:${subject.id} requires platform${missing.length > 1 ? 's' : ''} ` +
275
+ `${missing.join(', ')}, but the published manifest at ${coordinate} serves only ` +
276
+ `${present.length > 0 ? present.join(', ') : '(nothing runnable)'}. ` +
277
+ 'The most likely cause is that the supplementary-architecture lane has not run ' +
278
+ 'or has not finished for this commit: arm64 builds in a separate release-arm64 ' +
279
+ 'workflow triggered by workflow_run AFTER the amd64 build succeeds, and only then ' +
280
+ 'is it merged into the canonical tag as a multi-arch index. Check that ' +
281
+ "release-arm64 ran green for this repository at commit " +
282
+ `${subject.sourceCommit}, and that SKIP_SUPPLEMENTARY_ARCHES is not standing the ` +
283
+ 'lane down.'
284
+ );
285
+ }
286
+
287
+ async function mapWithConcurrency(items, limit, worker) {
288
+ const results = new Array(items.length);
289
+ let cursor = 0;
290
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
291
+ while (cursor < items.length) {
292
+ const index = cursor;
293
+ cursor += 1;
294
+ results[index] = await worker(items[index], index);
295
+ }
296
+ });
297
+ await Promise.all(runners);
298
+ return results;
299
+ }
300
+
301
+ /**
302
+ * Verify that EVERY subject's published manifest carries EVERY platform its
303
+ * `requiredPlatforms` declares. Throws on the first uncertainty of any kind.
304
+ *
305
+ * Returns a per-subject report suitable for embedding in a delivery manifest.
306
+ */
307
+ export async function verifyDistributionPlatforms({
308
+ imageLock,
309
+ // ONE ROOT FOR BOTH READS IN THIS FILE. This defaulted to
310
+ // `dataRoot()` while its sibling `loadImageLockForVerification`
311
+ // ninety lines below defaulted to `editionsRoot()` — two defaults for one
312
+ // fact, in one file, and they DIFFER the moment a root is configured. The
313
+ // lock then came from the configured root and the registry CREDENTIALS from
314
+ // this repository.
315
+ //
316
+ // The two halves fail with opposite loudness, which is why only the quiet
317
+ // one survived: a lock read against the wrong root is an ENOENT, and a
318
+ // credential read against the wrong root simply authenticates against a
319
+ // different declaration file and proceeds. `seal-image-digests.mjs` already
320
+ // compensated at ITS call site (`distributionsRoot: root`) and pinned it
321
+ // behaviourally in `seal-image-digests.test.mjs`; the other two call sites —
322
+ // `deliver-images.mjs` and this file's own `main()` — did not, so the
323
+ // compensation covered one caller rather than the seam.
324
+ //
325
+ // With no root configured `editionsRoot() === dataRoot()`, so this is
326
+ // byte-identical for every unconfigured run.
327
+ distributionsRoot = editionsRoot(),
328
+ env = process.env,
329
+ fetchImpl = globalThis.fetch,
330
+ // The oras runner `(args, { stdin }) => Buffer`; null runs the pinned oras.
331
+ run = null,
332
+ concurrency = DEFAULT_CONCURRENCY,
333
+ // false (the release): the first subject that fails aborts the run. true:
334
+ // every subject is verified and the failures are RETURNED beside the
335
+ // successes, so a caller can name every missing coordinate at once. It never
336
+ // swallows one: the caller decides what each failure means.
337
+ collectFailures = false,
338
+ }) {
339
+ const declarations = loadRegistryCredentialDeclarations(distributionsRoot);
340
+
341
+ // Resolve every credential BEFORE the first request. A run that would die on
342
+ // subject 300 for a missing token must die on subject 0 instead.
343
+ const clients = new Map();
344
+ for (const subject of imageLock.subjects) {
345
+ const { authority } = splitImageRef(subject.imageRef);
346
+ if (clients.has(authority)) {continue;}
347
+ const credential = resolveRegistryCredential({ authority, declarations, env });
348
+ clients.set(
349
+ authority,
350
+ new OrasRegistry({ credential, fetchImpl, ...(run ? { run } : {}) }),
351
+ );
352
+ }
353
+
354
+ const failures = [];
355
+ const verified = await mapWithConcurrency(
356
+ imageLock.subjects,
357
+ concurrency,
358
+ async (subject) => {
359
+ try {
360
+ return await verifySubject(subject);
361
+ } catch (error) {
362
+ if (!collectFailures) {throw error;}
363
+ failures.push({ subject, error });
364
+ return undefined;
365
+ }
366
+ },
367
+ );
368
+ const subjects = verified.filter((entry) => entry !== undefined);
369
+
370
+ async function verifySubject(subject) {
371
+ const { authority, repository } = splitImageRef(subject.imageRef);
372
+ const coordinate = `${subject.imageRef}:${subject.contentHash}`;
373
+ // A subject that requires no platform cannot be verified — every check
374
+ // would pass vacuously and the image would ship unexamined.
375
+ if (
376
+ !Array.isArray(subject.requiredPlatforms) ||
377
+ subject.requiredPlatforms.length === 0
378
+ ) {
379
+ throw new Error(
380
+ `subject ${subject.kind}:${subject.id} declares no requiredPlatforms; ` +
381
+ 'there is nothing to verify and a vacuous pass is not a verification',
382
+ );
383
+ }
384
+ const client = clients.get(authority);
385
+ const published = await resolvePublishedPlatforms({
386
+ client,
387
+ repository,
388
+ tag: subject.contentHash,
389
+ coordinate,
390
+ });
391
+ const present = published.platforms.map((entry) =>
392
+ platformIdentity(entry.platform),
393
+ );
394
+ const presentSet = new Set(present);
395
+ const required = subject.requiredPlatforms.map(platformIdentity);
396
+ const missing = required.filter((identity) => !presentSet.has(identity));
397
+ if (missing.length > 0) {
398
+ throw new Error(
399
+ missingPlatformMessage({
400
+ distributionId: imageLock.distributionId,
401
+ subject,
402
+ coordinate,
403
+ missing,
404
+ present,
405
+ }),
406
+ );
407
+ }
408
+ return {
409
+ subjectKind: subject.kind,
410
+ subjectId: subject.id,
411
+ coordinate,
412
+ manifestDigest: published.indexDigest,
413
+ multiArchIndex: published.isIndex,
414
+ requiredPlatforms: required,
415
+ publishedPlatforms: present.sort(),
416
+ platformManifestDigests: published.platforms
417
+ .map((entry) => ({
418
+ platform: platformIdentity(entry.platform),
419
+ manifestDigest: entry.manifestDigest,
420
+ }))
421
+ .sort((left, right) =>
422
+ left.platform < right.platform ? -1 : left.platform > right.platform ? 1 : 0,
423
+ ),
424
+ };
425
+ }
426
+
427
+ return {
428
+ distributionId: imageLock.distributionId,
429
+ verifiedAt: new Date().toISOString(),
430
+ subjectCount: subjects.length,
431
+ registries: [...clients.keys()].sort(),
432
+ subjects,
433
+ ...(collectFailures ? { failures } : {}),
434
+ };
435
+ }
436
+
437
+ export async function loadImageLockForVerification(
438
+ distributionId,
439
+ distributionsRoot = editionsRoot(),
440
+ ) {
441
+ assertCanonicalDistributionId(distributionId);
442
+ const schemas = await loadKernelDistributionSchemas();
443
+ const editionDir = join(distributionsRoot, distributionId);
444
+ assertReleaseEditionTree(editionDir);
445
+ const raw = JSON.parse(readFileSync(join(editionDir, 'images.lock.json'), 'utf8'));
446
+ assertReleaseContent(raw, `${editionDir}/images.lock.json`);
447
+ return validateImmutableImageLock(schemas.parseImageLock(raw), schemas);
448
+ }
449
+
450
+ async function main() {
451
+ // `--editions-root <dir>` is consumed by the ONE shared resolver in
452
+ // `lib.mjs` (off `process.argv`), so it must be stripped before the
453
+ // positional parse below. It was NOT, and the arity check rejected it: the
454
+ // flag this whole plane is built around could never be passed to this
455
+ // command at all, while `editionsRoot()` had already honoured it.
456
+ const [distributionId, ...rest] = process.argv.slice(2);
457
+ if (!distributionId || rest.length > 0) {
458
+ throw new Error(
459
+ 'usage: xema edition verify-platforms ' +
460
+ `<distributionId>`,
461
+ );
462
+ }
463
+ // ONE root, resolved once, passed to BOTH reads explicitly. The defaults
464
+ // would now agree, but a caller that states the root cannot be made wrong by
465
+ // a later default drifting — which is exactly how the two in this file came
466
+ // to disagree.
467
+ const root = editionsRoot();
468
+ const imageLock = await loadImageLockForVerification(distributionId, root);
469
+ const report = await verifyDistributionPlatforms({
470
+ imageLock,
471
+ distributionsRoot: root,
472
+ });
473
+ const indexes = report.subjects.filter((subject) => subject.multiArchIndex).length;
474
+ console.log(
475
+ `[dist:verify-platforms] distribution "${report.distributionId}": ${report.subjectCount} ` +
476
+ `subjects verified against ${report.registries.join(', ')} ` +
477
+ `(${indexes} multi-arch indexes, ${report.subjectCount - indexes} single-manifest).`,
478
+ );
479
+ for (const subject of report.subjects) {
480
+ console.log(
481
+ ` ok ${subject.subjectKind}:${subject.subjectId} -> ${subject.publishedPlatforms.join(', ')}`,
482
+ );
483
+ }
484
+ }
485
+
486
+ if (isEntryModule(import.meta.url)) {
487
+ main().catch((error) => {
488
+ console.error(`[dist:verify-platforms] ${error.message}`);
489
+ console.error(
490
+ `[dist:verify-platforms] platform verification is fail-closed: nothing above was assumed. ` +
491
+ `Registry credentials are declared in ${REGISTRY_CREDENTIALS_FILE}.`,
492
+ );
493
+ process.exit(1);
494
+ });
495
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // why — explain why a biome or platform service is in an edition.
3
+ //
4
+ // Usage:
5
+ // node why.mjs <edition-id> <biome-id | platform-service-name>
6
+ //
7
+ // Reads ONLY the committed `<edition>/distribution.lock.json`, through the
8
+ // kernel's tolerant reader, and renders its positive-closure `resolution`
9
+ // section: every root class the entity was named in, and the one chain of
10
+ // REQUIRED edges (dependency or resource) that pulled it in from a root. The
11
+ // output is deterministic — it is a rendering of committed bytes, never a
12
+ // re-resolution — so two runs over one lock print the same text.
13
+ //
14
+ // Exit status: 0 when the entity is in the edition and explained, 1 when it is
15
+ // not in the edition or the edition records no provenance (a selector-model
16
+ // edition), 2 on a usage or read error.
17
+
18
+ import { readFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+
21
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule } from './edition-workspace.mjs';
22
+ import { loadKernelDistributionSchemas } from './lib.mjs';
23
+ import { explainInclusion } from './positive-closure.mjs';
24
+
25
+ if (isEntryModule(import.meta.url)) {
26
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Editions, WorkspaceRoot.Kernel]);
27
+ }
28
+
29
+ async function main() {
30
+ const [editionId, name, ...rest] = process.argv.slice(2);
31
+ if (!editionId || !name || rest.length > 0) {
32
+ console.error(
33
+ 'usage: node why.mjs <edition-id> <biome-id | platform-service-name>',
34
+ );
35
+ return 2;
36
+ }
37
+ const schemas = await loadKernelDistributionSchemas();
38
+ const lock = schemas.parseDistributionLockForReading(
39
+ JSON.parse(
40
+ readFileSync(join(editionsRoot(), editionId, 'distribution.lock.json'), 'utf8'),
41
+ ),
42
+ );
43
+ const { found, lines } = explainInclusion(schemas, lock, name);
44
+ for (const line of lines) {console.log(line);}
45
+ return found ? 0 : 1;
46
+ }
47
+
48
+ main().then(
49
+ (code) => {
50
+ process.exitCode = code;
51
+ },
52
+ (error) => {
53
+ console.error(`[why] ${error.message}`);
54
+ process.exitCode = 2;
55
+ },
56
+ );