@xemahq/xema 0.26.0 → 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 (229) 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 -1
  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 +54179 -0
  28. package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -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 +315 -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 +270 -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 +207 -0
  152. package/edition/edition-constants.mjs +57 -0
  153. package/edition/edition-contract.json +23 -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 +176 -0
  158. package/edition/evidence-checkout.mjs +418 -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 +256 -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-evidence-replay-state.mjs +242 -0
  167. package/edition/image-evidence.mjs +1225 -0
  168. package/edition/kernel-contract-authority.mjs +443 -0
  169. package/edition/lib.mjs +3018 -0
  170. package/edition/no-follow-file.mjs +94 -0
  171. package/edition/oci-evidence-verbs.mjs +392 -0
  172. package/edition/oci-image-source-evidence.mjs +239 -0
  173. package/edition/oras.mjs +648 -0
  174. package/edition/pin-source.mjs +227 -0
  175. package/edition/plan-image-evidence-refresh.mjs +408 -0
  176. package/edition/platform-lock-readability.mjs +892 -0
  177. package/edition/platform-service-class.mjs +18 -0
  178. package/edition/platform-service-source-authority.mjs +105 -0
  179. package/edition/positive-closure.mjs +162 -0
  180. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  181. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  182. package/edition/produce-image-source-evidence.mjs +765 -0
  183. package/edition/producer-evidence-bundle.mjs +157 -0
  184. package/edition/registry-credentials.mjs +193 -0
  185. package/edition/release-catalogue.mjs +39 -0
  186. package/edition/release-channel-guard.mjs +99 -0
  187. package/edition/release-edition-artifact.mjs +158 -0
  188. package/edition/remedy-command.mjs +89 -0
  189. package/edition/repin-evidence-trust.mjs +101 -0
  190. package/edition/required-resource-provider.mjs +58 -0
  191. package/edition/resolve-chart-set.mjs +415 -0
  192. package/edition/resolve-customer-edition.mjs +288 -0
  193. package/edition/resolve-distribution.mjs +2318 -0
  194. package/edition/resolve-edition.mjs +186 -0
  195. package/edition/resolve-images.mjs +1606 -0
  196. package/edition/seal-image-digests.mjs +855 -0
  197. package/edition/sealed-evidence-accept.mjs +82 -0
  198. package/edition/source-authority.mjs +725 -0
  199. package/edition/source-closure.mjs +116 -0
  200. package/edition/strict-json.mjs +121 -0
  201. package/edition/string-order.mjs +5 -0
  202. package/edition/trust-policy-arming.mjs +120 -0
  203. package/edition/trusted-executable.mjs +166 -0
  204. package/edition/verifier-binding.mjs +45 -0
  205. package/edition/verifier-files.mjs +8 -0
  206. package/edition/verifier-kernel-schemas.mjs +22 -0
  207. package/edition/verifier-provenance.mjs +64 -0
  208. package/edition/verify-image-evidence.mjs +430 -0
  209. package/edition/verify-image-platforms.mjs +495 -0
  210. package/edition/why.mjs +56 -0
  211. package/edition/workspace-sections.mjs +195 -0
  212. package/examples/06-biome-sources.yaml +1 -0
  213. package/package.json +14 -6
  214. package/dist/commands/dist.d.ts +0 -16
  215. package/dist/commands/dist.d.ts.map +0 -1
  216. package/dist/commands/dist.js +0 -182
  217. package/dist/commands/dist.js.map +0 -1
  218. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  219. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  220. package/dist/runtime/distribution/available-biome.js +0 -156
  221. package/dist/runtime/distribution/available-biome.js.map +0 -1
  222. package/dist/runtime/distribution/compose.d.ts +0 -23
  223. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  224. package/dist/runtime/distribution/compose.js +0 -48
  225. package/dist/runtime/distribution/compose.js.map +0 -1
  226. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  227. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  228. package/dist/runtime/runner/chart-trust.js +0 -270
  229. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,55 @@
