@xemahq/xema 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/distribution-assets.manifest.json +1 -2
  3. package/dist/cli.js +2 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/dev.js +1 -0
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/down.d.ts.map +1 -1
  8. package/dist/commands/down.js +7 -1
  9. package/dist/commands/down.js.map +1 -1
  10. package/dist/commands/edition.d.ts +3 -0
  11. package/dist/commands/edition.d.ts.map +1 -0
  12. package/dist/commands/edition.js +46 -0
  13. package/dist/commands/edition.js.map +1 -0
  14. package/dist/commands/init.js +2 -2
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/serve.js +1 -0
  17. package/dist/commands/serve.js.map +1 -1
  18. package/dist/commands/up.js +1 -0
  19. package/dist/commands/up.js.map +1 -1
  20. package/dist/dev/env-defaults.d.ts.map +1 -1
  21. package/dist/dev/env-defaults.js +5 -2
  22. package/dist/dev/env-defaults.js.map +1 -1
  23. package/dist/dev/infra-preflight.d.ts +3 -2
  24. package/dist/dev/infra-preflight.d.ts.map +1 -1
  25. package/dist/dev/infra-preflight.js +18 -8
  26. package/dist/dev/infra-preflight.js.map +1 -1
  27. package/dist/edition/verify-image-evidence.bundle.mjs +54283 -0
  28. package/dist/edition/verify-image-evidence.bundle.provenance.json +2877 -0
  29. package/dist/runtime/config/schema.d.ts +1 -0
  30. package/dist/runtime/config/schema.d.ts.map +1 -1
  31. package/dist/runtime/custody/local-custody.d.ts +12 -0
  32. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  33. package/dist/runtime/custody/local-custody.js +22 -0
  34. package/dist/runtime/custody/local-custody.js.map +1 -0
  35. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  36. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  37. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  38. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  39. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  40. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  41. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  42. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  43. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  44. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  45. package/dist/runtime/custody/openbao-http-client.js +154 -0
  46. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  47. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  48. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  49. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  50. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  51. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  52. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  53. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  54. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  55. package/dist/runtime/edition/run-entry.d.ts +9 -0
  56. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  57. package/dist/runtime/edition/run-entry.js +36 -0
  58. package/dist/runtime/edition/run-entry.js.map +1 -0
  59. package/dist/runtime/edition/verb-map.d.ts +60 -0
  60. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  61. package/dist/runtime/edition/verb-map.js +146 -0
  62. package/dist/runtime/edition/verb-map.js.map +1 -0
  63. package/dist/runtime/env-seeder.d.ts +2 -0
  64. package/dist/runtime/env-seeder.d.ts.map +1 -1
  65. package/dist/runtime/env-seeder.js +18 -3
  66. package/dist/runtime/env-seeder.js.map +1 -1
  67. package/dist/runtime/launch.d.ts +2 -1
  68. package/dist/runtime/launch.d.ts.map +1 -1
  69. package/dist/runtime/launch.js +31 -7
  70. package/dist/runtime/launch.js.map +1 -1
  71. package/dist/runtime/paths.d.ts +1 -2
  72. package/dist/runtime/paths.d.ts.map +1 -1
  73. package/dist/runtime/paths.js +0 -10
  74. package/dist/runtime/paths.js.map +1 -1
  75. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  76. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  77. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  78. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  79. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  80. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  81. package/dist/runtime/runner/chart-preflight.js +30 -69
  82. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  83. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  84. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  85. package/dist/runtime/runner/chart-pull.js +96 -0
  86. package/dist/runtime/runner/chart-pull.js.map +1 -0
  87. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  88. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  89. package/dist/runtime/runner/chart-resolver.js +16 -62
  90. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  91. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  92. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  93. package/dist/runtime/runner/chart-set-lock.js +55 -99
  94. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  95. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  96. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  97. package/dist/runtime/runner/helm-cli.js +9 -7
  98. package/dist/runtime/runner/helm-cli.js.map +1 -1
  99. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  100. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  101. package/dist/runtime/runner/helm-runner.js +18 -16
  102. package/dist/runtime/runner/helm-runner.js.map +1 -1
  103. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  104. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  105. package/dist/runtime/sources/kinds/git.js +8 -5
  106. package/dist/runtime/sources/kinds/git.js.map +1 -1
  107. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  108. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  109. package/dist/runtime/sources/manifest-walk.js +8 -3
  110. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  111. package/dist/runtime/sources/schema.d.ts +3 -0
  112. package/dist/runtime/sources/schema.d.ts.map +1 -1
  113. package/dist/runtime/sources/schema.js +6 -1
  114. package/dist/runtime/sources/schema.js.map +1 -1
  115. package/dist/runtime/supervisor.d.ts +2 -0
  116. package/dist/runtime/supervisor.d.ts.map +1 -1
  117. package/dist/runtime/supervisor.js +1 -0
  118. package/dist/runtime/supervisor.js.map +1 -1
  119. package/edition/README.md +313 -0
  120. package/edition/assemble-producer-input.mjs +1251 -0
  121. package/edition/authorization-base.mjs +347 -0
  122. package/edition/biome-content.mjs +268 -0
  123. package/edition/build-biome-index.mjs +175 -0
  124. package/edition/build-platform-service-index.mjs +288 -0
  125. package/edition/build-runtime-image-source-index.mjs +821 -0
  126. package/edition/built-verifier.mjs +55 -0
  127. package/edition/catalogue-edition.mjs +49 -0
  128. package/edition/catalogue-fetch.mjs +64 -0
  129. package/edition/catalogue.mjs +135 -0
  130. package/edition/chart-set-source-commit.mjs +92 -0
  131. package/edition/check-committed-image-evidence.mjs +383 -0
  132. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  133. package/edition/comment-blanker.mjs +75 -0
  134. package/edition/connector-source-authority.mjs +60 -0
  135. package/edition/customer-definition.mjs +183 -0
  136. package/edition/customer-flags.mjs +7 -0
  137. package/edition/develop-customer-edition.mjs +99 -0
  138. package/edition/develop-edition-resolve.mjs +65 -0
  139. package/edition/develop-edition.mjs +274 -0
  140. package/edition/develop-freeze.mjs +173 -0
  141. package/edition/develop-image-coordinate.mjs +54 -0
  142. package/edition/develop-seal.mjs +77 -0
  143. package/edition/develop-snapshot-inputs.mjs +104 -0
  144. package/edition/dist-check.mjs +986 -0
  145. package/edition/distribution-evidence-set.mjs +987 -0
  146. package/edition/distribution-paths.mjs +191 -0
  147. package/edition/edition-artifact.mjs +284 -0
  148. package/edition/edition-biome-manifests.mjs +85 -0
  149. package/edition/edition-channel.mjs +6 -0
  150. package/edition/edition-chart-set.mjs +93 -0
  151. package/edition/edition-config.mjs +205 -0
  152. package/edition/edition-constants.mjs +57 -0
  153. package/edition/edition-contract.json +30 -0
  154. package/edition/edition-list.mjs +35 -0
  155. package/edition/edition-locks.mjs +69 -0
  156. package/edition/edition-protocol.mjs +228 -0
  157. package/edition/edition-workspace.mjs +187 -0
  158. package/edition/evidence-checkout.mjs +439 -0
  159. package/edition/evidence-grammars.mjs +16 -0
  160. package/edition/evidence-trust-pin.mjs +304 -0
  161. package/edition/evidence-trust-policy.mjs +294 -0
  162. package/edition/external-source-walk.mjs +463 -0
  163. package/edition/first-party-source-ids.mjs +59 -0
  164. package/edition/fleet-coordinate-finality.mjs +350 -0
  165. package/edition/git-source-auth.mjs +52 -0
  166. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  167. package/edition/image-evidence-replay-state.mjs +242 -0
  168. package/edition/image-evidence.mjs +1233 -0
  169. package/edition/kernel-contract-authority.mjs +443 -0
  170. package/edition/lib.mjs +3021 -0
  171. package/edition/no-follow-file.mjs +94 -0
  172. package/edition/oci-evidence-verbs.mjs +392 -0
  173. package/edition/oci-image-source-evidence.mjs +239 -0
  174. package/edition/oras.mjs +648 -0
  175. package/edition/pin-source.mjs +227 -0
  176. package/edition/plan-image-evidence-refresh.mjs +408 -0
  177. package/edition/platform-lock-readability.mjs +892 -0
  178. package/edition/platform-service-class.mjs +18 -0
  179. package/edition/platform-service-source-authority.mjs +105 -0
  180. package/edition/positive-closure.mjs +162 -0
  181. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  182. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  183. package/edition/produce-image-source-evidence.mjs +765 -0
  184. package/edition/producer-evidence-bundle.mjs +157 -0
  185. package/edition/registry-credentials.mjs +193 -0
  186. package/edition/release-catalogue.mjs +39 -0
  187. package/edition/release-channel-guard.mjs +109 -0
  188. package/edition/release-edition-artifact.mjs +157 -0
  189. package/edition/remedy-command.mjs +89 -0
  190. package/edition/repin-evidence-trust.mjs +101 -0
  191. package/edition/required-resource-provider.mjs +58 -0
  192. package/edition/resolve-chart-set.mjs +415 -0
  193. package/edition/resolve-customer-edition.mjs +288 -0
  194. package/edition/resolve-distribution.mjs +2318 -0
  195. package/edition/resolve-edition.mjs +186 -0
  196. package/edition/resolve-images.mjs +1606 -0
  197. package/edition/seal-image-digests.mjs +855 -0
  198. package/edition/sealed-evidence-accept.mjs +82 -0
  199. package/edition/source-authority.mjs +725 -0
  200. package/edition/source-closure.mjs +116 -0
  201. package/edition/strict-json.mjs +121 -0
  202. package/edition/string-order.mjs +5 -0
  203. package/edition/trust-policy-arming.mjs +120 -0
  204. package/edition/trusted-executable.mjs +166 -0
  205. package/edition/verifier-binding.mjs +45 -0
  206. package/edition/verifier-files.mjs +8 -0
  207. package/edition/verifier-kernel-schemas.mjs +22 -0
  208. package/edition/verifier-provenance.mjs +64 -0
  209. package/edition/verify-image-evidence.mjs +458 -0
  210. package/edition/verify-image-platforms.mjs +495 -0
  211. package/edition/why.mjs +56 -0
  212. package/edition/workspace-sections.mjs +195 -0
  213. package/examples/06-biome-sources.yaml +1 -0
  214. package/package.json +14 -6
  215. package/dist/commands/dist.d.ts +0 -16
  216. package/dist/commands/dist.d.ts.map +0 -1
  217. package/dist/commands/dist.js +0 -182
  218. package/dist/commands/dist.js.map +0 -1
  219. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  220. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  221. package/dist/runtime/distribution/available-biome.js +0 -156
  222. package/dist/runtime/distribution/available-biome.js.map +0 -1
  223. package/dist/runtime/distribution/compose.d.ts +0 -23
  224. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  225. package/dist/runtime/distribution/compose.js +0 -48
  226. package/dist/runtime/distribution/compose.js.map +0 -1
  227. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  228. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  229. package/dist/runtime/runner/chart-trust.js +0 -270
  230. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,157 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { isAbsolute, join } from 'node:path';
