@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,26 @@
1
+ // image-lock-service-rows — the one answer to "which image row serves this service or runtime component of this biome".
2
+ //
3
+ // A schemaVersion-4 images lock gives a service composed into a process NO subject of its own: it is a
4
+ // `members[]` entry (`{service, packageDir}`) of a `composed-process` subject whose `owners` are the member biomes.
5
+ // Every CLI read that asks whether a lock or catalogue carries the image of a biome's service goes through here, so
6
+ // no reader can miss the composed form.
7
+
8
+ /** Whether `subject` is owned by the biome `biomeId`. */
9
+ function isOwnedByBiome(subject, biomeId, enums) {
10
+ return subject.owners.some((owner) => owner.kind === enums.RuntimeImageOwnerKind.Biome && owner.biomeId === biomeId);
11
+ }
12
+
13
+ /**
14
+ * The subject carrying the image of `kind:id` for the biome `biomeId`, or undefined.
15
+ * A service is served by (a) a Service subject with that id owned by the biome, or (b) a subject owned by the biome
16
+ * whose `members[]` names the service. The member-to-subject step is the kernel's `dedicatedServiceImageSubjects`
17
+ * (`enums.dedicatedServiceImageSubjects`), applied to the lock's `schemaVersion` and the biome's subjects; the same rule the launcher's `loadImageLock` applies; this file adds only the
18
+ * biome-owner filter. The biome's own `services` list supplies `id`.
19
+ */
20
+ export function subjectServingBiome(schemaVersion, subjects, biomeId, kind, id, enums) {
21
+ const owned = subjects.filter((subject) => isOwnedByBiome(subject, biomeId, enums));
22
+ if (kind !== enums.RuntimeImageSubjectKind.Service) {
23
+ return owned.find((subject) => subject.kind === kind && subject.id === id);
24
+ }
25
+ return enums.dedicatedServiceImageSubjects({ schemaVersion, subjects: owned }).get(id);
26
+ }
@@ -0,0 +1,19 @@
1
+ // The evidence lane's keys over a lock subject. The identity itself, `imageSubjectIdentity`, is the one function of
2
+ // `@xemahq/contracts/image-subject-identity`, shared with the CLI's lock loader.
3
+
4
+ import { imageSubjectIdentity } from '@xemahq/contracts/image-subject-identity';
5
+
6
+ const COMPOSED_PROCESS_KIND = 'composed-process';
7
+
8
+ /**
9
+ * The key of a subject in the evidence lane: `kind\0id` for every kind but a composed-process artifact, which is its
10
+ * `imageSubjectIdentity` (so two live generations of one composed artifact get distinct producer stems and index keys).
11
+ */
12
+ export function evidenceSubjectKey({ kind, id, imageRef, contentHash }) {
13
+ return kind === COMPOSED_PROCESS_KIND ? imageSubjectIdentity({ kind, id, imageRef, contentHash }) : `${kind}\0${id}`;
14
+ }
15
+
16
+ /** The key of one image of a completeness statement, which names its image by repository and manifest digest, not by content hash. */
17
+ export function completenessImageKey({ subjectKind, subjectId, imageRepository, manifestDigest }) {
18
+ return subjectKind === COMPOSED_PROCESS_KIND ? `${subjectKind}:${imageRepository}@${manifestDigest}` : `${subjectKind}\0${subjectId}`;
19
+ }
@@ -0,0 +1,216 @@
1
+ // images-from-record — a develop resolve's image digests taken from a BUILD RECORD, with no registry probe and no rehash.
2
+ //
3
+ // The dev lane's job writes the edition's image build record (`develop image-record`, from the channel records
4
+ // `select-records --channel` selected) and hands `edition develop resolve --images-from <file> --channel <tag>` that
5
+ // record. Each entry names one image subject of the edition by the identity images.lock.json and the seal key it by
6
+ // (`<kind>:<id>`), and carries `{repository, contentHash, digest, platforms, sourceSha}`:
7
+ //
8
+ // images what the record's writer resolved itself (the declared currentResolution exception of image-record).
9
+ // satellites a producer's develop digest, from its channel build record. `resolve --channel` checks each against
10
+ // the producer's <root>/build-records/<name>:<tag> record (develop-channel-satellites.mjs): the record
11
+ // must still sit at the commit the snapshot pins and list the entry's {name, digest}. The producer is the
12
+ // subject's declared `firstPartySources[repo].repositoryUrl`, never the record's.
13
+ //
14
+ // Which list an entry sits in carries no meaning for the seal: a subject's digest is taken from wherever the record lists
15
+ // it, `images` or `satellites` (only a `satellites` entry is then held to its producer's channel record). The only
16
+ // refusals are a first-party subject with no entry (no digest), named, and a subject listed twice with two digests.
17
+ // A composed subject also needs its members' keys (`members[{service, key}]`): the kernel's image lock (schemaVersion 5)
18
+ // requires a key on every member row, and this lane computes none, so they come from the record or the subject is
19
+ // refused. contentHash and sourceSha are carried into the images lock as the record states them, never recomputed, and
20
+ // the lock's hash algorithm is not checked (validateImageLockStructure): this lane computes no hash. A third-party
21
+ // subject (a pinned OCI biome's image) is not built by the job and keeps its pinned seal; a record entry no subject
22
+ // takes is ignored.
23
+ //
24
+ // The record schema is the kernel's (`ImageBuildRecordSchema` / `parseImageBuildRecord`, @xemahq/contracts, edition
25
+ // protocol v41), loaded through the kernel loader; this module owns no parser of its own.
26
+
27
+ import { canonicalJson } from './canonical-evidence-json.mjs';
28
+ import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
29
+ import { RefusalError } from './collected-refusals.mjs';
30
+ import { EditionChannel } from './edition-channel.mjs';
31
+ import { sealImageSubjects } from './edition-image-seal.mjs';
32
+ import { firstPartySources } from './first-party-source-ids.mjs';
33
+ import { compareStrings, loadKernelDistributionSchemas, sha256Hex, validateImageLockStructure } from './lib.mjs';
34
+ import { readStrictJsonFile } from './strict-json.mjs';
35
+
36
+ /** Which list of the record an entry came from: how far it is trusted. */
37
+ export const RecordedImageOrigin = Object.freeze({
38
+ Built: 'images',
39
+ Satellite: 'satellites',
40
+ });
41
+
42
+ /** A build record that is malformed, or does not cover the edition exactly. */
43
+ export class ImageBuildRecordError extends RefusalError {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = 'ImageBuildRecordError';
47
+ }
48
+ }
49
+
50
+ /** The subject key the image lock, the probe and the seal use. */
51
+ export const subjectKeyOf = (subject) => `${subject.kind}:${subject.id}`;
52
+
53
+ const platformKey = (platform) => [platform.os, platform.architecture, platform.variant ?? ''].join('/');
54
+
55
+ /** The kernel's image-build-record exports, refused as a set when any is absent (a kernel before edition protocol v41). */
56
+ export function requireImageBuildRecordKernel(schemas) {
57
+ const names = ['ImageBuildRecordSchema', 'parseImageBuildRecord', 'IMAGE_BUILD_RECORD_SCHEMA_VERSION'];
58
+ const missing = names.filter((name) => schemas?.[name] === undefined);
59
+ if (missing.length > 0) {
60
+ throw new ImageBuildRecordError(`the kernel's distribution module does not export ${missing.join(', ')}; the image build record needs a kernel at edition protocol v41 or newer`);
61
+ }
62
+ return schemas;
63
+ }
64
+
65
+ /**
66
+ * Parse an image build record with the kernel's own parser (`parseImageBuildRecord`, the ONE contract: both the writer
67
+ * `develop image-record` and this reader go through it): `{schemaVersion: 1, images: [entry], satellites: [entry]}`, every
68
+ * list sorted by subject, no subject twice, no unknown key. A refusal is an `ImageBuildRecordError` carrying the kernel's message.
69
+ *
70
+ * @param {unknown} document
71
+ * @param {string} source where the record came from (named in the refusal)
72
+ * @param {{loadKernel?: () => Promise<object>}} [injections] tests replace the kernel loader
73
+ */
74
+ export async function parseImageBuildRecord(document, source, { loadKernel = loadKernelDistributionSchemas } = {}) {
75
+ const kernel = requireImageBuildRecordKernel(await loadKernel());
76
+ try {
77
+ return kernel.parseImageBuildRecord(document);
78
+ } catch (error) {
79
+ throw new ImageBuildRecordError(`image build record ${source} is refused: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * sha256 (hex) of the parsed record's canonical JSON (canonical-evidence-json.mjs: keys sorted), every list in its
85
+ * canonical order: entries by subject (parse enforces it) and each entry's platforms by os/architecture/variant. It is
86
+ * the `imageBuildRecord` input of the develop edition's target tag (snapshotEditionTag).
87
+ */
88
+ export function imageBuildRecordDigest(record) {
89
+ const canonicalEntry = (entry) => ({ ...entry, platforms: [...entry.platforms].sort((a, b) => compareStrings(platformKey(a), platformKey(b))) });
90
+ return sha256Hex(canonicalJson({
91
+ schemaVersion: record.schemaVersion,
92
+ ...Object.fromEntries(Object.values(RecordedImageOrigin).map((origin) => [origin, record[origin].map(canonicalEntry)])),
93
+ }));
94
+ }
95
+
96
+ /** Read and parse the record file `--images-from` names (duplicate JSON keys refused). */
97
+ export async function readImageBuildRecord(path) {
98
+ return parseImageBuildRecord(readStrictJsonFile(path), path);
99
+ }
100
+
101
+ /**
102
+ * The record as `resolveImages` consumes it (`recordedImages`): `take` gives each first-party subject its recorded
103
+ * content hash, source commit and (composed) member keys, and `settle` refuses, all at once, every subject with no
104
+ * digest, every subject listed twice with two digests, and every composed subject whose entry lacks its member keys.
105
+ *
106
+ * @param {{images: object[], satellites: object[]}} record a parsed record
107
+ * @param {{distributionId: string}} context
108
+ */
109
+ export function createRecordedImages(record, { distributionId }) {
110
+ const entries = new Map();
111
+ const problems = [];
112
+ for (const origin of Object.values(RecordedImageOrigin)) {
113
+ for (const entry of record[origin]) {
114
+ const incumbent = entries.get(entry.subject);
115
+ if (incumbent !== undefined && incumbent.entry.digest !== entry.digest) {
116
+ problems.push(`${entry.subject} is listed twice with two digests: ${incumbent.entry.digest} (record.${incumbent.origin}) and ${entry.digest} (record.${origin})`);
117
+ }
118
+ if (incumbent === undefined) {entries.set(entry.subject, { origin, entry });}
119
+ }
120
+ }
121
+ const taken = new Map();
122
+ let settled = false;
123
+
124
+ return {
125
+ take(subject) {
126
+ const key = subjectKeyOf(subject);
127
+ const recorded = entries.get(key);
128
+ if (recorded === undefined) {
129
+ problems.push(`${key} (${subject.repo}@${subject.sourceCommit}) has no digest: it is not in the record`);
130
+ return { sourceCommit: subject.sourceCommit, contentHash: null };
131
+ }
132
+ if (taken.has(key)) {throw new Error(`images-from-record: subject ${key} was taken twice`);}
133
+ const { entry } = recorded;
134
+ taken.set(key, { subject, ...recorded });
135
+ return { sourceCommit: entry.sourceSha, contentHash: entry.contentHash, ...recordedMembers(key, subject, entry, problems) };
136
+ },
137
+ settle() {
138
+ settled = true;
139
+ if (problems.length > 0) {
140
+ throw new ImageBuildRecordError(`the image build record cannot seal "${distributionId}" (${problems.length} problem(s); no probe fallback):\n ${problems.join('\n ')}`);
141
+ }
142
+ },
143
+ /** The satellite entries taken, each with the subject it covers, for the channel check. */
144
+ satelliteClaims() {
145
+ if (!settled) {throw new Error('images-from-record: satellite claims are read only after settle()');}
146
+ return [...taken.values()].filter(({ origin }) => origin === RecordedImageOrigin.Satellite).map(({ subject, entry }) => ({ subject, entry }));
147
+ },
148
+ /** The recorded entry of a subject, or undefined for a subject the record does not cover (a third-party one). */
149
+ entryOf(subject) {
150
+ return taken.get(subjectKeyOf(subject))?.entry;
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * A composed subject's members with the keys its record entry carries (`members[{service, key}]`), never computed here.
157
+ * An entry without members, or without a key for a subject member, is a problem named once; a subject that is
158
+ * not composed gives nothing.
159
+ */
160
+ export function recordedMembers(key, subject, entry, problems) {
161
+ if (subject.members === undefined) {return {};}
162
+ if (entry.members === undefined) {
163
+ problems.push(`record lists composed ${key} with no members[{service,key}]; the producer's build record must carry them`);
164
+ return {};
165
+ }
166
+ const keyOf = new Map(entry.members.map((member) => [member.service, member.key]));
167
+ const wanted = subject.members.map((member) => member.service);
168
+ // The image may carry more services than this edition runs from it: only a member with no key is a problem.
169
+ const absent = wanted.filter((service) => !keyOf.has(service));
170
+ if (absent.length > 0) {
171
+ problems.push(`${key}: the record's members carry no key for ${absent.join(', ')}`);
172
+ return {};
173
+ }
174
+ return { members: subject.members.map((member) => ({ ...member, key: keyOf.get(member.service) })) };
175
+ }
176
+
177
+ /** The `owner/name` a satellite subject's develop identity is bound to: its repository's declared URL. */
178
+ export function satelliteProducerOf(subject) {
179
+ const sources = firstPartySources('naming a satellite producer');
180
+ if (!Object.hasOwn(sources, subject.repo)) {
181
+ throw new ImageBuildRecordError(`${subjectKeyOf(subject)}: ${subject.repo} is not a declared first-party source; a satellite producer is never named by the record`);
182
+ }
183
+ const context = `firstPartySources["${subject.repo}"].repositoryUrl`;
184
+ return repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(sources[subject.repo].repositoryUrl, context), context);
185
+ }
186
+
187
+ /**
188
+ * The develop images lock of a snapshot from the record: the subjects as `resolveImages` structures them, each
189
+ * first-party one with the record's content hash and source commit, coverage settled, satellites checked against their
190
+ * producers' channel records (`verifySatellites`, develop-channel-satellites.mjs).
191
+ *
192
+ * @returns {Promise<{imageLock: object, recorded: ReturnType<typeof createRecordedImages>}>}
193
+ */
194
+ export async function resolveImagesFromRecord({ id, inputs, lock, record, resolveImages, verifySatellites, previousLock = null }) {
195
+ const recorded = createRecordedImages(record, { distributionId: id });
196
+ const { imageLock } = await resolveImages(id, { inputs, lock, recordedImages: recorded, previousLock });
197
+ await verifySatellites(recorded.satelliteClaims());
198
+ return { imageLock, recorded };
199
+ }
200
+
201
+ /**
202
+ * Seal the lock from the record: each recorded subject takes the record's develop repository and digest, with no
203
+ * registry read. A third-party subject (the only kind the record does not cover) keeps its pinned seal.
204
+ */
205
+ export async function sealImagesFromRecord({ imageLock, recorded, thirdPartyPins, ...sealOptions }) {
206
+ const unrecorded = imageLock.subjects.filter((subject) => recorded.entryOf(subject) === undefined);
207
+ const pinned = unrecorded.length === 0 ? [] : await sealImageSubjects({ channel: EditionChannel.Develop, distributionId: imageLock.distributionId, subjects: unrecorded, thirdPartyPins, ...sealOptions });
208
+ const pinnedByKey = new Map(pinned.map((subject) => [subjectKeyOf(subject), subject]));
209
+ const subjects = imageLock.subjects.map((subject) => {
210
+ const entry = recorded.entryOf(subject);
211
+ if (entry === undefined) {return pinnedByKey.get(subjectKeyOf(subject));}
212
+ return { ...subject, imageRef: entry.repository, imageDigest: entry.digest };
213
+ });
214
+ const schemas = await loadKernelDistributionSchemas();
215
+ return validateImageLockStructure(schemas.parseImageLock({ ...imageLock, subjects }), schemas);
216
+ }
@@ -0,0 +1,13 @@
1
+ // indexes-at-pins — the platform-service, biome and runtime-image-source
2
+ // indexes of a source universe pinned at ARBITRARY repository pins.
3
+ //
4
+ // The release builders regenerate the committed indexes over a pinned source
5
+ // authority; this runs the same builders, in memory, over any pin set. A develop
6
+ // snapshot (develop-snapshot-inputs.mjs) and a vendor edition that holds
7
+ // repositories (edition-pinned-inputs.mjs) both derive their indexes here, so
8
+ // there is no second builder. The implementation lives beside the runtime index
9
+ // builder, whose repository join (`--repo` with no committed pin) uses it too.
10
+ // No fallback: the authority has no delegate, so a repository the pins do not
11
+ // name refuses by name instead of resolving to a tip.
12
+
13
+ export { buildIndexesAtPins, buildIndexesWithJoiningRepository } from './build-runtime-image-source-index.mjs';
@@ -0,0 +1,105 @@
1
+ // infra-images-lock — an edition's NON-distribution image set, digest-pinned.
2
+ //
3
+ // `<edition>/infra-images.lock.json` is the one declaration of the backing,
4
+ // monitoring and inference images an appliance carries beside the images its
5
+ // `images.lock.json` names. The appliance bundler (airgap/build-bundle.sh) and
6
+ // the mirror read it; `xema-edition check` refuses a lock that is malformed.
7
+ //
8
+ // A ref is an immutable VERSION TAG (the bundle re-copies it to transports that
9
+ // take no digest destination); the digest is what that tag pointed at when the
10
+ // lock was resolved, and what a consumer copies FROM. An entry whose registry
11
+ // refuses an anonymous read is `unresolvable` with a REQUIRED reason: it is a
12
+ // recorded hole, and a real bundle build refuses while one exists.
13
+ //
14
+ // This module is pure (no registry, no process state), so it moves with the
15
+ // pipeline tooling without a rewrite.
16
+
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+
20
+ export const INFRA_IMAGES_LOCK_FILE = 'infra-images.lock.json';
21
+ export const INFRA_IMAGES_LOCK_SCHEMA_VERSION = 1;
22
+
23
+ /** Which part of the bundle an image serves; `--no-monitoring` drops exactly the monitoring group. */
24
+ export const InfraImageGroup = Object.freeze({
25
+ BackingInfra: 'backing-infra',
26
+ Monitoring: 'monitoring',
27
+ Inference: 'inference',
28
+ });
29
+
30
+ export const InfraImagePin = Object.freeze({
31
+ Digest: 'digest',
32
+ Unresolvable: 'unresolvable',
33
+ });
34
+
35
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
36
+ const GROUPS = new Set(Object.values(InfraImageGroup));
37
+ const PINS = new Set(Object.values(InfraImagePin));
38
+
39
+ /** The refusal of a ref that is not `<authority>/<repository>:<immutable tag>`. */
40
+ function refProblem(ref) {
41
+ if (typeof ref !== 'string' || ref.length === 0) {return 'ref must be a non-empty string';}
42
+ if (ref.includes('@')) {return 'ref must be a tag; the digest is its own field';}
43
+ const slash = ref.indexOf('/');
44
+ if (slash <= 0 || slash === ref.length - 1) {return 'ref has no registry authority and repository';}
45
+ const last = ref.slice(ref.lastIndexOf('/') + 1);
46
+ const colon = last.lastIndexOf(':');
47
+ if (colon <= 0 || colon === last.length - 1) {return 'ref carries no tag';}
48
+ if (last.slice(colon + 1) === 'latest') {return "ref is ':latest', which is mutable and not a pin";}
49
+ return null;
50
+ }
51
+
52
+ /** Every problem of an infra-images lock, as sentences; empty when the lock is sound. */
53
+ export function infraImagesLockProblems(lock, { distributionId } = {}) {
54
+ const problems = [];
55
+ if (typeof lock !== 'object' || lock === null || Array.isArray(lock)) {return ['the lock is not a JSON object'];}
56
+ if (lock.schemaVersion !== INFRA_IMAGES_LOCK_SCHEMA_VERSION) {
57
+ problems.push(`schemaVersion must be ${INFRA_IMAGES_LOCK_SCHEMA_VERSION} (got ${JSON.stringify(lock.schemaVersion)})`);
58
+ }
59
+ if (distributionId !== undefined && lock.distributionId !== distributionId) {
60
+ problems.push(`distributionId must be "${distributionId}" (got ${JSON.stringify(lock.distributionId)})`);
61
+ }
62
+ if (!Array.isArray(lock.images) || lock.images.length === 0) {
63
+ problems.push('images must be a non-empty array');
64
+ return problems;
65
+ }
66
+ const seen = new Set();
67
+ for (const [position, entry] of lock.images.entries()) {
68
+ const where = `images[${position}] (${entry?.ref ?? '?'})`;
69
+ const refIssue = refProblem(entry?.ref);
70
+ if (refIssue) {problems.push(`${where}: ${refIssue}`);}
71
+ else if (seen.has(entry.ref)) {problems.push(`${where}: ref is listed twice`);}
72
+ else {seen.add(entry.ref);}
73
+ if (!GROUPS.has(entry?.group)) {problems.push(`${where}: group must be one of ${[...GROUPS].join(', ')}`);}
74
+ if (!PINS.has(entry?.pin)) {
75
+ problems.push(`${where}: pin must be one of ${[...PINS].join(', ')}`);
76
+ continue;
77
+ }
78
+ const expected = entry.pin === InfraImagePin.Digest ? ['digest', 'group', 'pin', 'ref'] : ['group', 'pin', 'reason', 'ref'];
79
+ if (Object.keys(entry).sort().join(',') !== expected.join(',')) {
80
+ problems.push(`${where}: a "${entry.pin}" entry carries exactly ${expected.join(', ')}`);
81
+ } else if (entry.pin === InfraImagePin.Digest && !DIGEST_PATTERN.test(entry.digest)) {
82
+ problems.push(`${where}: digest must be "sha256:<64 lowercase hex>"`);
83
+ } else if (entry.pin === InfraImagePin.Unresolvable && (typeof entry.reason !== 'string' || entry.reason.trim() === '')) {
84
+ problems.push(`${where}: an unresolvable entry requires a reason`);
85
+ }
86
+ }
87
+ return problems;
88
+ }
89
+
90
+ /** The edition's lock, or `null` when it declares none (an edition need not carry infra images). */
91
+ export function readInfraImagesLock(editionDir) {
92
+ const file = join(editionDir, INFRA_IMAGES_LOCK_FILE);
93
+ if (!existsSync(file)) {return null;}
94
+ return JSON.parse(readFileSync(file, 'utf8'));
95
+ }
96
+
97
+ /** Unresolvable entries: the recorded holes a real bundle build refuses on. */
98
+ export function unresolvableInfraImages(lock) {
99
+ return lock.images.filter((entry) => entry.pin === InfraImagePin.Unresolvable);
100
+ }
101
+
102
+ /** The lock as committed: two-space JSON, one trailing newline. */
103
+ export function serializeInfraImagesLock(lock) {
104
+ return `${JSON.stringify(lock, null, 2)}\n`;
105
+ }
@@ -0,0 +1,30 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ import { kernelDistPath } from './edition-workspace.mjs';
4
+
5
+ const requireFromHere = createRequire(import.meta.url);
6
+
7
+ let schema;
8
+
9
+ /**
10
+ * Is `value` a capability domain? The kernel's `CapabilityDomainSchema` is the
11
+ * ONE grammar; it is read from the distribution subpath, the only plane the
12
+ * edition protocol exposes to this library. Nothing here restates the pattern.
13
+ *
14
+ * @param {unknown} value
15
+ * @returns {boolean}
16
+ */
17
+ export function isKernelCapabilityDomain(value) {
18
+ if (schema === undefined) {
19
+ const path = kernelDistPath('distribution', 'index.js');
20
+ const mod = requireFromHere(path);
21
+ if (typeof mod.CapabilityDomainSchema?.safeParse !== 'function') {
22
+ throw new Error(
23
+ `Built kernel distribution module at ${path} does not export CapabilityDomainSchema — ` +
24
+ 'the dist is stale; rebuild kernel-contracts at a commit that carries it.',
25
+ );
26
+ }
27
+ schema = mod.CapabilityDomainSchema;
28
+ }
29
+ return schema.safeParse(value).success;
30
+ }