@xemahq/xema 0.33.5 → 0.34.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.
@@ -78,7 +78,6 @@ import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-compone
78
78
  import { fleetChartSetLockPath, writeEditionChartSet } from './edition-chart-set.mjs';
79
79
  import { workspaceSection } from './edition-config.mjs';
80
80
  import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
81
- import { splitEditionDefinition } from './edition-source-pins.mjs';
82
81
  import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, kernelPackageDir } from './edition-workspace.mjs';
83
82
  import { walkDeclaredSources } from './external-source-walk.mjs';
84
83
  import {
@@ -105,6 +104,7 @@ import { closureBiomeFacts } from './positive-closure.mjs';
105
104
  import { assertReleaseContent, assertReleaseEditionTree } from './release-channel-guard.mjs';
106
105
  import { commandFor } from './remedy-command.mjs';
107
106
  import { soleResourceProvider } from './required-resource-provider.mjs';
107
+ import { readStorePublisherTrust } from './store-publisher-trust.mjs';
108
108
  import { readStrictJsonFile } from './strict-json.mjs';
109
109
  import { RuntimeSubjectKind } from './workspace-sections.mjs';
110
110
 
@@ -262,7 +262,8 @@ export function loadEditionManifest(parseDistribution, id) {
262
262
  "(core class, the classes the edition's include names, and services a shipped biome's API requires)",
263
263
  );
264
264
  }
265
- const manifest = parseDistribution(splitEditionDefinition(raw, id).document);
265
+ // `sourcePins` reaches the kernel unchanged: its Distribution schema validates it.
266
+ const manifest = parseDistribution(raw);
266
267
  if (manifest.id !== id) {
267
268
  throw new Error(`distribution at ${path} declares id "${manifest.id}", but it was addressed as "${id}"`);
268
269
  }
