@xemahq/xema 0.33.6 → 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.
- package/dist/edition/verify-image-evidence.bundle.mjs +4191 -1650
- package/dist/edition/verify-image-evidence.bundle.provenance.json +645 -465
- package/edition/README.md +2 -2
- package/edition/biome-content.mjs +11 -7
- package/edition/catalogue.mjs +73 -3
- package/edition/develop-customer-edition.mjs +1 -0
- package/edition/edition-pinned-inputs.mjs +2 -2
- package/edition/edition-protocol.mjs +2 -2
- package/edition/edition-source-pins.mjs +23 -21
- package/edition/edition-workspace.mjs +1 -0
- package/edition/image-evidence.mjs +17 -8
- package/edition/lib.mjs +9 -3
- package/edition/operation-grant-admission.mjs +18 -3
- package/edition/resolve-customer-edition.mjs +25 -42
- package/edition/resolve-distribution.mjs +50 -4
- package/edition/seal-image-digests.mjs +16 -8
- package/edition/store-publisher-trust.mjs +40 -0
- package/package.json +4 -4
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/xema-dev/xema-cli.git",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"@xemahq/biome-sdk": "^18.1.0",
|
|
53
53
|
"@xemahq/biome-supply-chain": "^0.5.0",
|
|
54
54
|
"@xemahq/distribution-source-hash": "^0.2.2",
|
|
55
|
-
"@xemahq/dsl": "^0.
|
|
55
|
+
"@xemahq/dsl": "^0.24.0",
|
|
56
56
|
"@xemahq/identity-client": "^0.16.0",
|
|
57
|
-
"@xemahq/contracts": "^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:
|
|
95
|
+
"sourceDigest": "sha256:86028f64405f33d817413fe305f1263168fe9aa57714b65d43a2ff27e304b1c4"
|
|
96
96
|
}
|
|
97
97
|
}
|