@xemahq/xema 0.27.0 → 0.29.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 (63) hide show
  1. package/dist/commands/edition.d.ts.map +1 -1
  2. package/dist/commands/edition.js +8 -7
  3. package/dist/commands/edition.js.map +1 -1
  4. package/dist/dev/env-defaults.d.ts.map +1 -1
  5. package/dist/dev/env-defaults.js +0 -1
  6. package/dist/dev/env-defaults.js.map +1 -1
  7. package/dist/edition/verify-image-evidence.bundle.mjs +231 -50
  8. package/dist/edition/verify-image-evidence.bundle.provenance.json +55 -43
  9. package/dist/runtime/edition/verb-map.d.ts +2 -1
  10. package/dist/runtime/edition/verb-map.d.ts.map +1 -1
  11. package/dist/runtime/edition/verb-map.js +2 -0
  12. package/dist/runtime/edition/verb-map.js.map +1 -1
  13. package/dist/runtime/runner/chart-set-lock.d.ts +4 -1
  14. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  15. package/dist/runtime/runner/chart-set-lock.js +39 -4
  16. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  17. package/edition/README.md +76 -41
  18. package/edition/build-biome-index.mjs +1 -1
  19. package/edition/build-platform-service-index.mjs +1 -1
  20. package/edition/build-runtime-image-source-index.mjs +64 -20
  21. package/edition/catalogue-fetch.mjs +36 -9
  22. package/edition/catalogue-pin.mjs +71 -0
  23. package/edition/catalogue.mjs +11 -0
  24. package/edition/chart-set-source-commit.mjs +17 -7
  25. package/edition/checkout-indexed-source-repositories.mjs +17 -17
  26. package/edition/customer-flags.mjs +0 -1
  27. package/edition/develop-customer-edition.mjs +23 -5
  28. package/edition/develop-edition.mjs +147 -48
  29. package/edition/develop-freeze.mjs +69 -25
  30. package/edition/develop-seal.mjs +39 -25
  31. package/edition/dist-check.mjs +1 -1
  32. package/edition/edition-artifact.mjs +79 -18
  33. package/edition/edition-biome-manifests.mjs +2 -3
  34. package/edition/edition-chart-set.mjs +59 -6
  35. package/edition/edition-config.mjs +45 -21
  36. package/edition/edition-constants.mjs +10 -4
  37. package/edition/edition-contract-parse.mjs +52 -0
  38. package/edition/edition-contract.json +21 -5
  39. package/edition/edition-workspace.mjs +23 -9
  40. package/edition/evidence-checkout.mjs +22 -1
  41. package/edition/evidence-trust-policy.mjs +142 -78
  42. package/edition/external-source-walk.mjs +48 -61
  43. package/edition/git-source-auth.mjs +104 -25
  44. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  45. package/edition/image-evidence.mjs +21 -3
  46. package/edition/kernel-contract-authority.mjs +23 -1
  47. package/edition/lib.mjs +16 -24
  48. package/edition/oras.mjs +1 -1
  49. package/edition/pin-source.mjs +6 -1
  50. package/edition/produce-image-delivery-entitlement-evidence.mjs +12 -12
  51. package/edition/registry-credentials.mjs +11 -1
  52. package/edition/release-channel-guard.mjs +11 -1
  53. package/edition/release-edition-artifact.mjs +5 -6
  54. package/edition/resolve-chart-set.mjs +50 -30
  55. package/edition/resolve-customer-edition.mjs +63 -7
  56. package/edition/resolve-distribution.mjs +1 -1
  57. package/edition/resolve-edition.mjs +12 -11
  58. package/edition/resolve-images.mjs +36 -15
  59. package/edition/seal-image-digests.mjs +31 -7
  60. package/edition/source-closure.mjs +1 -2
  61. package/edition/trust-policy-arming-verb.mjs +68 -0
  62. package/edition/verify-image-evidence.mjs +40 -12
  63. package/package.json +3 -3
