@xemahq/xema 0.25.1 → 0.27.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 (239) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/appliance-base/images.lock.json +126 -126
  3. package/dist/assets/appliance-lean/images.lock.json +123 -123
  4. package/dist/assets/cloud/images.lock.json +126 -126
  5. package/dist/assets/core/images.lock.json +120 -120
  6. package/dist/assets/distribution-assets.manifest.json +15 -16
  7. package/dist/assets/minimal/images.lock.json +108 -108
  8. package/dist/assets/oss/images.lock.json +126 -126
  9. package/dist/assets/starter/images.lock.json +123 -123
  10. package/dist/cli.js +2 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/dev.js +1 -0
  13. package/dist/commands/dev.js.map +1 -1
  14. package/dist/commands/down.d.ts.map +1 -1
  15. package/dist/commands/down.js +7 -1
  16. package/dist/commands/down.js.map +1 -1
  17. package/dist/commands/edition.d.ts +3 -0
  18. package/dist/commands/edition.d.ts.map +1 -0
  19. package/dist/commands/edition.js +46 -0
  20. package/dist/commands/edition.js.map +1 -0
  21. package/dist/commands/init.js +2 -2
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/serve.js +1 -0
  24. package/dist/commands/serve.js.map +1 -1
  25. package/dist/commands/up.js +1 -0
  26. package/dist/commands/up.js.map +1 -1
  27. package/dist/dev/env-defaults.d.ts.map +1 -1
  28. package/dist/dev/env-defaults.js +6 -1
  29. package/dist/dev/env-defaults.js.map +1 -1
  30. package/dist/dev/infra-preflight.d.ts +3 -2
  31. package/dist/dev/infra-preflight.d.ts.map +1 -1
  32. package/dist/dev/infra-preflight.js +18 -8
  33. package/dist/dev/infra-preflight.js.map +1 -1
  34. package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
  35. package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
  36. package/dist/runtime/backing-registration.d.ts.map +1 -1
  37. package/dist/runtime/backing-registration.js +7 -2
  38. package/dist/runtime/backing-registration.js.map +1 -1
  39. package/dist/runtime/config/schema.d.ts +1 -0
  40. package/dist/runtime/config/schema.d.ts.map +1 -1
  41. package/dist/runtime/custody/local-custody.d.ts +12 -0
  42. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  43. package/dist/runtime/custody/local-custody.js +22 -0
  44. package/dist/runtime/custody/local-custody.js.map +1 -0
  45. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  46. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  47. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  48. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  49. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  50. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  51. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  52. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  53. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  54. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  55. package/dist/runtime/custody/openbao-http-client.js +154 -0
  56. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  57. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  58. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  59. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  60. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  61. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  62. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  63. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  64. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  65. package/dist/runtime/edition/run-entry.d.ts +9 -0
  66. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  67. package/dist/runtime/edition/run-entry.js +36 -0
  68. package/dist/runtime/edition/run-entry.js.map +1 -0
  69. package/dist/runtime/edition/verb-map.d.ts +60 -0
  70. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  71. package/dist/runtime/edition/verb-map.js +146 -0
  72. package/dist/runtime/edition/verb-map.js.map +1 -0
  73. package/dist/runtime/env-seeder.d.ts +3 -0
  74. package/dist/runtime/env-seeder.d.ts.map +1 -1
  75. package/dist/runtime/env-seeder.js +20 -3
  76. package/dist/runtime/env-seeder.js.map +1 -1
  77. package/dist/runtime/launch.d.ts +3 -1
  78. package/dist/runtime/launch.d.ts.map +1 -1
  79. package/dist/runtime/launch.js +37 -8
  80. package/dist/runtime/launch.js.map +1 -1
  81. package/dist/runtime/paths.d.ts +1 -2
  82. package/dist/runtime/paths.d.ts.map +1 -1
  83. package/dist/runtime/paths.js +0 -10
  84. package/dist/runtime/paths.js.map +1 -1
  85. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  86. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  87. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  88. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  89. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  90. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  91. package/dist/runtime/runner/chart-preflight.js +30 -69
  92. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  93. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  94. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  95. package/dist/runtime/runner/chart-pull.js +96 -0
  96. package/dist/runtime/runner/chart-pull.js.map +1 -0
  97. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  98. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  99. package/dist/runtime/runner/chart-resolver.js +16 -62
  100. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  101. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  102. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  103. package/dist/runtime/runner/chart-set-lock.js +55 -99
  104. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  105. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  106. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  107. package/dist/runtime/runner/helm-cli.js +9 -7
  108. package/dist/runtime/runner/helm-cli.js.map +1 -1
  109. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  110. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  111. package/dist/runtime/runner/helm-runner.js +18 -16
  112. package/dist/runtime/runner/helm-runner.js.map +1 -1
  113. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  114. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  115. package/dist/runtime/sources/kinds/git.js +8 -5
  116. package/dist/runtime/sources/kinds/git.js.map +1 -1
  117. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  118. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  119. package/dist/runtime/sources/manifest-walk.js +8 -3
  120. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  121. package/dist/runtime/sources/schema.d.ts +3 -0
  122. package/dist/runtime/sources/schema.d.ts.map +1 -1
  123. package/dist/runtime/sources/schema.js +6 -1
  124. package/dist/runtime/sources/schema.js.map +1 -1
  125. package/dist/runtime/supervisor.d.ts +3 -0
  126. package/dist/runtime/supervisor.d.ts.map +1 -1
  127. package/dist/runtime/supervisor.js +2 -0
  128. package/dist/runtime/supervisor.js.map +1 -1
  129. package/edition/README.md +315 -0
  130. package/edition/assemble-producer-input.mjs +1251 -0
  131. package/edition/authorization-base.mjs +347 -0
  132. package/edition/biome-content.mjs +268 -0
  133. package/edition/build-biome-index.mjs +175 -0
  134. package/edition/build-platform-service-index.mjs +288 -0
  135. package/edition/build-runtime-image-source-index.mjs +821 -0
  136. package/edition/built-verifier.mjs +55 -0
  137. package/edition/catalogue-edition.mjs +49 -0
  138. package/edition/catalogue-fetch.mjs +64 -0
  139. package/edition/catalogue.mjs +135 -0
  140. package/edition/chart-set-source-commit.mjs +92 -0
  141. package/edition/check-committed-image-evidence.mjs +383 -0
  142. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  143. package/edition/comment-blanker.mjs +75 -0
  144. package/edition/connector-source-authority.mjs +60 -0
  145. package/edition/customer-definition.mjs +183 -0
  146. package/edition/customer-flags.mjs +7 -0
  147. package/edition/develop-customer-edition.mjs +99 -0
  148. package/edition/develop-edition-resolve.mjs +65 -0
  149. package/edition/develop-edition.mjs +274 -0
  150. package/edition/develop-freeze.mjs +173 -0
  151. package/edition/develop-image-coordinate.mjs +54 -0
  152. package/edition/develop-seal.mjs +77 -0
  153. package/edition/develop-snapshot-inputs.mjs +104 -0
  154. package/edition/dist-check.mjs +986 -0
  155. package/edition/distribution-evidence-set.mjs +987 -0
  156. package/edition/distribution-paths.mjs +191 -0
  157. package/edition/edition-artifact.mjs +270 -0
  158. package/edition/edition-biome-manifests.mjs +85 -0
  159. package/edition/edition-channel.mjs +6 -0
  160. package/edition/edition-chart-set.mjs +93 -0
  161. package/edition/edition-config.mjs +207 -0
  162. package/edition/edition-constants.mjs +57 -0
  163. package/edition/edition-contract.json +23 -0
  164. package/edition/edition-list.mjs +35 -0
  165. package/edition/edition-locks.mjs +69 -0
  166. package/edition/edition-protocol.mjs +228 -0
  167. package/edition/edition-workspace.mjs +176 -0
  168. package/edition/evidence-checkout.mjs +418 -0
  169. package/edition/evidence-grammars.mjs +16 -0
  170. package/edition/evidence-trust-pin.mjs +304 -0
  171. package/edition/evidence-trust-policy.mjs +256 -0
  172. package/edition/external-source-walk.mjs +463 -0
  173. package/edition/first-party-source-ids.mjs +59 -0
  174. package/edition/fleet-coordinate-finality.mjs +350 -0
  175. package/edition/git-source-auth.mjs +52 -0
  176. package/edition/image-evidence-replay-state.mjs +242 -0
  177. package/edition/image-evidence.mjs +1225 -0
  178. package/edition/kernel-contract-authority.mjs +443 -0
  179. package/edition/lib.mjs +3018 -0
  180. package/edition/no-follow-file.mjs +94 -0
  181. package/edition/oci-evidence-verbs.mjs +392 -0
  182. package/edition/oci-image-source-evidence.mjs +239 -0
  183. package/edition/oras.mjs +648 -0
  184. package/edition/pin-source.mjs +227 -0
  185. package/edition/plan-image-evidence-refresh.mjs +408 -0
  186. package/edition/platform-lock-readability.mjs +892 -0
  187. package/edition/platform-service-class.mjs +18 -0
  188. package/edition/platform-service-source-authority.mjs +105 -0
  189. package/edition/positive-closure.mjs +162 -0
  190. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  191. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  192. package/edition/produce-image-source-evidence.mjs +765 -0
  193. package/edition/producer-evidence-bundle.mjs +157 -0
  194. package/edition/registry-credentials.mjs +193 -0
  195. package/edition/release-catalogue.mjs +39 -0
  196. package/edition/release-channel-guard.mjs +99 -0
  197. package/edition/release-edition-artifact.mjs +158 -0
  198. package/edition/remedy-command.mjs +89 -0
  199. package/edition/repin-evidence-trust.mjs +101 -0
  200. package/edition/required-resource-provider.mjs +58 -0
  201. package/edition/resolve-chart-set.mjs +415 -0
  202. package/edition/resolve-customer-edition.mjs +288 -0
  203. package/edition/resolve-distribution.mjs +2318 -0
  204. package/edition/resolve-edition.mjs +186 -0
  205. package/edition/resolve-images.mjs +1606 -0
  206. package/edition/seal-image-digests.mjs +855 -0
  207. package/edition/sealed-evidence-accept.mjs +82 -0
  208. package/edition/source-authority.mjs +725 -0
  209. package/edition/source-closure.mjs +116 -0
  210. package/edition/strict-json.mjs +121 -0
  211. package/edition/string-order.mjs +5 -0
  212. package/edition/trust-policy-arming.mjs +120 -0
  213. package/edition/trusted-executable.mjs +166 -0
  214. package/edition/verifier-binding.mjs +45 -0
  215. package/edition/verifier-files.mjs +8 -0
  216. package/edition/verifier-kernel-schemas.mjs +22 -0
  217. package/edition/verifier-provenance.mjs +64 -0
  218. package/edition/verify-image-evidence.mjs +430 -0
  219. package/edition/verify-image-platforms.mjs +495 -0
  220. package/edition/why.mjs +56 -0
  221. package/edition/workspace-sections.mjs +195 -0
  222. package/examples/06-biome-sources.yaml +1 -0
  223. package/package.json +18 -7
  224. package/dist/commands/dist.d.ts +0 -16
  225. package/dist/commands/dist.d.ts.map +0 -1
  226. package/dist/commands/dist.js +0 -182
  227. package/dist/commands/dist.js.map +0 -1
  228. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  229. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  230. package/dist/runtime/distribution/available-biome.js +0 -154
  231. package/dist/runtime/distribution/available-biome.js.map +0 -1
  232. package/dist/runtime/distribution/compose.d.ts +0 -23
  233. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  234. package/dist/runtime/distribution/compose.js +0 -48
  235. package/dist/runtime/distribution/compose.js.map +0 -1
  236. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  237. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  238. package/dist/runtime/runner/chart-trust.js +0 -270
  239. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,207 @@