3
+
4
+ import {
5
+ canonicalJson,
6
+ decodeExactUtf8,
7
+ sha256Digest,
8
+ } from './image-evidence.mjs';
9
+ import {
10
+ BUNDLE_MEDIA_TYPE,
11
+ validateProducedBundle,
12
+ } from './produce-image-source-evidence.mjs';
13
+
14
+ export const PRODUCER_EVIDENCE_INDEX_FILE = 'producer-evidence.json';
15
+ export const PRODUCER_EVIDENCE_INDEX_MEDIA_TYPE =
16
+ 'application/vnd.xema.image-source-evidence.index.v1+json';
17
+ export const PRODUCER_STATEMENT_MEDIA_TYPE = 'application/vnd.in-toto+json';
18
+ export const MAX_PRODUCER_EVIDENCE_FILE_BYTES = 4 * 1024 * 1024;
19
+
20
+ function assertRelativeArtifactPath(value, label) {
21
+ if (
22
+ typeof value !== 'string' ||
23
+ value.length === 0 ||
24
+ value.startsWith('/') ||
25
+ value.split('/').some((part) => !part || part === '.' || part === '..')
26
+ ) {
27
+ throw new Error(`${label} is not a contained relative path`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function readBounded(root, relativePath, label) {
33
+ const safePath = assertRelativeArtifactPath(relativePath, label);
34
+ const path = join(root, safePath);
35
+ const stat = statSync(path);
36
+ if (!stat.isFile() || stat.size > MAX_PRODUCER_EVIDENCE_FILE_BYTES) {
37
+ throw new Error(
38
+ `${label} must be a regular file no larger than ${MAX_PRODUCER_EVIDENCE_FILE_BYTES} bytes`,
39
+ );
40
+ }
41
+ return readFileSync(path);
42
+ }
43
+
44
+ /**
45
+ * Fields that identify the immutable build are stable. producerRun identifies
46
+ * one signing execution and intentionally changes on every retry/refresh.
47
+ */
48
+ export function stableProducerStatementBinding(statement) {
49
+ const { producerRun: _producerRun, ...predicate } = statement.predicate;
50
+ return {
51
+ _type: statement._type,
52
+ subject: statement.subject,
53
+ predicateType: statement.predicateType,
54
+ predicate,
55
+ };
56
+ }
57
+
58
+ export function producerStatementMatchesInput(statement, expectedStatement) {
59
+ return (
60
+ canonicalJson(stableProducerStatementBinding(statement)) ===
61
+ canonicalJson(stableProducerStatementBinding(expectedStatement))
62
+ );
63
+ }
64
+
65
+ /** Read and structurally validate one producer evidence directory. */
66
+ export function readProducerEvidenceDirectory({ directory, schemas }) {
67
+ if (!isAbsolute(directory)) {
68
+ throw new Error('producer evidence directory must be absolute');
69
+ }
70
+ if (!schemas?.parseImageProducerStatement) {
71
+ throw new Error('trusted kernel distribution schemas are required');
72
+ }
73
+ const indexBytes = readBounded(
74
+ directory,
75
+ PRODUCER_EVIDENCE_INDEX_FILE,
76
+ 'producer evidence index',
77
+ );
78
+ let index;
79
+ try {
80
+ index = JSON.parse(decodeExactUtf8(indexBytes, 'producer evidence index'));
81
+ } catch (error) {
82
+ throw new Error(`producer evidence index is not exact UTF-8 JSON: ${error.message}`);
83
+ }
84
+ if (
85
+ index?.schemaVersion !== 1 ||
86
+ !Array.isArray(index.producerEvidence) ||
87
+ index.producerEvidence.length === 0
88
+ ) {
89
+ throw new Error('producer evidence index is not non-empty schemaVersion 1');
90
+ }
91
+
92
+ const seen = new Set();
93
+ const records = [];
94
+ for (const entry of index.producerEvidence) {
95
+ const key = `${entry?.subjectKind}:${entry?.subjectId}`;
96
+ if (
97
+ typeof entry?.subjectKind !== 'string' ||
98
+ typeof entry?.subjectId !== 'string' ||
99
+ entry.evidence === null ||
100
+ typeof entry.evidence !== 'object' ||
101
+ seen.has(key)
102
+ ) {
103
+ throw new Error(`producer evidence index has a malformed or duplicate entry ${key}`);
104
+ }
105
+ seen.add(key);
106
+ const descriptor = entry.evidence;
107
+ const statementBytes = readBounded(
108
+ directory,
109
+ descriptor.statementPath,
110
+ `${key} statement`,
111
+ );
112
+ const bundleBytes = readBounded(
113
+ directory,
114
+ descriptor.bundlePath,
115
+ `${key} bundle`,
116
+ );
117
+ if (
118
+ descriptor.bundleMediaType !== BUNDLE_MEDIA_TYPE ||
119
+ sha256Digest(statementBytes) !== descriptor.statementDigest ||
120
+ sha256Digest(bundleBytes) !== descriptor.bundleDigest
121
+ ) {
122
+ throw new Error(`${key} artifacts do not match their descriptor`);
123
+ }
124
+ validateProducedBundle(statementBytes, bundleBytes);
125
+ const statement = schemas.parseImageProducerStatement(
126
+ JSON.parse(decodeExactUtf8(statementBytes, `${key} statement`)),
127
+ );
128
+ if (
129
+ statement.predicate.source.kind !== entry.subjectKind ||
130
+ statement.predicate.source.id !== entry.subjectId
131
+ ) {
132
+ throw new Error(`${key} index identity does not match its signed statement`);
133
+ }
134
+ records.push({
135
+ key,
136
+ entry,
137
+ descriptor,
138
+ statement,
139
+ statementBytes,
140
+ bundleBytes,
141
+ });
142
+ }
143
+ return { index, indexBytes, records };
144
+ }
145
+
146
+ export function producerEvidenceLayerFiles(records) {
147
+ return [
148
+ {
149
+ path: PRODUCER_EVIDENCE_INDEX_FILE,
150
+ mediaType: PRODUCER_EVIDENCE_INDEX_MEDIA_TYPE,
151
+ },
152
+ ...records.flatMap(({ descriptor }) => [
153
+ { path: descriptor.statementPath, mediaType: PRODUCER_STATEMENT_MEDIA_TYPE },
154
+ { path: descriptor.bundlePath, mediaType: BUNDLE_MEDIA_TYPE },
155
+ ]),
156
+ ];
157
+ }
@@ -0,0 +1,193 @@
1
+ // Registry credential DECLARATIONS and image-reference parsing.
2
+ //
3
+ // This module owns the committed `image-registry-credentials.json` contract:
4
+ // which registry authorities may be read, whether each is read anonymously or
5
+ // with a credential taken from a NAMED environment variable, and how an image
6
+ // reference splits into authority and repository. It performs no I/O against a
7
+ // registry; `oras.mjs` owns the transport.
8
+
9
+ import { readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ import { dataRoot } from './edition-workspace.mjs';
13
+ import { isCanonicalRegistryAuthority } from './lib.mjs';
14
+
15
+ export const REGISTRY_CREDENTIALS_FILE = 'image-registry-credentials.json';
16
+
17
+ /**
18
+ * Closed set. `anonymous` means "this registry is deliberately read without a
19
+ * credential"; `authenticated` means "a credential is mandatory". There is no
20
+ * third state, and in particular no "try authenticated, fall back to
21
+ * anonymous" — that fallback is exactly how a private registry silently
22
+ * degrades into an unverifiable one.
23
+ */
24
+ const REGISTRY_ACCESS_MODES = Object.freeze(['anonymous', 'authenticated']);
25
+
26
+
27
+ export const OCI_INDEX_MEDIA_TYPES = Object.freeze([
28
+ 'application/vnd.oci.image.index.v1+json',
29
+ 'application/vnd.docker.distribution.manifest.list.v2+json',
30
+ ]);
31
+
32
+ export const OCI_MANIFEST_MEDIA_TYPES = Object.freeze([
33
+ 'application/vnd.oci.image.manifest.v1+json',
34
+ 'application/vnd.docker.distribution.manifest.v2+json',
35
+ ]);
36
+
37
+ const ENV_VAR_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
38
+
39
+ // Docker Hub is the one registry whose wire authority is not its reference
40
+ // authority (`docker.io` resolves to `registry-1.docker.io`, and single-segment
41
+ // repositories are implicitly `library/*`). Implementing that rewrite silently
42
+ // would make one registry behave differently from every other. Refuse instead.
43
+ const AUTHORITIES_REQUIRING_EXPLICIT_REWRITE = new Set([
44
+ 'docker.io',
45
+ 'index.docker.io',
46
+ 'registry.hub.docker.com',
47
+ ]);
48
+
49
+ /**
50
+ * Split `registry.example.com/acme/service` into its registry authority and repository
51
+ * path. The image lock already guarantees the shape; this re-derives it rather
52
+ * than trusting a caller-supplied split.
53
+ */
54
+ export function splitImageRef(imageRef) {
55
+ if (typeof imageRef !== 'string' || imageRef.length === 0) {
56
+ throw new Error('image reference must be a non-empty string');
57
+ }
58
+ const slash = imageRef.indexOf('/');
59
+ if (slash <= 0 || slash === imageRef.length - 1) {
60
+ throw new Error(
61
+ `image reference "${imageRef}" has no registry authority and repository path`,
62
+ );
63
+ }
64
+ const authority = imageRef.slice(0, slash);
65
+ const repository = imageRef.slice(slash + 1);
66
+ if (!isCanonicalRegistryAuthority(authority)) {
67
+ throw new Error(
68
+ `image reference "${imageRef}" does not start with a canonical registry authority`,
69
+ );
70
+ }
71
+ return { authority, repository };
72
+ }
73
+
74
+ export function assertCredentialDeclaration(entry, index) {
75
+ const label = `${REGISTRY_CREDENTIALS_FILE} registries[${index}]`;
76
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
77
+ throw new Error(`${label} must be an object`);
78
+ }
79
+ if (
80
+ typeof entry.authority !== 'string' ||
81
+ !isCanonicalRegistryAuthority(entry.authority)
82
+ ) {
83
+ throw new Error(`${label}.authority must be a canonical registry authority`);
84
+ }
85
+ if (!REGISTRY_ACCESS_MODES.includes(entry.access)) {
86
+ throw new Error(
87
+ `${label}.access must be one of ${REGISTRY_ACCESS_MODES.join(', ')}`,
88
+ );
89
+ }
90
+ const keys = Object.keys(entry).sort().join(',');
91
+ if (entry.access === 'authenticated') {
92
+ if (keys !== 'access,authTokenEnv,authority,username') {
93
+ throw new Error(
94
+ `${label} must declare exactly access, authTokenEnv, authority, username`,
95
+ );
96
+ }
97
+ if (!ENV_VAR_NAME_PATTERN.test(entry.authTokenEnv)) {
98
+ throw new Error(
99
+ `${label}.authTokenEnv must be an UPPER_SNAKE environment variable NAME (never an inlined secret)`,
100
+ );
101
+ }
102
+ if (typeof entry.username !== 'string' || entry.username.length === 0) {
103
+ throw new Error(`${label}.username must be a non-empty string`);
104
+ }
105
+ } else if (keys !== 'access,authority') {
106
+ throw new Error(`${label} must declare exactly access and authority`);
107
+ }
108
+ return entry;
109
+ }
110
+
111
+ /**
112
+ * Read the committed registry credential DECLARATION. The file names
113
+ * environment variables; it never contains a secret. See
114
+ * `.claude/rules/biome-sources.md` constraint C2.
115
+ */
116
+ export function loadRegistryCredentialDeclarations(
117
+ distributionsRoot = dataRoot(),
118
+ ) {
119
+ const path = join(distributionsRoot, REGISTRY_CREDENTIALS_FILE);
120
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
121
+ const keys = Object.keys(raw ?? {}).sort().join(',');
122
+ if (
123
+ typeof raw !== 'object' ||
124
+ raw === null ||
125
+ Array.isArray(raw) ||
126
+ raw.schemaVersion !== 1 ||
127
+ !Array.isArray(raw.registries) ||
128
+ (keys !== 'registries,schemaVersion' &&
129
+ keys !== '$comment,registries,schemaVersion') ||
130
+ ('$comment' in raw &&
131
+ (!Array.isArray(raw.$comment) ||
132
+ raw.$comment.some((line) => typeof line !== 'string')))
133
+ ) {
134
+ throw new Error(`${REGISTRY_CREDENTIALS_FILE} is not strict schema v1`);
135
+ }
136
+ const byAuthority = new Map();
137
+ for (const [index, entry] of raw.registries.entries()) {
138
+ assertCredentialDeclaration(entry, index);
139
+ if (byAuthority.has(entry.authority)) {
140
+ throw new Error(
141
+ `${REGISTRY_CREDENTIALS_FILE} declares "${entry.authority}" more than once`,
142
+ );
143
+ }
144
+ byAuthority.set(entry.authority, entry);
145
+ }
146
+ return byAuthority;
147
+ }
148
+
149
+ /**
150
+ * Resolve a declared authority to the credential it will actually use.
151
+ *
152
+ * A NAMED-but-unset environment variable FAILS FAST. It must never fall
153
+ * through to an unauthenticated request: that is how a 403 on a private
154
+ * package turns into "the platform is missing" instead of "your token is
155
+ * missing".
156
+ */
157
+ export function resolveRegistryCredential({
158
+ authority,
159
+ declarations,
160
+ env = process.env,
161
+ }) {
162
+ const declaration = declarations.get(authority);
163
+ if (!declaration) {
164
+ throw new Error(
165
+ `registry "${authority}" has no entry in ${REGISTRY_CREDENTIALS_FILE}; ` +
166
+ 'declare it with access "anonymous" or "authenticated" before reading from it',
167
+ );
168
+ }
169
+ if (AUTHORITIES_REQUIRING_EXPLICIT_REWRITE.has(authority)) {
170
+ throw new Error(
171
+ `registry "${authority}" needs a documented authority/repository rewrite that this client deliberately does not perform`,
172
+ );
173
+ }
174
+ if (declaration.access === 'anonymous') {
175
+ return { authority, access: 'anonymous', basicAuth: null };
176
+ }
177
+ const token = env[declaration.authTokenEnv];
178
+ if (typeof token !== 'string' || token.length === 0) {
179
+ throw new Error(
180
+ `registry "${authority}" requires a pull credential in $${declaration.authTokenEnv}, ` +
181
+ 'and that variable is unset or empty. Export a token with read:packages ' +
182
+ 'scope for this registry; an unauthenticated read is NOT attempted, because ' +
183
+ 'a 403 on a private package is indistinguishable from a missing image.',
184
+ );
185
+ }
186
+ return {
187
+ authority,
188
+ access: 'authenticated',
189
+ basicAuth: Buffer.from(`${declaration.username}:${token}`, 'utf8').toString(
190
+ 'base64',
191
+ ),
192
+ };
193
+ }
@@ -0,0 +1,39 @@
1
+ // release-catalogue — the catalogue of a RELEASE offering, from its committed, sealed locks.
2
+
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ import { biomeSourceTreeIds } from './biome-content.mjs';
6
+ import { buildCatalogue } from './catalogue.mjs';
7
+ import { EditionChannel } from './edition-constants.mjs';
8
+ import { biomeIndexPath, platformServiceIndexPath, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
9
+ import { loadKernelDistributionSchemas } from './lib.mjs';
10
+ import { loadEditionManifest } from './resolve-distribution.mjs';
11
+ import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
12
+ import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
13
+
14
+ /** The catalogue of release offering `id`, from the committed locks (`locks` as `readCommittedEdition` returns them). */
15
+ export async function releaseCatalogue(id, locks) {
16
+ const schemas = await loadKernelDistributionSchemas();
17
+ const authority = createPinnedSourceAuthority({ pins: committedRepositoryPins() });
18
+ // The WRITER of the catalogue's tree ids: the real `git rev-parse <commit>:<path>` of every biome.
19
+ const sourceTreeIds = await biomeSourceTreeIds({
20
+ schemas,
21
+ lock: locks.distributionLock,
22
+ imageLock: locks.imageLock,
23
+ locate: sourceTreeLocator(externalSourceTrees(locks.distributionLock)),
24
+ vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
25
+ });
26
+ return buildCatalogue({
27
+ sourceTreeIds,
28
+ offeringId: id,
29
+ channel: EditionChannel.Release,
30
+ distributionLock: locks.distributionLock,
31
+ imageLock: locks.imageLock,
32
+ chartSetLock: locks.chartSetLock,
33
+ biomeIndex: JSON.parse(readFileSync(biomeIndexPath(), 'utf8')),
34
+ platformServiceIndex: schemas.parsePlatformServiceSourceIndex(JSON.parse(readFileSync(platformServiceIndexPath(), 'utf8'))),
35
+ contractAuthority: schemas.parseRuntimeImageSourceIndex(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8'))).contractAuthority,
36
+ leaf: loadEditionManifest(schemas.parseDistribution, id),
37
+ });
38
+ }
39
+
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // release-channel-guard — release tooling's content assertions over an editions
3
+ // tree.
4
+ //
5
+ // No check here reads a channel label, a schema name or a file name to decide
6
+ // what an edition IS: admission rests on the cosign signature of the artifact's
7
+ // manifest digest, and the descriptor's `provenance.channel` is a record, never
8
+ // a gate. What stays is what the BYTES must satisfy: a release images lock names
9
+ // release repositories only, and a release lock set is whole (the companion
10
+ // chart-set lock beside the distribution and authorization locks).
11
+ //
12
+ // node release-channel-guard.mjs <checkout-dir>
13
+ // refuses if any edition directory under <checkout-dir> breaks either.
14
+
15
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+
18
+ import { isDevelopImageRef } from './develop-image-coordinate.mjs';
19
+ import { EDITION_CHART_SET_FILE, assertRosteredChartSetLock } from './edition-chart-set.mjs';
20
+ import { EditionChannel, EditionLockFile } from './edition-constants.mjs';
21
+ import { isEntryModule } from './edition-workspace.mjs';
22
+
23
+ /** Throws when `value` (a parsed JSON document) names a develop image repository. `where` names it in the refusal. */
24
+ export function assertReleaseContent(value, where) {
25
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {return;}
26
+ const developSubject = (value.subjects ?? []).find((subject) => isDevelopImageRef(subject?.imageRef));
27
+ if (developSubject) {
28
+ throw new Error(
29
+ `${where} names the develop image repository ${developSubject.imageRef} (subject ${developSubject.kind}:${developSubject.id}); ` +
30
+ 'a release lock names release repositories only',
31
+ );
32
+ }
33
+ // A chart-set lock (one with a roster of publishers) carries release chart
34
+ // coordinates only: a develop chart set copied into a release is refused.
35
+ if (Array.isArray(value.publishers)) {
36
+ assertRosteredChartSetLock(value, where, EditionChannel.Release);
37
+ }
38
+ }
39
+
40
+ /** Throws when an edition directory holds a lock naming a develop image repository, or a release lock without its companions. */
41
+ export function assertReleaseEditionTree(dir) {
42
+ for (const file of [EditionLockFile.Distribution, EditionLockFile.Images, EditionLockFile.Authorization, EditionLockFile.ChartSet]) {
43
+ const path = join(dir, file);
44
+ if (existsSync(path)) {assertReleaseContent(JSON.parse(readFileSync(path, 'utf8')), path);}
45
+ }
46
+ assertReleaseLockSet(dir);
47
+ }
48
+
49
+ /**
50
+ * The release lock set. A directory holding the distribution or the
51
+ * authorization lock must hold the whole set, including the `chart-set.lock.json`
52
+ * the release writes with them and hashes into the edition's coordinate, so a
53
+ * lock copied alone into an edition directory is refused.
54
+ *
55
+ * What this guarantees is PRESENCE, not binding: the companion chart-set lock
56
+ * must exist beside the locks. It does not check that the set's locks agree
57
+ * with one another or with a recorded digest (nothing records one: the release
58
+ * envelope does not carry a lock-set digest), so it is not "hash-bound" and
59
+ * must not be described as such.
60
+ */
61
+ export const RELEASE_COMPANION_FILES = Object.freeze([EDITION_CHART_SET_FILE]);
62
+
63
+ /**
64
+ * The release locks an edition directory holds (the distribution and the
65
+ * authorization lock). A directory holding either IS a release edition: a
66
+ * develop resolve writes nothing into an editions tree, so a lock committed
67
+ * there was written by the release resolve.
68
+ */
69
+ export function heldReleaseLocks(dir) {
70
+ return [EditionLockFile.Distribution, EditionLockFile.Authorization].filter((file) => existsSync(join(dir, file)));
71
+ }
72
+
73
+ function assertReleaseLockSet(dir) {
74
+ const held = heldReleaseLocks(dir);
75
+ if (held.length === 0) {return;}
76
+ const missing = RELEASE_COMPANION_FILES.filter((file) => !existsSync(join(dir, file)));
77
+ if (missing.length > 0) {
78
+ throw new Error(
79
+ `${dir} holds ${held.join(' and ')} but not ${missing.join(' and ')}: a release edition carries its locks as one set, ` +
80
+ 'so a lock without its companions is not a release edition. ' +
81
+ 'Resolve the edition whole, or remove the stray lock.',
82
+ );
83
+ }
84
+ }
85
+
86
+ /** The guard over every direct subdirectory of a checkout (each is an edition or holds none). */
87
+ export function assertReleaseCheckout(root) {
88
+ let checked = 0;
89
+ for (const entry of readdirSync(root).sort()) {
90
+ if (entry.startsWith('.') || entry === 'node_modules') {continue;}
91
+ const dir = join(root, entry);
92
+ if (!statSync(dir).isDirectory()) {continue;}
93
+ assertReleaseEditionTree(dir);
94
+ checked += 1;
95
+ }
96
+ return checked;
97
+ }
98
+
99
+ if (isEntryModule(import.meta.url)) {
100
+ try {
101
+ const [root, ...rest] = process.argv.slice(2);
102
+ if (!root || rest.length > 0) {throw new Error('usage: node release-channel-guard.mjs <checkout-dir>');}
103
+ const checked = assertReleaseCheckout(resolve(root));
104
+ console.log(`[release-channel-guard] ${checked} directories hold no develop content.`);
105
+ } catch (error) {
106
+ console.error(`[release-channel-guard] ${error.message}`);
107
+ process.exit(1);
108
+ }
109
+ }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ // release-edition-artifact — write the OCI artifact of a RELEASE edition from
3
+ // the committed, sealed locks of the editions root.
4
+ //
5
+ // artifact <id> --channel release --registry <namespace> --coordinate <tag>
6
+ // --engine-image <repository>@sha256:<digest> --out <dir> [--local]
7
+ //
8
+ // The artifact has the layout of every edition (edition-artifact.mjs): the
9
+ // descriptor plus the four locks and the offering's catalogue.json, written to <dir>, and the target
10
+ // `<registry>/xema-edition-<id>:<coordinate>` is printed. The workflow pushes
11
+ // <dir> with oras and signs the manifest DIGEST with cosign; the tag is for
12
+ // humans. A lock set that is not sealed is refused: an unsealed images lock names
13
+ // no image digests, so nothing could be admitted against it.
14
+
15
+ import { readFileSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+
18
+ import { verifyBiomeContentHashes } from './biome-content.mjs';
19
+ import { writeCatalogue } from './catalogue.mjs';
20
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
21
+ import { LOCAL_WORKFLOW, assertEngineImage, editionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
22
+ import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
23
+ import { EditionChannel, EditionLockFile } from './edition-constants.mjs';
24
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule } from './edition-workspace.mjs';
25
+ import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
26
+ import { loadKernelDistributionSchemas, validateImmutableImageLock } from './lib.mjs';
27
+ import { releaseCatalogue } from './release-catalogue.mjs';
28
+ import { assertReleaseEditionTree } from './release-channel-guard.mjs';
29
+ import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
30
+ import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
31
+
32
+ if (isEntryModule(import.meta.url)) {
33
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
34
+ }
35
+
36
+ const VALUE_FLAGS = Object.freeze(['--channel', '--registry', '--coordinate', '--engine-image', '--out']);
37
+ const REQUIRED_FLAGS = VALUE_FLAGS;
38
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
39
+
40
+ /** Parse `artifact <id> --flag value ...`; only the release channel is written by this verb. */
41
+ export function parseReleaseArtifactArgs(argv) {
42
+ const [command, ...rest] = argv;
43
+ if (command !== 'artifact') {throw new Error(`unknown command ${JSON.stringify(command)}; expected artifact`);}
44
+ const options = {};
45
+ const positional = [];
46
+ for (let i = 0; i < rest.length; i += 1) {
47
+ const arg = rest[i];
48
+ if (arg === '--local') {
49
+ options[arg] = true;
50
+ } else if (VALUE_FLAGS.includes(arg)) {
51
+ const value = rest[i + 1];
52
+ if (value === undefined || (value.startsWith('--') && value.length > 2)) {throw new Error(`${arg} requires a value`);}
53
+ options[arg] = value;
54
+ i += 1;
55
+ } else if (arg.startsWith('--')) {
56
+ throw new Error(`unknown argument ${arg} for artifact`);
57
+ } else {
58
+ positional.push(arg);
59
+ }
60
+ }
61
+ if (positional.length !== 1) {throw new Error('artifact takes exactly one <distribution-id>');}
62
+ for (const required of REQUIRED_FLAGS) {
63
+ if (options[required] === undefined) {throw new Error(`artifact requires ${required}`);}
64
+ }
65
+ if (options['--channel'] !== EditionChannel.Release) {
66
+ throw new Error(
67
+ `--channel ${JSON.stringify(options['--channel'])}: this verb writes ${EditionChannel.Release} editions from the committed locks; ` +
68
+ 'a develop edition is resolved from a snapshot by `xema edition develop resolve`',
69
+ );
70
+ }
71
+ assertCanonicalDistributionId(positional[0]);
72
+ assertEngineImage(options['--engine-image']);
73
+ return { id: positional[0], options };
74
+ }
75
+
76
+ /** The images lock must be sealed: every subject names the registry digest of its image. */
77
+ export function assertSealedImageLock(lock, label) {
78
+ const subjects = lock?.subjects ?? [];
79
+ if (subjects.length === 0) {throw new Error(`${label} carries no subjects, so it seals nothing`);}
80
+ const unsealed = subjects.filter((subject) => !DIGEST_PATTERN.test(String(subject?.imageDigest)));
81
+ if (unsealed.length > 0) {
82
+ throw new Error(
83
+ `${label} is not sealed: ${unsealed.length} of ${subjects.length} subjects carry no imageDigest ` +
84
+ `(first: ${unsealed[0].kind}:${unsealed[0].id}). Run \`xema edition seal\` first.`,
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * The four committed locks of a release edition, validated. Nothing is published from a set
91
+ * that is not sealed and intact: the images lock names every digest, the distribution lock is
92
+ * the SEALED grammar (an unsealed one is refused), and every `biomeContentHash` is recomputed
93
+ * from its inputs and compared, so a lock edited after its seal is refused, naming the biome.
94
+ */
95
+ export async function readCommittedEdition(id, root = editionsRoot()) {
96
+ const dir = join(root, id);
97
+ assertReleaseEditionTree(dir);
98
+ const read = (file) => JSON.parse(readFileSync(join(dir, file), 'utf8'));
99
+ const schemas = await loadKernelDistributionSchemas();
100
+ const imageLock = validateImmutableImageLock(schemas.parseImageLock(read(EditionLockFile.Images)), schemas);
101
+ assertSealedImageLock(imageLock, join(dir, EditionLockFile.Images));
102
+ const distributionLock = read(EditionLockFile.Distribution);
103
+ let sealed;
104
+ try {
105
+ sealed = schemas.parseDistributionLock(distributionLock);
106
+ } catch (error) {
107
+ throw new Error(
108
+ `${join(dir, EditionLockFile.Distribution)} is not a sealed distribution lock, so nothing is published from it. ` +
109
+ `Run \`xema edition seal ${id}\` first. (${error.message})`,
110
+ { cause: error },
111
+ );
112
+ }
113
+ const authority = createPinnedSourceAuthority({ pins: committedRepositoryPins() });
114
+ await verifyBiomeContentHashes({
115
+ schemas,
116
+ lock: sealed,
117
+ imageLock,
118
+ locate: sourceTreeLocator(externalSourceTrees(sealed)),
119
+ vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
120
+ });
121
+ return {
122
+ distributionLock,
123
+ imageLock: read(EditionLockFile.Images),
124
+ authorizationLock: read(EditionLockFile.Authorization),
125
+ chartSetLock: assertRosteredChartSetLock(read(EditionLockFile.ChartSet), join(dir, EditionLockFile.ChartSet), EditionChannel.Release),
126
+ };
127
+ }
128
+
129
+ async function run(argv, env) {
130
+ const { id, options } = parseReleaseArtifactArgs(argv);
131
+ const target = editionTarget(id, options['--registry'], options['--coordinate']);
132
+ const workflow = options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env);
133
+ const locks = await readCommittedEdition(id);
134
+ // Refuses (EvidenceTrustRequiredError) unless the policy is ARMED on the
135
+ // release channel: a release artifact is never written not-armed.
136
+ const evidenceTrustPolicy = await descriptorEvidenceTrust({ channel: EditionChannel.Release });
137
+ const outDir = resolve(options['--out']);
138
+ writeEditionArtifact({
139
+ outDir,
140
+ channel: EditionChannel.Release,
141
+ distributionId: id,
142
+ pins: {},
143
+ workflow,
144
+ evidenceTrustPolicy,
145
+ engineImage: options['--engine-image'],
146
+ ...locks,
147
+ });
148
+ writeCatalogue(outDir, await releaseCatalogue(id, locks));
149
+ console.log(target);
150
+ }
151
+
152
+ if (isEntryModule(import.meta.url)) {
153
+ run(process.argv.slice(2), process.env).catch((error) => {
154
+ console.error(`[edition-artifact] ${error.message}`);
155
+ process.exit(1);
156
+ });
157
+ }