@xemahq/xema 0.29.1 → 0.30.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 (39) hide show
  1. package/README.md +60 -389
  2. package/dist/commands/edition.js +3 -3
  3. package/dist/commands/edition.js.map +1 -1
  4. package/dist/edition/verify-image-evidence.bundle.mjs +8715 -807
  5. package/dist/edition/verify-image-evidence.bundle.provenance.json +873 -405
  6. package/edition/README.md +53 -273
  7. package/edition/adapter-bundled-versions.mjs +107 -0
  8. package/edition/adapter-host-versions.mjs +41 -0
  9. package/edition/authorization-base.mjs +18 -9
  10. package/edition/build-runtime-image-source-index.mjs +77 -4
  11. package/edition/catalogue.mjs +15 -12
  12. package/edition/customer-definition.mjs +3 -20
  13. package/edition/develop-customer-edition.mjs +2 -2
  14. package/edition/develop-edition-resolve.mjs +10 -2
  15. package/edition/develop-edition.mjs +56 -62
  16. package/edition/develop-freeze.mjs +14 -3
  17. package/edition/dist-check.mjs +1 -0
  18. package/edition/distribution-evidence-set.mjs +32 -5
  19. package/edition/edition-channel.mjs +15 -1
  20. package/edition/edition-locks.mjs +16 -6
  21. package/edition/edition-protocol.mjs +30 -8
  22. package/edition/edition-workspace.mjs +8 -0
  23. package/edition/image-evidence.mjs +133 -25
  24. package/edition/kernel-package.mjs +193 -0
  25. package/edition/lib.mjs +73 -1
  26. package/edition/oci-evidence-verbs.mjs +2 -2
  27. package/edition/plan-image-evidence-refresh.mjs +2 -2
  28. package/edition/produce-distribution-completeness-evidence.mjs +5 -0
  29. package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
  30. package/edition/release-catalogue.mjs +10 -3
  31. package/edition/resolve-chart-set.mjs +1 -1
  32. package/edition/resolve-customer-edition.mjs +29 -33
  33. package/edition/resolve-distribution.mjs +5 -1
  34. package/edition/resolve-edition.mjs +3 -2
  35. package/edition/seal-image-digests.mjs +16 -4
  36. package/edition/trust-policy-arming-verb.mjs +2 -10
  37. package/edition/verify-image-evidence.mjs +1 -0
  38. package/examples/README.md +3 -2
  39. package/package.json +3 -3
@@ -63,6 +63,7 @@
63
63
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
64
64
  import { join } from 'node:path';
65
65
 
66
+ import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest } from './adapter-bundled-versions.mjs';
66
67
  import {
67
68
  adoptedFromLock,
68
69
  editionAuthorizationModuleInputs,
@@ -2212,8 +2213,9 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs } =
2212
2213
  * this exact writer so an in-process refresh cannot drift from the standalone
2213
2214
  * command's serialization or diagnostics.
2214
2215
  */
2215
- export async function writeResolvedDistribution(id, { log = true, materialize } = {}) {
2216
+ export async function writeResolvedDistribution(id, { log = true, materialize, fetchManifest = fetchRegistryManifest } = {}) {
2216
2217
  const { lock, index } = await resolveDistributionWithIndex(id, { materialize });
2218
+ await assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(index), fetchManifest });
2217
2219
  const outPath = join(editionsRoot(), id, 'distribution.lock.json');
2218
2220
  assertReleaseContent(lock, `the distribution lock for "${id}"`);
2219
2221
  assertReleaseEditionTree(join(editionsRoot(), id));
@@ -2285,12 +2287,14 @@ export async function composeAuthorizationBase(id, lock, index) {
2285
2287
  const kernel = await loadKernelAuthorizationComposer({ kernelPackageDir: kernelPackageDir() });
2286
2288
  // Refuses an edition that carries a module under a pin with no composer.
2287
2289
  const inputs = editionAuthorizationModuleInputs(lock.biomes, new Map(index.map((e) => [e.id, e])), kernel);
2290
+ const { biomeOriginsOfLock } = await loadKernelDistributionSchemas();
2288
2291
  let sealed = null;
2289
2292
  if (kernel !== null) {
2290
2293
  sealed = sealEditionAuthorizationBase({
2291
2294
  distributionId: id,
2292
2295
  inputs,
2293
2296
  kernel,
2297
+ biomeOrigins: biomeOriginsOfLock(lock),
2294
2298
  adopted: adoptedFromLock(readAuthorizationLock(editionDir)),
2295
2299
  runTests: fgaComposedModelTestRunner(resolveFgaCli()),
2296
2300
  });
@@ -50,14 +50,15 @@ import { withoutSealedDigests } from './seal-image-digests.mjs';
50
50
  import { readStrictJsonFile } from './strict-json.mjs';
51
51
 
52
52
  // A customer resolve (`--definition`) has no vendor editions root: `--out` IS its
53
- // editions root, the directory its `<id>/` locks are written under.
53
+ // editions root, the directory its `<id>/` locks are written under. It takes no kernel root:
54
+ // the kernel is the catalogue's, downloaded from npm into the cache root.
54
55
  if (isEntryModule(import.meta.url)) {
55
56
  if (process.argv.includes(DEFINITION_FLAG)) {
56
57
  const at = process.argv.indexOf('--out');
57
58
  if (at < 0 || process.argv[at + 1] === undefined) {throw new Error(`--out <dir> is required with ${DEFINITION_FLAG}`);}
58
59
  mkdirSync(process.argv[at + 1], { recursive: true });
59
60
  process.argv.splice(at, 1, '--editions-root');
60
- configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache], { refuseOthers: true });
61
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Cache], { refuseOthers: true });
61
62
  } else {
62
63
  configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache]);