@@ -1366,6 +1367,17 @@ export function resolveDistributionFromInputs(
1366
1367
  const resourceProviderCandidates = index.filter(
1367
1368
  (e) => isDeliverable(e, composed?.webProducerRepos) && entrySourceAdmissible(e, declaredSourceNames),
1368
1369
  );
1370
+ // service name -> the server biomes that own it, for the contract-edge
1371
+ // exclusion check below.
1372
+ const serverOwnersOfService = new Map();
1373
+ for (const candidate of index) {
1374
+ if (candidate.target !== 'server') {continue;}
1375
+ for (const service of candidate.services) {
1376
+ const owners = serverOwnersOfService.get(service) ?? [];
1377
+ owners.push(candidate);
1378
+ serverOwnersOfService.set(service, owners);
1379
+ }
1380
+ }
1369
1381
  const requireBiomeOwner = (consumer, edge) => {
1370
1382
  const owner = edge.owner;
1371
1383
  if (owner.target !== 'server') {
@@ -1549,6 +1561,28 @@ export function resolveDistributionFromInputs(
1549
1561
  }
1550
1562
  }
1551
1563
  }
1564
+ // A REQUIRED dependency-contract edge (the kernel's
1565
+ // `contractRequiredServiceTargets`) needs its target service in the
1566
+ // edition whether or not a `requiresServices` edge states it. Refuse an
1567
+ // excluded owner; never pull one in here.
1568
+ if (entry.dependencyContract !== undefined) {
1569
+ for (const target of schemas.contractRequiredServiceTargets(entry.dependencyContract)) {
1570
+ if (excludedPlatformServices.has(target)) {
1571
+ throw new Error(
1572
+ `distribution "${id}": biome "${entry.id}" dependency contract requires service ` +
1573
+ `"${target}", but the distribution explicitly excludes it`,
1574
+ );
1575
+ }
1576
+ for (const owner of serverOwnersOfService.get(target) ?? []) {
1577
+ if (excludedBiomes.has(owner.id)) {
1578
+ throw new Error(
1579
+ `distribution "${id}": biome "${entry.id}" dependency contract requires service ` +
1580
+ `"${target}" owned by explicitly excluded biome "${owner.id}"`,
1581
+ );
1582
+ }
1583
+ }
1584
+ }
1585
+ }
1552
1586
  for (const edge of entry.serviceDependencies ?? []) {
1553
1587
  if (!entry.services.includes(edge.consumerService)) {
1554
1588
  throw new Error(
@@ -2157,6 +2191,10 @@ export function resolveDistributionFromInputs(
2157
2191
  // carries exactly the bytes it always did. Not in `inputHash`: derived from
2158
2192
  // the selected biomes, whose `sourceManifestHash` the hash already covers.
2159
2193
  if (nexusBindings.length > 0) {lock.nexusBindings = nexusBindings;}
2194
+ // Store publisher trust — written ONLY when the data root states it, appended
2195
+ // last for the same reason: an edition without it carries exactly the bytes it
2196
+ // always did. Not in `inputHash`; the kernel lock schema validates it.
2197
+ if (composed?.storePublisherTrust !== undefined) {lock.storePublisherTrust = composed.storePublisherTrust;}
2160
2198
 
2161
2199
  // (7) Re-derive the ownership index through the KERNEL so the table this
2162
2200
  // resolver emits is validated by the exact code every consumer will read
@@ -2172,7 +2210,9 @@ export function resolveDistributionFromInputs(
2172
2210
  const parsed = schemas.parseUnsealedDistributionLock(lock);
2173
2211
  // (9) Admit every declared service-to-service operation requirement; a
2174
2212
  // refusal throws, so a lock with one is never returned to be written.
2175
- assertOperationGrantsAdmitted(schemas, parsed);
2213
+ // An optional operation left ungranted is reported, one line each, and
2214
+ // never fails (the resolve and `edition check` both run through here).
2215
+ assertOperationGrantsAdmitted(schemas, parsed, { report: (line) => console.log(`[operations] ${line}`) });
2176
2216
  assertOrganizationSweepsAdmitted(schemas, parsed);
2177
2217
  return validateImmutableDistributionLock(lock);
2178
2218
  }
@@ -2238,11 +2278,17 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs, re
2238
2278
  ...(materialize === undefined ? {} : { materialize }),
2239
2279
  });
2240
2280
  const index = mergeExternalSourceEntries(platformIndex, entries, platformServiceIndex);
2281
+ // A caller that injects inputs states the trust itself (a customer edition
2282
+ // states none); otherwise the data root's file is read.
2283
+ const storePublisherTrust =
2284
+ inputs !== undefined && Object.hasOwn(inputs, 'storePublisherTrust')
2285
+ ? inputs.storePublisherTrust
2286
+ : readStorePublisherTrust();
2241
2287
  const lock = resolveDistributionFromInputs(
2242
2288
  id,
2243
2289
  index,
2244
2290
  schemas,
2245
- { ...composed, sourceRecords },
2291
+ { ...composed, sourceRecords, storePublisherTrust },
2246
2292
  new Date().toISOString(),
2247
2293
  platformServiceIndex,
2248
2294
  );
@@ -116,6 +116,7 @@ import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest, l
116
116
  import { sealLock, verifyBiomeContentHashes } from './biome-content.mjs';
117
117
  import { fetchCatalogue } from './catalogue-fetch.mjs';
118
118
  import { readCataloguePin } from './catalogue-pin.mjs';
119
+ import { parseCustomerDefinition } from './customer-definition.mjs';
119
120
  import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG } from './customer-flags.mjs';
120
121
  import { EditionChannel } from './edition-constants.mjs';
121
122
  import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
@@ -133,6 +134,7 @@ import { assertReleaseEditionTree } from './release-channel-guard.mjs';
133
134
  import { commandForSelf } from './remedy-command.mjs';
134
135
  import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
135
136
  import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
137
+ import { readStrictJsonFile } from './strict-json.mjs';
136
138
  import {
137
139
  loadImageLockForVerification,
138
140
  verifyDistributionPlatforms,
@@ -480,7 +482,7 @@ export async function buildSealedEditionLock(options) {
480
482
  }
481
483
 
482
484
  /** The shared in-memory half of the seal: reads both locks, applies `digests`, seals or verifies. Writes nothing. */
483
- async function prepareEditionSeal({ distributionId, root, digests = new Map(), check = false, vendoredCommit, catalogue, catalogueDigest, fetchManifest = fetchRegistryManifest, checkAdapters = true }) {
485
+ async function prepareEditionSeal({ distributionId, root, digests = new Map(), check = false, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, fetchManifest = fetchRegistryManifest, checkAdapters = true }) {
484
486
  const schemas = await loadKernelDistributionSchemas();
485
487
  const lockPath = join(root, distributionId, 'distribution.lock.json');
486
488
  const imagesPath = join(root, distributionId, 'images.lock.json');
@@ -522,6 +524,7 @@ async function prepareEditionSeal({ distributionId, root, digests = new Map(), c
522
524
  vendoredCommit: pinned,
523
525
  catalogue,
524
526
  catalogueDigest,
527
+ storePublisherAnchorKeys,
525
528
  check,
526
529
  lockPath,
527
530
  });
@@ -544,8 +547,8 @@ async function prepareEditionSeal({ distributionId, root, digests = new Map(), c
544
547
  * can leave (images sealed, distribution unsealed) is refused by every reader of
545
548
  * a sealed edition.
546
549
  */
547
- export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest, fetchManifest = fetchRegistryManifest }) {
548
- const { prepared } = await prepareEditionSeal({ distributionId, root, digests, check, vendoredCommit, catalogue, catalogueDigest, fetchManifest });
550
+ export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, fetchManifest = fetchRegistryManifest }) {
551
+ const { prepared } = await prepareEditionSeal({ distributionId, root, digests, check, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, fetchManifest });
549
552
  const { result, rawImages, applied, lockPath, imagesPath } = prepared;
550
553
  replaceLocksTogether([
551
554
  ...(applied > 0 ? [{ path: imagesPath, text: `${JSON.stringify(rawImages, null, 2)}\n` }] : []),
@@ -564,14 +567,14 @@ export async function sealEditionLocks({ distributionId, root, digests = new Map
564
567
  * The decision, with no files: an UNSEALED lock is sealed (refused under `check`); a SEALED lock is VERIFIED -
565
568
  * every hash recomputed, a mismatch refused naming the biome.
566
569
  */
567
- export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, check = false, lockPath = 'distribution.lock.json' }) {
570
+ export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, check = false, lockPath = 'distribution.lock.json' }) {
568
571
  if ('unsealedSchema' in lock) {
569
572
  if (check) {
570
573
  throw new Error(`${lockPath} is unsealed (no biomeContentHash); run \`xema edition seal ${lock.distributionId}\``);
571
574
  }
572
- return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest }) };
575
+ return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys }) };
573
576
  }
574
- await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest });
577
+ await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys });
575
578
  return { sealed: false, lock };