@@ -0,0 +1,71 @@
1
+ // catalogue-pin — `catalogue.pin.json`, the catalogues a customer edition is resolved against.
2
+ //
3
+ // It sits in the SAME directory as the customer's definition file:
4
+ // { "schemaVersion": 1, "develop": "oci://<repository>@sha256:<64 hex>" | null, "release": ... | null }
5
+ // Exactly these keys, both channel keys present; null states that the edition is not produced on
6
+ // that channel. A reference is parsed by `parseCatalogueRef`, and the repository channel of the
7
+ // reference under a key must be that key. This is the only reader of the file: every customer
8
+ // resolve takes the catalogue reference of its channel from here.
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { dirname, join } from 'node:path';
12
+
13
+ import { parseCatalogueRef } from './catalogue-fetch.mjs';
14
+ import { CatalogueError } from './catalogue.mjs';
15
+ import { EditionChannel } from './edition-constants.mjs';
16
+ import { readStrictJsonFile } from './strict-json.mjs';
17
+
18
+ export const CATALOGUE_PIN_FILE = 'catalogue.pin.json';
19
+ export const CATALOGUE_PIN_SCHEMA_VERSION = 1;
20
+
21
+ const SCHEMA_VERSION_KEY = 'schemaVersion';
22
+
23
+ /** The catalogue pin file that sits beside `definitionFile`. */
24
+ export const cataloguePinFile = (definitionFile) => join(dirname(definitionFile), CATALOGUE_PIN_FILE);
25
+
26
+ /** The strict parse of a pin document read from `file`; returns it unchanged. */
27
+ export function parseCataloguePins(document, file) {
28
+ const channels = Object.values(EditionChannel);
29
+ const expected = [SCHEMA_VERSION_KEY, ...channels].sort().join();
30
+ if (document === null || typeof document !== 'object' || Array.isArray(document) || Object.keys(document).sort().join() !== expected) {
31
+ throw new CatalogueError(`${file} must be exactly {${[SCHEMA_VERSION_KEY, ...channels].join(', ')}}, every key present`);
32
+ }
33
+ if (document[SCHEMA_VERSION_KEY] !== CATALOGUE_PIN_SCHEMA_VERSION) {
34
+ throw new CatalogueError(`${file}: ${SCHEMA_VERSION_KEY} must be ${CATALOGUE_PIN_SCHEMA_VERSION}`);
35
+ }
36
+ for (const channel of channels) {
37
+ const ref = document[channel];
38
+ if (ref === null) {continue;}
39
+ if (typeof ref !== 'string') {
40
+ throw new CatalogueError(`${file}: "${channel}" must be an oci://<repository>@sha256:<64 hex> reference or null`);
41
+ }
42
+ let parsed;
43
+ try {
44
+ parsed = parseCatalogueRef(ref);
45
+ } catch (error) {
46
+ throw new CatalogueError(`${file}: "${channel}": ${error.message}`, { cause: error });
47
+ }
48
+ if (parsed.edition.channel !== channel) {
49
+ throw new CatalogueError(
50
+ `${file}: "${channel}" pins ${ref}, whose repository is a ${parsed.edition.channel} repository; a ${channel} catalogue is pinned in a ${channel} repository`,
51
+ );
52
+ }
53
+ }
54
+ return document;
55
+ }
56
+
57
+ /** The catalogue reference pinned for `channel` beside `definitionFile`; a null pin or a missing file is refused. */
58
+ export function readCataloguePin({ definitionFile, channel }) {
59
+ const file = cataloguePinFile(definitionFile);
60
+ if (!existsSync(file)) {
61
+ throw new CatalogueError(`${file} does not exist: a customer edition pins its catalogues in ${CATALOGUE_PIN_FILE}, beside the definition`);
62
+ }
63
+ const ref = parseCataloguePins(readStrictJsonFile(file), file)[channel];
64
+ if (ref === undefined) {
65
+ throw new CatalogueError(`${file}: ${JSON.stringify(channel)} is not a channel (${Object.values(EditionChannel).join(' | ')})`);
66
+ }
67
+ if (ref === null) {
68
+ throw new CatalogueError(`${file}: no ${channel} catalogue is pinned for this edition`);
69
+ }
70
+ return ref;
71
+ }
@@ -133,3 +133,14 @@ export function writeCatalogue(outDir, catalogue) {
133
133
  writeFileSync(path, serializeJson(catalogue));
134
134
  return path;
135
135
  }
136
+
137
+ const KERNEL_COMMIT = /^[0-9a-f]{40}$/;
138
+
139
+ /** The commit the catalogue's contract authority was taken at (40 hex); read without the kernel schemas, which are not checked out yet where this is used. */
140
+ export function contractAuthoritySourceCommit(catalogue) {
141
+ const commit = catalogue.contractAuthority.sourceCommit;
142
+ if (typeof commit !== 'string' || !KERNEL_COMMIT.test(commit)) {
143
+ throw new CatalogueError(`${CATALOGUE_FILE}: contractAuthority.sourceCommit is not a 40-hex commit: ${JSON.stringify(commit)}`);
144
+ }
145
+ return commit;
146
+ }
@@ -3,10 +3,11 @@
3
3
  //
4
4
  // The publisher stamps its index chart with `xema.dev/chart-set-source-sha`
5
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
6
+ // chart at the digest the committed fleet `chart-set.lock.json` pins (a develop
7
+ // lock has none and is refused), and prints one JSON document. It refuses,
8
+ // printing nothing, when the stamp is absent or is not a 40-hex commit, when the
9
+ // registry does not serve the locked digest, or when the index's roster is not
10
+ // the one the lock records for that publisher: the commit it would name would then be
10
11
  // the commit of a different set than the locked one.
11
12
 
12
13
  import { EditionChannel } from './edition-channel.mjs';
@@ -19,9 +20,10 @@ import {
19
20
  INDEX_ROSTER_ANNOTATION,
20
21
  chartRegistryClient,
21
22
  chartSetLockPath,
22
- indexAnnotations,
23
+ manifestAnnotations,
23
24
  parseIndexRoster,
24
25
  readLock,
26
+ registryRepository,
25
27
  } from './resolve-chart-set.mjs';
