@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,270 @@
1
+ // The third-party source pin file (`third-party-source-pins.json`, XEMA-1611 design section 3).
2
+ //
3
+ // ONE reviewed record per third-party image source: the explicit grant that lets a biome from an
4
+ // organisation outside the vendor fleet enter an edition with sealed, fully evidenced images. It
5
+ // is validated here (the CLI), not by a kernel contract, and it is written only by the trusted
6
+ // writer. The file lives at the root of the data root in use, beside the evidence trust policy;
7
+ // there is no vendor path and no vendor key.
8
+ //
9
+ // * A MISSING, EMPTY or UNPARSABLE (not JSON) file means "no third-party source": the loader
10
+ // returns an empty set and a reason every consumer puts in its refusal.
11
+ // * A file that parses but is not canonical JSON or does not match the schema THROWS: it is a
12
+ // reviewed record, and a record that is present but wrong is never read as "no sources".
13
+ // * The file's sha256 joins the evidence trust pin (`thirdPartySourcePinsDigest`).
14
+ // * Each artifact records the workflow that signed it: the bundle's `signerWorkflow` (its `signerPolicyId` row) and
15
+ // each image's `producerWorkflow` (its `policyId` row). They may differ (one workflow publishes the bundle, another
16
+ // builds the images); all are workflows of `repository`, signed from refs/heads/main at `sourceCommit`.
17
+ // * `runnerEnvironment` is OPTIONAL and, when present, is exactly `self-hosted`: the publisher's declaration that its
18
+ // signer workflows run on its own (dedicated, label-pinned) runners. Absent means `github-hosted`; there is no
19
+ // second spelling of the default. See `third-party-certificate-extensions.mjs` for what a self-hosted pin requires.
20
+
21
+ import { existsSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+
24
+ import { parseCanonicalJsonBytes, sha256Digest } from './canonical-evidence-json.mjs';
25
+ import {
26
+ CANONICAL_IDENTIFIER,
27
+ COMMIT_PATTERN,
28
+ GITHUB_REPOSITORY_PATTERN,
29
+ SHA256_DIGEST_PATTERN,
30
+ SHA256_HEX_PATTERN,
31
+ } from './evidence-grammars.mjs';
32
+ import { parseDeclaredRunnerEnvironment } from './fulcio-certificate-extensions.mjs';
33
+ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
34
+
35
+ export const THIRD_PARTY_SOURCE_PINS_FILE = 'third-party-source-pins.json';
36
+ export const THIRD_PARTY_SOURCE_PINS_SCHEMA_VERSION = 1;
37
+
38
+ const MAX_PINS_BYTES = 2 * 1024 * 1024;
39
+ const LABEL = THIRD_PARTY_SOURCE_PINS_FILE;
40
+
41
+ const SOURCE_NAME = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
42
+ const RUNTIME_IDENTITY = /^[a-z0-9](?:[a-z0-9._-]{0,254}[a-z0-9])?$/;
43
+ const GITHUB_NUMERIC_ID = /^[1-9][0-9]*$/;
44
+ const OCI_REPOSITORY = /^[a-z0-9]+(?:[.-][a-z0-9]+)*(?::[0-9]+)?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+$/;
45
+ const OCI_REF_BY_DIGEST = new RegExp(`${OCI_REPOSITORY.source.slice(0, -1)}@sha256:[a-f0-9]{64}$`);
46
+ const WORKFLOW_PATH = /^\.github\/workflows\/[A-Za-z0-9._-]+\.ya?ml$/;
47
+ const WORKFLOW_PATH_EXPECTATION = 'a .github/workflows/*.yml|yaml path';
48
+ const SUBJECT_KINDS = Object.freeze(['service', 'platform', 'host', 'runtime-component', 'composed-process']);
49
+ const SOURCE_HASH_ALGORITHMS = Object.freeze([
50
+ 'xema-image-input-sha256-v1',
51
+ 'xema-image-input-sha256-v2',
52
+ 'xema-image-input-sha256-v3',
53
+ ]);
54
+
55
+ const SOURCE_KEYS = Object.freeze([
56
+ 'bundle',
57
+ 'images',
58
+ 'name',
59
+ 'repository',
60
+ 'repositoryId',
61
+ 'repositoryOwnerId',
62
+ 'repositoryUrl',
63
+ 'sourceCommit',
64
+ ]);
65
+ /** The one optional source key: present only to declare a non-default runner environment. */
66
+ const RUNNER_ENVIRONMENT_KEY = 'runnerEnvironment';
67
+ const SOURCE_KEYS_WITH_RUNNER = Object.freeze([...SOURCE_KEYS, RUNNER_ENVIRONMENT_KEY].sort());
68
+ const BUNDLE_KEYS = Object.freeze([
69
+ 'biomeId',
70
+ 'bundleDigest',
71
+ 'ociRef',
72
+ 'signerPolicyId',
73
+ 'signerWorkflow',
74
+ 'sourceManifestHash',
75
+ 'statementDigest',
76
+ ]);
77
+ const IMAGE_KEYS = Object.freeze([
78
+ 'bundleDigest',
79
+ 'imageRef',
80
+ 'manifestDigest',
81
+ 'policyId',
82
+ 'producerWorkflow',
83
+ 'sourceHashAlgorithm',
84
+ 'sourceTreeHash',
85
+ 'statementDigest',
86
+ 'subjectId',
87
+ 'subjectKind',
88
+ ]);
89
+
90
+ /** Why a data root has no third-party source. */
91
+ export const ThirdPartyPinsAbsence = Object.freeze({
92
+ Missing: 'missing',
93
+ Empty: 'empty',
94
+ Unparsable: 'unparsable',
95
+ });
96
+
97
+ const exactKeys = (value, keys) =>
98
+ value !== null &&
99
+ typeof value === 'object' &&
100
+ !Array.isArray(value) &&
101
+ Object.keys(value).sort().join() === keys.join();
102
+
103
+ function field(path, value, test, expectation) {
104
+ if (typeof value !== 'string' || !test(value)) {
105
+ throw new Error(`${LABEL}: ${path} must be ${expectation}`);
106
+ }
107
+ return value;
108
+ }
109
+ const matching = (pattern) => (value) => pattern.test(value);
110
+
111
+ function requireKeys(value, keys, path) {
112
+ if (!exactKeys(value, keys)) {
113
+ throw new Error(`${LABEL}: ${path} must have exactly the keys {${keys.join(', ')}}`);
114
+ }
115
+ }
116
+
117
+ function parseImage(image, path) {
118
+ requireKeys(image, IMAGE_KEYS, path);
119
+ const oneOf = (list) => (value) => list.includes(value);
120
+ return Object.freeze({
121
+ subjectKind: field(`${path}.subjectKind`, image.subjectKind, oneOf(SUBJECT_KINDS), `one of ${SUBJECT_KINDS.join(', ')}`),
122
+ subjectId: field(`${path}.subjectId`, image.subjectId, matching(CANONICAL_IDENTIFIER), 'a canonical identifier'),
123
+ imageRef: field(`${path}.imageRef`, image.imageRef, matching(OCI_REPOSITORY), 'an OCI repository reference without tag or digest'),
124
+ manifestDigest: field(`${path}.manifestDigest`, image.manifestDigest, matching(SHA256_DIGEST_PATTERN), 'sha256:<64 hex>'),
125
+ sourceTreeHash: field(`${path}.sourceTreeHash`, image.sourceTreeHash, matching(SHA256_HEX_PATTERN), '64 lowercase hex digits'),
126
+ sourceHashAlgorithm: field(`${path}.sourceHashAlgorithm`, image.sourceHashAlgorithm, oneOf(SOURCE_HASH_ALGORITHMS), `one of ${SOURCE_HASH_ALGORITHMS.join(', ')}`),
127
+ statementDigest: field(`${path}.statementDigest`, image.statementDigest, matching(SHA256_DIGEST_PATTERN), 'sha256:<64 hex>'),
128
+ bundleDigest: field(`${path}.bundleDigest`, image.bundleDigest, matching(SHA256_DIGEST_PATTERN), 'sha256:<64 hex>'),
129
+ policyId: field(`${path}.policyId`, image.policyId, matching(CANONICAL_IDENTIFIER), 'a canonical policy id'),
130
+ producerWorkflow: field(`${path}.producerWorkflow`, image.producerWorkflow, matching(WORKFLOW_PATH), WORKFLOW_PATH_EXPECTATION),
131
+ });
132
+ }
133
+
134
+ function parseSource(source, path) {
135
+ const declaresRunner = source !== null && typeof source === 'object' && Object.hasOwn(source, RUNNER_ENVIRONMENT_KEY);
136
+ requireKeys(source, declaresRunner ? SOURCE_KEYS_WITH_RUNNER : SOURCE_KEYS, path);
137
+ const name = field(`${path}.name`, source.name, matching(SOURCE_NAME), 'a lowercase source name');
138
+ const repository = field(`${path}.repository`, source.repository, matching(GITHUB_REPOSITORY_PATTERN), 'owner/name');
139
+ const repositoryUrl = field(
140
+ `${path}.repositoryUrl`,
141
+ source.repositoryUrl,
142
+ (value) => value === `https://github.com/${repository}` || value === `https://github.com/${repository}.git`,
143
+ `https://github.com/${repository}[.git] (the URL of repository)`,
144
+ );
145
+ requireKeys(source.bundle, BUNDLE_KEYS, `${path}.bundle`);
146
+ const sourceCommit = field(`${path}.sourceCommit`, source.sourceCommit, matching(COMMIT_PATTERN), 'an exact 40-hex commit');
147
+ const bundle = Object.freeze({
148
+ ociRef: field(`${path}.bundle.ociRef`, source.bundle.ociRef, matching(OCI_REF_BY_DIGEST), '<repository>@sha256:<64 hex> (pinned by digest)'),
149
+ biomeId: field(`${path}.bundle.biomeId`, source.bundle.biomeId, matching(RUNTIME_IDENTITY), 'a lowercase biome id'),
150
+ sourceManifestHash: field(`${path}.bundle.sourceManifestHash`, source.bundle.sourceManifestHash, matching(SHA256_HEX_PATTERN), '64 lowercase hex digits'),
151
+ signerPolicyId: field(`${path}.bundle.signerPolicyId`, source.bundle.signerPolicyId, matching(CANONICAL_IDENTIFIER), 'a canonical policy id'),
152
+ signerWorkflow: field(`${path}.bundle.signerWorkflow`, source.bundle.signerWorkflow, matching(WORKFLOW_PATH), WORKFLOW_PATH_EXPECTATION),
153
+ statementDigest: field(`${path}.bundle.statementDigest`, source.bundle.statementDigest, matching(SHA256_DIGEST_PATTERN), 'sha256:<64 hex>'),
154
+ bundleDigest: field(`${path}.bundle.bundleDigest`, source.bundle.bundleDigest, matching(SHA256_DIGEST_PATTERN), 'sha256:<64 hex>'),
155
+ });
156
+ if (!Array.isArray(source.images) || source.images.length === 0) {
157
+ throw new Error(`${LABEL}: ${path}.images must list at least one image`);
158
+ }
159
+ const images = source.images.map((image, index) => parseImage(image, `${path}.images[${index}]`));
160
+ return Object.freeze({
161
+ name,
162
+ repository,
163
+ repositoryUrl,
164
+ repositoryId: field(`${path}.repositoryId`, source.repositoryId, matching(GITHUB_NUMERIC_ID), 'a decimal GitHub repository id'),
165
+ repositoryOwnerId: field(`${path}.repositoryOwnerId`, source.repositoryOwnerId, matching(GITHUB_NUMERIC_ID), 'a decimal GitHub owner id'),
166
+ sourceCommit,
167
+ ...(declaresRunner
168
+ ? {
169
+ // A present key must hold the one declared value: `parseDeclaredRunnerEnvironment` refuses null and the rest.
170
+ runnerEnvironment: parseDeclaredRunnerEnvironment(source.runnerEnvironment ?? null, `${LABEL}: ${path}.${RUNNER_ENVIRONMENT_KEY}`),
171
+ }
172
+ : {}),
173
+ bundle,
174
+ images: Object.freeze(images),
175
+ });
176
+ }
177
+
178
+ function assertUnique(sources, pick, what) {
179
+ const seen = new Map();
180
+ sources.forEach((source, index) => {
181
+ for (const value of pick(source)) {
182
+ if (seen.has(value)) {
183
+ throw new Error(`${LABEL}: ${what} "${value}" is pinned by sources[${seen.get(value)}] and sources[${index}]`);
184
+ }
185
+ seen.set(value, index);
186
+ }
187
+ });
188
+ }
189
+
190
+ /** Validate a parsed pin document. Throws on the first violation, naming the key. */
191
+ export function parseThirdPartySourcePins(document) {
192
+ requireKeys(document, ['schemaVersion', 'sources'], 'the document');
193
+ if (document.schemaVersion !== THIRD_PARTY_SOURCE_PINS_SCHEMA_VERSION) {
194
+ throw new Error(`${LABEL}: schemaVersion must be ${THIRD_PARTY_SOURCE_PINS_SCHEMA_VERSION}`);
195
+ }
196
+ if (!Array.isArray(document.sources)) {
197
+ throw new Error(`${LABEL}: sources must be an array`);
198
+ }
199
+ const sources = document.sources.map((source, index) => parseSource(source, `sources[${index}]`));
200
+ assertUnique(sources, (source) => [source.name], 'source name');
201
+ assertUnique(sources, (source) => [source.repository], 'repository');
202
+ assertUnique(sources, (source) => [source.repositoryId], 'repositoryId');
203
+ assertUnique(sources, (source) => [source.bundle.biomeId], 'biomeId');
204
+ assertUnique(sources, (source) => source.images.map((image) => image.imageRef), 'imageRef');
205
+ sources.forEach((source, index) => {
206
+ const ids = source.images.map((image) => image.subjectId);
207
+ if (new Set(ids).size !== ids.length) {
208
+ throw new Error(`${LABEL}: sources[${index}].images repeats a subjectId`);
209
+ }
210
+ });
211
+ return Object.freeze(sources);
212
+ }
213
+
214
+ /** The pinned sources, with lookups. An empty set carries the `reason` consumers put in their refusal. */
215
+ function sourceSet(sources, absence) {
216
+ const byName = new Map(sources.map((source) => [source.name, source]));
217
+ const byRepository = new Map(sources.map((source) => [source.repository, source]));
218
+ const reason =
219
+ absence === null
220
+ ? null
221
+ : `${THIRD_PARTY_SOURCE_PINS_FILE} is ${absence} in the data root, so no third-party source is pinned`;
222
+ return Object.freeze({
223
+ sources,
224
+ absence,
225
+ reason,
226
+ byName: (name) => byName.get(name),
227
+ byRepository: (repository) => byRepository.get(repository),
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Read the pin file's exact bytes from the data root: `null` when the file does not exist.
233
+ * This is what the evidence trust pin digests.
234
+ */
235
+ export function readThirdPartySourcePinsBytes(dataRoot) {
236
+ if (!existsSync(join(dataRoot, THIRD_PARTY_SOURCE_PINS_FILE))) {
237
+ return null;
238
+ }
239
+ return readBoundedNoFollowFile(dataRoot, THIRD_PARTY_SOURCE_PINS_FILE, LABEL, MAX_PINS_BYTES).bytes;
240
+ }
241
+
242
+ /** sha256 of the pin file's bytes, or `null` when the data root has no such file. */
243
+ export function thirdPartySourcePinsDigest(dataRoot) {
244
+ const bytes = readThirdPartySourcePinsBytes(dataRoot);
245
+ return bytes === null ? null : sha256Digest(bytes);
246
+ }
247
+
248
+ /**
249
+ * Load the third-party source pins of the data root in use.
250
+ *
251
+ * @param {string} dataRoot
252
+ * @returns {{sources: readonly object[], absence: string|null, reason: string|null,
253
+ * byName: (name: string) => object|undefined, byRepository: (repository: string) => object|undefined}}
254
+ */
255
+ export function loadThirdPartySourcePins(dataRoot) {
256
+ const bytes = readThirdPartySourcePinsBytes(dataRoot);
257
+ if (bytes === null) {
258
+ return sourceSet([], ThirdPartyPinsAbsence.Missing);
259
+ }
260
+ if (bytes.length === 0) {
261
+ return sourceSet([], ThirdPartyPinsAbsence.Empty);
262
+ }
263
+ try {
264
+ JSON.parse(bytes.toString('utf8'));
265
+ } catch {
266
+ return sourceSet([], ThirdPartyPinsAbsence.Unparsable);
267
+ }
268
+ const document = parseCanonicalJsonBytes(bytes, LABEL);
269
+ return sourceSet(parseThirdPartySourcePins(document), null);
270
+ }
@@ -0,0 +1,54 @@
1
+ // Check (e) of a third-party source pin (XEMA-1611 design section 3): who may be pinned next to whom.
2
+ //
3
+ // It reuses the pin loader's uniqueness (names, repositories, repository ids, biome ids, image references) and the
4
+ // trust policy's image ownership (one publisher per image repository, no first-party namespace for a foreign owner),
5
+ // and adds what only the data root's own indexes can answer: the third-party repository owner is not a first-party
6
+ // owner, the biome id does not collide with a biome, platform service or supplemental subject, and the name and
7
+ // repository are not repositories of the runtime image source index.
8
+
9
+ import { parseThirdPartySourcePins, THIRD_PARTY_SOURCE_PINS_SCHEMA_VERSION } from './third-party-source-pins.mjs';
10
+ import { assertTrustPolicyImageOwnership, firstPartyOwnersFromIndex, isFirstPartyImageNamespace } from './trust-policy-image-ownership.mjs';
11
+
12
+ const repositoryPathOf = (repositoryUrl) => new URL(repositoryUrl).pathname.replace(/^\//u, '').replace(/\.git$/u, '');
13
+
14
+ /**
15
+ * Refuse a set of pinned sources that breaks (e). Throws naming the source and the collision.
16
+ *
17
+ * @param {{sources: readonly object[], policy: object, runtimeSourceIndex: object, biomeIndex: object[], platformServiceIndex: {services: object[]}}} data
18
+ */
19
+ export function assertThirdPartyUniqueness({ sources, policy, runtimeSourceIndex, biomeIndex, platformServiceIndex }) {
20
+ parseThirdPartySourcePins({ schemaVersion: THIRD_PARTY_SOURCE_PINS_SCHEMA_VERSION, sources });
21
+ const firstPartyOwners = firstPartyOwnersFromIndex(runtimeSourceIndex);
22
+ assertTrustPolicyImageOwnership({ policy, firstPartyOwners });
23
+ const biomeIds = new Set(biomeIndex.map((biome) => biome.id));
24
+ const subjectIds = new Set([
25
+ ...platformServiceIndex.services.map((service) => service.name),
26
+ ...runtimeSourceIndex.supplementalSubjects.map((subject) => subject.id),
27
+ ]);
28
+ const indexRepositories = new Set(runtimeSourceIndex.repositoryPins.flatMap((pin) => [pin.repo, repositoryPathOf(pin.repositoryUrl).toLowerCase()]));
29
+ const indexRepositoryNames = new Set(runtimeSourceIndex.repositoryPins.map((pin) => pin.repo.split('/').at(-1)));
30
+ for (const source of sources) {
31
+ const where = `third-party source "${source.name}"`;
32
+ const owner = source.repository.split('/')[0].toLowerCase();
33
+ if (firstPartyOwners.has(owner)) {
34
+ throw new Error(`${where}: repository ${source.repository} is owned by "${owner}", a first-party owner (the owner of an index repository pin, or xemahq)`);
35
+ }
36
+ if (indexRepositories.has(source.repository.toLowerCase())) {
37
+ throw new Error(`${where}: repository ${source.repository} is a repository of the runtime image source index`);
38
+ }
39
+ if (indexRepositories.has(source.name) || indexRepositoryNames.has(source.name)) {
40
+ throw new Error(`${where}: the name collides with a repository of the runtime image source index`);
41
+ }
42
+ if (biomeIds.has(source.bundle.biomeId)) {
43
+ throw new Error(`${where}: biomeId "${source.bundle.biomeId}" collides with a biome of the biome index`);
44
+ }
45
+ if (subjectIds.has(source.bundle.biomeId)) {
46
+ throw new Error(`${where}: biomeId "${source.bundle.biomeId}" collides with a platform service or supplemental subject of the data root`);
47
+ }
48
+ for (const image of source.images) {
49
+ if (isFirstPartyImageNamespace(image.imageRef, firstPartyOwners)) {
50
+ throw new Error(`${where}: image ${image.imageRef} is in a first-party namespace (<registry>/<first-party owner>/<any>)`);
51
+ }
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,118 @@
1
+ // The `kind: oci` branch of the edition source walk (XEMA-1611 design section 4).
2
+ //
3
+ // A declared OCI source names a pin of the data root's `third-party-source-pins.json` (written only by the trusted
4
+ // writer). Walking it pulls the pinned bundle BY DIGEST, holds its manifest to the pinned `sourceManifestHash`, re-runs
5
+ // checks (a) to (d) of the writer from the evidence COMMITTED under the data root (`verifyThirdPartySourceEvidence`, the
6
+ // one verifier the writer and `--check-third-party` use), and projects the manifest into an index entry through
7
+ // `manifestToIndexEntry`. It reads no git repository and writes no lock `source`: the lock's `LockedBiomeSource` is
8
+ // git-only, so the row is held to its pin by the image plane instead (`third-party-subjects.mjs`).
9
+
10
+ import { assertEditionChannel } from './edition-channel.mjs';
11
+ import { dataRoot as configuredDataRoot } from './edition-workspace.mjs';
12
+ import { loadKernelBiomeContracts, loadKernelDistributionSchemas, manifestToIndexEntry } from './lib.mjs';
13
+ import { createRegistryClientResolver } from './oci-image-source-evidence.mjs';
14
+ import { disposeRegistrySessions } from './oras.mjs';
15
+ import { fetchBundleGeneratedIndex, fetchBundleManifest } from './third-party-bundle-registry.mjs';
16
+ import { loadThirdPartySourceContext } from './third-party-source-context.mjs';
17
+ import { verifyThirdPartySourceEvidence } from './third-party-source-evidence.mjs';
18
+ import { COSIGN_BIN_ENV } from './third-party-source-flags.mjs';
19
+ import { loadThirdPartySourcePins } from './third-party-source-pins.mjs';
20
+ import { OCI_BUNDLE_SOURCE_KIND } from './third-party-subjects.mjs';
21
+
22
+ /**
23
+ * @param {object} options
24
+ * @param {{name: string, kind: string, registry: string}} options.source the declared `oci` source
25
+ * @param {{reason: string|null, byName: (name: string) => object|undefined}} options.pins
26
+ * @param {string} options.dataRoot the evidence under it is the committed evidence of the pin
27
+ * @param {object} options.context `loadThirdPartySourceContext`
28
+ * @param {(authority: string) => object} options.clientFor
29
+ * @param {(value: unknown) => object} options.parseBundleSource the kernel's bundle source schema
30
+ * @returns {Promise<object>} the (unstamped) index entry of the bundle's biome
31
+ */
32
+ async function walkThirdPartySource({ source, pin, dataRoot, context, clientFor, parseBundleSource, channel }) {
33
+ const where = `source "${source.name}"`;
34
+ if (!pin.bundle.ociRef.startsWith(`${source.registry}/`)) {
35
+ throw new Error(`${where} declares registry ${source.registry}, but its pinned bundle ${pin.bundle.ociRef} is not served from it`);
36
+ }
37
+ // (a) to (d) from the committed evidence: the signature, the provenance subject (= the pinned bundle digest), the
38
+ // certificate extensions and every image's evidence.
39
+ const evidence = verifyThirdPartySourceEvidence({ source: pin, evidenceRoot: dataRoot, context, channel });
40
+ const path = biomeDirectory(pin, evidence.images, where);
41
+
42
+ const { manifest } = await fetchBundleManifest({ clientFor, ociRef: pin.bundle.ociRef });
43
+ const actualHash = await context.schemas.computeBiomeSourceManifestHash(manifest);
44
+ if (actualHash !== pin.bundle.sourceManifestHash) {
45
+ throw new Error(`${where}: bundle ${pin.bundle.ociRef} hashes its manifest to ${actualHash}, but the pin records ${pin.bundle.sourceManifestHash}`);
46
+ }
47
+ if (manifest?.xema?.id !== pin.bundle.biomeId) {
48
+ throw new Error(`${where}: bundle ${pin.bundle.ociRef} declares biome ${JSON.stringify(manifest?.xema?.id)}, but the pin is for "${pin.bundle.biomeId}"`);
49
+ }
50
+ const generatedIndex = await fetchBundleGeneratedIndex({ clientFor, ociRef: pin.bundle.ociRef });
51
+ const entry = await manifestToIndexEntry({
52
+ raw: manifest,
53
+ generatedIndex,
54
+ rel: `${source.name}/${pin.bundle.ociRef}`,
55
+ repo: pin.repository,
56
+ path,
57
+ });
58
+ if ((entry.hostModules ?? []).length > 0) {
59
+ throw new Error(`${where}: biome "${entry.id}" ships host modules, which a third-party OCI bundle is not admitted to carry`);
60
+ }
61
+ // Where the bundle lives is the pin's statement, never the manifest's: a manifest that names another bundle source is refused.
62
+ const bundleSource = parseBundleSource({ kind: OCI_BUNDLE_SOURCE_KIND, ociRef: pin.bundle.ociRef });
63
+ if (entry.bundleSource !== undefined && JSON.stringify(entry.bundleSource) !== JSON.stringify(bundleSource)) {
64
+ throw new Error(`${where}: biome "${entry.id}" declares bundleSource ${JSON.stringify(entry.bundleSource)}, which is not the pinned bundle ${pin.bundle.ociRef}`);
65
+ }
66
+ return { ...entry, bundleSource };
67
+ }
68
+
69
+ /**
70
+ * The biome's directory in the publisher's repository: the `source.path` its verified image statements all state (a
71
+ * subject's `path` must equal it at admission). A pin with no image has no subject to bind, so the repository root.
72
+ */
73
+ function biomeDirectory(pin, statements, where) {
74
+ const paths = [...new Set(statements.map((statement) => statement.predicate.source.path))];
75
+ if (paths.length > 1) {
76
+ throw new Error(`${where}: the pinned images' evidence states different source paths (${paths.join(', ')}), so the biome has no one directory`);
77
+ }
78
+ return paths[0] ?? pin.repository;
79
+ }
80
+
81
+ /** The pin of a declared OCI source, or the refusal that names why there is none (a missing or empty pin file included). */
82
+ function requirePin(pins, source) {
83
+ const pin = pins.byName(source.name);
84
+ if (pin === undefined) {
85
+ throw new Error(`source "${source.name}" is kind "oci", but ${pins.reason ?? `the data root's third-party source pins hold no pin named "${source.name}"`}`);
86
+ }
87
+ return pin;
88
+ }
89
+
90
+ /**
91
+ * Walk every declared OCI source: `{source, entry}` per source, the index entry of its bundle unstamped (the caller stamps it). The data
92
+ * root, schemas, cosign binary, registry client and verification context are the process's unless `thirdParty` injects
93
+ * them (the contract suite drives a fixture registry and a digest-pinned cosign double).
94
+ *
95
+ * @param {object[]} sources declared sources of kind `oci`
96
+ * `channel` is the `EditionChannel` of the resolve (required): develop does not hold a third-party statement to the contract authority.
97
+ * @param {{channel: string, env?: NodeJS.ProcessEnv, thirdParty?: {dataRoot?: string, schemas?: object, cosignBinary?: string, clientFor?: Function, context?: object}}} options
98
+ */
99
+ export async function walkOciSources(sources, { channel, env = process.env, thirdParty = {} } = {}) {
100
+ assertEditionChannel(channel);
101
+ const root = thirdParty.dataRoot ?? configuredDataRoot();
102
+ const pins = loadThirdPartySourcePins(root);
103
+ const pinned = sources.map((source) => [source, requirePin(pins, source)]);
104
+ const schemas = thirdParty.schemas ?? (await loadKernelDistributionSchemas());
105
+ const context = thirdParty.context ?? loadThirdPartySourceContext({ dataRoot: root, schemas, cosignBinary: thirdParty.cosignBinary ?? env[COSIGN_BIN_ENV] });
106
+ const clientFor = thirdParty.clientFor ?? createRegistryClientResolver({ dataRoot: root });
107
+ const parseBundleSource = (value) => loadKernelBiomeContracts().BiomeBundleSourceSpecSchema.parse(value);
108
+ try {
109
+ const entries = [];
110
+ for (const [source, pin] of pinned) {
111
+ entries.push({ source, entry: await walkThirdPartySource({ source, pin, dataRoot: root, context, clientFor, parseBundleSource, channel }) });
112
+ }
113
+ return entries;
114
+ } finally {
115
+ if (thirdParty.context === undefined) {context.dispose();}
116
+ if (thirdParty.clientFor === undefined) {disposeRegistrySessions();}
117
+ }
118
+ }