576
579
  }
577
580
 
@@ -689,7 +692,11 @@ async function main() {
689
692
  const catalogue = fetched?.catalogue;
690
693
  const catalogueDigest = fetched?.digest;
691
694
  // A customer seal reads the kernel of its catalogue (from npm), proved its contract authority, before any kernel schema is loaded.
692
- if (catalogue !== undefined) {await adoptCatalogueKernel(catalogue);}
695
+ // Its definition's Store publisher selection is what the lock's `storePublisherTrust` is checked against (biome-content.mjs).
696
+ const storePublisherAnchorKeys =
697
+ catalogue === undefined
698
+ ? undefined
699
+ : parseCustomerDefinition(await adoptCatalogueKernel(catalogue), readStrictJsonFile(catalogueFlags.get(DEFINITION_FLAG))).definition.storePublisherAnchorKeys;
693
700
  const imageLock = await loadImageLockForVerification(distributionId, root);
694
701
 
695
702
  // Throws on ANY unresolvable subject or missing required platform, before a
@@ -812,7 +819,7 @@ async function main() {
812
819
 
813
820
  if (writes.length === 0) {
814
821
  console.log(`[${label}] lock is fully sealed and agrees with the registry.`);
815
- await sealEditionLocks({ distributionId, root, check, label, catalogue, catalogueDigest });
822
+ await sealEditionLocks({ distributionId, root, check, label, catalogue, catalogueDigest, storePublisherAnchorKeys });
816
823
  return;
817
824
  }
818
825
 
@@ -898,6 +905,7 @@ async function main() {
898
905
  label,
899
906
  catalogue,
900
907
  catalogueDigest,
908
+ storePublisherAnchorKeys,
901
909
  });
902
910
  }