26
28
 
27
29
  if (isEntryModule(import.meta.url)) {
@@ -55,9 +57,17 @@ function rosterDifference(lockCharts, indexRoster) {
55
57
  */
56
58
  export async function chartSetSourceCommits({ client, lock }) {
57
59
  const publishers = [];
58
- for (const { index, repo, version, charts } of lock.publishers) {
60
+ for (const { index, repo, version, indexDigest, charts } of lock.publishers) {
59
61
  const label = `${index}@${version}`;
60
- const annotations = await indexAnnotations(client, index, version);
62
+ if (indexDigest === null) {
63
+ throw new Error(`${label}: a develop chart set names its deploy commit in the snapshot pins, not in an index chart (indexDigest is null)`);
64
+ }
65
+ // The index is read BY the digest the lock pins, so a re-pushed tag cannot answer.
66
+ const served = await client.getManifestByDigest(registryRepository(index), indexDigest);
67
+ if (served.digest !== indexDigest) {
68
+ throw new Error(`${label}: the registry served index manifest ${served.digest} but ${CHART_SET_LOCK_FILE} pins ${indexDigest}`);
69
+ }
70
+ const annotations = await manifestAnnotations(client, index, version, served);
61
71
  const difference = rosterDifference(charts, parseIndexRoster(annotations[INDEX_ROSTER_ANNOTATION], label));
62
72
  if (difference !== null) {
63
73
  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`);
@@ -6,7 +6,7 @@ import { dirname, resolve } from 'node:path';
6
6
 
7
7
  import { editionWorkspaceConfig } from './edition-config.mjs';
8
8
  import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
9
- import { gitAuthEnv } from './git-source-auth.mjs';
9
+ import { holdSource, withSourceGitEnv } from './git-source-auth.mjs';
10
10
  import { applyPinSource, snapshotPinSource } from './pin-source.mjs';
11
11
  import { readStrictJsonFile } from './strict-json.mjs';
12
12
 
@@ -45,29 +45,30 @@ function git(args, env) {
45
45
  execFileSync('git', args, { env, stdio: 'inherit' });
46
46
  }
47
47
 
48
- export function checkoutIndexedSources({ workspaceRoot, token, index }) {
48
+ export function checkoutIndexedSources({ workspaceRoot, index, env = process.env }) {
49
49
  if (!workspaceRoot) {throw new Error('workspaceRoot is required');}
50
- if (!token) {throw new Error('EVIDENCE_REPOSITORIES_TOKEN is required');}
51
50
  const workspace = editionWorkspaceConfig();
52
- const env = gitAuthEnv(workspace.sourceRepositoryUrlPrefix, workspace.sourceAuthUsername, token);
53
51
  const plan = sourceCheckoutPlan(index, workspace);
52
+ for (const entry of plan) {holdSource({ name: entry.repo, url: entry.repositoryUrl }, workspace, env);}
54
53
  for (const entry of plan) {
55
54
  const target = resolve(workspaceRoot, entry.repo);
56
55
  if (existsSync(target)) {
57
56
  throw new Error(`${entry.repo} target already exists; refusing to reuse an unverified checkout`);
58
57
  }
59
- mkdirSync(dirname(target), { recursive: true });
60
- git(['init', '--quiet', target], env);
61
- git(['-C', target, 'remote', 'add', 'origin', entry.repositoryUrl], env);
62
- git(['-C', target, 'fetch', '--quiet', '--depth=1', 'origin', entry.sourceCommit], env);
63
- git(['-C', target, 'checkout', '--quiet', '--detach', entry.sourceCommit], env);
64
- const actual = execFileSync('git', ['-C', target, 'rev-parse', 'HEAD'], {
65
- env,
66
- encoding: 'utf8',
67
- }).trim();
68
- if (actual !== entry.sourceCommit) {
69
- throw new Error(`${entry.repo} resolved ${actual}, expected ${entry.sourceCommit}`);
70
- }
58
+ withSourceGitEnv({ name: entry.repo, url: entry.repositoryUrl }, { workspace, env }, (gitEnv) => {
59
+ mkdirSync(dirname(target), { recursive: true });
60
+ git(['init', '--quiet', target], gitEnv);
61
+ git(['-C', target, 'remote', 'add', 'origin', entry.repositoryUrl], gitEnv);
62
+ git(['-C', target, 'fetch', '--quiet', '--depth=1', 'origin', entry.sourceCommit], gitEnv);
63
+ git(['-C', target, 'checkout', '--quiet', '--detach', entry.sourceCommit], gitEnv);
64
+ const actual = execFileSync('git', ['-C', target, 'rev-parse', 'HEAD'], {
65
+ env: gitEnv,
66
+ encoding: 'utf8',
67
+ }).trim();
68
+ if (actual !== entry.sourceCommit) {
69
+ throw new Error(`${entry.repo} resolved ${actual}, expected ${entry.sourceCommit}`);
70
+ }
71
+ });
71
72
  console.log(`[source-checkout] ${entry.repo} @ ${entry.sourceCommit}`);
72
73
  }
73
74
  return plan.length;
@@ -94,7 +95,6 @@ if (isEntryModule(import.meta.url)) {
94
95
  const index = indexForCheckout(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8')), process.argv.slice(2));
95
96
  const count = checkoutIndexedSources({
96
97
  workspaceRoot: sourcesRoot(),
97
- token: process.env.EVIDENCE_REPOSITORIES_TOKEN,
98
98
  index,
99
99
  });
100
100
  console.log(`[source-checkout] materialized ${count} indexed repositories at exact commits`);
@@ -1,6 +1,5 @@
1
1
  // The flags that make a resolve a CUSTOMER resolve (against a catalogue).
2
2
  export const DEFINITION_FLAG = '--definition';
3
- export const CATALOGUE_FLAG = '--catalogue';
4
3
  export const CHANNEL_FLAG = '--channel';
5
4
  export const SOURCE_IMAGE_FLAG = '--source-image';
6
5
  export const CATALOGUE_USERNAME_FLAG = '--catalogue-username';
@@ -10,12 +10,16 @@
10
10
  // sealed against the develop image repositories.
11
11
 
12
12
  import { sealLock } from './biome-content.mjs';
13
- import { sealDevelopImages } from './develop-seal.mjs';
13
+ import { parseCatalogueRef } from './catalogue-fetch.mjs';
14
+ import { readCataloguePin } from './catalogue-pin.mjs';
15
+ import { sealDevelopImageSubjects } from './develop-seal.mjs';
16
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
14
17
  import { CATALOGUE_PIN_KEY, EditionChannel } from './edition-constants.mjs';
15
18
  import { loadKernelDistributionSchemas } from './lib.mjs';
16
- import { snapshotPinNamespace, SnapshotPinNamespace, sortedSnapshotPins, sourcePinKey } from './pin-source.mjs';
19
+ import { isSourcePinKey, sortedSnapshotPins, sourcePinKey } from './pin-source.mjs';
17
20
  import { resolveCustomerEdition } from './resolve-customer-edition.mjs';
18
21
  import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
22
+ import { readStrictJsonFile } from './strict-json.mjs';
19
23
 
20
24
  const SHA_PATTERN = /^[0-9a-f]{40}$/;
21
25
 
@@ -39,11 +43,11 @@ export function customerDevelopPins({ pins, declaredSourceNames, catalogueDigest
39
43
  for (const [key, value] of Object.entries(pins)) {
40
44
  if (key === CATALOGUE_PIN_KEY) {
41
45
  if (value !== catalogueDigest) {
42
- throw new CustomerDevelopPinsError(`--pins "${CATALOGUE_PIN_KEY}" is ${JSON.stringify(value)}, but --catalogue is ${catalogueDigest}`);
46
+ throw new CustomerDevelopPinsError(`--pins "${CATALOGUE_PIN_KEY}" is ${JSON.stringify(value)}, but the pinned develop catalogue is ${catalogueDigest}`);
43
47
  }
44
48
  continue;
45
49
  }
46
- if (snapshotPinNamespace(key) !== SnapshotPinNamespace.Source) {
50
+ if (!isSourcePinKey(key)) {
47
51
  throw new CustomerDevelopPinsError(`--pins "${key}" is not one of the customer's own repositories: a customer develop pins only sources/<name> (the vendor's pins are the catalogue's)`);
48
52
  }
49
53
  if (typeof value !== 'string' || !SHA_PATTERN.test(value)) {
@@ -79,7 +83,7 @@ export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue,
79
83
  catalogueDigest,
80
84
  ...(verifyAuthority === undefined ? {} : { verifyAuthority }),
81
85
  ...(materialize === undefined ? {} : { materialize }),
82
- sealOwn: (imageLock) => sealDevelopImages({ imageLock, env, ...sealOptions }),
86
+ sealOwn: ({ distributionId, subjects }) => sealDevelopImageSubjects({ distributionId, subjects, env, ...sealOptions }),
83
87
  });
84
88
  // The customer edition is sealed here: every biomeContentHash is recomputed through the
85
89
  // same code as the vendor's, vendor biomes on the CATALOGUE's authority (biome-content.mjs).
@@ -97,3 +101,17 @@ export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue,
97
101
  });
98
102
  return { ...resolved, distributionLock, pins: frozen };
99
103
  }
104
+
105
+ /**
106
+ * What a develop verb of a customer edition derives from its definition file and its `--pins` file with no
107
+ * network: the id (the definition's), the pinned develop catalogue reference and digest, and the frozen
108
+ * provenance pins (the customer's `sources/<name>` commits plus that digest).
109
+ */
110
+ export function customerDevelopFrame({ definitionFile, pinsFile }) {
111
+ const rawDefinition = readStrictJsonFile(definitionFile);
112
+ assertCanonicalDistributionId(rawDefinition?.id);
113
+ const catalogueRef = readCataloguePin({ definitionFile, channel: EditionChannel.Develop });
114
+ const { digest } = parseCatalogueRef(catalogueRef);
115
+ const pins = customerDevelopPins({ pins: readStrictJsonFile(pinsFile), declaredSourceNames: declaredSourceNamesOf(rawDefinition), catalogueDigest: digest });
116
+ return { id: rawDefinition.id, rawDefinition, catalogueRef, catalogueDigest: digest, pins };
117
+ }
@@ -1,23 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  // develop-edition — the develop edition's command line.
3
3
  //
4
- // resolve <id> --pins <file> --out <dir> --registry <namespace>
4
+ // resolve <id> --pins <file> --out <dir> --registry <root>
5
5
  // --engine-image <repository>@sha256:<digest> [--local]
6
6
  // resolve the edition from the snapshot, seal it against the develop image
7
7
  // repositories, write the descriptor and the four locks to <dir>, print the target.
8
8
  // The workflow run is recorded from GITHUB_REPOSITORY / GITHUB_RUN_ID;
9
9
  // --local stamps `local` instead, for a resolve outside GitHub Actions.
10
- // resolve --definition <file> --catalogue oci://<repo>@sha256:<digest> --pins <file> --out <dir> --registry <namespace>
10
+ // resolve --definition <file> --pins <file> --out <dir> --registry <root>
11
11
  // --engine-image <repository>@sha256:<digest> [--source-image <source>=<registry>/<prefix>]...
12
12
  // [--catalogue-username <name> --catalogue-token-env <VAR>] [--local]
13
- // a CUSTOMER develop edition: the definition resolved against the catalogue (see resolve-customer-edition.mjs),
14
- // frozen at the customer's own pins. --pins holds exactly one `sources/<name>` commit per source the definition
15
- // declares; the catalogue digest is frozen into the descriptor's provenance pins as `catalogue`. The id is the
16
- // definition's. Takes --data-root, --editions-root, --sources-root, --kernel, --cache-dir (no --deploy-root).
17
- // target <id> --pins <file> --registry <namespace>
18
- // print <namespace>/xema-edition-<id>:<snapshot hash>. The namespace is always the caller's
19
- // (e.g. registry.example.com/acme); the repository prefix comes from
20
- // edition-contract.json, and no namespace is implied.
13
+ // a CUSTOMER develop edition: the definition resolved against the develop catalogue pinned in
14
+ // catalogue.pin.json beside it (see catalogue-pin.mjs and resolve-customer-edition.mjs), frozen at the
15
+ // customer's own pins. --pins holds exactly one `sources/<name>` commit per source the definition
16
+ // declares (`freeze-sources --definition` writes it); the catalogue digest is frozen into the descriptor's
17
+ // provenance pins as `catalogue`. The id is the definition's. Takes --data-root, --editions-root,
18
+ // --sources-root, --kernel, --cache-dir (no --deploy-root).
19
+ // target <id> --pins <file> --registry <root>
20
+ // print <root>/<develop segment>/xema-edition-<id>:<snapshot hash>. The root is always the caller's
21
+ // registry root (e.g. registry.example.com/acme); the develop namespace segment and the repository
22
+ // prefix come from edition-contract.json, and no root is implied.
23
+ // target --definition <file> --pins <file> --registry <root>
24
+ // the same for a customer edition (id = the definition's; the tag covers the pinned catalogue digest).
21
25
  // freeze-graph --data-sha <sha> --kernel-sha <sha>
22
26
  // --deploy-sha <sha> [--editions-root-url <https url>]
23
27
  // [--overrides <json>] --out <file>
@@ -26,12 +30,21 @@
26
30
  // repositories are pinned at what the workflow checked out; write the pins.
27
31
  // check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
28
32
  // refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
33
+ // check-existing --definition <file> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
34
+ // the same for a customer artifact; its descriptor's catalogue provenance pin must also equal the
35
+ // pinned develop catalogue digest.
29
36
  // check-overrides --overrides <json>
30
37
  // refuse a pins override that would select the tooling this run executes
31
38
  // (the edition tooling's own repository); the workflow runs it before any other checkout.
32
39
  // freeze-sources <id> --pins <file> [--overrides <json>] --out <file>
33
40
  // add one pin per source the edition's chain declares (run after the editions
34
41
  // root is checked out and passed as --editions-root); write the final pins.
42
+ // freeze-sources --definition <file> --out <file>
43
+ // a CUSTOMER edition: one `sources/<name>` pin per source the definition declares, each the head of
44
+ // refs/heads/develop of that source's repository, resolved once; written exclusively (wx).
45
+ // kernel-commit --definition <file> [--catalogue-username <name> --catalogue-token-env <VAR>]
46
+ // fetch the pinned develop catalogue and print its contractAuthority.sourceCommit (one 40-hex line):
47
+ // the commit the kernel contracts are checked out at. Takes --data-root only (no --kernel).
35
48
  //
36
49
  // Tokens come from the environment (EVIDENCE_REPOSITORIES_TOKEN, XEMA_GHCR_PULL_TOKEN),
37
50
  // never argv. Nothing here writes git or publishes a release edition.
@@ -40,11 +53,12 @@ import { writeFileSync } from 'node:fs';
40
53
  import { resolve } from 'node:path';
41
54
 
42
55
  import { fetchCatalogue } from './catalogue-fetch.mjs';
43
- import { writeCatalogue } from './catalogue.mjs';
44
- import { CATALOGUE_FLAG, CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG, SOURCE_IMAGE_FLAG } from './customer-flags.mjs';
45
- import { resolveCustomerDevelopEdition } from './develop-customer-edition.mjs';
56
+ import { readCataloguePin } from './catalogue-pin.mjs';
57
+ import { contractAuthoritySourceCommit, writeCatalogue } from './catalogue.mjs';
58
+ import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG, SOURCE_IMAGE_FLAG } from './customer-flags.mjs';
59
+ import { customerDevelopFrame, resolveCustomerDevelopEdition } from './develop-customer-edition.mjs';
46
60
  import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
