@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,237 @@
1
+ // catalogue — what an offering's edition artifact offers to a customer edition.
2
+ //
3
+ // `catalogue.json` is one more titled layer of an offering's edition artifact
4
+ // (edition-artifact.mjs): the availability, pins, image digests, chart-set and
5
+ // declared ceiling of the offering, and NOTHING that can add a component. A
6
+ // customer edition is resolved against it (resolve-customer-edition.mjs) by the
7
+ // digest of the artifact that carries it.
8
+ //
9
+ // `channel` selects the `kernel` row's shape and nothing else (kernel-pin-source.mjs): a release row names the published
10
+ // package (name, version, integrity); a develop row names the member checkout (repositoryUrl, sourceCommit, contentDigest).
11
+
12
+ import { writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+
15
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
16
+ import { EDITION_OFFERING_LAYERS, EditionChannel } from './edition-constants.mjs';
17
+ import { isKernelFromMember } from './kernel-pin-source.mjs';
18
+ import { serializeJson } from './lib.mjs';
19
+ import { verifiedPlatformServiceIndex } from './platform-service-ownership.mjs';
20
+
21
+ export const CATALOGUE_FILE = EDITION_OFFERING_LAYERS.catalogue;
22
+ // v2 (edition protocol v8): `ceiling.storePublisherTrust`, the Store publishers the offering
23
+ // vouches for, from which a customer definition selects (`storePublisherAnchorKeys`).
24
+ export const CATALOGUE_SCHEMA = 'xema.edition-catalogue/v2';
25
+
26
+ const TOP_KEYS = ['schema', 'offeringId', 'channel', 'biomes', 'sourceTreeIds', 'platformServices', 'images', 'chartSet', 'contractAuthority', 'kernel', 'ceiling'];
27
+ const IMAGES_KEYS = ['sourceHashAlgorithm', 'subjects', 'platformServiceBindings', 'webBiomes'];
28
+ const CEILING_KEYS = ['trustPolicy', 'trustCeiling', 'storePublisherTrust'];
29
+ const KERNEL_KEYS = ['name', 'version', 'integrity'];
30
+ // A develop catalogue's kernel is the pinned member checkout, not a published package: its identity, no version.
31
+ const DEVELOP_KERNEL_KEYS = ['repositoryUrl', 'sourceCommit', 'contentDigest'];
32
+ const KERNEL_PACKAGE = '@xemahq/contracts';
33
+ const EXACT_SEMVER = /^\d+\.\d+\.\d+$/;
34
+ const NPM_INTEGRITY = /^sha512-[A-Za-z0-9+/]{86}==$/;
35
+ const DIGEST = /^sha256:[a-f0-9]{64}$/;
36
+ const GIT_TREE_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
37
+
38
+ export class CatalogueError extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = 'CatalogueError';
42
+ }
43
+ }
44
+
45
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
46
+
47
+ function exactKeys(value, keys, where) {
48
+ if (!isObject(value) || Object.keys(value).sort().join() !== [...keys].sort().join()) {
49
+ throw new CatalogueError(`${where} must be exactly {${keys.join(', ')}}`);
50
+ }
51
+ }
52
+
53
+ /** The strict parse of a catalogue: exact keys, closed vocabularies, every image row sealed. Returns it unchanged. */
54
+ export function parseCatalogue(value) {
55
+ exactKeys(value, TOP_KEYS, CATALOGUE_FILE);
56
+ if (value.schema !== CATALOGUE_SCHEMA) {
57
+ throw new CatalogueError(`${CATALOGUE_FILE}: schema is ${JSON.stringify(value.schema)}, expected ${JSON.stringify(CATALOGUE_SCHEMA)}`);
58
+ }
59
+ assertCanonicalDistributionId(value.offeringId);
60
+ if (!Object.values(EditionChannel).includes(value.channel)) {
61
+ throw new CatalogueError(`${CATALOGUE_FILE}: channel must be one of ${Object.values(EditionChannel).join(' | ')}`);
62
+ }
63
+ if (!Array.isArray(value.biomes) || value.biomes.some((row) => !isObject(row) || typeof row.id !== 'string')) {
64
+ throw new CatalogueError(`${CATALOGUE_FILE}: biomes must be an array of biome-index rows`);
65
+ }
66
+ // `sourceTreeIds` is the vendor's git tree id of each biome at its pinned commit. A customer holds no
67
+ // checkout of the vendor source, so it takes these ON THIS CATALOGUE'S AUTHORITY (biome-content.mjs).
68
+ if (!isObject(value.sourceTreeIds)) {throw new CatalogueError(`${CATALOGUE_FILE}: sourceTreeIds must map each biome id to its git tree id`);}
69
+ const rowIds = value.biomes.map((row) => row.id).sort();
70
+ const treeIdKeys = Object.keys(value.sourceTreeIds).sort();
71
+ if (JSON.stringify(rowIds) !== JSON.stringify(treeIdKeys)) {
72
+ throw new CatalogueError(`${CATALOGUE_FILE}: sourceTreeIds must name exactly the biomes of \`biomes\` (biomes: ${rowIds.join(',')}; tree ids: ${treeIdKeys.join(',')})`);
73
+ }
74
+ for (const [biomeId, treeId] of Object.entries(value.sourceTreeIds)) {
75
+ if (!GIT_TREE_ID.test(String(treeId))) {throw new CatalogueError(`${CATALOGUE_FILE}: sourceTreeIds.${biomeId} is not a git tree id (40 or 64 lowercase hex)`);}
76
+ }
77
+ if (!Array.isArray(value.platformServices) || value.platformServices.some((row) => !isObject(row) || typeof row.name !== 'string')) {
78
+ throw new CatalogueError(`${CATALOGUE_FILE}: platformServices must be an array of platform-service-index rows`);
79
+ }
80
+ exactKeys(value.images, IMAGES_KEYS, `${CATALOGUE_FILE} images`);
81
+ for (const key of IMAGES_KEYS.slice(1)) {
82
+ if (!Array.isArray(value.images[key])) {throw new CatalogueError(`${CATALOGUE_FILE}: images.${key} must be an array`);}
83
+ }
84
+ const unsealed = value.images.subjects.filter((subject) => !DIGEST.test(String(subject?.imageDigest)));
85
+ if (unsealed.length > 0) {
86
+ throw new CatalogueError(`${CATALOGUE_FILE}: ${unsealed.length} image rows carry no imageDigest (first: ${unsealed[0]?.kind}:${unsealed[0]?.id}); a catalogue offers sealed images only`);
87
+ }
88
+ if (!isObject(value.chartSet)) {throw new CatalogueError(`${CATALOGUE_FILE}: chartSet must be the offering's chart-set lock`);}
89
+ if (!isObject(value.contractAuthority)) {throw new CatalogueError(`${CATALOGUE_FILE}: contractAuthority must be the runtime image source index's authority`);}
90
+ if (isKernelFromMember(value.channel)) {
91
+ // develop: the row is the contract authority's own checkout identity; it never names a version or an integrity.
92
+ exactKeys(value.kernel, DEVELOP_KERNEL_KEYS, `${CATALOGUE_FILE} kernel (develop)`);
93
+ for (const key of DEVELOP_KERNEL_KEYS) {
94
+ if (typeof value.kernel[key] !== 'string' || value.kernel[key] !== value.contractAuthority[key]) {
95
+ throw new CatalogueError(`${CATALOGUE_FILE}: a develop catalogue's kernel.${key} must equal its contractAuthority.${key}`);
96
+ }
97
+ }
98
+ } else {
99
+ exactKeys(value.kernel, KERNEL_KEYS, `${CATALOGUE_FILE} kernel`);
100
+ if (value.kernel.name !== KERNEL_PACKAGE) {throw new CatalogueError(`${CATALOGUE_FILE}: kernel.name must be ${KERNEL_PACKAGE}, not ${JSON.stringify(value.kernel.name)}`);}
101
+ if (typeof value.kernel.version !== 'string' || !EXACT_SEMVER.test(value.kernel.version)) {
102
+ throw new CatalogueError(`${CATALOGUE_FILE}: kernel.version must be an exact x.y.z version, not ${JSON.stringify(value.kernel.version)}`);
103
+ }
104
+ if (typeof value.kernel.integrity !== 'string' || !NPM_INTEGRITY.test(value.kernel.integrity)) {
105
+ throw new CatalogueError(`${CATALOGUE_FILE}: kernel.integrity must be an npm sha512 integrity (sha512-<base64>)`);
106
+ }
107
+ }
108
+ exactKeys(value.ceiling, CEILING_KEYS, `${CATALOGUE_FILE} ceiling`);
109
+ return value;
110
+ }
111
+
112
+ /**
113
+ * The catalogue of one resolved offering. Biome and platform-service rows are
114
+ * exactly those the offering's distribution lock ships (an offering that draws
115
+ * from a declared source is not a vendor offering and is refused).
116
+ */
117
+ export function buildCatalogue({
118
+ offeringId,
119
+ channel,
120
+ distributionLock,
121
+ imageLock,
122
+ chartSetLock,
123
+ biomeIndex,
124
+ platformServiceIndex,
125
+ contractAuthority,
126
+ kernel,
127
+ leaf,
128
+ sourceTreeIds,
129
+ readManifest,
130
+ }) {
131
+ const sourced = distributionLock.biomes.filter((biome) => biome.source !== undefined);
132
+ if (sourced.length > 0) {
133
+ throw new CatalogueError(`offering "${offeringId}" draws ${sourced.map((b) => b.id).join(', ')} from a declared source; only a vendor offering publishes a catalogue`);
134
+ }
135
+ const shippedBiomes = new Set(distributionLock.biomes.map((biome) => biome.id));
136
+ const shippedServices = new Set(distributionLock.platformServices.map((service) => service.name));
137
+ const biomes = biomeIndex.filter((row) => shippedBiomes.has(row.id));
138
+ const missing = [...shippedBiomes].filter((id) => !biomes.some((row) => row.id === id));
139
+ if (missing.length > 0) {throw new CatalogueError(`offering "${offeringId}": the biome index has no row for ${missing.join(', ')}`);}
140
+ // The customer trusts this catalogue's ownership rows without sources, so they are
141
+ // re-derived from the pinned manifests here, before the catalogue seals.
142
+ const platformServices = verifiedPlatformServiceIndex(platformServiceIndex, { names: shippedServices, readManifest }).services.filter((row) => shippedServices.has(row.name));
143
+ return parseCatalogue({
144
+ schema: CATALOGUE_SCHEMA,
145
+ offeringId,
146
+ channel,
147
+ biomes: [...biomes].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
148
+ sourceTreeIds,
149
+ platformServices: [...platformServices].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
150
+ images: {
151
+ sourceHashAlgorithm: imageLock.sourceHashAlgorithm,
152
+ subjects: imageLock.subjects,
153
+ platformServiceBindings: imageLock.platformServiceBindings,
154
+ webBiomes: imageLock.webBiomes,
155
+ },
156
+ chartSet: chartSetLock,
157
+ contractAuthority,
158
+ kernel,
159
+ ceiling: {
160
+ trustPolicy: leaf?.trustPolicy ?? null,
161
+ trustCeiling: leaf?.trustCeiling ?? null,
162
+ // The Store publishers the offering's lock vouches for: a customer edition copies only those it selects.
163
+ storePublisherTrust: distributionLock.storePublisherTrust ?? null,
164
+ },
165
+ });
166
+ }
167
+
168
+ /** A customer lock outside its catalogue's ceiling, or a selection or dependency the catalogue does not offer. */
169
+ export class CatalogueCeilingError extends Error {
170
+ constructor(message) {
171
+ super(message);
172
+ this.name = 'CatalogueCeilingError';
173
+ }
174
+ }
175
+
176
+ /**
177
+ * The catalogue's declared ceiling, read by the kernel's grammars: `trustPolicy` is the offering's
178
+ * origin allow list, `trustCeiling` its trust-domain assignments, `storePublisherTrust` the Store
179
+ * publishers the offering vouches for (each a kernel `LockedStorePublisher`). `null` states none.
180
+ */
181
+ export function catalogueCeiling(catalogue, schemas) {
182
+ const read = (key, schema) => {
183
+ const value = catalogue.ceiling[key];
184
+ if (value === null) {return undefined;}
185
+ const parsed = schema.safeParse(value);
186
+ if (!parsed.success) {
187
+ throw new CatalogueError(
188
+ `${CATALOGUE_FILE}: ceiling.${key} of "${catalogue.offeringId}" is not the kernel's shape: ` +
189
+ parsed.error.issues.map((issue) => `[${issue.path.join('.')}] ${issue.message}`).join('; '),
190
+ );
191
+ }
192
+ return parsed.data;
193
+ };
194
+ return {
195
+ trustPolicy: read('trustPolicy', schemas.DistributionTrustPolicySchema),
196
+ trustCeiling: read('trustCeiling', schemas.ProcessTrustPolicySchema),
197
+ storePublisherTrust: read('storePublisherTrust', schemas.LockedStorePublisherSchema.array()),
198
+ };
199
+ }
200
+
201
+ /**
202
+ * The Store publishers a customer lock carries: the entries of the catalogue's parsed ceiling
203
+ * (`catalogueCeiling(...).storePublisherTrust`) whose anchorKey the definition selects
204
+ * (`storePublisherAnchorKeys`), copied as they are. No selection, or none of it offered, is no
205
+ * field; a key the catalogue does not vouch for is refused by the kernel's ceiling check, not here.
206
+ */
207
+ export function selectedStorePublisherTrust(base, storePublisherAnchorKeys) {
208
+ if (storePublisherAnchorKeys === undefined) {return undefined;}
209
+ const selected = new Set(storePublisherAnchorKeys);
210
+ const rows = (base ?? []).filter((row) => selected.has(row.anchorKey));
211
+ return rows.length > 0 ? rows : undefined;
212
+ }
213
+
214
+ /**
215
+ * The kernel's `customerLockCeilingIssues` is the one check of a customer lock against its catalogue:
216
+ * the biomes' trust ceilings, and the Store publishers (base = the catalogue's, selected = the
217
+ * definition's `storePublisherAnchorKeys`, customer = the lock's).
218
+ */
219
+ export function assertCustomerLockWithinCatalogueCeiling({ schemas, catalogue, lock, storePublisherAnchorKeys }) {
220
+ const ceiling = catalogueCeiling(catalogue, schemas);
221
+ const issues = schemas.customerLockCeilingIssues(ceiling.trustCeiling, lock.biomes, {
222
+ base: ceiling.storePublisherTrust,
223
+ selected: storePublisherAnchorKeys,
224
+ customer: lock.storePublisherTrust,
225
+ });
226
+ if (issues.length > 0) {
227
+ throw new CatalogueCeilingError(`catalogue "${catalogue.offeringId}" (${catalogue.channel}): "${lock.distributionId}" is outside the catalogue's ceiling: ${issues.join('; ')}`);
228
+ }
229
+ }
230
+
231
+ /** Write `catalogue.json` into an artifact directory (deterministic serialization); returns its path. */
232
+ export function writeCatalogue(outDir, catalogue) {
233
+ parseCatalogue(catalogue);
234
+ const path = join(outDir, CATALOGUE_FILE);
235
+ writeFileSync(path, serializeJson(catalogue));
236
+ return path;
237
+ }
@@ -0,0 +1,97 @@
1
+ // `xema-edition chart-set source-commit` - the commit each chart-set publisher
2
+ // closed its LOCKED set at.
3
+ //
4
+ // The publisher stamps its index chart with `xema.dev/chart-set-source-sha`
5
+ // next to `xema.dev/chart-set-charts`. This verb reads that stamp from the index
6
+ // chart at the digest the committed fleet `chart-set.lock.json` pins and prints one JSON document. It refuses,
7
+ // printing nothing, when the stamp is absent or is not a 40-hex commit, when the
8
+ // registry does not serve the locked digest, or when the index's roster is not
9
+ // the one the lock records for that publisher: the commit it would name would then be
10
+ // the commit of a different set than the locked one.
11
+
12
+ import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
13
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule } from './edition-workspace.mjs';
14
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
15
+ import { serializeJson } from './lib.mjs';
16
+ import {
17
+ CHART_SET_LOCK_FILE,
18
+ INDEX_ROSTER_ANNOTATION,
19
+ chartRegistryClient,
20
+ chartSetLockPath,
21
+ manifestAnnotations,
22
+ parseIndexRoster,
23
+ readLock,
24
+ registryRepository,
25
+ } from './resolve-chart-set.mjs';
26
+
27
+ if (isEntryModule(import.meta.url)) {
28
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
29
+ }
30
+
31
+ export const CHART_SET_SOURCE_SCHEMA = 'xema.edition-chart-set-source/v1';
32
+ export const INDEX_SOURCE_ANNOTATION = 'xema.dev/chart-set-source-sha';
33
+
34
+ /** Why the index's roster is not the lock's roster for the publisher, or null when they are the same set. */
35
+ function rosterDifference(lockCharts, indexRoster) {
36
+ const names = [...new Set([...Object.keys(lockCharts), ...Object.keys(indexRoster)])].sort();
37
+ for (const name of names) {
38
+ const locked = lockCharts[name];
39
+ const indexed = indexRoster[name];
40
+ if (locked === undefined) {return `the index carries ${name}, which the lock does not`;}
41
+ if (indexed === undefined) {return `the lock records ${name}, which the index does not carry`;}
42
+ if (locked.version !== indexed.version) {
43
+ return `${name} is ${indexed.version} in the index and ${locked.version} in the lock`;
44
+ }
45
+ if (indexed.digest !== undefined && indexed.digest !== locked.digest) {
46
+ return `${name}:${locked.version} is ${indexed.digest} in the index and ${locked.digest} in the lock`;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * The source-commit document for every publisher of `lock`, read through
54
+ * `client` (the chart registry client). Throws on the first refusal.
55
+ */
56
+ export async function chartSetSourceCommits({ client, lock }) {
57
+ const publishers = [];
58
+ for (const { index, repo, version, indexDigest, charts } of lock.publishers) {
59
+ const label = `${index}@${version}`;
60
+ // The index is read BY the digest the lock pins, so a re-pushed tag cannot answer.
61
+ const served = await client.getManifestByDigest(registryRepository(index), indexDigest);
62
+ if (served.digest !== indexDigest) {
63
+ throw new Error(`${label}: the registry served index manifest ${served.digest} but ${CHART_SET_LOCK_FILE} pins ${indexDigest}`);
64
+ }
65
+ const annotations = await manifestAnnotations(client, index, version, served);
66
+ const difference = rosterDifference(charts, parseIndexRoster(annotations[INDEX_ROSTER_ANNOTATION], label));
67
+ if (difference !== null) {
68
+ throw new Error(`${label}: the index roster is not the set ${CHART_SET_LOCK_FILE} locks (${difference}), so its source commit is not the locked set's`);
69
+ }
70
+ const sourceCommit = annotations[INDEX_SOURCE_ANNOTATION];
71
+ if (typeof sourceCommit !== 'string' || !COMMIT_PATTERN.test(sourceCommit)) {
72
+ throw new Error(`${label} carries no ${INDEX_SOURCE_ANNOTATION} that is a 40 lowercase hex commit (got ${JSON.stringify(sourceCommit)})`);
73
+ }
74
+ publishers.push({ index, repo, version, sourceCommit });
75
+ }
76
+ return { schema: CHART_SET_SOURCE_SCHEMA, publishers };
77
+ }
78
+
79
+ async function main(argv) {
80
+ if (argv.length > 0) {
81
+ throw new Error(`unexpected argument "${argv[0]}"; usage: xema-edition chart-set source-commit --data-root <dir> --kernel <dir>`);
82
+ }
83
+ const lock = readLock();
84
+ if (lock === null) {
85
+ throw new Error(`${chartSetLockPath()} is missing or is not JSON: run 'xema-edition chart-set resolve' first`);
86
+ }
87
+ assertRosteredChartSetLock(lock, CHART_SET_LOCK_FILE);
88
+ const document = await chartSetSourceCommits({ client: chartRegistryClient(), lock });
89
+ process.stdout.write(serializeJson(document));
90
+ }
91
+
92
+ if (isEntryModule(import.meta.url)) {
93
+ main(process.argv.slice(2)).catch((error) => {
94
+ process.stderr.write(`chart-set source-commit: ${error.message}\n`);
95
+ process.exit(1);
96
+ });
97
+ }