63
64
  }
@@ -112,18 +112,20 @@
112
112
  import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
113
113
  import { join } from 'node:path';
114
114
 
115
+ import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest, lockMountsAdapters } from './adapter-bundled-versions.mjs';
115
116
  import { sealLock, verifyBiomeContentHashes } from './biome-content.mjs';
116
117
  import { fetchCatalogue } from './catalogue-fetch.mjs';
117
118
  import { readCataloguePin } from './catalogue-pin.mjs';
118
119
  import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG } from './customer-flags.mjs';
119
120
  import { EditionChannel } from './edition-constants.mjs';
120
- import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
121
+ import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
121
122
  import {
122
123
  assertCoordinatesFinal,
123
124
  collectFleetPlatformRequirements,
124
125
  isNotFinalYet,
125
126
  readFleetImageLocks,
126
127
  } from './fleet-coordinate-finality.mjs';
128
+ import { adoptCatalogueKernel } from './kernel-package.mjs';
127
129
  import { loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
128
130
  import { assertPlatformCanParseLock } from './platform-lock-readability.mjs';
129
131
  import { assertReleaseEditionTree } from './release-channel-guard.mjs';
@@ -138,7 +140,8 @@ import {
138
140
  if (isEntryModule(import.meta.url)) {
139
141
  // The customer form (it names --definition) reads exactly these roots - the data root (the vendor's
140
142
  // runtime-image-source-index.json), the editions root, the sources root (the readability check of the
141
- // vendor repositories), the kernel and the cache (external source trees) - and refuses any other.
143
+ // vendor repositories) and the cache (external source trees, and the catalogue's kernel from npm) - and
144
+ // refuses any other, the kernel root included. A vendor seal takes the kernel root too.
142
145
  let namesDefinition;
143
146
  try {
144
147
  namesDefinition = parseSealArgs(process.argv.slice(2)).catalogueFlags.has(DEFINITION_FLAG);
@@ -147,7 +150,9 @@ if (isEntryModule(import.meta.url)) {
147
150
  process.exit(1);
148
151
  }
149
152
  configureEditionWorkspaceFromProcess(
150
- [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache],
153
+ namesDefinition
154
+ ? [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Cache]
155
+ : [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache],
151
156
  { refuseOthers: namesDefinition },
152
157
  );
153
158
  }
@@ -479,7 +484,7 @@ function replaceLocksTogether(documents) {
479
484
  * can leave (images sealed, distribution unsealed) is refused by every reader of
480
485
  * a sealed edition.
481
486
  */
482
- export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest }) {
487
+ export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest, fetchManifest = fetchRegistryManifest }) {
483
488
  const schemas = await loadKernelDistributionSchemas();
484
489
  const lockPath = join(root, distributionId, 'distribution.lock.json');
485
490
  const imagesPath = join(root, distributionId, 'images.lock.json');
@@ -505,6 +510,11 @@ export async function sealEditionLocks({ distributionId, root, digests = new Map
505
510
  const imageLock = schemas.parseImageLock(rawImages);
506
511
  const pinned = vendoredCommit ?? ((repo) => createPinnedSourceAuthority({ pins: committedRepositoryPins() }).vendoredSourceCommitForRepository(repo));
507
512
  const lockDocument = raw !== null && typeof raw === 'object' && 'unsealedSchema' in raw ? schemas.parseUnsealedDistributionLock(raw) : schemas.parseDistributionLock(raw);
513
+ if (lockMountsAdapters(lockDocument)) {
514
+ // The adapter host's row: the verified catalogue's for a customer, the data root's biome index for the vendor.
515
+ const rows = catalogue === undefined ? JSON.parse(readFileSync(biomeIndexPath(), 'utf8')) : catalogue.biomes;
516
+ await assertAdapterBundlesMatchHost({ lock: lockDocument, hostRow: adapterHostRow(rows), fetchManifest });
517
+ }
508
518
  const result = await sealOrVerifyLock({
509
519
  schemas,
510
520
  lock: lockDocument,
@@ -657,6 +667,8 @@ async function main() {
657
667
  : undefined;
658
668
  const catalogue = fetched?.catalogue;
659
669
  const catalogueDigest = fetched?.digest;
670
+ // A customer seal reads the kernel of its catalogue (from npm), proved its contract authority, before any kernel schema is loaded.
671
+ if (catalogue !== undefined) {await adoptCatalogueKernel(catalogue);}
660
672
  const imageLock = await loadImageLockForVerification(distributionId, root);
661
673
 
662
674
  // Throws on ANY unresolvable subject or missing required platform, before a
@@ -9,7 +9,7 @@
9
9
  // usage error. A data root that declares no policy is `unarmed`.
10
10
  // It reads the policy the way `xema edition check` does and never writes.
11
11
 
12
- import { EditionChannel } from './edition-channel.mjs';
12
+ import { EditionChannel, parseChannelArgs } from './edition-channel.mjs';
13
13
  import {
14
14
  WorkspaceRoot,
15
15
  configureEditionWorkspaceFromArgv,
@@ -23,14 +23,6 @@ export const ARMING_USAGE = `usage: xema edition arming --channel <${Object.valu
23
23
  /** The process exit codes of the verb. */
24
24
  export const ArmingExit = Object.freeze({ Armed: 0, NotArmed: 1, Usage: 2 });
25
25
 
26
- /** The channel of `--channel <channel>` among `rest`; anything else is a usage error. */
27
- export function parseArmingArgs(rest) {
28
- if (rest.length !== 2 || rest[0] !== '--channel' || !Object.values(EditionChannel).includes(rest[1])) {
29
- throw new Error(`--channel <${Object.values(EditionChannel).join('|')}> is required and nothing else is accepted (got ${rest.length === 0 ? 'nothing' : JSON.stringify(rest.join(' '))})`);
30
- }
31
- return { channel: rest[1] };
32
- }
33
-
34
26
  /**
35
27
  * Judge the configured data root's policy on `channel`, writing the one JSON
36
28
  * line to `stdout` and each unmet entry to `stderr`. Returns the exit code.
@@ -54,7 +46,7 @@ async function main() {
54
46
  let channel;
55
47
  try {
56
48
  const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
57
- ({ channel } = parseArmingArgs(rest));
49
+ ({ channel } = parseChannelArgs(rest));
58
50
  } catch (error) {
59
51
  process.stderr.write(`arming: ${error.message}\n${ARMING_USAGE}\n`);
60
52
  process.exitCode = ArmingExit.Usage;
@@ -397,6 +397,7 @@ export async function verifyEvidence({ argv, env, bundlePath }) {
397
397
  completenessStatementDigest: result.completenessStatementDigest,
398
398
  customerId: options.customerId,
399
399
  distributionId: options.distributionId,
400
+ distributionLock: result.distributionLock,
400
401
  channel,
401
402
  mode: options.mode,
402
403
  targetRegistry: options.targetRegistry,
@@ -1,5 +1,6 @@
1
1
  # `xema.config.yaml` examples
2
2
 
3
+
3
4
  Copy one of these to `xema.config.yaml` (or `~/.xema/config.yaml`) and edit:
4
5
 
5
6
  ```bash
@@ -14,6 +15,6 @@ xema up
14
15
  | [`03-byo-infra.yaml`](./03-byo-infra.yaml) | Bring-your-own Postgres / Keycloak / object store; launcher runs only the rest. |
15
16
  | [`04-subset-knowledge.yaml`](./04-subset-knowledge.yaml) | Run only a slice (tiers / services) to save resources. |
16
17
  | [`05-appliance-helm.yaml`](./05-appliance-helm.yaml) | Deploy to Kubernetes / an appliance via Helm. |
18
+ | [`06-biome-sources.yaml`](./06-biome-sources.yaml) | Pull biomes from workspace, distribution and git sources. |
17
19
 
18
- Precedence: `--config <path>` → `./xema.config.yaml` → `~/.xema/config.yaml` →
19
- built-in defaults. Every value shown is the default unless noted.
20
+ Config precedence: `--config <path>`, then `./xema.config.yaml`, then `~/.xema/config.yaml`, then built-in defaults.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -54,7 +54,7 @@
54
54
  "@xemahq/distribution-source-hash": "^0.3.0",
55
55
  "@xemahq/dsl": "^0.23.0",
56
56
  "@xemahq/identity-client": "^0.16.0",
57
- "@xemahq/contracts": "^0.76.0",
57
+ "@xemahq/contracts": "^0.78.0",
58
58
  "@xemahq/kernel-state-runtime": "^0.8.0",
59
59
  "@xemahq/license-verifier": "^0.2.0",
60
60
  "@xemahq/service-registry-nest": "^0.12.2",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:82760f14feea4fe0626220e0f71344375964f15d36201d1e7000ebfb6b2e4ecd"
95
+ "sourceDigest": "sha256:d594a2838a0fbee09de46b9b6637afd9f9437a2989a2bfe30f2336ddc20f0523"
96
96
  }
97
97
  }