47
- import { assertNoToolingOverride, freezeGraph, freezeSources, lsRemoteDevelopHead } from './develop-freeze.mjs';
61
+ import { assertNoToolingOverride, freezeCustomerSources, freezeGraph, freezeSources, lsRemoteDevelopHead } from './develop-freeze.mjs';
48
62
  import { assertCanonicalDistributionId } from './distribution-paths.mjs';
49
63
  import { LOCAL_WORKFLOW, assertEngineImage, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
50
64
  import { editionWorkspaceConfig } from './edition-config.mjs';
@@ -57,38 +71,73 @@ import { parseSourceImages } from './resolve-customer-edition.mjs';
57
71
  import { composeEdition } from './resolve-distribution.mjs';
58
72
  import { readStrictJsonFile } from './strict-json.mjs';
59
73
 
74
+ // The roots a verb takes. A customer form (it names --definition) takes fewer: `target` none, `check-existing`
75
+ // the data root and the kernel, `freeze-sources` and `kernel-commit` the data root only (`kernel-commit` runs
76
+ // before the kernel is checked out), `resolve` all but the deploy root.
77
+ const VENDOR_ROOTS = Object.freeze([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache]);
78
+ const CUSTOMER_ROOTS = Object.freeze({
79
+ resolve: [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache],
80
+ target: [],
81
+ 'check-existing': [WorkspaceRoot.Data, WorkspaceRoot.Kernel],
82
+ 'freeze-sources': [WorkspaceRoot.Data],
83
+ 'kernel-commit': [WorkspaceRoot.Data],
84
+ });
85
+
60
86
  if (isEntryModule(import.meta.url)) {
61
- configureEditionWorkspaceFromProcess(
62
- process.argv.includes(DEFINITION_FLAG)
63
- ? [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache, WorkspaceRoot.SourceHash]
64
- : [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache, WorkspaceRoot.SourceHash],
65
- );
87
+ const [command] = process.argv.slice(2);
88
+ const customerForm = process.argv.includes(DEFINITION_FLAG) || command === 'kernel-commit';
89
+ configureEditionWorkspaceFromProcess(customerForm ? (CUSTOMER_ROOTS[command] ?? VENDOR_ROOTS) : VENDOR_ROOTS, { refuseOthers: customerForm && CUSTOMER_ROOTS[command] !== undefined });
66
90
  }
67
91
 
68
- // The flags only a customer develop resolve takes, and the one of them that repeats.
69
- const CUSTOMER_VALUES = Object.freeze([DEFINITION_FLAG, CATALOGUE_FLAG, CATALOGUE_USERNAME_FLAG, CATALOGUE_TOKEN_ENV_FLAG, SOURCE_IMAGE_FLAG]);
70
- const CUSTOMER_REQUIRED = Object.freeze([DEFINITION_FLAG, CATALOGUE_FLAG]);
92
+ // Per verb: the vendor form (`positional` id, `values`, `switches`) and, where the verb has one, the customer
93
+ // form: the flags only it takes, the further flags it requires, and the vendor flags it refuses.
94
+ const CREDENTIAL_FLAGS = Object.freeze([CATALOGUE_USERNAME_FLAG, CATALOGUE_TOKEN_ENV_FLAG]);
71
95
  const REPEATABLE = Object.freeze([SOURCE_IMAGE_FLAG]);
72
96
 
73
97
  const COMMANDS = Object.freeze({
74
- resolve: { positional: true, values: ['--pins', '--out', '--registry', '--engine-image', ...CUSTOMER_VALUES], switches: ['--local'] },
75
- target: { positional: true, values: ['--pins', '--registry'], switches: [] },
98
+ resolve: {
99
+ positional: true,
100
+ values: ['--pins', '--out', '--registry', '--engine-image'],
101
+ switches: ['--local'],
102
+ required: ['--pins', '--out', '--registry', '--engine-image'],
103
+ customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [] },
104
+ },
105
+ target: {
106
+ positional: true,
107
+ values: ['--pins', '--registry'],
108
+ switches: [],
109
+ required: ['--pins', '--registry'],
110
+ customer: { values: [DEFINITION_FLAG], required: ['--pins', '--registry'], refused: [] },
111
+ },
76
112
  'freeze-graph': {
77
113
  positional: false,
78
114
  values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--out'],
79
115
  switches: [],
116
+ required: ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
117
+ },
118
+ 'freeze-sources': {
119
+ positional: true,
120
+ values: ['--pins', '--overrides', '--out'],
121
+ switches: [],
122
+ required: ['--pins', '--out'],
123
+ customer: { values: [DEFINITION_FLAG], required: ['--out'], refused: ['--pins', '--overrides'] },
124
+ },
125
+ 'check-overrides': { positional: false, values: ['--overrides'], switches: [], required: ['--overrides'] },
126
+ 'check-existing': {
127
+ positional: true,
128
+ values: ['--pins', '--artifact', '--engine-image'],
129
+ switches: [],
130
+ required: ['--pins', '--artifact', '--engine-image'],
131
+ customer: { values: [DEFINITION_FLAG], required: ['--pins', '--artifact', '--engine-image'], refused: [] },
132
+ },
133
+ 'kernel-commit': {
134
+ positional: false,
135
+ values: [],
136
+ switches: [],
137
+ required: [],
138
+ customerOnly: true,
139
+ customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS], required: [], refused: [] },
80
140
  },
