@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,96 @@
1
+ // definition-checkout — a distribution definition named as a repository triple, and the checkout that must hold it.
2
+ //
3
+ // The catalogue-free customer develop resolve (develop-catalogue-free-resolve.mjs) names the vendor offering it builds
4
+ // on as `--offering <owner>/<repo>,<path>,<dataRoot>` and the customer definition as `--definition-at` in the same form:
5
+ // the repository, the distribution directory and the data root inside it, each repository-relative. The offering is
6
+ // read from the vendor data checkout, which must BE that triple: the repository the data root declares
7
+ // (`dataRootRepositoryUrl`), clean at the pinned data commit, with the data root and the editions root's distribution
8
+ // directory where the triple says.
9
+
10
+ import { execFileSync } from 'node:child_process';
11
+ import { existsSync, realpathSync } from 'node:fs';
12
+ import { basename, join, normalize, relative } from 'node:path';
13
+
14
+ import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
15
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
16
+ import { editionWorkspaceConfig } from './edition-config.mjs';
17
+ import { dataRoot, editionsRoot } from './edition-workspace.mjs';
18
+
19
+ const REPOSITORY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/;
20
+ const RELATIVE_SEGMENT = /^[A-Za-z0-9._-]+$/;
21
+
22
+ export class DataRootRepositoryUndeclaredError extends Error {
23
+ constructor() {
24
+ super('edition-workspace.json declares no dataRootRepositoryUrl; the definition checkout cannot be held to the data root\'s repository');
25
+ this.name = 'DataRootRepositoryUndeclaredError';
26
+ }
27
+ }
28
+
29
+ /** The data root's repository URL, from the workspace declaration; refused by name when it declares none. */
30
+ export function declaredDataRootRepositoryUrl() {
31
+ const url = editionWorkspaceConfig().dataRootRepositoryUrl;
32
+ if (url === undefined) {throw new DataRootRepositoryUndeclaredError();}
33
+ return url;
34
+ }
35
+
36
+ /** A repository-relative path: `.` or `/`-separated plain segments, never absolute, never `..`. */
37
+ function relativePath(text, what, flag) {
38
+ if (text === '.') {return '.';}
39
+ const segments = text.split('/');
40
+ if (text === '' || segments.some((segment) => !RELATIVE_SEGMENT.test(segment) || segment === '.' || segment === '..')) {
41
+ throw new Error(`${flag} ${what} ${JSON.stringify(text)} is not a repository-relative path ("." or plain segments joined by "/")`);
42
+ }
43
+ return segments.join('/');
44
+ }
45
+
46
+ /** `<owner>/<repo>,<path>,<dataRoot>` (of `flag`) -> `{ repository, path, dataRoot }`. */
47
+ export function parseDefinitionTriple(text, flag) {
48
+ const parts = typeof text === 'string' ? text.split(',') : [];
49
+ if (parts.length !== 3) {
50
+ throw new Error(`${flag} is <owner>/<repo>,<path>,<dataRoot>, got ${JSON.stringify(text)}`);
51
+ }
52
+ const [repository, path, root] = parts;
53
+ if (!REPOSITORY_PATTERN.test(repository)) {
54
+ throw new Error(`${flag} repository ${JSON.stringify(repository)} is not <owner>/<repo>`);
55
+ }
56
+ const definition = { repository, path: relativePath(path, 'path', flag), dataRoot: relativePath(root, 'dataRoot', flag) };
57
+ if (definition.path === '.') {throw new Error(`${flag} path names the distribution directory, never the repository root`);}
58
+ assertCanonicalDistributionId(basename(definition.path));
59
+ return definition;
60
+ }
61
+
62
+ const runGitText = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
63
+
64
+ /**
65
+ * Consistency with the caller's checkout: the data root checkout is the definition, the repository the data root
66
+ * declares, clean (untracked files included) at `dataSha`, with the data root and the editions root's distribution
67
+ * directory at the definition's dataRoot and path. Returns the id.
68
+ */
69
+ export function assertDefinitionCheckout({ definition, dataSha, flag, shaLabel, git = runGitText }) {
70
+ const declared = canonicalPublicHttpsRepositoryUrl(declaredDataRootRepositoryUrl(), 'the data root\'s dataRootRepositoryUrl');
71
+ const named = canonicalPublicHttpsRepositoryUrl(`https://github.com/${definition.repository}.git`, `${flag} repository`);
72
+ if (declared !== named) {
73
+ throw new Error(`${flag} names ${definition.repository}, but the data root is the repository ${declared}: the data root is the definition's own`);
74
+ }
75
+ const top = realpathSync(git(['rev-parse', '--show-toplevel'], dataRoot()));
76
+ const head = git(['rev-parse', 'HEAD'], top);
77
+ if (head !== dataSha) {
78
+ throw new Error(`${shaLabel} is ${dataSha}, but the definition checkout ${top} is at ${head}`);
79
+ }
80
+ const dirty = git(['status', '--porcelain', '--untracked-files=all'], top);
81
+ if (dirty !== '') {
82
+ throw new Error(`the definition checkout ${top} has uncommitted or untracked changes: the definition is read only as committed at ${shaLabel}`);
83
+ }
84
+ const at = (dir) => normalize(relative(top, realpathSync(dir)) || '.');
85
+ if (at(dataRoot()) !== normalize(definition.dataRoot)) {
86
+ throw new Error(`--data-root is ${at(dataRoot())} of the definition checkout, but ${flag} names the data root ${definition.dataRoot}`);
87
+ }
88
+ const id = basename(definition.path);
89
+ if (!existsSync(join(editionsRoot(), id))) {
90
+ throw new Error(`the editions root holds no "${id}", but ${flag} names ${definition.path}`);
91
+ }
92
+ if (at(join(editionsRoot(), id)) !== normalize(definition.path)) {
93
+ throw new Error(`the editions root holds "${id}" at ${at(join(editionsRoot(), id))} of the definition checkout, but ${flag} names ${definition.path}`);
94
+ }
95
+ return id;
96
+ }
@@ -0,0 +1,122 @@
1
+ // develop-build-record — reading a member's develop build record from the registry, the one core the dev lane's channel
2
+ // readers share (develop-channel-record.mjs: select-records, image-record and resolve --channel).
3
+ //
4
+ // A record is the single build-record layer of the manifest a tag names under <root>/build-records/<name>, parsed with
5
+ // the CLI's own @xemahq/contracts BuildRecordSchema and held to its member (producer repository, branch). Nothing here verifies a
6
+ // signature: the dev lane's records are unsigned and their trust is the registry write ACL.
7
+ //
8
+ // Two failure classes: `RecordRefused` (the record is missing, or the record itself says no) and a plain Error (the
9
+ // registry could not answer), which the operator retries rather than reads.
10
+
11
+ import { BUILD_RECORD_MEDIA_TYPE, BuildRecordSchema, buildRecordTag, compareBuildRecordRuns, parseBuildRecordTag } from '@xemahq/contracts/distribution';
12
+
13
+ import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
14
+ import { RecordRefused } from './record-refused.mjs';
15
+ import { registryClientFactory } from './registry-client-factory.mjs';
16
+ import { splitImageRef } from './registry-credentials.mjs';
17
+ import { isNotPublishedYet } from './seal-image-digests.mjs';
18
+
19
+ /** The repository segment, under the registry root, that holds a member's build records. */
20
+ export const BUILD_RECORDS_SEGMENT = 'build-records';
21
+ /** The record's producer is a kind this CLI has no claims for: a closed set, never guessed at. */
22
+ export class UnknownBuildRecordProducerError extends RecordRefused {
23
+ constructor(repo, kind) {
24
+ super(repo, `the record's producer kind ${JSON.stringify(kind)} is not one this CLI can read`);
25
+ this.name = 'UnknownBuildRecordProducerError';
26
+ }
27
+ }
28
+
29
+ /** The build-record contract of the `@xemahq/contracts` this CLI itself depends on: no kernel checkout, no workspace root. */
30
+ export const CLI_BUILD_RECORD_CONTRACT = Object.freeze({ BUILD_RECORD_MEDIA_TYPE, BuildRecordSchema, buildRecordTag, parseBuildRecordTag, compareBuildRecordRuns });
31
+
32
+ /** The member's `owner/name`, from its declared repository URL. */
33
+ export const memberOwnerName = (memberRepositoryUrl) =>
34
+ repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(memberRepositoryUrl, 'repositoryUrl'), 'repositoryUrl');
35
+
36
+ /**
37
+ * What a record's producer says, read through the ONE switch over its kind: the repository that called the
38
+ * record workflow (the cosign caller claim, held equal to the member's) and the run the snapshot names.
39
+ */
40
+ export function producerClaims({ record, repo, memberRepository }) {
41
+ switch (record.producer?.kind) {
42
+ case 'github-actions': {
43
+ const { repository, runNumber, runAttempt } = record.producer;
44
+ if (repository !== memberRepository) {throw new RecordRefused(repo, `record is produced by repository ${repository}, not ${memberRepository}`);}
45
+ return { repository, snapshot: `${runNumber}.${runAttempt}` };
46
+ }
47
+ default:
48
+ throw new UnknownBuildRecordProducerError(repo, record.producer?.kind);
49
+ }
50
+ }
51
+
52
+ /** Run one registry client call; ONLY a failure of the client itself is "the registry did not answer". */
53
+ async function fromRegistry(repo, what, call) {
54
+ try {
55
+ return await call();
56
+ } catch (error) {
57
+ throw new Error(`select-records: ${repo}: the registry did not answer for ${what}: ${error.message}`, { cause: error });
58
+ }
59
+ }
60
+
61
+ /** Read one record by its tag: manifest by digest, its single layer, parsed against the kernel's schema. */
62
+ export async function readRecord({ kernel, client, repository, repo, tag, absentIsRefusal = false }) {
63
+ let served;
64
+ try {
65
+ served = await fromRegistry(repo, `${repository}:${tag}`, () => client.getManifestByTag(repository, tag));
66
+ } catch (error) {
67
+ if (absentIsRefusal && isNotPublishedYet(error.cause)) {throw new RecordRefused(repo, `${repository}:${tag} does not exist`);}
68
+ throw error;
69
+ }
70
+ const manifest = await fromRegistry(repo, `${repository}@${served.digest}`, () => client.getManifestByDigest(repository, served.digest));
71
+ const layers = manifest.document.layers;
72
+ if (!Array.isArray(layers) || layers.length !== 1) {
73
+ throw new RecordRefused(repo, `record ${tag} has ${Array.isArray(layers) ? layers.length : 'no'} layers, not exactly one`);
74
+ }
75
+ const [layer] = layers;
76
+ if (layer.mediaType !== kernel.BUILD_RECORD_MEDIA_TYPE) {
77
+ throw new RecordRefused(repo, `record ${tag} layer media type is ${JSON.stringify(layer.mediaType)}, not ${kernel.BUILD_RECORD_MEDIA_TYPE}`);
78
+ }
79
+ const bytes = await fromRegistry(repo, `${repository}@${layer.digest}`, () => client.getBlob(repository, layer.digest));
80
+ let document;
81
+ try {
82
+ document = JSON.parse(bytes.toString('utf8'));
83
+ } catch (error) {
84
+ throw new RecordRefused(repo, `record ${tag} is not JSON: ${error.message}`);
85
+ }
86
+ const parsed = kernel.BuildRecordSchema.safeParse(document);
87
+ if (!parsed.success) {throw new RecordRefused(repo, `record ${tag} does not parse as a build record: ${parsed.error.message}`);}
88
+ return { record: parsed.data, digest: served.digest };
89
+ }
90
+
91
+ /** The record content check both lanes share: it is for the branch asked. */
92
+ export function assertRecordBranch({ record, repo, tag, branch }) {
93
+ if (record.branch !== branch) {throw new RecordRefused(repo, `record ${tag} is for branch ${record.branch}, not ${branch}`);}
94
+ }
95
+
96
+ /** The registry repository that holds a member's build records: under the develop store root the member declares. */
97
+ export function recordsRepositoryOf(member) {
98
+ if (member.developImageRegistry === undefined) {
99
+ throw new RecordRefused(member.repo, 'declares no developImageRegistry, so its build records repository is unknown');
100
+ }
101
+ return `${member.developImageRegistry}/${BUILD_RECORDS_SEGMENT}/${member.repo.split('/').at(-1)}`;
102
+ }
103
+
104
+ /**
105
+ * A member's build-records repository, the authority it lives on and that authority's registry client. The client is
106
+ * bound to the authority, so `repository` is the path under it; `ref` is the full reference, for messages.
107
+ */
108
+ export function recordsClientOf({ clientFor, member }) {
109
+ const ref = recordsRepositoryOf(member);
110
+ const { authority, repository } = splitImageRef(ref);
111
+ return { ref, repository, authority, client: clientFor(authority) };
112
+ }
113
+
114
+ /**
115
+ * The CLI's build-record contract and the registry clients, with no cosign and no trust policy: what a read of a channel tag needs. The
116
+ * injections are replaced by tests.
117
+ */
118
+ export async function withChannelReader({ env, loadKernel = async () => CLI_BUILD_RECORD_CONTRACT, clientsOverride }, use) {
119
+ const kernel = await loadKernel();
120
+ const { clientFor } = registryClientFactory({ clientsOverride, env });
121
+ return use({ kernel, clientFor });
122
+ }
@@ -0,0 +1,282 @@
1
+ // develop-catalogue-free-resolve — the catalogue-free CUSTOMER develop resolve:
2
+ //
3
+ // xema-edition develop resolve --definition-at <owner>/<repo>,<path>,<dataRoot> --offering <owner>/<repo>,<path>,<dataRoot>
4
+ // --pins <file> --out <dir> --registry <root> --engine-repository <repository> --engine-signer <certificate identity>
5
+ // --cosign <abs path> --offering-previous-lock <file> --offering-previous-lock-sha256 <hex>
6
+ // (--previous-lock <file> --previous-lock-sha256 <hex> | --first-seal) [--source-image <source>=<registry>/<prefix>]... [--local]
7
+ //
8
+ // The OFFICIAL lane (a customer definition built on a vendor offering): it keeps its verification. It resolves the
9
+ // vendor offering it builds on IN THE SAME CALL instead of pinning a published catalogue: no catalogue.pin.json is read (its presence beside the definition is refused, never
10
+ // a fallback), and no public flag substitutes a catalogue file. Two workspaces, two steps, one CLI:
11
+ //
12
+ // vendor a child process of this CLI on the VENDOR roots (the roots this call was given), the vendor step
13
+ // (develop-catalogue-free-vendor.mjs): the offering resolved on the VERIFIED lane (the engine found and
14
+ // cosign-proven from --engine-repository/--engine-signer, the images probed by content hash, the git-sourced
15
+ // bundles cosign-verified), writing the vendor artifact and catalogue.json into a scratch directory. It is
16
+ // not the vendor `develop resolve` verb, which is the channel lane and verifies none of this. --offering names
17
+ // the offering's directory inside the vendor data checkout, held to it (definition-checkout.mjs: the checkout
18
+ // is the declared repository, clean at the vendor data pin, the data root and the directory where the triple
19
+ // says). Never defaulted.
20
+ // customer the definition repository fetched at --pins "definition-repository" (the walk's git source fetch, by
21
+ // exact commit) and its tree at that commit checked out from the object store into this call's private
22
+ // scratch before the vendor step runs (a temporary index, symlinks as plain files): the shared fetch cache
23
+ // is never read again, so a cache tree that moves or changes mid-run cannot change what is resolved. The
24
+ // workspace is re-stated on the scratch copy's roots (data = <copy>/<dataRoot>, editions = the parent of
25
+ // <copy>/<path>, a fresh sources root, the same cache), and the customer resolve run against the catalogue
26
+ // the vendor step wrote. The edition's pins are the catalogue-free form
27
+ // (edition-contract.json provenancePins.customerForms): the vendor pins, the customer's own
28
+ // sources/<name> commits, "definition-repository" and the definition digest, never a catalogue pin.
29
+ // The catalogue CONTENT digest is sha256 of the EXACT catalogue.json bytes the vendor step wrote (parsed
30
+ // strictly, so a duplicate key refuses), the convention of the catalogue layer digest of an offering
31
+ // artifact (produce-edition-catalogue-evidence's predicate catalogueDigest). It goes into the distribution
32
+ // lock only, as catalogueContentDigest (kernel edition protocol v37), never catalogueDigest: no catalogue
33
+ // artifact exists, so the lock never names one (catalogue-lock-field.mjs).
34
+ //
35
+ // --pins is the COMPLETE frozen set; nothing here reads a head. It is split once, before anything is fetched: a
36
+ // sources/<name> key the definition declares is the customer's own, every other key but "definition-repository" goes
37
+ // to the vendor step (which refuses what its offering does not declare). A source name both the offering and the
38
+ // definition declare is refused before the vendor step: its one pin could not say whose source it is.
39
+
40
+ import { execFileSync, spawnSync } from 'node:child_process';
41
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
42
+ import { tmpdir } from 'node:os';
43
+ import { basename, dirname, join, resolve } from 'node:path';
44
+
45
+ import { sha256Digest } from './canonical-evidence-json.mjs';
46
+ import { CATALOGUE_PIN_FILE } from './catalogue-pin.mjs';
47
+ import { CATALOGUE_FILE, parseCatalogue } from './catalogue.mjs';
48
+ import { customerPreviousSeal, customerSourceImages, declaredSourceNamesOf, resolveCustomerChannelEdition } from './customer-edition-verbs.mjs';
49
+ import { DEFINITION_AT_FLAG, OFFERING_FLAG, OFFERING_PREVIOUS_LOCK_FLAG, OFFERING_PREVIOUS_LOCK_SHA256_FLAG } from './customer-flags.mjs';
50
+ import { assertDefinitionCheckout, parseDefinitionTriple } from './definition-checkout.mjs';
51
+ import { vendorStepArgs } from './develop-catalogue-free-vendor.mjs';
52
+ import { customerDevelopGitBundleVerifier } from './develop-git-bundles.mjs';
53
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
54
+ import { LOCAL_WORKFLOW, parseEditionDescriptor, snapshotEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
55
+ import { resetEditionWorkspaceConfig } from './edition-config.mjs';
56
+ import { CATALOGUE_PIN_KEY, DEFINITION_PIN_KEY, DEFINITION_REPOSITORY_PIN_KEY, EDITION_DESCRIPTOR_FILE, EditionChannel, EditionKind, VENDOR_MEMBER_PIN_PATTERN } from './edition-constants.mjs';
57
+ import { WorkspaceRoot, cacheDir, configureEditionWorkspace, configuredWorkspace, editionsRoot, workspaceArgv } from './edition-workspace.mjs';
58
+ import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
59
+ import { materializeGitSource } from './external-source-walk.mjs';
60
+ import { serializeJson } from './lib.mjs';
61
+ import { dataPinKey, isSourcePinKey, sortedSnapshotPins, sourceNameOfPinKey } from './pin-source.mjs';
62
+ import { parseStrictJson, readStrictJsonFile } from './strict-json.mjs';
63
+
64
+ export { DEFINITION_REPOSITORY_PIN_KEY };
65
+ /** The definition file in a definition directory (the editions-root layout). */
66
+ const DEFINITION_FILE = 'xema-distribution.json';
67
+ const SHA_PATTERN = /^[0-9a-f]{40}$/;
68
+
69
+ /**
70
+ * `sha256:<hex>` of the exact catalogue.json bytes the vendor step wrote (the catalogue layer digest convention): the
71
+ * lock's catalogueContentDigest, never a pin.
72
+ */
73
+ export const catalogueContentDigestOf = (bytes) => sha256Digest(bytes);
74
+
75
+ /**
76
+ * --pins checked and split once, before anything is fetched: `{ definitionRepositoryCommit, members }`, the members
77
+ * every other key. "catalogue" and "definition" are computed, never input.
78
+ */
79
+ export function splitCatalogueFreePins(pins) {
80
+ if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {throw new Error('--pins must be an object mapping pin key -> 40-hex commit');}
81
+ for (const computed of [CATALOGUE_PIN_KEY, DEFINITION_PIN_KEY]) {
82
+ if (computed in pins) {throw new Error(`--pins names "${computed}": a catalogue-free resolve computes the definition digest and pins no catalogue`);}
83
+ }
84
+ const { [DEFINITION_REPOSITORY_PIN_KEY]: definitionRepositoryCommit, ...members } = pins;
85
+ if (typeof definitionRepositoryCommit !== 'string' || !SHA_PATTERN.test(definitionRepositoryCommit)) {
86
+ throw new Error(`--pins "${DEFINITION_REPOSITORY_PIN_KEY}" must be the 40-hex commit the definition repository is read at, got ${JSON.stringify(definitionRepositoryCommit)}`);
87
+ }
88
+ if (!Object.keys(members).some((key) => VENDOR_MEMBER_PIN_PATTERN.test(key))) {
89
+ throw new Error('--pins carries no vendor member pin (repos/<x> or submodules/<x>): a catalogue-free resolve resolves the vendor offering from them');
90
+ }
91
+ return { definitionRepositoryCommit, members };
92
+ }
93
+
94
+ /**
95
+ * The members of --pins between the two steps: `{ own, vendor }`, own the sources/<name> keys the definition declares.
96
+ * `offering` and `definition` are `{ label, sourceNames }`: a source name both declare is refused, naming both.
97
+ */
98
+ export function partitionCatalogueFreePins(members, { offering, definition }) {
99
+ const vendorNames = new Set(offering.sourceNames);
100
+ const shared = [...new Set(definition.sourceNames)].filter((name) => vendorNames.has(name));
101
+ if (shared.length > 0) {
102
+ throw new Error(`the source${shared.length === 1 ? '' : 's'} ${shared.map((name) => `"${name}"`).join(', ')} ${shared.length === 1 ? 'is' : 'are'} declared by both the offering ${offering.label} and the definition ${definition.label}: a catalogue-free resolve splits each sources/<name> pin to one of them, so a source name is declared once`);
103
+ }
104
+ const ownNames = new Set(definition.sourceNames);
105
+ const own = {};
106
+ const vendor = {};
107
+ for (const [key, commit] of Object.entries(members)) {
108
+ if (isSourcePinKey(key) && ownNames.has(sourceNameOfPinKey(key))) {own[key] = commit;} else {vendor[key] = commit;}
109
+ }
110
+ return { own, vendor: sortedSnapshotPins(vendor) };
111
+ }
112
+
113
+ /** The definition repository at `commit`, through the source walk's fetch by exact commit; returns its (shared cache) directory. */
114
+ export function fetchDefinitionRepository({ repository, commit, env }) {
115
+ const source = { kind: 'git', name: DEFINITION_REPOSITORY_PIN_KEY, url: `https://github.com/${repository}.git`, ref: commit };
116
+ return materializeGitSource({ source, cacheRoot: join(cacheDir(), 'definition-repositories', ...repository.split('/')), env }).directory;
117
+ }
118
+
119
+ /**
120
+ * The tree of `commit` in the repository at `repository`, written into the new directory `into` from the object store
121
+ * (a temporary index beside `into`, never the repository's own index or worktree; symlinks as plain files, so nothing
122
+ * read from `into` resolves outside it). Returns `into`.
123
+ */
124
+ export function materializeDefinitionCommit({ repository, commit, into }) {
125
+ mkdirSync(into);
126
+ const env = { ...process.env, GIT_INDEX_FILE: `${into}.index`, GIT_NO_LAZY_FETCH: '1' };
127
+ const git = (args) => execFileSync('git', ['-C', repository, '-c', 'core.symlinks=false', ...args], { env, stdio: ['ignore', 'pipe', 'pipe'] });
128
+ try {
129
+ git(['read-tree', `${commit}^{tree}`]);
130
+ git(['checkout-index', '--all', `--prefix=${into}/`]);
131
+ } finally {
132
+ rmSync(`${into}.index`, { force: true });
133
+ }
134
+ return into;
135
+ }
136
+
137
+ /** The argv of the vendor step's child (after the node binary): the offering's own previous seal, the engine flags, the vendor roots. */
138
+ export function vendorResolveArgs({ offeringId, pinsFile, outDir, options, roots }) {
139
+ return vendorStepArgs({
140
+ offeringId,
141
+ pinsFile,
142
+ outDir,
143
+ registry: options['--registry'],
144
+ engine: { repository: options['--engine-repository'], signer: options['--engine-signer'] },
145
+ cosignBinary: options['--cosign'],
146
+ previousLock: { path: options[OFFERING_PREVIOUS_LOCK_FLAG], sha256: options[OFFERING_PREVIOUS_LOCK_SHA256_FLAG] },
147
+ roots,
148
+ });
149
+ }
150
+
151
+ /** The vendor step: a child of this CLI (develop-catalogue-free-vendor.mjs) on the vendor roots, writing into `outDir`. */
152
+ export function runVendorResolve({ offeringId, pinsFile, outDir, options, env, roots }) {
153
+ const result = spawnSync(process.execPath, vendorResolveArgs({ offeringId, pinsFile, outDir, options, roots }), { env, stdio: ['ignore', 'ignore', 'inherit'] });
154
+ if (result.error !== undefined || result.status !== 0) {
155
+ throw new Error(`the vendor resolve of the offering "${offeringId}" failed (${result.error?.message ?? `exit ${result.status ?? result.signal}`}); its refusal is above`);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * The catalogue the vendor step wrote, its digest, the engine it proved, and the proof the vendor artifact is that offering
161
+ * at those pins. (The digest is of `bytes` as read, the same bytes parsed: never a re-serialisation.)
162
+ */
163
+ export function readVendorCatalogue({ outDir, offeringId, vendorPins }) {
164
+ const path = join(outDir, CATALOGUE_FILE);
165
+ const bytes = readFileSync(path);
166
+ const catalogue = parseCatalogue(parseStrictJson(bytes.toString('utf8'), path));
167
+ if (catalogue.offeringId !== offeringId || catalogue.channel !== EditionChannel.Develop) {
168
+ throw new Error(`the vendor step wrote the catalogue of "${catalogue.offeringId}" (${catalogue.channel}), not of the develop offering "${offeringId}"`);
169
+ }
170
+ const descriptor = parseEditionDescriptor(readStrictJsonFile(join(outDir, EDITION_DESCRIPTOR_FILE)));
171
+ if (serializeJson(sortedSnapshotPins(descriptor.provenance.pins)) !== serializeJson(vendorPins)) {
172
+ throw new Error(`the vendor step resolved "${offeringId}" at other pins than the vendor pins it was given`);
173
+ }
174
+ return { catalogue, catalogueContentDigest: catalogueContentDigestOf(bytes), engineImage: descriptor.engine.image };
175
+ }
176
+
177
+ /** Run `body` with the workspace on `roots`, restoring the previous roots after. */
178
+ async function onRoots(roots, body) {
179
+ const restored = configuredWorkspace();
180
+ resetEditionWorkspaceConfig();
181
+ configureEditionWorkspace(roots);
182
+ try {
183
+ return await body();
184
+ } finally {
185
+ resetEditionWorkspaceConfig();
186
+ configureEditionWorkspace(restored);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * The catalogue-free customer develop resolve (see the header). Writes the artifact to --out and returns
192
+ * `{ id, pins, target, catalogueContentDigest, engineImage }`. The edition records the engine the vendor step proved, and
193
+ * carries the customer's own composed generations from --previous-lock (or --first-seal) under the customer's id.
194
+ * `fetchDefinition`, `vendorResolve`, `resolveCustomer`, `evidenceTrust` and `write` are replaced by tests; `print`
195
+ * receives the target.
196
+ */
197
+ export async function resolveCatalogueFree({
198
+ options,
199
+ env = process.env,
200
+ fetchDefinition = fetchDefinitionRepository,
201
+ vendorResolve = runVendorResolve,
202
+ resolveCustomer = resolveCustomerChannelEdition,
203
+ evidenceTrust = descriptorEvidenceTrust,
204
+ write = writeEditionArtifact,
205
+ print = (line) => console.log(line),
206
+ }) {
207
+ const definition = parseDefinitionTriple(options[DEFINITION_AT_FLAG], DEFINITION_AT_FLAG);
208
+ const offering = parseDefinitionTriple(options[OFFERING_FLAG], OFFERING_FLAG);
209
+ const rawPins = readStrictJsonFile(options['--pins']);
210
+ const offeringId = assertDefinitionCheckout({ definition: offering, dataSha: rawPins?.[dataPinKey()], flag: OFFERING_FLAG, shaLabel: `--pins "${dataPinKey()}"` });
211
+ const { definitionRepositoryCommit, members } = splitCatalogueFreePins(rawPins);
212
+ const offeringLabel = `${offering.repository}:${offering.path}/${DEFINITION_FILE}`;
213
+ const offeringSourceNames = declaredSourceNamesOf(readStrictJsonFile(join(editionsRoot(), offeringId, DEFINITION_FILE)));
214
+ const vendorRoots = configuredWorkspace();
215
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-catalogue-free-'));
216
+ try {
217
+ const cached = fetchDefinition({ repository: definition.repository, commit: definitionRepositoryCommit, env });
218
+ const checkout = materializeDefinitionCommit({ repository: cached, commit: definitionRepositoryCommit, into: join(scratch, DEFINITION_REPOSITORY_PIN_KEY) });
219
+ const definitionDir = join(checkout, definition.path);
220
+ const definitionLabel = `${definition.repository}:${definition.path}/${DEFINITION_FILE}`;
221
+ if (existsSync(join(definitionDir, CATALOGUE_PIN_FILE))) {
222
+ throw new Error(`${definition.repository}:${definition.path} carries ${CATALOGUE_PIN_FILE}: a definition is resolved against its pinned catalogue (--definition <file>) or catalogue-free, never both`);
223
+ }
224
+ const rawDefinition = readStrictJsonFile(join(definitionDir, DEFINITION_FILE));
225
+ assertCanonicalDistributionId(rawDefinition?.id);
226
+ if (rawDefinition.id !== basename(definition.path)) {
227
+ throw new Error(`${definitionLabel} is "${rawDefinition.id}", not the directory's "${basename(definition.path)}"`);
228
+ }
229
+ const split = partitionCatalogueFreePins(members, {
230
+ offering: { label: offeringLabel, sourceNames: offeringSourceNames },
231
+ definition: { label: definitionLabel, sourceNames: declaredSourceNamesOf(rawDefinition) },
232
+ });
233
+ const sourceImages = customerSourceImages(options, rawDefinition);
234
+ const vendorOut = join(scratch, 'vendor');
235
+ const pinsFile = join(scratch, 'vendor-pins.json');
236
+ writeFileSync(pinsFile, serializeJson(split.vendor));
237
+ vendorResolve({ offeringId, pinsFile, outDir: vendorOut, options, env, roots: workspaceArgv() });
238
+ const { catalogue, catalogueContentDigest, engineImage } = readVendorCatalogue({ outDir: vendorOut, offeringId, vendorPins: split.vendor });
239
+ const sources = join(scratch, 'sources');
240
+ mkdirSync(sources);
241
+ const customerRoots = {
242
+ [WorkspaceRoot.Data]: join(checkout, definition.dataRoot),
243
+ [WorkspaceRoot.Editions]: dirname(definitionDir),
244
+ [WorkspaceRoot.Sources]: sources,
245
+ [WorkspaceRoot.Cache]: vendorRoots[WorkspaceRoot.Cache],
246
+ };
247
+ return await onRoots(customerRoots, async () => {
248
+ const { id, pins, ...resolved } = await resolveCustomer({
249
+ channel: EditionChannel.Develop,
250
+ rawDefinition,
251
+ catalogue,
252
+ catalogueContentDigest,
253
+ pins: split.own,
254
+ vendorPins: { ...split.vendor, [DEFINITION_REPOSITORY_PIN_KEY]: definitionRepositoryCommit },
255
+ previousSeal: customerPreviousSeal(EditionChannel.Develop, options),
256
+ sourceImages,
257
+ env,
258
+ // The customer's git-sourced OCI bundles, verified under the definition's own pinned policy (its data root).
259
+ developGitBundles: customerDevelopGitBundleVerifier({ channel: EditionChannel.Develop, cosignBinary: options['--cosign'], env }),
260
+ });
261
+ const evidenceTrustPolicy = await evidenceTrust({ kind: EditionKind.Resolved, channel: EditionChannel.Develop, consumesCatalogue: true });
262
+ const target = snapshotEditionTarget(EditionChannel.Develop, id, pins, { registry: options['--registry'], engineImage, sourceImages });
263
+ write({
264
+ outDir: resolve(options['--out']),
265
+ channel: EditionChannel.Develop,
266
+ distributionId: id,
267
+ pins,
268
+ workflow: options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env),
269
+ evidenceTrustPolicy,
270
+ engineImage,
271
+ distributionLock: resolved.distributionLock,
272
+ imageLock: resolved.imageLock,
273
+ authorizationLock: resolved.authorizationLock,
274
+ chartSetLock: resolved.chartSetLock,
275
+ });
276
+ print(target);
277
+ return { id, pins, target, catalogueContentDigest, engineImage };
278
+ });
279
+ } finally {
280
+ rmSync(scratch, { recursive: true, force: true });
281
+ }
282
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // develop-catalogue-free-vendor — the VENDOR step of the catalogue-free customer develop resolve
3
+ // (develop-catalogue-free-resolve.mjs), an internal entry of the OFFICIAL lane, never a verb:
4
+ //
5
+ // node develop-catalogue-free-vendor.mjs <offering id> --pins <file> --out <dir> --registry <root>
6
+ // --engine-repository <repository> --engine-signer <certificate identity> --cosign <absolute-path>
7
+ // --previous-lock <file> --previous-lock-sha256 <hex> <the vendor roots>
8
+ //
9
+ // The catalogue-free resolve runs it as a child process of this CLI on the vendor roots (the roots it was given), so
10
+ // the vendor resolve has its own kernel load and workspace, and writes the vendor artifact and catalogue.json into the
11
+ // parent's scratch directory. It resolves the offering on the VERIFIED lane (`verifiedVendorLane`): the engine found
12
+ // and cosign-proven (develop-engine-image.mjs), every image probed by its computed content hash, every git-sourced OCI
13
+ // bundle cosign-verified against its frozen source (develop-git-bundles.mjs `createDevelopGitBundleVerifier`), the
14
+ // bundle signers asserted, the third-party pins held to the evidence trust pin, and the descriptor's evidence trust the
15
+ // data root's pinned policy (`descriptorEvidenceTrust`). It seals against the OFFERING's own previous seal, held to its
16
+ // sha256 and the offering's id. The descriptor records LOCAL_WORKFLOW: it never leaves the scratch directory.
17
+ //
18
+ // The channel lane's vendor `develop resolve` (develop-edition.mjs) takes none of this: its engine and digests are
19
+ // the caller's and the build record's.
20
+
21
+ import { resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { parseArgs } from 'node:util';
24
+
25
+ import { writeCatalogue } from './catalogue.mjs';
26
+ import { DEVELOP_VENDOR_ROOTS } from './customer-edition-verbs.mjs';
27
+ import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
28
+ import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
29
+ import { createDevelopGitBundleVerifier } from './develop-git-bundles.mjs';
30
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
31
+ import { LOCAL_WORKFLOW, writeEditionArtifact } from './edition-artifact.mjs';
32
+ import { EditionChannel, EditionKind } from './edition-constants.mjs';
33
+ import { configureEditionWorkspaceFromProcess, dataRoot, isEntryModule } from './edition-workspace.mjs';
34
+ import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
35
+ import { resolvePreviousSeal } from './resolve-images.mjs';
36
+ import { readStrictJsonFile } from './strict-json.mjs';
37
+ import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs';
38
+
39
+ /** The entry the catalogue-free resolve spawns. */
40
+ export const VENDOR_STEP_ENTRY = fileURLToPath(import.meta.url);
41
+
42
+ /** The step's flags, every one required (`--name` -> parseArgs key `name`). */
43
+ const VENDOR_STEP_FLAGS = Object.freeze(['pins', 'out', 'registry', 'engine-repository', 'engine-signer', 'cosign', 'previous-lock', 'previous-lock-sha256']);
44
+
45
+ /** The argv of the vendor step (after the node binary), the roots appended as `workspaceArgv()` states them. */
46
+ export function vendorStepArgs({ offeringId, pinsFile, outDir, registry, engine, cosignBinary, previousLock, roots }) {
47
+ return [
48
+ VENDOR_STEP_ENTRY, offeringId, '--pins', pinsFile, '--out', outDir, '--registry', registry,
49
+ '--engine-repository', engine.repository, '--engine-signer', engine.signer, '--cosign', cosignBinary,
50
+ '--previous-lock', previousLock.path, '--previous-lock-sha256', previousLock.sha256,
51
+ ...roots,
52
+ ];
53
+ }
54
+
55
+ /** `<offering id> --flag value ...`, every flag required, nothing else admitted. */
56
+ export function parseVendorStepArgs(argv) {
57
+ const { values, positionals } = parseArgs({
58
+ args: argv,
59
+ options: Object.fromEntries(VENDOR_STEP_FLAGS.map((name) => [name, { type: 'string' }])),
60
+ allowPositionals: true,
61
+ strict: true,
62
+ });
63
+ if (positionals.length !== 1) {throw new Error(`the catalogue-free vendor step takes exactly one <offering id>, got ${positionals.length}`);}
64
+ const missing = VENDOR_STEP_FLAGS.filter((name) => values[name] === undefined);
65
+ if (missing.length > 0) {throw new Error(`the catalogue-free vendor step requires ${missing.map((name) => `--${name}`).join(', ')}`);}
66
+ assertCanonicalDistributionId(positionals[0]);
67
+ return { offeringId: positionals[0], ...values };
68
+ }
69
+
70
+ /**
71
+ * The verified lane of the vendor step (see the header), for `resolveDevelopEdition`.
72
+ *
73
+ * @param {object} request
74
+ * @param {object} request.pins the vendor pins (the engine is the one published for their xema-deploy commit)
75
+ * @param {{repository: string, signer: string}} request.engine
76
+ * @param {string} request.cosignBinary absolute path of cosign, held to the data root's pinned policy
77
+ */
78
+ export function verifiedVendorLane({ pins, engine, cosignBinary, env = process.env }) {
79
+ return Object.freeze({
80
+ engineImage: () => resolveDevelopEngineImage({ pins, engine, cosignBinary, env }),
81
+ developGitBundles: createDevelopGitBundleVerifier({ cosignBinary, dataRoot: dataRoot(), env }),
82
+ checkBundleSigners: true,
83
+ thirdPartyPins: () => loadTrustedThirdPartySourcePins(dataRoot()),
84
+ });
85
+ }
86
+
87
+ /** The vendor step: resolve the offering on the verified lane and write its artifact and catalogue to --out. */
88
+ export async function runVendorStep({ argv, env = process.env, resolveEdition = resolveDevelopEdition, evidenceTrust = descriptorEvidenceTrust }) {
89
+ const args = parseVendorStepArgs(argv);
90
+ const pins = readStrictJsonFile(args.pins);
91
+ const { catalogue, engineImage, ...resolved } = await resolveEdition({
92
+ id: args.offeringId,
93
+ pins,
94
+ env,
95
+ lane: verifiedVendorLane({ pins, engine: { repository: args['engine-repository'], signer: args['engine-signer'] }, cosignBinary: args.cosign, env }),
96
+ previousLock: await resolvePreviousSeal({ distributionId: args.offeringId, previousLockPath: args['previous-lock'], previousLockSha256: args['previous-lock-sha256'] }),
97
+ });
98
+ const outDir = resolve(args.out);
99
+ writeEditionArtifact({
100
+ outDir,
101
+ channel: EditionChannel.Develop,
102
+ distributionId: args.offeringId,
103
+ pins,
104
+ workflow: LOCAL_WORKFLOW,
105
+ evidenceTrustPolicy: await evidenceTrust({ kind: EditionKind.Resolved, channel: EditionChannel.Develop }),
106
+ engineImage,
107
+ ...resolved,
108
+ });
109
+ writeCatalogue(outDir, catalogue);
110
+ }
111
+
112
+ if (isEntryModule(import.meta.url)) {
113
+ configureEditionWorkspaceFromProcess(DEVELOP_VENDOR_ROOTS);
114
+ runVendorStep({ argv: process.argv.slice(2) }).catch((error) => {
115
+ console.error(`[develop-catalogue-free-vendor] ${error.message}`);
116
+ process.exit(1);
117
+ });
118
+ }
@@ -0,0 +1,23 @@
1
+ // develop-channel-evidence — the evidence trust a vendor develop edition records: its images come from a registry
2
+ // channel (`resolve --images-from <record> --channel <tag>`), the only form of a vendor develop resolve.
3
+ //
4
+ // Channel images are baked unsigned: their trust is the registry write ACL, so there is no signature evidence for the
5
+ // image-evidence verifier to check, and nothing here constructs the verifier or reads the data root's evidence-trust pin
6
+ // (which records the verifier build). The edition therefore records `not-armed`, the descriptor state that says no
7
+ // evidence was signature-verified; `resolve` says why once on stderr, and `check-existing` holds a pulled artifact to
8
+ // the same state. The pinned verifier is the official lane's (evidence-trust-policy.mjs), never read here.
9
+
10
+ import { EvidenceTrustState } from './edition-constants.mjs';
11
+
12
+ /** What a channel-resolved develop edition says, once, in place of loading the evidence verifier. */
13
+ export const CHANNEL_EVIDENCE_NOTICE =
14
+ 'develop channel: image evidence is the registry write ACL (images are baked unsigned); the evidence verifier and its pin are not loaded';
15
+
16
+ /** The evidence trust state every vendor develop edition records. */
17
+ export const DEVELOP_CHANNEL_EVIDENCE_TRUST = EvidenceTrustState.NotArmed;
18
+
19
+ /** The state a vendor develop resolve records, with its notice on `log`. */
20
+ export function developResolveEvidenceTrust({ log = console.error } = {}) {
21
+ log(CHANNEL_EVIDENCE_NOTICE);
22
+ return DEVELOP_CHANNEL_EVIDENCE_TRUST;
23
+ }