1
+ // The deployment-specific coordinates the edition library needs and must not
2
+ // carry: which image registry, chart registry and source
3
+ // organisation a workspace belongs to. They are DATA, read from
4
+ // `<dataRoot>/edition-workspace.json`; there is no default for any of them, and
5
+ // a file with a missing, unknown or malformed key is refused. `evidenceTrustPolicy`
6
+ // declares the data root's evidence gating; its `required` flag has no default.
7
+
8
+ import { join } from 'node:path';
9
+
10
+ import { dataRoot } from './edition-workspace.mjs';
11
+ import { readStrictJsonFile } from './strict-json.mjs';
12
+ import {
13
+ parseConfinedBiomes,
14
+ parseConnectorSources,
15
+ parseFirstPartySources,
16
+ parsePinRoles,
17
+ parsePlatformServiceSources,
18
+ parseRuntimeImageSources,
19
+ } from './workspace-sections.mjs';
20
+
21
+ /** A refusal of edition-workspace.json itself. */
22
+ export class EditionWorkspaceConfigError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = 'EditionWorkspaceConfigError';
26
+ }
27
+ }
28
+
29
+ export const EDITION_WORKSPACE_CONFIG_FILE = 'edition-workspace.json';
30
+ export const EDITION_WORKSPACE_CONFIG_SCHEMA_VERSION = 1;
31
+
32
+ // The product-data sections. Each is optional in the file (a customer data root
33
+ // legitimately has none of some), and each is REQUIRED at the moment an
34
+ // operation reads it: `workspaceSection` refuses a missing one by name.
35
+ const SECTIONS = Object.freeze({
36
+ firstPartySources: parseFirstPartySources,
37
+ platformServiceSources: parsePlatformServiceSources,
38
+ connectorSources: parseConnectorSources,
39
+ runtimeImageSources: parseRuntimeImageSources,
40
+ pinRoles: parsePinRoles,
41
+ confinedBiomes: parseConfinedBiomes,
42
+ });
43
+
44
+ const KEYS = Object.freeze([
45
+ 'schemaVersion',
46
+ 'imageRegistry',
47
+ 'chartRegistry',
48
+ 'chartPublishers',
49
+ 'sourceRepositoryUrlPrefix',
50
+ 'sourceAuthUsername',
51
+ 'evidenceTrustPolicy',
52
+ ...Object.keys(SECTIONS),
53
+ ]);
54
+
55
+ const IMAGE_REGISTRY = /^[a-z0-9.-]+(:[0-9]+)?(\/[a-z0-9._-]+)+$/;
56
+ const CHART_REGISTRY = /^oci:\/\/[a-z0-9.-]+(:[0-9]+)?(\/[a-z0-9._-]+)+$/;
57
+ // A data-root-relative POSIX path: no leading slash, no empty, `.` or `..` segment.
58
+ const DATA_ROOT_RELATIVE_PATH = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
59
+ const SOURCE_URL_PREFIX = /^https:\/\/[a-z0-9.-]+(\/[A-Za-z0-9._-]+)+\/$/;
60
+
61
+ function requireString(config, key, pattern, shape) {
62
+ const value = config[key];
63
+ if (typeof value !== 'string' || !pattern.test(value)) {
64
+ throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE}: "${key}" must be ${shape}; got ${JSON.stringify(value)}`);
65
+ }
66
+ return value;
67
+ }
68
+
69
+ /** The git username first-party sources authenticate as: declared, never defaulted (GitHub says `x-access-token`, other forges differ). */
70
+ function requireSourceAuthUsername(config) {
71
+ const value = config.sourceAuthUsername;
72
+ if (typeof value !== 'string' || value.length === 0 || /[\s:]/.test(value)) {
73
+ throw new EditionWorkspaceConfigError(
74
+ `${EDITION_WORKSPACE_CONFIG_FILE}: "sourceAuthUsername" is required: the non-empty git username (no whitespace or ":") ` +
75
+ `that first-party source tokens authenticate as; got ${JSON.stringify(value)}`,
76
+ );
77
+ }
78
+ return value;
79
+ }
80
+
81
+ function requirePublishers(config) {
82
+ const value = config.chartPublishers;
83
+ if (!Array.isArray(value) || value.length === 0) {
84
+ throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE}: "chartPublishers" must be a non-empty array of {index, repo}`);
85
+ }
86
+ const seen = new Set();
87
+ return Object.freeze(
88
+ value.map((entry) => {
89
+ const keys = entry !== null && typeof entry === 'object' ? Object.keys(entry).sort() : [];
90
+ if (keys.join() !== 'index,repo' || typeof entry.index !== 'string' || typeof entry.repo !== 'string' || !entry.index || !entry.repo) {
91
+ throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE}: every "chartPublishers" entry is exactly {index, repo}, both non-empty strings`);
92
+ }
93
+ if (seen.has(entry.index)) {
94
+ throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE}: duplicate chart publisher index "${entry.index}"`);
95
+ }
96
+ seen.add(entry.index);
97
+ return Object.freeze({ index: entry.index, repo: entry.repo });
98
+ }),
99
+ );
100
+ }
101
+
102
+ const EVIDENCE_TRUST_POLICY_KEYS = Object.freeze(['pin', 'policy', 'required']);
103
+
104
+ function isDataRootRelativePath(value) {
105
+ return (
106
+ typeof value === 'string' &&
107
+ DATA_ROOT_RELATIVE_PATH.test(value) &&
108
+ value.split('/').every((segment) => segment !== '.' && segment !== '..')
109
+ );
110
+ }
111
+
112
+ /**
113
+ * The MANDATORY `evidenceTrustPolicy` declaration. `required` is a boolean with
114
+ * no default: whether this data root's editions MUST be evidence-gated. The
115
+ * policy is either declared (`policy` and `pin`, two data-root-relative paths)
116
+ * or absent (both null); a declared policy is enforced whether or not it is
117
+ * required. `required: true` with no policy is a state the shape allows and
118
+ * `xema edition check` refuses (EvidenceTrustRequiredError), so the refusal
119
+ * names the missing policy rather than a malformed file.
120
+ */
121
+ function requireEvidenceTrustPolicy(config) {
122
+ if (!Object.hasOwn(config, 'evidenceTrustPolicy')) {
123
+ throw new EditionWorkspaceConfigError(
124
+ `${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy" is required: {"required": true, "policy": ..., "pin": ...} ` +
125
+ 'or {"required": false} (no default: say whether this data root\'s editions must be evidence-gated)',
126
+ );
127
+ }
128
+ const value = config.evidenceTrustPolicy;
129
+ const shapeError = () =>
130
+ new EditionWorkspaceConfigError(
131
+ `${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy" is exactly {required: boolean} or {required, policy, pin} with both paths data-root-relative`,
132
+ );
133
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {throw shapeError();}
134
+ const keys = Object.keys(value).sort();
135
+ if (!keys.every((key) => EVIDENCE_TRUST_POLICY_KEYS.includes(key)) || typeof value.required !== 'boolean') {throw shapeError();}
136
+ const declared = keys.length > 1;
137
+ if (declared && keys.join() !== EVIDENCE_TRUST_POLICY_KEYS.join()) {throw shapeError();}
138
+ if (!declared) {return Object.freeze({ required: value.required, policy: null, pin: null });}
139
+ for (const key of ['policy', 'pin']) {
140
+ if (!isDataRootRelativePath(value[key])) {
141
+ throw new EditionWorkspaceConfigError(
142
+ `${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy.${key}" must be a data-root-relative POSIX path without "." or ".." segments; got ${JSON.stringify(value[key])}`,
143
+ );
144
+ }
145
+ }
146
+ if (value.policy === value.pin) {
147
+ throw new EditionWorkspaceConfigError(`${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy.policy" and ".pin" must be different files`);
148
+ }
149
+ return Object.freeze({ required: value.required, policy: value.policy, pin: value.pin });
150
+ }
151
+
152
+ /** Validate a parsed config document; returns the frozen, typed config. */
153
+ export function parseEditionWorkspaceConfig(document) {
154
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
155
+ throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE} must be a JSON object`);
156
+ }
157
+ for (const key of Object.keys(document)) {
158
+ if (!KEYS.includes(key)) {throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE}: unknown key "${key}"`);}
159
+ }
160
+ if (document.schemaVersion !== EDITION_WORKSPACE_CONFIG_SCHEMA_VERSION) {
161
+ throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE}: schemaVersion must be ${EDITION_WORKSPACE_CONFIG_SCHEMA_VERSION}`);
162
+ }
163
+ const sections = Object.fromEntries(
164
+ Object.entries(SECTIONS)
165
+ .filter(([key]) => document[key] !== undefined)
166
+ .map(([key, parse]) => [key, parse(document[key])]),
167
+ );
168
+ return Object.freeze({
169
+ ...sections,
170
+ imageRegistry: requireString(document, 'imageRegistry', IMAGE_REGISTRY, '<host>/<namespace> with no scheme or trailing slash'),
171
+ chartRegistry: requireString(document, 'chartRegistry', CHART_REGISTRY, 'oci://<host>/<path>'),
172
+ chartPublishers: requirePublishers(document),
173
+ sourceRepositoryUrlPrefix: requireString(document, 'sourceRepositoryUrlPrefix', SOURCE_URL_PREFIX, 'https://<host>/<organisation>/ with a trailing slash'),
174
+ sourceAuthUsername: requireSourceAuthUsername(document),
175
+ evidenceTrustPolicy: requireEvidenceTrustPolicy(document),
176
+ });
177
+ }
178
+
179
+ let cached;
180
+
181
+ /** The workspace config, read once per data root. */
182
+ export function editionWorkspaceConfig() {
183
+ const root = dataRoot();
184
+ if (cached?.root !== root) {
185
+ cached = { root, config: parseEditionWorkspaceConfig(readStrictJsonFile(join(root, EDITION_WORKSPACE_CONFIG_FILE))) };
186
+ }
187
+ return cached.config;
188
+ }
189
+
190
+ /** Forget the cached config (a test that rewrites the file in place). */
191
+ export function resetEditionWorkspaceConfig() {
192
+ cached = undefined;
193
+ }
194
+
195
+ /**
196
+ * One product-data section, refused by name when the data root does not carry
197
+ * it. `needed` says which operation wanted it, so the refusal is actionable.
198
+ */
199
+ export function workspaceSection(name, needed) {
200
+ const section = editionWorkspaceConfig()[name];
201
+ if (section === undefined) {
202
+ throw new Error(
203
+ `${EDITION_WORKSPACE_CONFIG_FILE} in ${dataRoot()} has no "${name}" section, which ${needed} needs`,
204
+ );
205
+ }
206
+ return section;
207
+ }
@@ -0,0 +1,57 @@
1
+ // edition-constants — the closed vocabulary of the edition OCI artifact, in one
2
+ // place so the writer, the guards and the workflow cannot disagree about what an
3
+ // edition IS. ONE layout serves both channels: an artifact at
4
+ // `<registry>/xema-edition-<id>`, addressed by digest, carrying a titled
5
+ // descriptor layer plus the lock layers.
6
+
7
+ import { EditionChannel } from './edition-channel.mjs';
8
+ import { EDITION_CHART_SET_FILE } from './edition-chart-set.mjs';
9
+ import editionContract from './edition-contract.json' with { type: 'json' };
10
+
11
+ /**
12
+ * The vocabulary itself lives in edition-contract.json, the ONE source:
13
+ * xema-deploy vendors that file byte for byte and builds its constants from it,
14
+ * so the producer and the consumer of an edition cannot disagree about its names.
15
+ * It is a JSON import, not a read at run time, so a bundle (the evidence
16
+ * verifier) carries it.
17
+ */
18
+ export const EDITION_CONTRACT = Object.freeze(structuredClone(editionContract));
19
+
20
+ /** The descriptor layer's file name; its `org.opencontainers.image.title`. */
21
+ export const EDITION_DESCRIPTOR_FILE = EDITION_CONTRACT.descriptorFile;
22
+ export const EDITION_DESCRIPTOR_SCHEMA = EDITION_CONTRACT.descriptorSchema;
23
+ /** The ONE artifactType of every edition artifact, develop or release. */
24
+ export const EDITION_ARTIFACT_TYPE = EDITION_CONTRACT.artifactType;
25
+ /** The repository name prefix: `<registry>/<prefix><id>`. */
26
+ export const EDITION_REPOSITORY_PREFIX = EDITION_CONTRACT.repositoryPrefix;
27
+
28
+ export { EditionChannel };
29
+
30
+ /** Whether the data root's editions are evidence-gated, as the descriptor records it. */
31
+ export const EvidenceTrustState = Object.freeze({ Armed: 'armed', NotArmed: 'not-armed' });
32
+
33
+ /** The owner-relative segment of a develop image repository: `<host>/<owner>/<segment>/<name>`. */
34
+ export const DEVELOP_IMAGE_NAMESPACE_SEGMENT = EDITION_CONTRACT.imageNamespaceSegment;
35
+
36
+ /** The titled layers only an OFFERING artifact carries (not locks): the catalogue a customer edition resolves against. */
37
+ export const EDITION_OFFERING_LAYERS = Object.freeze({ ...EDITION_CONTRACT.offeringLayers });
38
+
39
+ /** The one provenance pin that is not a commit: the digest of the catalogue artifact a customer edition was resolved against. */
40
+ export const CATALOGUE_PIN_KEY = 'catalogue';
41
+ if (!(CATALOGUE_PIN_KEY in EDITION_CONTRACT.provenancePins)) {
42
+ throw new Error('edition-contract.json provenancePins does not declare the catalogue pin');
43
+ }
44
+
45
+ /** The lock files an edition can carry. */
46
+ export const EditionLockFile = Object.freeze({
47
+ Distribution: 'distribution.lock.json',
48
+ Images: 'images.lock.json',
49
+ Authorization: 'authorization.lock.json',
50
+ ChartSet: EDITION_CHART_SET_FILE,
51
+ });
52
+
53
+ /** The locks of an edition, in the order the descriptor lists them and the artifact pushes them; the same for both channels. */
54
+ export const EDITION_LOCKS = Object.freeze([...EDITION_CONTRACT.locks]);
55
+
56
+ /** Every file of the artifact, in the order it is pushed. */
57
+ export const EDITION_FILES = Object.freeze([EDITION_DESCRIPTOR_FILE, ...EDITION_LOCKS]);
@@ -0,0 +1,23 @@
1
+ {
2
+ "schema": "xema.edition-contract/v1",
3
+ "artifactType": "application/vnd.xema.edition.v1+json",
4
+ "repositoryPrefix": "xema-edition-",
5
+ "descriptorFile": "xema-edition.json",
6
+ "descriptorSchema": "xema.edition/v1",
7
+ "locks": ["distribution.lock.json", "images.lock.json", "authorization.lock.json", "chart-set.lock.json"],
8
+ "offeringLayers": { "catalogue": "catalogue.json" },
9
+ "provenancePins": {
10
+ "catalogue": "the sha256 digest (sha256:<64 hex>) of the offering artifact a customer edition was resolved against; every other pin is a 40-hex commit"
11
+ },
12
+ "imageNamespaceSegment": "dev",
13
+ "evidenceTrust": {
14
+ "states": {
15
+ "armed": "the data root declares an evidence trust policy that is pinned and ARMED on the channel of the edition",
16
+ "not-armed": "a release edition: exactly, the data root's evidenceTrustPolicy.required is false and it has no policy armed on the release channel; a develop edition: its policy is not armed on the develop channel, whatever required says"
17
+ },
18
+ "refusal": {
19
+ "code": "EVIDENCE_TRUST_REQUIRED_UNARMED",
20
+ "when": "evidenceTrustPolicy.required is true while the policy is not armed on the release channel (none declared, or declared but UNARMED or PARTIALLY ARMED there): `xema edition check` fails, no release edition is sealed and no release artifact is written, so a release descriptor never records not-armed for a required data root. Arming is judged per channel: a develop edition admits not-armed"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // `xema edition list`: the offering ids of an editions root, as JSON.
3
+ //
4
+ // usage: xema edition list --editions-root <dir>
5
+ // stdout: {"schemaVersion":1,"editions":["<id>",…]} in canonical id order.
6
+
7
+ import { discoverDistributionIds } from './distribution-paths.mjs';
8
+ import {
9
+ WorkspaceRoot,
10
+ configureEditionWorkspaceFromArgv,
11
+ editionsRoot,
12
+ isEntryModule,
13
+ } from './edition-workspace.mjs';
14
+ import { serializeJson } from './lib.mjs';
15
+
16
+ export function listEditions({ editionsRoot: root }) {
17
+ return { schemaVersion: 1, editions: discoverDistributionIds(root) };
18
+ }
19
+
20
+ function main() {
21
+ const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [WorkspaceRoot.Editions]);
22
+ if (rest.length > 0) {
23
+ throw new Error('usage: xema edition list --editions-root <dir>');
24
+ }
25
+ process.stdout.write(serializeJson(listEditions({ editionsRoot: editionsRoot() })));
26
+ }
27
+
28
+ if (isEntryModule(import.meta.url)) {
29
+ try {
30
+ main();
31
+ } catch (error) {
32
+ console.error(`[edition-list] ${error.message}`);
33
+ process.exitCode = 1;
34
+ }
35
+ }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ // `xema edition locks <id>`: one edition's validated locks, as JSON.
3
+ //
4
+ // usage: xema edition locks <id> --data-root <dir> --editions-root <dir> --kernel <dir>
5
+ //
6
+ // The locks are read and validated exactly as the evidence verifier reads
7
+ // them: `images.lock.json` and `distribution.lock.json` under the kernel
8
+ // schema, immutable coordinates only, and the image lock's kernel contract
9
+ // authority equal to the data root's runtime image source index. A consumer
10
+ // reads this output instead of parsing or validating a lock itself.
11
+
12
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
13
+ import {
14
+ WorkspaceRoot,
15
+ configureEditionWorkspaceFromArgv,
16
+ dataRoot,
17
+ editionsRoot,
18
+ isEntryModule,
19
+ } from './edition-workspace.mjs';
20
+ import { loadValidatedLocks } from './image-evidence.mjs';
21
+ import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
22
+
23
+ const USAGE =
24
+ 'usage: xema edition locks <id> --data-root <dir> --editions-root <dir> --kernel <dir>';
25
+
26
+ export async function readEditionLocks({ distributionId, dataRoot: data, editionsRoot: editions, schemas }) {
27
+ assertCanonicalDistributionId(distributionId);
28
+ const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex } =
29
+ await loadValidatedLocks(distributionId, data, schemas, { editionsRoot: editions });
30
+ return {
31
+ schemaVersion: 1,
32
+ distributionId,
33
+ contractAuthority: runtimeSourceIndex.contractAuthority,
34
+ distributionLock,
35
+ imageLock,
36
+ repositoryPins: [...repositoryPins.values()].sort((left, right) =>
37
+ compareStrings(left.repo, right.repo),
38
+ ),
39
+ };
40
+ }
41
+
42
+ async function main() {
43
+ const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
44
+ WorkspaceRoot.Data,
45
+ WorkspaceRoot.Editions,
46
+ WorkspaceRoot.Kernel,
47
+ ]);
48
+ if (rest.length !== 1) {
49
+ throw new Error(USAGE);
50
+ }
51
+ const schemas = await loadKernelDistributionSchemas();
52
+ process.stdout.write(
53
+ serializeJson(
54
+ await readEditionLocks({
55
+ distributionId: rest[0],
56
+ dataRoot: dataRoot(),
57
+ editionsRoot: editionsRoot(),
58
+ schemas,
59
+ }),
60
+ ),
61
+ );
62
+ }
63
+
64
+ if (isEntryModule(import.meta.url)) {
65
+ main().catch((error) => {
66
+ console.error(`[edition-locks] ${error.message}`);
67
+ process.exitCode = 1;
68
+ });
69
+ }
@@ -0,0 +1,228 @@
1
+ // The edition protocol this library speaks, and the check that a kernel speaks
2
+ // it too.
3
+ //
4
+ // The kernel (`@xemahq/contracts/edition-protocol`) exports the identity of the
5
+ // code this library executes as DATA: `{contentDigest, version}` in
6
+ // `dist/edition-protocol/edition-protocol.json`, and the edition contract in
7
+ // `dist/edition-protocol/edition-contract.schema.json` (the identity, the
8
+ // hashing convention, the entry subpaths this library may load, and every
9
+ // kernel source file those subpaths load at runtime). `contentDigest` is the
10
+ // kernel contract authority digest over the exact bytes of those files.
11
+ //
12
+ // The digest is a CONSERVATIVE, deterministic stand-in for protocol semantics.
13
+ // Any change to accepted or refused edition semantics MUST change it; a
14
+ // behaviour-preserving refactor MAY change it too, and the cost is a repin,
15
+ // which is accepted. Semantic equivalence isn't computable, so it is never
16
+ // detected.
17
+ //
18
+ // This library declares the versions it supports, each pinned to one digest,
19
+ // and vendors a copy of each supported contract (`vendored/`, held to its pin
20
+ // by `edition-protocol.test.mjs`). A kernel is accepted only when its identity
21
+ // names a supported version, its digest is the one this library pins for that
22
+ // version, and its contract file states that same identity. Nothing else about
23
+ // the kernel is compared: a kernel change outside the protocol's source closure
24
+ // needs no CLI release; a change inside it needs the CLI to add support.
25
+ // Checking a kernel reads two files and executes nothing from it.
26
+
27
+ import { realpathSync } from 'node:fs';
28
+
29
+ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
30
+
31
+ /** The edition protocols this library interprets: version -> content digest. */
32
+ export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
33
+ Object.freeze({
34
+ version: 1,
35
+ contentDigest: 'd5b7ce85aa6596730527da64a7f6682684039398a88fd168429f6e4a3f0438bd',
36
+ }),
37
+ ]);
38
+
39
+ /** Where a kernel package publishes its identity and its contract, package-relative. */
40
+ export const KERNEL_EDITION_PROTOCOL_FILE = 'dist/edition-protocol/edition-protocol.json';
41
+ export const KERNEL_EDITION_CONTRACT_FILE = 'dist/edition-protocol/edition-contract.schema.json';
42
+
43
+ /** This package's vendored copy of a supported contract, package-relative. */
44
+ export const vendoredEditionContractFile = (version) =>
45
+ `edition/vendored/edition-contract-v${version}.schema.json`;
46
+
47
+ const MAX_IDENTITY_BYTES = 4 * 1024;
48
+ const MAX_CONTRACT_BYTES = 8 * 1024 * 1024;
49
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
50
+
51
+ /** Why a kernel's edition protocol was refused. A closed set. */
52
+ export const EditionProtocolMismatchReason = Object.freeze({
53
+ /** The kernel publishes no edition protocol identity. */
54
+ Undeclared: 'undeclared',
55
+ /** The identity file is not exactly `{contentDigest, version}`. */
56
+ Malformed: 'malformed',
57
+ /** This library supports no protocol of that version. */
58
+ UnsupportedVersion: 'unsupported-version',
59
+ /** The version is supported, but pinned to a different digest. */
60
+ DigestMismatch: 'digest-mismatch',
61
+ /** The kernel's contract file does not state its own identity. */
62
+ ContractMismatch: 'contract-mismatch',
63
+ /** The kernel's protocol is not the one compiled into the running verifier. */
64
+ BundleMismatch: 'bundle-mismatch',
65
+ });
66
+
67
+ export class EditionProtocolMismatchError extends Error {
68
+ constructor(reason, message) {
69
+ super(message);
70
+ this.name = 'EditionProtocolMismatchError';
71
+ this.reason = reason;
72
+ }
73
+ }
74
+
75
+ const describe = ({ version, contentDigest }) => `edition protocol v${version} (${contentDigest})`;
76
+
77
+ const supportedList = () =>
78
+ SUPPORTED_EDITION_PROTOCOLS.map((protocol) => `v${protocol.version}`).join(', ');
79
+
80
+ /** Parse identity bytes strictly: exactly a positive integer version and a lowercase SHA-256 hex digest. */
81
+ export function parseEditionProtocolIdentity(bytes, label) {
82
+ let identity;
83
+ try {
84
+ identity = JSON.parse(bytes.toString('utf8'));
85
+ } catch (error) {
86
+ throw new EditionProtocolMismatchError(
87
+ EditionProtocolMismatchReason.Malformed,
88
+ `${label} is not JSON: ${error.message}`,
89
+ );
90
+ }
91
+ if (
92
+ identity === null ||
93
+ typeof identity !== 'object' ||
94
+ Array.isArray(identity) ||
95
+ Object.keys(identity).sort().join() !== 'contentDigest,version' ||
96
+ !Number.isSafeInteger(identity.version) ||
97
+ identity.version < 1 ||
98
+ typeof identity.contentDigest !== 'string' ||
99
+ !SHA256_HEX.test(identity.contentDigest)
100
+ ) {
101
+ throw new EditionProtocolMismatchError(
102
+ EditionProtocolMismatchReason.Malformed,
103
+ `${label} must be exactly {"contentDigest":"<64 lowercase hex>","version":<positive integer>}`,
104
+ );
105
+ }
106
+ return Object.freeze({ contentDigest: identity.contentDigest, version: identity.version });
107
+ }
108
+
109
+ /** The `{version, contentDigest}` a contract file states, or undefined. */
110
+ function contractIdentity(contractBytes) {
111
+ let contract;
112
+ try {
113
+ contract = JSON.parse(contractBytes.toString('utf8'));
114
+ } catch {
115
+ return undefined;
116
+ }
117
+ if (
118
+ contract === null ||
119
+ typeof contract !== 'object' ||
120
+ !Number.isSafeInteger(contract.version) ||
121
+ typeof contract.contentDigest !== 'string'
122
+ ) {
123
+ return undefined;
124
+ }
125
+ return { version: contract.version, contentDigest: contract.contentDigest };
126
+ }
127
+
128
+ /**
129
+ * Refuse an identity this library does not support, and a contract file that
130
+ * does not state that identity. Returns the identity.
131
+ */
132
+ export function assertSupportedEditionProtocol({ identity, contractBytes, source }) {
133
+ const supported = SUPPORTED_EDITION_PROTOCOLS.find(
134
+ (protocol) => protocol.version === identity.version,
135
+ );
136
+ if (supported === undefined) {
137
+ throw new EditionProtocolMismatchError(
138
+ EditionProtocolMismatchReason.UnsupportedVersion,
139
+ `${source} declares ${describe(identity)}; this CLI supports ${supportedList()}. ` +
140
+ 'Use a CLI release that supports it, or pin a kernel that speaks a supported protocol.',
141
+ );
142
+ }
143
+ if (supported.contentDigest !== identity.contentDigest) {
144
+ throw new EditionProtocolMismatchError(
145
+ EditionProtocolMismatchReason.DigestMismatch,
146
+ `${source} declares ${describe(identity)}; this CLI pins v${supported.version} to ` +
147
+ `${supported.contentDigest} (this CLI supports ${supportedList()}). The kernel changed the edition ` +
148
+ 'contract without a new protocol version.',
149
+ );
150
+ }
151
+ const stated = contractIdentity(contractBytes);
152
+ if (stated?.version !== identity.version || stated?.contentDigest !== identity.contentDigest) {
153
+ throw new EditionProtocolMismatchError(
154
+ EditionProtocolMismatchReason.ContractMismatch,
155
+ `${source} declares ${describe(identity)}, but its edition contract file states ` +
156
+ `${stated === undefined ? 'no identity' : describe(stated)}`,
157
+ );
158
+ }
159
+ return identity;
160
+ }
161
+
162
+ /**
163
+ * Read and check the edition protocol of the built kernel package at
164
+ * `kernelPackageDir`. Reads two files, no-follow and bounded; executes nothing.
165
+ */
166
+ export function readKernelEditionProtocol(kernelPackageDir) {
167
+ const source = `the kernel at ${kernelPackageDir}`;
168
+ let identityBytes;
169
+ try {
170
+ ({ bytes: identityBytes } = readBoundedNoFollowFile(
171
+ kernelPackageDir,
172
+ KERNEL_EDITION_PROTOCOL_FILE,
173
+ `${source}: ${KERNEL_EDITION_PROTOCOL_FILE}`,
174
+ MAX_IDENTITY_BYTES,
175
+ ));
176
+ } catch (error) {
177
+ if (error?.code !== 'ENOENT') {throw error;}
178
+ throw new EditionProtocolMismatchError(
179
+ EditionProtocolMismatchReason.Undeclared,
180
+ `${source} publishes no edition protocol (${KERNEL_EDITION_PROTOCOL_FILE} is absent); ` +
181
+ `this CLI supports ${supportedList()}. Pin a kernel that publishes its edition protocol identity.`,
182
+ );
183
+ }
184
+ const identity = parseEditionProtocolIdentity(
185
+ identityBytes,
186
+ `${source}: ${KERNEL_EDITION_PROTOCOL_FILE}`,
187
+ );
188
+ const { bytes: contractBytes } = readBoundedNoFollowFile(
189
+ kernelPackageDir,
190
+ KERNEL_EDITION_CONTRACT_FILE,
191
+ `${source}: ${KERNEL_EDITION_CONTRACT_FILE}`,
192
+ MAX_CONTRACT_BYTES,
193
+ );
194
+ return assertSupportedEditionProtocol({ identity, contractBytes, source });
195
+ }
196
+
197
+ const checkedKernels = new Map();
198
+
199
+ /**
200
+ * {@link readKernelEditionProtocol}, once per kernel directory. Every loader
201
+ * that executes kernel code calls it first, so no code path interprets a
202
+ * kernel whose protocol this library does not support.
203
+ */
204
+ export function assertKernelEditionProtocol(kernelPackageDir) {
205
+ const key = realpathSync(kernelPackageDir);
206
+ let identity = checkedKernels.get(key);
207
+ if (identity === undefined) {
208
+ identity = readKernelEditionProtocol(key);
209
+ checkedKernels.set(key, identity);
210
+ }
211
+ return identity;
212
+ }
213
+
214
+ /**
215
+ * The running verifier interprets evidence with the contracts compiled into
216
+ * it, so the kernel the workspace states must speak exactly that protocol.
217
+ */
218
+ export function assertKernelSpeaksBundledProtocol({ kernelPackageDir, bundled }) {
219
+ const stated = readKernelEditionProtocol(kernelPackageDir);
220
+ if (stated.version !== bundled.version || stated.contentDigest !== bundled.contentDigest) {
221
+ throw new EditionProtocolMismatchError(
222
+ EditionProtocolMismatchReason.BundleMismatch,
223
+ `the kernel at ${kernelPackageDir} declares ${describe(stated)}, but this verifier was built ` +
224
+ `with ${describe(bundled)}`,
225
+ );
226
+ }
227
+ return stated;
228
+ }