81
- 'freeze-sources': { positional: true, values: ['--pins', '--overrides', '--out'], switches: [] },
82
- 'check-overrides': { positional: false, values: ['--overrides'], switches: [] },
83
- 'check-existing': { positional: true, values: ['--pins', '--artifact', '--engine-image'], switches: [] },
84
- });
85
- const REQUIRED = Object.freeze({
86
- resolve: ['--pins', '--out', '--registry', '--engine-image'],
87
- target: ['--pins', '--registry'],
88
- 'freeze-graph': ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
89
- 'freeze-sources': ['--pins', '--out'],
90
- 'check-overrides': ['--overrides'],
91
- 'check-existing': ['--pins', '--artifact', '--engine-image'],
92
141
  });
93
142
 
94
143
  /** Parse `<command> [<id>] --flag value ...` against the closed vocabulary of the command. */
@@ -98,13 +147,14 @@ export function parseDevelopEditionArgs(argv) {
98
147
  if (spec === undefined) {
99
148
  throw new Error(`unknown command ${JSON.stringify(command)}; expected ${Object.keys(COMMANDS).join(' | ')}`);
100
149
  }
150
+ const customerValues = spec.customer?.values ?? [];
101
151
  const options = {};
102
152
  const positional = [];
103
153
  for (let i = 0; i < rest.length; i += 1) {
104
154
  const arg = rest[i];
105
155
  if (spec.switches.includes(arg)) {
106
156
  options[arg] = true;
107
- } else if (spec.values.includes(arg)) {
157
+ } else if (spec.values.includes(arg) || customerValues.includes(arg)) {
108
158
  const value = rest[i + 1];
109
159
  if (value === undefined || (value.startsWith('--') && value.length > 2)) {
110
160
  throw new Error(`${arg} requires a value`);
@@ -123,21 +173,22 @@ export function parseDevelopEditionArgs(argv) {
123
173
  positional.push(arg);
124
174
  }
125
175
  }
126
- const customer = command === 'resolve' && CUSTOMER_VALUES.some((flag) => options[flag] !== undefined);
176
+ const customer = spec.customer !== undefined && (spec.customerOnly === true || customerValues.some((flag) => options[flag] !== undefined));
127
177
  if (customer) {
128
- for (const required of CUSTOMER_REQUIRED) {
129
- if (options[required] === undefined) {throw new Error(`a customer develop resolve requires ${required}`);}
178
+ if (options[DEFINITION_FLAG] === undefined) {throw new Error(`a customer ${command} requires ${DEFINITION_FLAG}`);}
179
+ for (const refused of spec.customer.refused) {
180
+ if (options[refused] !== undefined) {throw new Error(`a customer ${command} takes no ${refused}`);}
130
181
  }
131
- if (positional.length !== 0) {throw new Error('a customer develop resolve takes no <distribution-id>: the id is the definition\'s');}
182
+ if (positional.length !== 0) {throw new Error(`a customer ${command} takes no <distribution-id>: the id is the definition's`);}
132
183
  } else if (spec.positional ? positional.length !== 1 : positional.length !== 0) {
133
184
  throw new Error(`${command} takes ${spec.positional ? 'exactly one <distribution-id>' : 'no positional argument'}`);
134
185
  }
135
- for (const required of REQUIRED[command]) {
186
+ for (const required of customer ? spec.customer.required : spec.required) {
136
187
  if (options[required] === undefined) {throw new Error(`${command} requires ${required}`);}
137
188
  }
138
189
  if (spec.positional && !customer) {assertCanonicalDistributionId(positional[0]);}
139
190
  if (options['--engine-image'] !== undefined) {assertEngineImage(options['--engine-image']);}
140
- return { command, id: positional[0], options };
191
+ return { command, id: positional[0], options, customer };
141
192
  }
142
193
 
143
194
  function parseOverrides(text) {
@@ -154,15 +205,15 @@ function writeOnce(path, value) {
154
205
  }
155
206
 
156
207
  function headResolver(env) {
157
- const token = env.EVIDENCE_REPOSITORIES_TOKEN;
158
- const username = editionWorkspaceConfig().sourceAuthUsername;
159
- return (url) => lsRemoteDevelopHead(url, { username, token });
208
+ return (source) => lsRemoteDevelopHead(source, { workspace: editionWorkspaceConfig(), env });
160
209
  }
161
210
 
162
211
  /** A customer develop resolve: the artifact (descriptor and four locks, no catalogue: it is not an offering) goes to --out. */
163
212
  async function resolveCustomer(options, env) {
213
+ const ref = readCataloguePin({ definitionFile: options[DEFINITION_FLAG], channel: EditionChannel.Develop });
164
214
  const { catalogue, digest } = await fetchCatalogue({
165
- ref: options[CATALOGUE_FLAG],
215
+ ref,
216
+ channel: EditionChannel.Develop,
166
217
  username: options[CATALOGUE_USERNAME_FLAG],
167
218
  tokenEnv: options[CATALOGUE_TOKEN_ENV_FLAG],
168
219
  env,
@@ -194,15 +245,37 @@ async function resolveCustomer(options, env) {
194
245
  console.log(target);
195
246
  }
196
247
 
248
+ /**
249
+ * The commit the pinned develop catalogue's contract authority was taken at. The kernel contracts are
250
+ * not checked out yet when this runs, so nothing here validates against kernel schemas: the catalogue
251
+ * parser is kernel-free, and the one field read is `contractAuthority.sourceCommit`, held to 40 hex.
252
+ */
253
+ async function customerKernelCommit(options, env) {
254
+ const ref = readCataloguePin({ definitionFile: options[DEFINITION_FLAG], channel: EditionChannel.Develop });
255
+ const { catalogue } = await fetchCatalogue({
256
+ ref,
257
+ channel: EditionChannel.Develop,
258
+ username: options[CATALOGUE_USERNAME_FLAG],
259
+ tokenEnv: options[CATALOGUE_TOKEN_ENV_FLAG],
260
+ env,
261
+ });
262
+ console.log(contractAuthoritySourceCommit(catalogue));
263
+ }
264
+
197
265
  async function run(argv, env) {
198
- const { command, id, options } = parseDevelopEditionArgs(argv);
266
+ const { command, id, options, customer } = parseDevelopEditionArgs(argv);
199
267
  switch (command) {
200
268
  case 'target': {
269
+ if (customer) {
270
+ const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
271
+ console.log(developEditionTarget(frame.id, frame.pins, { registry: options['--registry'] }));
272
+ return;
273
+ }
201
274
  console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'] }));
202
275
  return;
203
276
  }
204
277
  case 'resolve': {
205
- if (options[DEFINITION_FLAG] !== undefined) {return resolveCustomer(options, env);}
278
+ if (customer) {return resolveCustomer(options, env);}
206
279
  const pins = readStrictJsonFile(options['--pins']);
207
280
  const workflow = options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env);
208
281
  const target = developEditionTarget(id, pins, { registry: options['--registry'] });
@@ -224,6 +297,19 @@ async function run(argv, env) {
224
297
  return;
225
298
  }
226
299
  case 'check-existing': {
300
+ if (customer) {
301
+ const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
302
+ assertExistingArtifactMatches({
303
+ artifactDir: resolve(options['--artifact']),
304
+ distributionId: frame.id,
305
+ channel: EditionChannel.Develop,
306
+ evidenceTrustPolicy: await descriptorEvidenceTrust({ channel: EditionChannel.Develop }),
307
+ pins: frame.pins,
308
+ engineImage: options['--engine-image'],
309
+ catalogueDigest: frame.catalogueDigest,
310
+ });
311
+ return;
312
+ }
227
313
  assertExistingArtifactMatches({
228
314
  artifactDir: resolve(options['--artifact']),
229
315
  distributionId: id,
@@ -252,6 +338,17 @@ async function run(argv, env) {
252
338
  return;
253
339
  }
254
340
  case 'freeze-sources': {
341
+ if (customer) {
342
+ writeOnce(
343
+ options['--out'],
344
+ freezeCustomerSources({
345
+ definition: readStrictJsonFile(options[DEFINITION_FLAG]),
346
+ resolveHead: headResolver(env),
347
+ sourceRepositoryUrlPrefix: editionWorkspaceConfig().sourceRepositoryUrlPrefix,
348
+ }),
349
+ );
350
+ return;
351
+ }
255
352
  const pins = readStrictJsonFile(options['--pins']);
256
353
  const { parseDistribution } = await loadKernelDistributionSchemas();
257
354
  const declaredSources = composeEdition(parseDistribution, id, snapshotPinSource(pins)).declaredSources;
@@ -261,6 +358,8 @@ async function run(argv, env) {
261
358
  );
262
359
  return;
263
360
  }
361
+ case 'kernel-commit':
362
+ return customerKernelCommit(options, env);
264
363
  default:
265
364
  throw new Error(`unhandled command ${command}`);
266
365
  }