@xemahq/xema 0.33.6 → 0.34.1
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-edition.mjs +4 -4
- 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/release-edition-artifact.mjs +9 -15
- package/edition/resolve-customer-edition.mjs +25 -42
- package/edition/resolve-distribution.mjs +50 -4
- package/edition/seal-image-digests.mjs +36 -26
- package/edition/sealed-distribution-lock.mjs +22 -0
- 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
|
);
|
|
@@ -98,8 +98,9 @@
|
|
|
98
98
|
// state a pin advance leaves, owner ruling 2026-09-29), as
|
|
99
99
|
// ONE line on stdout, exit 0. A malformed, empty or
|
|
100
100
|
// PARTIALLY sealed lock exits 1: a broken artifact.
|
|
101
|
-
// OFFLINE and structural: it reads the
|
|
102
|
-
// else
|
|
101
|
+
// OFFLINE and structural: it reads the locks and nothing
|
|
102
|
+
// else (the distribution lock through the built kernel,
|
|
103
|
+
// `--kernel`, only when the images lock is PUBLISHABLE). That is deliberate rather than a shortcut — a
|
|
103
104
|
// publication gate that needs a registry credential can
|
|
104
105
|
// fail for reasons that have nothing to do with the artifact
|
|
105
106
|
// being published, and a gate people learn to re-run is a
|
|
@@ -116,6 +117,7 @@ import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest, l
|
|
|
116
117
|
import { sealLock, verifyBiomeContentHashes } from './biome-content.mjs';
|
|
117
118
|
import { fetchCatalogue } from './catalogue-fetch.mjs';
|
|
118
119
|
import { readCataloguePin } from './catalogue-pin.mjs';
|
|
120
|
+
import { parseCustomerDefinition } from './customer-definition.mjs';
|
|
119
121
|
import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG } from './customer-flags.mjs';
|
|
120
122
|
import { EditionChannel } from './edition-constants.mjs';
|
|
121
123
|
import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
|
|
@@ -132,7 +134,9 @@ import { assertPlatformCanParseLock } from './platform-lock-readability.mjs';
|
|
|
132
134
|
import { assertReleaseEditionTree } from './release-channel-guard.mjs';
|
|
133
135
|
import { commandForSelf } from './remedy-command.mjs';
|
|
134
136
|
import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
|
|
137
|
+
import { readSealedDistributionLock } from './sealed-distribution-lock.mjs';
|
|
135
138
|
import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
|
|
139
|
+
import { readStrictJsonFile } from './strict-json.mjs';
|
|
136
140
|
import {
|
|
137
141
|
loadImageLockForVerification,
|
|
138
142
|
verifyDistributionPlatforms,
|
|
@@ -304,10 +308,10 @@ export const PublicationVerdict = Object.freeze({
|
|
|
304
308
|
* some services by digest and silently leave the rest un-pinned.
|
|
305
309
|
*
|
|
306
310
|
* IT CHECKS SHAPE AS WELL AS PRESENCE. This gate reads the lock with
|
|
307
|
-
* `JSON.parse
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
+
* `JSON.parse`, so the kernel's digest regex is not upstream of it and this
|
|
312
|
+
* file's own `SHA256_DIGEST` is the only rule on the images lock. (The sealed
|
|
313
|
+
* grammar of the distribution lock is the kernel's, asked of it separately once
|
|
314
|
+
* the images lock reads PUBLISHABLE.)
|
|
311
315
|
*/
|
|
312
316
|
export function classifyPublicationState({
|
|
313
317
|
distributionId,
|
|
@@ -480,7 +484,7 @@ export async function buildSealedEditionLock(options) {
|
|
|
480
484
|
}
|
|
481
485
|
|
|
482
486
|
/** 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 }) {
|
|
487
|
+
async function prepareEditionSeal({ distributionId, root, digests = new Map(), check = false, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, fetchManifest = fetchRegistryManifest, checkAdapters = true }) {
|
|
484
488
|
const schemas = await loadKernelDistributionSchemas();
|
|
485
489
|
const lockPath = join(root, distributionId, 'distribution.lock.json');
|
|
486
490
|
const imagesPath = join(root, distributionId, 'images.lock.json');
|
|
@@ -522,6 +526,7 @@ async function prepareEditionSeal({ distributionId, root, digests = new Map(), c
|
|
|
522
526
|
vendoredCommit: pinned,
|
|
523
527
|
catalogue,
|
|
524
528
|
catalogueDigest,
|
|
529
|
+
storePublisherAnchorKeys,
|
|
525
530
|
check,
|
|
526
531
|
lockPath,
|
|
527
532
|
});
|
|
@@ -544,8 +549,8 @@ async function prepareEditionSeal({ distributionId, root, digests = new Map(), c
|
|
|
544
549
|
* can leave (images sealed, distribution unsealed) is refused by every reader of
|
|
545
550
|
* a sealed edition.
|
|
546
551
|
*/
|
|
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 });
|
|
552
|
+
export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, fetchManifest = fetchRegistryManifest }) {
|
|
553
|
+
const { prepared } = await prepareEditionSeal({ distributionId, root, digests, check, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, fetchManifest });
|
|
549
554
|
const { result, rawImages, applied, lockPath, imagesPath } = prepared;
|
|
550
555
|
replaceLocksTogether([
|
|
551
556
|
...(applied > 0 ? [{ path: imagesPath, text: `${JSON.stringify(rawImages, null, 2)}\n` }] : []),
|
|
@@ -564,14 +569,14 @@ export async function sealEditionLocks({ distributionId, root, digests = new Map
|
|
|
564
569
|
* The decision, with no files: an UNSEALED lock is sealed (refused under `check`); a SEALED lock is VERIFIED -
|
|
565
570
|
* every hash recomputed, a mismatch refused naming the biome.
|
|
566
571
|
*/
|
|
567
|
-
export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, check = false, lockPath = 'distribution.lock.json' }) {
|
|
572
|
+
export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, check = false, lockPath = 'distribution.lock.json' }) {
|
|
568
573
|
if ('unsealedSchema' in lock) {
|
|
569
574
|
if (check) {
|
|
570
575
|
throw new Error(`${lockPath} is unsealed (no biomeContentHash); run \`xema edition seal ${lock.distributionId}\``);
|
|
571
576
|
}
|
|
572
|
-
return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest }) };
|
|
577
|
+
return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys }) };
|
|
573
578
|
}
|
|
574
|
-
await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest });
|
|
579
|
+
await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys });
|
|
575
580
|
return { sealed: false, lock };
|
|
576
581
|
}
|
|
577
582
|
|
|
@@ -595,19 +600,12 @@ async function main() {
|
|
|
595
600
|
);
|
|
596
601
|
}
|
|
597
602
|
|
|
598
|
-
// PUBLICATION GATE — offline and structural.
|
|
599
|
-
//
|
|
600
|
-
//
|
|
603
|
+
// PUBLICATION GATE — offline (no network, no credential) and structural. The images lock is
|
|
604
|
+
// classified with no kernel at all. A PUBLISHABLE verdict then also reads the distribution lock
|
|
605
|
+
// through the BUILT kernel (`readSealedDistributionLock`, the predicate `xema edition artifact`
|
|
606
|
+
// uses), so the publication workflow provisions the built kernel before it asks (`--kernel`).
|
|
607
|
+
// It returns BEFORE `loadImageLockForVerification`, which needs sibling repositories and the registry.
|
|
601
608
|
//
|
|
602
|
-
// The distinction is the IMPORT versus the CALL, and getting it wrong once
|
|
603
|
-
// cost a needless refactor: importing `verify-image-platforms.mjs`,
|
|
604
|
-
// `oras.mjs` and `lib.mjs` is all FINE without a kernel —
|
|
605
|
-
// measured, all three load in a flat checkout — because
|
|
606
|
-
// `loadKernelDistributionSchemas()` is invoked at USE time, not at module
|
|
607
|
-
// load. What fails in the publication checkout is CALLING
|
|
608
|
-
// `loadImageLockForVerification`. So the fix is to not call it, which is what
|
|
609
|
-
// this early return does; making the imports dynamic bought nothing and was
|
|
610
|
-
// reverted.
|
|
611
609
|
// EVERY mode, the offline publication verdict included: a release edition
|
|
612
610
|
// never carries develop content, whatever the sealer is asked to do with it.
|
|
613
611
|
assertReleaseEditionTree(join(editionsRoot(), distributionId));
|
|
@@ -624,6 +622,13 @@ async function main() {
|
|
|
624
622
|
// including an empty stdout from a crash — is a failure there, never a
|
|
625
623
|
// withhold. The human explanation goes to stderr.
|
|
626
624
|
if (state.verdict === PublicationVerdict.Publishable) {
|
|
625
|
+
// The artifact verb reads the distribution lock through this same predicate; a verdict that
|
|
626
|
+
// passed an unsealed one would be the first thing `xema edition artifact` contradicts.
|
|
627
|
+
readSealedDistributionLock({
|
|
628
|
+
schemas: await loadKernelDistributionSchemas(),
|
|
629
|
+
lockPath: join(editionsRoot(), distributionId, 'distribution.lock.json'),
|
|
630
|
+
distributionId,
|
|
631
|
+
});
|
|
627
632
|
console.error(
|
|
628
633
|
`[dist:seal-digests --publication-verdict] "${distributionId}": all ` +
|
|
629
634
|
`${state.subjectCount} subject(s) carry a canonical imageDigest; publishable. ` +
|
|
@@ -689,7 +694,11 @@ async function main() {
|
|
|
689
694
|
const catalogue = fetched?.catalogue;
|
|
690
695
|
const catalogueDigest = fetched?.digest;
|
|
691
696
|
// A customer seal reads the kernel of its catalogue (from npm), proved its contract authority, before any kernel schema is loaded.
|
|
692
|
-
|
|
697
|
+
// Its definition's Store publisher selection is what the lock's `storePublisherTrust` is checked against (biome-content.mjs).
|
|
698
|
+
const storePublisherAnchorKeys =
|
|
699
|
+
catalogue === undefined
|
|
700
|
+
? undefined
|
|
701
|
+
: parseCustomerDefinition(await adoptCatalogueKernel(catalogue), readStrictJsonFile(catalogueFlags.get(DEFINITION_FLAG))).definition.storePublisherAnchorKeys;
|
|
693
702
|
const imageLock = await loadImageLockForVerification(distributionId, root);
|
|
694
703
|
|
|
695
704
|
// Throws on ANY unresolvable subject or missing required platform, before a
|
|
@@ -812,7 +821,7 @@ async function main() {
|
|
|
812
821
|
|
|
813
822
|
if (writes.length === 0) {
|
|
814
823
|
console.log(`[${label}] lock is fully sealed and agrees with the registry.`);
|
|
815
|
-
await sealEditionLocks({ distributionId, root, check, label, catalogue, catalogueDigest });
|
|
824
|
+
await sealEditionLocks({ distributionId, root, check, label, catalogue, catalogueDigest, storePublisherAnchorKeys });
|
|
816
825
|
return;
|
|
817
826
|
}
|
|
818
827
|
|
|
@@ -898,6 +907,7 @@ async function main() {
|
|
|
898
907
|
label,
|
|
899
908
|
catalogue,
|
|
900
909
|
catalogueDigest,
|
|
910
|
+
storePublisherAnchorKeys,
|
|
901
911
|
});
|
|
902
912
|
}
|
|
903
913
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// sealed-distribution-lock — the one predicate for "this committed distribution.lock.json is the SEALED grammar".
|
|
2
|
+
// `xema edition artifact` and the `xema edition seal --publication-verdict` gate both ask it, so the verdict can
|
|
3
|
+
// never say PUBLISHABLE for a lock the artifact verb then refuses.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse the committed distribution lock at `lockPath` as the sealed grammar; an unsealed (or otherwise
|
|
9
|
+
* unparseable) lock is refused, naming the file. Returns the parsed sealed lock.
|
|
10
|
+
*/
|
|
11
|
+
export function readSealedDistributionLock({ schemas, lockPath, distributionId }) {
|
|
12
|
+
const raw = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
13
|
+
try {
|
|
14
|
+
return { raw, sealed: schemas.parseDistributionLock(raw) };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`${lockPath} is not a sealed distribution lock, so nothing is published from it. ` +
|
|
18
|
+
`Run \`xema edition seal ${distributionId}\` first. (${error.message})`,
|
|
19
|
+
{ cause: error },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -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.1",
|
|
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:f36703385f5feb7476f762f26e0b8e5b6740dab8c59225abc4cdf14d32f0c48e"
|
|
96
96
|
}
|
|
97
97
|
}
|