1
+ // Where this CLI's built evidence verifier sits. The verifier is ONE bundle,
2
+ // built at CLI build time from the CLI's own sources and its locked
3
+ // dependencies, and shipped in the npm tarball beside a provenance record.
4
+ //
5
+ // The package directory is the one place the library derives a location from
6
+ // where it sits: the bundle is a property of the installed CLI, not of any
7
+ // workspace root. Callers that need another location (tests) pass it.
8
+
9
+ import { lstatSync, readFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ import { verifierDigests } from './evidence-trust-pin.mjs';
14
+ import {
15
+ VERIFIER_BUNDLE_FILE,
16
+ VERIFIER_DIST_SUBPATH,
17
+ VERIFIER_PROVENANCE_FILE,
18
+ } from './verifier-files.mjs';
19
+
20
+ const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
21
+
22
+ /** The built verifier's two files under `packageDir` (this package by default). */
23
+ export function locateBuiltVerifier(packageDir = PACKAGE_DIR) {
24
+ const directory = join(packageDir, ...VERIFIER_DIST_SUBPATH);
25
+ return Object.freeze({
26
+ bundlePath: join(directory, VERIFIER_BUNDLE_FILE),
27
+ provenancePath: join(directory, VERIFIER_PROVENANCE_FILE),
28
+ });
29
+ }
30
+
31
+ /**
32
+ * The digests of the built verifier a pin records. A missing file is refused,
33
+ * naming the path - never skipped.
34
+ */
35
+ export function readBuiltVerifierDigests(locateVerifier = locateBuiltVerifier) {
36
+ const { bundlePath, provenancePath } = locateVerifier();
37
+ for (const path of [bundlePath, provenancePath]) {
38
+ let regular = false;
39
+ try {
40
+ regular = lstatSync(path).isFile();
41
+ } catch (error) {
42
+ if (error.code !== 'ENOENT') {throw error;}
43
+ }
44
+ if (!regular) {
45
+ throw new Error(
46
+ `this CLI's built evidence verifier is missing: ${path} is not a regular file ` +
47
+ `(it is produced by the package build)`,
48
+ );
49
+ }
50
+ }
51
+ return verifierDigests({
52
+ bundleBytes: readFileSync(bundlePath),
53
+ provenanceBytes: readFileSync(provenancePath),
54
+ });
55
+ }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // catalogue-edition — write the catalogue of a RELEASE offering from its committed, sealed locks.
3
+ //
4
+ // catalogue <id> --out <dir>
5
+ //
6
+ // Writes <dir>/catalogue.json (catalogue.mjs). `xema edition artifact` writes the
7
+ // same file into the offering artifact; this verb is for inspecting it or
8
+ // publishing it on its own.
9
+
10
+ import { mkdirSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+
13
+ import { writeCatalogue } from './catalogue.mjs';
14
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
15
+ import {
16
+ WorkspaceRoot,
17
+ configureEditionWorkspaceFromProcess,
18
+ isEntryModule,
19
+ } from './edition-workspace.mjs';
20
+ import { releaseCatalogue } from './release-catalogue.mjs';
21
+ import { readCommittedEdition } from './release-edition-artifact.mjs';
22
+
23
+ if (isEntryModule(import.meta.url)) {
24
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
25
+ }
26
+
27
+ /** `{ id, out }` of `catalogue <distribution-id> --out <dir>`; anything else is the usage error. */
28
+ export function parseCatalogueArgs(argv) {
29
+ const [command, id, flag, out, ...extra] = argv;
30
+ if (command !== 'catalogue' || id === undefined || flag !== '--out' || out === undefined || out.startsWith('--') || extra.length > 0) {
31
+ throw new Error('usage: catalogue <distribution-id> --out <dir>');
32
+ }
33
+ assertCanonicalDistributionId(id);
34
+ return { id, out };
35
+ }
36
+
37
+ async function run(argv) {
38
+ const { id, out } = parseCatalogueArgs(argv);
39
+ const dir = resolve(out);
40
+ mkdirSync(dir, { recursive: true });
41
+ console.log(writeCatalogue(dir, await releaseCatalogue(id, await readCommittedEdition(id))));
42
+ }
43
+
44
+ if (isEntryModule(import.meta.url)) {
45
+ run(process.argv.slice(2)).catch((error) => {
46
+ console.error(`[edition-catalogue] ${error.message}`);
47
+ process.exit(1);
48
+ });
49
+ }
@@ -0,0 +1,64 @@
1
+ // catalogue-fetch — read the catalogue layer of an offering artifact, by digest.
2
+ //
3
+ // The reference is `oci://<registry>/<repository>@sha256:<digest>`: a tag is
4
+ // refused, because a catalogue is pinned, not followed. The manifest and the
5
+ // layer are fetched with the one OCI client (`oras.mjs`, the pinned oras), which verifies that
6
+ // every body hashes to the digest it was asked for. Credentials are named by
7
+ // environment variable (never inlined): `--catalogue-username` with
8
+ // `--catalogue-token-env`, or neither for an anonymous read.
9
+
10
+ import { Buffer } from 'node:buffer';
11
+
12
+ import { CATALOGUE_FILE, CatalogueError, parseCatalogue } from './catalogue.mjs';
13
+ import { EDITION_ARTIFACT_TYPE } from './edition-constants.mjs';
14
+ import { OrasRegistry } from './oras.mjs';
15
+ import { splitImageRef } from './registry-credentials.mjs';
16
+
17
+ const REF = /^oci:\/\/([^@]+)@(sha256:[a-f0-9]{64})$/;
18
+ const CATALOGUE_MAX_BYTES = 32 * 1024 * 1024;
19
+ const TITLE = 'org.opencontainers.image.title';
20
+
21
+ /** `{ authority, repository, digest }` of an `oci://<repository>@sha256:<digest>` reference; a tag is refused. */
22
+ export function parseCatalogueRef(ref) {
23
+ const match = REF.exec(String(ref));
24
+ if (match === null) {
25
+ throw new CatalogueError(`--catalogue ${JSON.stringify(ref)} is not oci://<registry>/<repository>@sha256:<64 hex>: a catalogue is pinned by digest, never by tag`);
26
+ }
27
+ const { authority, repository } = splitImageRef(match[1]);
28
+ return { authority, repository, digest: match[2] };
29
+ }
30
+
31
+ /** The credential the client needs: named by env var, or anonymous. */
32
+ export function catalogueCredential({ authority, username, tokenEnv, env = process.env }) {
33
+ if ((username === undefined) !== (tokenEnv === undefined)) {
34
+ throw new CatalogueError('--catalogue-username and --catalogue-token-env go together (or neither, for an anonymous read)');
35
+ }
36
+ if (tokenEnv === undefined) {return { authority, access: 'anonymous', basicAuth: null };}
37
+ const token = env[tokenEnv];
38
+ if (typeof token !== 'string' || token.length === 0) {
39
+ throw new CatalogueError(`--catalogue-token-env names ${tokenEnv}, which is unset or empty`);
40
+ }
41
+ return { authority, access: 'authenticated', basicAuth: Buffer.from(`${username}:${token}`, 'utf8').toString('base64') };
42
+ }
43
+
44
+ /** Fetch and strictly parse the catalogue of the artifact at `ref`. `client` is injectable for tests. */
45
+ export async function fetchCatalogue({ ref, username, tokenEnv, env = process.env, client }) {
46
+ const { authority, repository, digest } = parseCatalogueRef(ref);
47
+ const registry = client ?? new OrasRegistry({ credential: catalogueCredential({ authority, username, tokenEnv, env }) });
48
+ const manifest = await registry.getManifestByDigest(repository, digest);
49
+ if (manifest.isIndex || manifest.document.artifactType !== EDITION_ARTIFACT_TYPE) {
50
+ throw new CatalogueError(`${ref} is not an edition artifact (artifactType ${JSON.stringify(manifest.document.artifactType)}, expected ${EDITION_ARTIFACT_TYPE})`);
51
+ }
52
+ const layers = (manifest.document.layers ?? []).filter((layer) => layer.annotations?.[TITLE] === CATALOGUE_FILE);
53
+ if (layers.length !== 1) {
54
+ throw new CatalogueError(`${ref} carries ${layers.length} layers titled ${CATALOGUE_FILE}; an offering artifact carries exactly one`);
55
+ }
56
+ const bytes = await registry.getBlob(repository, layers[0].digest, CATALOGUE_MAX_BYTES);
57
+ let document;
58
+ try {
59
+ document = JSON.parse(bytes.toString('utf8'));
60
+ } catch (error) {
61
+ throw new CatalogueError(`${ref}: ${CATALOGUE_FILE} is not JSON: ${error.message}`);
62
+ }
63
+ return { catalogue: parseCatalogue(document), digest };
64
+ }
@@ -0,0 +1,135 @@
1
+ // catalogue — what an offering's edition artifact offers to a customer edition.
2
+ //
3
+ // `catalogue.json` is one more titled layer of an offering's edition artifact
4
+ // (edition-artifact.mjs): the availability, pins, image digests, chart-set and
5
+ // declared ceiling of the offering, and NOTHING that can add a component. A
6
+ // customer edition is resolved against it (resolve-customer-edition.mjs) by the
7
+ // digest of the artifact that carries it.
8
+ //
9
+ // `channel` is provenance only: no reader branches on it.
10
+
11
+ import { writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
15
+ import { EDITION_OFFERING_LAYERS, EditionChannel } from './edition-constants.mjs';
16
+ import { serializeJson } from './lib.mjs';
17
+
18
+ export const CATALOGUE_FILE = EDITION_OFFERING_LAYERS.catalogue;
19
+ export const CATALOGUE_SCHEMA = 'xema.edition-catalogue/v1';
20
+
21
+ const TOP_KEYS = ['schema', 'offeringId', 'channel', 'biomes', 'sourceTreeIds', 'platformServices', 'images', 'chartSet', 'contractAuthority', 'ceiling'];
22
+ const IMAGES_KEYS = ['sourceHashAlgorithm', 'subjects', 'platformServiceBindings', 'webBiomes'];
23
+ const CEILING_KEYS = ['trustPolicy', 'trustCeiling'];
24
+ const DIGEST = /^sha256:[a-f0-9]{64}$/;
25
+ const GIT_TREE_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
26
+
27
+ export class CatalogueError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'CatalogueError';
31
+ }
32
+ }
33
+
34
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
35
+
36
+ function exactKeys(value, keys, where) {
37
+ if (!isObject(value) || Object.keys(value).sort().join() !== [...keys].sort().join()) {
38
+ throw new CatalogueError(`${where} must be exactly {${keys.join(', ')}}`);
39
+ }
40
+ }
41
+
42
+ /** The strict parse of a catalogue: exact keys, closed vocabularies, every image row sealed. Returns it unchanged. */
43
+ export function parseCatalogue(value) {
44
+ exactKeys(value, TOP_KEYS, CATALOGUE_FILE);
45
+ if (value.schema !== CATALOGUE_SCHEMA) {
46
+ throw new CatalogueError(`${CATALOGUE_FILE}: schema is ${JSON.stringify(value.schema)}, expected ${JSON.stringify(CATALOGUE_SCHEMA)}`);
47
+ }
48
+ assertCanonicalDistributionId(value.offeringId);
49
+ if (!Object.values(EditionChannel).includes(value.channel)) {
50
+ throw new CatalogueError(`${CATALOGUE_FILE}: channel must be one of ${Object.values(EditionChannel).join(' | ')}`);
51
+ }
52
+ if (!Array.isArray(value.biomes) || value.biomes.some((row) => !isObject(row) || typeof row.id !== 'string')) {
53
+ throw new CatalogueError(`${CATALOGUE_FILE}: biomes must be an array of biome-index rows`);
54
+ }
55
+ // `sourceTreeIds` is the vendor's git tree id of each biome at its pinned commit. A customer holds no
56
+ // checkout of the vendor source, so it takes these ON THIS CATALOGUE'S AUTHORITY (biome-content.mjs).
57
+ if (!isObject(value.sourceTreeIds)) {throw new CatalogueError(`${CATALOGUE_FILE}: sourceTreeIds must map each biome id to its git tree id`);}
58
+ const rowIds = value.biomes.map((row) => row.id).sort();
59
+ const treeIdKeys = Object.keys(value.sourceTreeIds).sort();
60
+ if (JSON.stringify(rowIds) !== JSON.stringify(treeIdKeys)) {
61
+ throw new CatalogueError(`${CATALOGUE_FILE}: sourceTreeIds must name exactly the biomes of \`biomes\` (biomes: ${rowIds.join(',')}; tree ids: ${treeIdKeys.join(',')})`);
62
+ }
63
+ for (const [biomeId, treeId] of Object.entries(value.sourceTreeIds)) {
64
+ if (!GIT_TREE_ID.test(String(treeId))) {throw new CatalogueError(`${CATALOGUE_FILE}: sourceTreeIds.${biomeId} is not a git tree id (40 or 64 lowercase hex)`);}
65
+ }
66
+ if (!Array.isArray(value.platformServices) || value.platformServices.some((row) => !isObject(row) || typeof row.name !== 'string')) {
67
+ throw new CatalogueError(`${CATALOGUE_FILE}: platformServices must be an array of platform-service-index rows`);
68
+ }
69
+ exactKeys(value.images, IMAGES_KEYS, `${CATALOGUE_FILE} images`);
70
+ for (const key of IMAGES_KEYS.slice(1)) {
71
+ if (!Array.isArray(value.images[key])) {throw new CatalogueError(`${CATALOGUE_FILE}: images.${key} must be an array`);}
72
+ }
73
+ const unsealed = value.images.subjects.filter((subject) => !DIGEST.test(String(subject?.imageDigest)));
74
+ if (unsealed.length > 0) {
75
+ throw new CatalogueError(`${CATALOGUE_FILE}: ${unsealed.length} image rows carry no imageDigest (first: ${unsealed[0]?.kind}:${unsealed[0]?.id}); a catalogue offers sealed images only`);
76
+ }
77
+ if (!isObject(value.chartSet)) {throw new CatalogueError(`${CATALOGUE_FILE}: chartSet must be the offering's chart-set lock`);}
78
+ if (!isObject(value.contractAuthority)) {throw new CatalogueError(`${CATALOGUE_FILE}: contractAuthority must be the runtime image source index's authority`);}
79
+ exactKeys(value.ceiling, CEILING_KEYS, `${CATALOGUE_FILE} ceiling`);
80
+ return value;
81
+ }
82
+
83
+ /**
84
+ * The catalogue of one resolved offering. Biome and platform-service rows are
85
+ * exactly those the offering's distribution lock ships (an offering that draws
86
+ * from a declared source is not a vendor offering and is refused).
87
+ */
88
+ export function buildCatalogue({
89
+ offeringId,
90
+ channel,
91
+ distributionLock,
92
+ imageLock,
93
+ chartSetLock,
94
+ biomeIndex,
95
+ platformServiceIndex,
96
+ contractAuthority,
97
+ leaf,
98
+ sourceTreeIds,
99
+ }) {
100
+ const sourced = distributionLock.biomes.filter((biome) => biome.source !== undefined);
101
+ if (sourced.length > 0) {
102
+ throw new CatalogueError(`offering "${offeringId}" draws ${sourced.map((b) => b.id).join(', ')} from a declared source; only a vendor offering publishes a catalogue`);
103
+ }
104
+ const shippedBiomes = new Set(distributionLock.biomes.map((biome) => biome.id));
105
+ const shippedServices = new Set(distributionLock.platformServices.map((service) => service.name));
106
+ const biomes = biomeIndex.filter((row) => shippedBiomes.has(row.id));
107
+ const missing = [...shippedBiomes].filter((id) => !biomes.some((row) => row.id === id));
108
+ if (missing.length > 0) {throw new CatalogueError(`offering "${offeringId}": the biome index has no row for ${missing.join(', ')}`);}
109
+ const platformServices = platformServiceIndex.services.filter((row) => shippedServices.has(row.name));
110
+ return parseCatalogue({
111
+ schema: CATALOGUE_SCHEMA,
112
+ offeringId,
113
+ channel,
114
+ biomes: [...biomes].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
115
+ sourceTreeIds,
116
+ platformServices: [...platformServices].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
117
+ images: {
118
+ sourceHashAlgorithm: imageLock.sourceHashAlgorithm,
119
+ subjects: imageLock.subjects,
120
+ platformServiceBindings: imageLock.platformServiceBindings,
121
+ webBiomes: imageLock.webBiomes,
122
+ },
123
+ chartSet: chartSetLock,
124
+ contractAuthority,
125
+ ceiling: { trustPolicy: leaf?.trustPolicy ?? null, trustCeiling: leaf?.trustCeiling ?? null },
126
+ });
127
+ }
128
+
129
+ /** Write `catalogue.json` into an artifact directory (deterministic serialization); returns its path. */
130
+ export function writeCatalogue(outDir, catalogue) {
131
+ parseCatalogue(catalogue);
132
+ const path = join(outDir, CATALOGUE_FILE);
133
+ writeFileSync(path, serializeJson(catalogue));
134
+ return path;
135
+ }
@@ -0,0 +1,92 @@
1
+ // `xema edition chart-set source-commit` - the commit each chart-set publisher
2
+ // closed its LOCKED set at.
3
+ //
4
+ // The publisher stamps its index chart with `xema.dev/chart-set-source-sha`
5
+ // next to `xema.dev/chart-set-charts`. This verb reads that stamp from the index
6
+ // chart at the version the committed fleet `chart-set.lock.json` pins, and
7
+ // prints one JSON document. It refuses, printing nothing, when the stamp is
8
+ // absent or is not a 40-hex commit, or when the index's roster is not the one
9
+ // the lock records for that publisher: the commit it would name would then be
10
+ // the commit of a different set than the locked one.
11
+
12
+ import { EditionChannel } from './edition-channel.mjs';
13
+ import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
14
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule } from './edition-workspace.mjs';
15
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
16
+ import { serializeJson } from './lib.mjs';
17
+ import {
18
+ CHART_SET_LOCK_FILE,
19
+ INDEX_ROSTER_ANNOTATION,
20
+ chartRegistryClient,
21
+ chartSetLockPath,
22
+ indexAnnotations,
23
+ parseIndexRoster,
24
+ readLock,
25
+ } from './resolve-chart-set.mjs';
26
+
27
+ if (isEntryModule(import.meta.url)) {
28
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
29
+ }
30
+
31
+ export const CHART_SET_SOURCE_SCHEMA = 'xema.edition-chart-set-source/v1';
32
+ export const INDEX_SOURCE_ANNOTATION = 'xema.dev/chart-set-source-sha';
33
+
34
+ /** Why the index's roster is not the lock's roster for the publisher, or null when they are the same set. */
35
+ function rosterDifference(lockCharts, indexRoster) {
36
+ const names = [...new Set([...Object.keys(lockCharts), ...Object.keys(indexRoster)])].sort();
37
+ for (const name of names) {
38
+ const locked = lockCharts[name];
39
+ const indexed = indexRoster[name];
40
+ if (locked === undefined) {return `the index carries ${name}, which the lock does not`;}
41
+ if (indexed === undefined) {return `the lock records ${name}, which the index does not carry`;}
42
+ if (locked.version !== indexed.version) {
43
+ return `${name} is ${indexed.version} in the index and ${locked.version} in the lock`;
44
+ }
45
+ if (indexed.digest !== undefined && indexed.digest !== locked.digest) {
46
+ return `${name}:${locked.version} is ${indexed.digest} in the index and ${locked.digest} in the lock`;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * The source-commit document for every publisher of `lock`, read through
54
+ * `client` (the chart registry client). Throws on the first refusal.
55
+ */
56
+ export async function chartSetSourceCommits({ client, lock }) {
57
+ const publishers = [];
58
+ for (const { index, repo, version, charts } of lock.publishers) {
59
+ const label = `${index}@${version}`;
60
+ const annotations = await indexAnnotations(client, index, version);
61
+ const difference = rosterDifference(charts, parseIndexRoster(annotations[INDEX_ROSTER_ANNOTATION], label));
62
+ if (difference !== null) {
63
+ throw new Error(`${label}: the index roster is not the set ${CHART_SET_LOCK_FILE} locks (${difference}), so its source commit is not the locked set's`);
64
+ }
65
+ const sourceCommit = annotations[INDEX_SOURCE_ANNOTATION];
66
+ if (typeof sourceCommit !== 'string' || !COMMIT_PATTERN.test(sourceCommit)) {
67
+ throw new Error(`${label} carries no ${INDEX_SOURCE_ANNOTATION} that is a 40 lowercase hex commit (got ${JSON.stringify(sourceCommit)})`);
68
+ }
69
+ publishers.push({ index, repo, version, sourceCommit });
70
+ }
71
+ return { schema: CHART_SET_SOURCE_SCHEMA, publishers };
72
+ }
73
+
74
+ async function main(argv) {
75
+ if (argv.length > 0) {
76
+ throw new Error(`unexpected argument "${argv[0]}"; usage: xema edition chart-set source-commit --data-root <dir> --kernel <dir>`);
77
+ }
78
+ const lock = readLock();
79
+ if (lock === null) {
80
+ throw new Error(`${chartSetLockPath()} is missing or is not JSON: run 'xema edition chart-set resolve' first`);
81
+ }
82
+ assertRosteredChartSetLock(lock, CHART_SET_LOCK_FILE, EditionChannel.Release);
83
+ const document = await chartSetSourceCommits({ client: chartRegistryClient(), lock });
84
+ process.stdout.write(serializeJson(document));
85
+ }
86
+
87
+ if (isEntryModule(import.meta.url)) {
88
+ main(process.argv.slice(2)).catch((error) => {
89
+ process.stderr.write(`chart-set source-commit: ${error.message}\n`);
90
+ process.exit(1);
91
+ });
92
+ }