903
911
 
@@ -0,0 +1,40 @@
1
+ // The Store publishers a vendor edition's data root trusts: read from
2
+ // `store-publisher-identities.json` and written to the lock as
3
+ // `storePublisherTrust`. The kernel lock schema is the ONE validator (non-empty,
4
+ // sorted by anchorKey, unique, https issuer, no refs/pull/); it runs at the
5
+ // resolver's `parseUnsealedDistributionLock` step, so nothing here re-checks it.
6
+
7
+ import { existsSync } from 'node:fs';
8
+
9
+ import { storePublisherIdentitiesPath } from './edition-workspace.mjs';
10
+ import { readStrictJsonFile } from './strict-json.mjs';
11
+
12
+ /** The data root's trusted Store publishers, or `undefined` when it holds no such file. */
13
+ export function readStorePublisherTrust() {
14
+ const path = storePublisherIdentitiesPath();
15
+ if (!existsSync(path)) {return undefined;}
16
+ const document = readStrictJsonFile(path);
17
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
18
+ throw new Error(`${path}: must be a JSON object`);
19
+ }
20
+ const unknown = Object.keys(document).filter((key) => key !== 'schemaVersion' && key !== 'publishers');
21
+ if (unknown.length > 0) {
22
+ throw new Error(`${path}: unknown top-level key ${unknown.map((key) => JSON.stringify(key)).join(', ')}`);
23
+ }
24
+ if (document.schemaVersion === 1) {
25
+ throw new Error(
26
+ `${path}: schemaVersion 1 states no signer; move the file to schemaVersion 2 (\`oidcIssuer\` and \`identity\` per anchor)`,
27
+ );
28
+ }
29
+ if (document.schemaVersion !== 2) {
30
+ throw new Error(`${path}: schemaVersion must be 2, got ${JSON.stringify(document.schemaVersion)}`);
31
+ }
32
+ const { publishers } = document;
33
+ if (!Array.isArray(publishers) || publishers.some((row) => row === null || typeof row !== 'object' || Array.isArray(row))) {
34
+ throw new Error(`${path}: publishers must be an array of objects`);
35
+ }
36
+ // Only `sourceRepositories` (vendor-ops' key) is dropped; every other key reaches the kernel's
37
+ // strict schema unchanged, so a stray or misspelt key is refused there, never dropped here.
38
+ return publishers.map(({ sourceRepositories: _vendorOps, ...row }) => row);
39
+ }
40
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.33.5",
3
+ "version": "0.34.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -51,10 +51,10 @@
51
51
  "@nestjs/platform-express": "^11.1.28",
52
52
  "@xemahq/biome-sdk": "^18.1.0",
53
53
  "@xemahq/biome-supply-chain": "^0.5.0",
54
- "@xemahq/distribution-source-hash": "^0.3.0",
55
- "@xemahq/dsl": "^0.23.0",
54
+ "@xemahq/distribution-source-hash": "^0.2.2",
55
+ "@xemahq/dsl": "^0.24.0",
56
56
  "@xemahq/identity-client": "^0.16.0",
57
- "@xemahq/contracts": "^0.80.0",
57
+ "@xemahq/contracts": "^0.83.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:2454817b84225bef480391ca4e8b54c1f2114fb6e3ea60daa96eaa6b810584f2"
95
+ "sourceDigest": "sha256:86028f64405f33d817413fe305f1263168fe9aa57714b65d43a2ff27e304b1c4"
96
96
  }
97
97
  }