@xemahq/xema 0.53.0 → 0.54.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/README.md +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +66 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +1 -0
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +916 -860
- package/dist/edition/verify-image-evidence.bundle.provenance.json +495 -489
- package/dist/runtime/edition/verb-map.d.ts +2 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +2 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/edition/README.md +54 -2
- package/edition/adapter-bundled-versions.mjs +2 -1
- package/edition/authorization-base.mjs +9 -8
- package/edition/biome-content.mjs +23 -29
- package/edition/candidate-edition.mjs +692 -0
- package/edition/checkout-indexed-source-repositories.mjs +6 -8
- package/edition/collected-refusals.mjs +59 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +18 -8
- package/edition/customer-edition-verbs.mjs +30 -8
- package/edition/develop-chart-set.mjs +7 -6
- package/edition/develop-edition-invariants.mjs +2 -1
- package/edition/develop-edition-resolve.mjs +109 -31
- package/edition/develop-edition.mjs +49 -8
- package/edition/develop-engine-image.mjs +95 -0
- package/edition/develop-image-probe.mjs +2 -1
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-snapshot-inputs.mjs +4 -3
- package/edition/develop-verify-image-record.mjs +23 -4
- package/edition/edition-artifact.mjs +17 -7
- package/edition/edition-protocol.mjs +5 -2
- package/edition/images-from-record.mjs +362 -0
- package/edition/lib.mjs +1 -19
- package/edition/locked-bundle-signer.mjs +15 -3
- package/edition/operation-grant-admission.mjs +51 -9
- package/edition/resolve-distribution.mjs +95 -82
- package/edition/resolve-images.mjs +12 -5
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/workspace-sections.mjs +23 -3
- package/package.json +7 -3
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
sealEditionAuthorizationBase,
|
|
75
75
|
writeAuthorizationLock,
|
|
76
76
|
} from './authorization-base.mjs';
|
|
77
|
+
import { RefusalError, captureRefusal, isRefusal, throwCollectedRefusals } from './collected-refusals.mjs';
|
|
77
78
|
import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-components.mjs';
|
|
78
79
|
import { fleetChartSetLockPath, writeEditionChartSet } from './edition-chart-set.mjs';
|
|
79
80
|
import { workspaceSection } from './edition-config.mjs';
|
|
@@ -95,16 +96,8 @@ import {
|
|
|
95
96
|
validateImmutableDistributionLock,
|
|
96
97
|
validateServiceDependencyOwnerParity,
|
|
97
98
|
} from './lib.mjs';
|
|
98
|
-
import { BundleSignerPolicyAbsentError, GitSourcedHostModuleBundleError, loadBundleSignerSelection } from './locked-bundle-signer.mjs';
|
|
99
|
-
import {
|
|
100
|
-
assertInstalledOperationCeilingAdmitted,
|
|
101
|
-
assertOperationGrantsAdmitted,
|
|
102
|
-
assertExactAudiencesAdmitted,
|
|
103
|
-
assertOrganizationSweepsAdmitted,
|
|
104
|
-
assertEventSubscriptionsAdmitted,
|
|
105
|
-
assertOrgContextRelaysAdmitted,
|
|
106
|
-
assertStoreWorkloadCeilingAdmitted,
|
|
107
|
-
} from './operation-grant-admission.mjs';
|
|
99
|
+
import { BundleSignerPolicyAbsentError, BundleSignerRefusedError, GitSourcedHostModuleBundleError, loadBundleSignerSelection } from './locked-bundle-signer.mjs';
|
|
100
|
+
import { admitDistributionLock } from './operation-grant-admission.mjs';
|
|
108
101
|
import { PUBLISHED_PIN_SOURCE, pinnedSource } from './pin-source.mjs';
|
|
109
102
|
import { verifiedPlatformServiceIndex } from './platform-service-ownership.mjs';
|
|
110
103
|
import { closureBiomeFacts } from './positive-closure.mjs';
|
|
@@ -151,7 +144,7 @@ export function lockedServiceProcessFacts(entry) {
|
|
|
151
144
|
const facts = entry.serviceProcessFacts ?? {};
|
|
152
145
|
const named = Object.keys(facts).sort();
|
|
153
146
|
if (serializeJson(named) !== serializeJson([...services].sort())) {
|
|
154
|
-
throw new
|
|
147
|
+
throw new RefusalError(
|
|
155
148
|
`biome "${entry.id}" carries process facts for [${named.join(', ')}] but ships ` +
|
|
156
149
|
`services [${services.join(', ')}] — every shipped service has exactly one ` +
|
|
157
150
|
'row; regenerate the biome index.',
|
|
@@ -181,14 +174,14 @@ function versionSatisfies(actual, range) {
|
|
|
181
174
|
};
|
|
182
175
|
const a = parse(actual);
|
|
183
176
|
if (!a) {
|
|
184
|
-
throw new
|
|
177
|
+
throw new RefusalError(`cannot parse actual version "${actual}"`);
|
|
185
178
|
}
|
|
186
179
|
const trimmed = range.trim();
|
|
187
180
|
const op = trimmed[0] === '^' || trimmed[0] === '~' ? trimmed[0] : '=';
|
|
188
181
|
const bare = op === '=' ? trimmed : trimmed.slice(1);
|
|
189
182
|
const r = parse(bare);
|
|
190
183
|
if (!r) {
|
|
191
|
-
throw new
|
|
184
|
+
throw new RefusalError(
|
|
192
185
|
`cannot parse version range "${range}" — only exact / ^ / ~ forms are supported by this bounded checker`,
|
|
193
186
|
);
|
|
194
187
|
}
|
|
@@ -222,7 +215,7 @@ function versionSatisfies(actual, range) {
|
|
|
222
215
|
const REFUSED_EXTENDS_FIELD = 'extends';
|
|
223
216
|
|
|
224
217
|
/** An edition definition that breaks the edition model (a refused field); a caller tells it from an I/O or schema error by class. */
|
|
225
|
-
export class EditionModelError extends
|
|
218
|
+
export class EditionModelError extends RefusalError {
|
|
226
219
|
constructor(message) {
|
|
227
220
|
super(message);
|
|
228
221
|
this.name = 'EditionModelError';
|
|
@@ -240,7 +233,7 @@ const REFUSED_PLATFORM_SERVICES_FIELD = 'platformServices';
|
|
|
240
233
|
* A selection or declared dependency the index does not hold. Against a
|
|
241
234
|
* catalogue it is the catalogue's ceiling: the caller names the catalogue.
|
|
242
235
|
*/
|
|
243
|
-
export class UnknownSelectionError extends
|
|
236
|
+
export class UnknownSelectionError extends RefusalError {
|
|
244
237
|
constructor(message) {
|
|
245
238
|
super(message);
|
|
246
239
|
this.name = 'UnknownSelectionError';
|
|
@@ -254,7 +247,7 @@ export class UnknownSelectionError extends Error {
|
|
|
254
247
|
export function loadEditionManifest(parseDistribution, id) {
|
|
255
248
|
const path = join(editionsRoot(), id, 'xema-distribution.json');
|
|
256
249
|
if (!existsSync(path)) {
|
|
257
|
-
throw new
|
|
250
|
+
throw new RefusalError(`distribution "${id}" not found: ${path} does not exist`);
|
|
258
251
|
}
|
|
259
252
|
return parseEditionManifest(parseDistribution, id, readStrictJsonFile(path), path);
|
|
260
253
|
}
|
|
@@ -271,7 +264,7 @@ export function parseEditionManifest(parseDistribution, id, raw, source) {
|
|
|
271
264
|
);
|
|
272
265
|
}
|
|
273
266
|
if (raw !== null && typeof raw === 'object' && Object.hasOwn(raw, REFUSED_PLATFORM_SERVICES_FIELD)) {
|
|
274
|
-
throw new
|
|
267
|
+
throw new RefusalError(
|
|
275
268
|
`edition "${id}": "${REFUSED_PLATFORM_SERVICES_FIELD}" is not part of the edition definition; ` +
|
|
276
269
|
'the platform substrate is drawn from platform-service-index.json by installation class ' +
|
|
277
270
|
"(core class, the classes the edition's include names, and services a shipped biome's API requires)",
|
|
@@ -280,7 +273,7 @@ export function parseEditionManifest(parseDistribution, id, raw, source) {
|
|
|
280
273
|
// `sourcePins` reaches the kernel unchanged: its Distribution schema validates it.
|
|
281
274
|
const manifest = parseDistribution(raw);
|
|
282
275
|
if (manifest.id !== id) {
|
|
283
|
-
throw new
|
|
276
|
+
throw new RefusalError(`distribution at ${source} declares id "${manifest.id}", but it was addressed as "${id}"`);
|
|
284
277
|
}
|
|
285
278
|
return manifest;
|
|
286
279
|
}
|
|
@@ -355,7 +348,7 @@ function webBiomeProducerRepo() {
|
|
|
355
348
|
const hosts = workspaceSection('runtimeImageSources', 'knowing which repository produces web biomes')
|
|
356
349
|
.supplementalSources.filter((source) => source.kind === RuntimeSubjectKind.Host);
|
|
357
350
|
if (hosts.length !== 1) {
|
|
358
|
-
throw new
|
|
351
|
+
throw new RefusalError(
|
|
359
352
|
`edition-workspace.json runtimeImageSources must declare exactly one "${RuntimeSubjectKind.Host}" source ` +
|
|
360
353
|
`(the image web biomes are bundled into); it declares ${hosts.length}`,
|
|
361
354
|
);
|
|
@@ -449,7 +442,7 @@ export function isUniversal(entry) {
|
|
|
449
442
|
*/
|
|
450
443
|
export function assertIndexDeclaresInstallationClasses(id, index) {
|
|
451
444
|
if (!index.some((entry) => entry.installationClass !== undefined)) {
|
|
452
|
-
throw new
|
|
445
|
+
throw new RefusalError(
|
|
453
446
|
`distribution "${id}": NO entry in the biome index carries an installationClass, so ` +
|
|
454
447
|
`the index cannot say which components are universal (installationClass core) ` +
|
|
455
448
|
`and every edition would resolve without its floor. The tier is never read in its ` +
|
|
@@ -460,14 +453,14 @@ export function assertIndexDeclaresInstallationClasses(id, index) {
|
|
|
460
453
|
const universal = universalInstallationClass();
|
|
461
454
|
for (const entry of [...index].sort(compareIndexEntries)) {
|
|
462
455
|
if (entry.tier === 'kernel' && entry.installationClass !== universal) {
|
|
463
|
-
throw new
|
|
456
|
+
throw new RefusalError(
|
|
464
457
|
`distribution "${id}": kernel-tier biome "${entry.id}" (${entry.repo}) declares ` +
|
|
465
458
|
`installationClass ${entry.installationClass === undefined ? '(none)' : `"${entry.installationClass}"`}; ` +
|
|
466
459
|
`every kernel-tier biome must declare "${universal}" — fix the manifest and regenerate the index.`,
|
|
467
460
|
);
|
|
468
461
|
}
|
|
469
462
|
if (entry.installationClass === universal && entry.audience === 'operator') {
|
|
470
|
-
throw new
|
|
463
|
+
throw new RefusalError(
|
|
471
464
|
`distribution "${id}": operator biome "${entry.id}" (${entry.repo}) declares installationClass ` +
|
|
472
465
|
`"${universal}" — an operator biome ships only where an edition names it, so it cannot be universal.`,
|
|
473
466
|
);
|
|
@@ -581,7 +574,7 @@ export function mergeExternalSourceEntries(
|
|
|
581
574
|
// customer's private biome shipping inside `oss`.
|
|
582
575
|
const unstamped = externalEntries.filter((entry) => entry.source === undefined);
|
|
583
576
|
if (unstamped.length > 0) {
|
|
584
|
-
throw new
|
|
577
|
+
throw new RefusalError(
|
|
585
578
|
`refusing to merge ${unstamped.length} UNSTAMPED external entr(ies) ` +
|
|
586
579
|
`(${unstamped.map((entry) => entry.id).join(', ')}) into the biome index: an entry ` +
|
|
587
580
|
'with no source stamp is platform-default and admissible to every edition.',
|
|
@@ -635,7 +628,7 @@ function isThirdPartyOciSourceEntry(entry, sourceRecords) {
|
|
|
635
628
|
function requireSourceRecord(entry, sourceRecords) {
|
|
636
629
|
const record = sourceRecords.get(entry.source);
|
|
637
630
|
if (record === undefined) {
|
|
638
|
-
throw new
|
|
631
|
+
throw new RefusalError(
|
|
639
632
|
`biome "${entry.id}" is stamped with source "${entry.source}", but no pinned source ` +
|
|
640
633
|
'record was produced for that name. A stamped entry with no record cannot be traced ' +
|
|
641
634
|
'to a revision, and emitting it unstamped would make it read as first-party.',
|
|
@@ -677,7 +670,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
|
|
|
677
670
|
}
|
|
678
671
|
if (existing.form === form) {
|
|
679
672
|
if (existing.policy !== policy) {
|
|
680
|
-
throw new
|
|
673
|
+
throw new RefusalError(
|
|
681
674
|
`conflicting installPolicy for biome "${entryId}": two ${form}-form selectors ` +
|
|
682
675
|
`stamp "${existing.policy}" and "${policy}" — resolve the manifest ambiguity`,
|
|
683
676
|
);
|
|
@@ -718,7 +711,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
|
|
|
718
711
|
entrySourceAdmissible(e, declaredSourceNames),
|
|
719
712
|
);
|
|
720
713
|
if (matches.length === 0) {
|
|
721
|
-
throw new
|
|
714
|
+
throw new RefusalError(
|
|
722
715
|
`include { installationClass: "${sel.installationClass}" } matches no biome in ` +
|
|
723
716
|
`the index. An edition that asks for a class nothing declares is asking for ` +
|
|
724
717
|
`something that cannot be built; name the biomes explicitly or drop the selector.`,
|
|
@@ -743,7 +736,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
|
|
|
743
736
|
);
|
|
744
737
|
if (inadmissible.length > 0 && inadmissible.length === idMatches.length) {
|
|
745
738
|
const names = [...new Set(inadmissible.map((e) => e.source))].sort();
|
|
746
|
-
throw new
|
|
739
|
+
throw new RefusalError(
|
|
747
740
|
`include { biome: "${sel.biome}" } names a biome drawn from source(s) ` +
|
|
748
741
|
`${names.map((name) => `"${name}"`).join(', ')}, which this edition does not ` +
|
|
749
742
|
`declare in sources[]. A biome is reachable only from a source its own ` +
|
|
@@ -757,7 +750,7 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
|
|
|
757
750
|
// naming an operator biome is how you force it in, while naming a web
|
|
758
751
|
// biome with no producer asks for something no build can make.
|
|
759
752
|
if (idMatches.every((e) => !isDeliverable(e, webProducerRepos))) {
|
|
760
|
-
throw new
|
|
753
|
+
throw new RefusalError(
|
|
761
754
|
`include { biome: "${sel.biome}" } names a target:web biome authored in ` +
|
|
762
755
|
`"${idMatches[0].repo}". Only web biomes under ` +
|
|
763
756
|
`"${(webProducerRepos ?? [webBiomeProducerRepo()]).join(', ')}" are bundled into the host image, so nothing ` +
|
|
@@ -772,20 +765,20 @@ function applySelectors(index, include, declaredSourceNames = new Set(), webProd
|
|
|
772
765
|
try {
|
|
773
766
|
return versionSatisfies(e.version, sel.version);
|
|
774
767
|
} catch (err) {
|
|
775
|
-
throw new
|
|
768
|
+
throw new RefusalError(
|
|
776
769
|
`version check for biome "${sel.biome}" failed: ${err.message}`,
|
|
777
770
|
);
|
|
778
771
|
}
|
|
779
772
|
})
|
|
780
773
|
: idMatches;
|
|
781
774
|
if (sel.version && matches.length === 0) {
|
|
782
|
-
throw new
|
|
775
|
+
throw new RefusalError(
|
|
783
776
|
`include { biome: "${sel.biome}", version: "${sel.version}" } matched ids but no index version satisfied the range ` +
|
|
784
777
|
`(available: ${idMatches.map((m) => m.version).join(', ')})`,
|
|
785
778
|
);
|
|
786
779
|
}
|
|
787
780
|
} else {
|
|
788
|
-
throw new
|
|
781
|
+
throw new RefusalError(`unrecognized selector shape: ${JSON.stringify(sel)}`);
|
|
789
782
|
}
|
|
790
783
|
for (const e of matches) {
|
|
791
784
|
const key = entryKey(e);
|
|
@@ -877,7 +870,7 @@ function selectByPositiveClosure({
|
|
|
877
870
|
: { kind };
|
|
878
871
|
if ('platformService' in root) {
|
|
879
872
|
if (!platformSourcesByName.has(root.platformService)) {
|
|
880
|
-
throw new
|
|
873
|
+
throw new RefusalError(
|
|
881
874
|
`distribution "${id}": ${cls} root names platform service ` +
|
|
882
875
|
`"${root.platformService}", which platform-service-index.json does not describe`,
|
|
883
876
|
);
|
|
@@ -957,7 +950,7 @@ function selectByPositiveClosure({
|
|
|
957
950
|
});
|
|
958
951
|
} catch (error) {
|
|
959
952
|
if (error instanceof BiomeClosureError) {
|
|
960
|
-
throw new
|
|
953
|
+
throw new RefusalError(`distribution "${id}": ${error.message}`, {
|
|
961
954
|
cause: error,
|
|
962
955
|
});
|
|
963
956
|
}
|
|
@@ -985,7 +978,7 @@ function selectByPositiveClosure({
|
|
|
985
978
|
for (const entry of [...selected.values()].sort(compareIndexEntries)) {
|
|
986
979
|
for (const required of entry.requires ?? []) {
|
|
987
980
|
if (!shippedIds.has(required)) {
|
|
988
|
-
throw new
|
|
981
|
+
throw new RefusalError(
|
|
989
982
|
`distribution "${id}": biome "${entry.id}" declares that it requires biome ` +
|
|
990
983
|
`"${required}", which positive closure did not include — its dependency ` +
|
|
991
984
|
'contract carries no required edge to it. Either the contract is missing ' +
|
|
@@ -999,7 +992,7 @@ function selectByPositiveClosure({
|
|
|
999
992
|
? shippedPlatformServices.has(edge.requiredService)
|
|
1000
993
|
: shippedIds.has(edge.owner.biomeId);
|
|
1001
994
|
if (!satisfied) {
|
|
1002
|
-
throw new
|
|
995
|
+
throw new RefusalError(
|
|
1003
996
|
`distribution "${id}": biome "${entry.id}" API "${edge.consumerService}" declares ` +
|
|
1004
997
|
`that it requires service "${edge.requiredService}" (owned by ` +
|
|
1005
998
|
`${edge.owner.kind === 'platform' ? 'platform service' : `biome "${edge.owner.biomeId}"`}), ` +
|
|
@@ -1050,7 +1043,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
|
|
|
1050
1043
|
for (const entry of selectedEntries) {
|
|
1051
1044
|
for (const provision of entry.providesResources ?? []) {
|
|
1052
1045
|
if (!(Array.isArray(entry.services) && entry.services.includes(provision.service))) {
|
|
1053
|
-
throw new
|
|
1046
|
+
throw new RefusalError(
|
|
1054
1047
|
`distribution "${distributionId}": biome "${entry.id}" declares that ` +
|
|
1055
1048
|
`"${provision.service}" provides ${provision.kind}, but it does not ` +
|
|
1056
1049
|
`ship that service`,
|
|
@@ -1070,7 +1063,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
|
|
|
1070
1063
|
`${requirement.kind} "${requirement.logicalName}" (${requirement.scope}-scoped, ` +
|
|
1071
1064
|
`${requirement.inclusion})`;
|
|
1072
1065
|
if (providers.length > 1) {
|
|
1073
|
-
throw new
|
|
1066
|
+
throw new RefusalError(
|
|
1074
1067
|
`distribution "${distributionId}": ${subject}, and ${providers.length} ` +
|
|
1075
1068
|
`shipped services provide that kind: ` +
|
|
1076
1069
|
`${providers.map((p) => `${p.biomeId}/${p.serviceName}`).join(', ')}. ` +
|
|
@@ -1080,7 +1073,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
|
|
|
1080
1073
|
}
|
|
1081
1074
|
if (providers.length === 0) {
|
|
1082
1075
|
if (requirement.inclusion === 'required') {
|
|
1083
|
-
throw new
|
|
1076
|
+
throw new RefusalError(
|
|
1084
1077
|
`distribution "${distributionId}": ${subject}, and no shipped service ` +
|
|
1085
1078
|
`provides ${requirement.kind}. Select the biome that provides it, ` +
|
|
1086
1079
|
`or exclude the consumer.`,
|
|
@@ -1096,6 +1089,7 @@ export function serviceResourceBinder(distributionId, selectedEntries, schemas)
|
|
|
1096
1089
|
logicalName: requirement.logicalName,
|
|
1097
1090
|
scope: requirement.scope,
|
|
1098
1091
|
inclusion: requirement.inclusion,
|
|
1092
|
+
...(requirement.sharing === undefined ? {} : { sharing: requirement.sharing }),
|
|
1099
1093
|
provider: providers[0],
|
|
1100
1094
|
});
|
|
1101
1095
|
}
|
|
@@ -1132,7 +1126,7 @@ export function nexusBinder(distributionId, selectedEntries, schemas) {
|
|
|
1132
1126
|
for (const entry of selectedEntries) {
|
|
1133
1127
|
for (const provision of entry.nexusProvides ?? []) {
|
|
1134
1128
|
if (!(Array.isArray(entry.services) && entry.services.includes(provision.component))) {
|
|
1135
|
-
throw new
|
|
1129
|
+
throw new RefusalError(
|
|
1136
1130
|
`distribution "${distributionId}": biome "${entry.id}" declares that ` +
|
|
1137
1131
|
`component "${provision.component}" provides nexus service ` +
|
|
1138
1132
|
`${provision.service}@${provision.version}, but it does not ship that ` +
|
|
@@ -1149,7 +1143,7 @@ export function nexusBinder(distributionId, selectedEntries, schemas) {
|
|
|
1149
1143
|
})),
|
|
1150
1144
|
);
|
|
1151
1145
|
if (!result.ok) {
|
|
1152
|
-
throw new
|
|
1146
|
+
throw new RefusalError(
|
|
1153
1147
|
`distribution "${distributionId}": ${result.refusals.length} Nexus ` +
|
|
1154
1148
|
`requirement(s) cannot be bound:\n` +
|
|
1155
1149
|
result.refusals
|
|
@@ -1160,7 +1154,7 @@ export function nexusBinder(distributionId, selectedEntries, schemas) {
|
|
|
1160
1154
|
return result.bindings;
|
|
1161
1155
|
}
|
|
1162
1156
|
|
|
1163
|
-
export function
|
|
1157
|
+
export function resolveUnadmittedDistributionFromInputs(
|
|
1164
1158
|
id,
|
|
1165
1159
|
index,
|
|
1166
1160
|
schemas,
|
|
@@ -1214,7 +1208,7 @@ export function resolveDistributionFromInputs(
|
|
|
1214
1208
|
}
|
|
1215
1209
|
for (const [biomeId, entries] of entriesById) {
|
|
1216
1210
|
if (entries.length > 1) {
|
|
1217
|
-
throw new
|
|
1211
|
+
throw new RefusalError(
|
|
1218
1212
|
`distribution "${id}": biome id "${biomeId}" resolves to multiple compound identities: ` +
|
|
1219
1213
|
entries
|
|
1220
1214
|
.map((entry) => `${entry.target}@${entry.repo}/${entry.path}`)
|
|
@@ -1227,7 +1221,7 @@ export function resolveDistributionFromInputs(
|
|
|
1227
1221
|
const platformSourcesByName = new Map();
|
|
1228
1222
|
for (const source of platformServiceIndex.services) {
|
|
1229
1223
|
if (platformSourcesByName.has(source.name)) {
|
|
1230
|
-
throw new
|
|
1224
|
+
throw new RefusalError(
|
|
1231
1225
|
`distribution "${id}": platform service "${source.name}" has ambiguous source identities in platform-service-index.json`,
|
|
1232
1226
|
);
|
|
1233
1227
|
}
|
|
@@ -1298,7 +1292,7 @@ export function resolveDistributionFromInputs(
|
|
|
1298
1292
|
const isBiome = entriesById.has(excludedId);
|
|
1299
1293
|
const platformService = platformServices.get(excludedId);
|
|
1300
1294
|
if (isBiome && platformService) {
|
|
1301
|
-
throw new
|
|
1295
|
+
throw new RefusalError(
|
|
1302
1296
|
`distribution "${id}" excludes "${excludedId}", which is BOTH a biome id and a ` +
|
|
1303
1297
|
`platform service name — the shared exclude namespace cannot choose safely`,
|
|
1304
1298
|
);
|
|
@@ -1307,7 +1301,7 @@ export function resolveDistributionFromInputs(
|
|
|
1307
1301
|
// Mirrors the universal-biome rule below: a core-class service is in
|
|
1308
1302
|
// every installation, so it can never be opted out of.
|
|
1309
1303
|
if (isUniversal(platformService)) {
|
|
1310
|
-
throw new
|
|
1304
|
+
throw new RefusalError(
|
|
1311
1305
|
`distribution "${id}" tries to exclude universal (installationClass core) platform ` +
|
|
1312
1306
|
`service "${excludedId}" — a universal service is in every edition and cannot be excluded`,
|
|
1313
1307
|
);
|
|
@@ -1316,7 +1310,7 @@ export function resolveDistributionFromInputs(
|
|
|
1316
1310
|
continue;
|
|
1317
1311
|
}
|
|
1318
1312
|
if (!isBiome) {
|
|
1319
|
-
throw new
|
|
1313
|
+
throw new RefusalError(
|
|
1320
1314
|
`distribution "${id}" excludes unknown biome or platform service "${excludedId}"`,
|
|
1321
1315
|
);
|
|
1322
1316
|
}
|
|
@@ -1368,7 +1362,7 @@ export function resolveDistributionFromInputs(
|
|
|
1368
1362
|
// (3) Apply exclude — but a universal biome cannot be excluded.
|
|
1369
1363
|
for (const ex of excludedBiomes) {
|
|
1370
1364
|
if (universalEntries.some((e) => e.id === ex)) {
|
|
1371
|
-
throw new
|
|
1365
|
+
throw new RefusalError(
|
|
1372
1366
|
`distribution "${id}" tries to exclude universal (installationClass core) biome "${ex}" — ` +
|
|
1373
1367
|
`a universal biome is in every edition and cannot be excluded`,
|
|
1374
1368
|
);
|
|
@@ -1405,7 +1399,7 @@ export function resolveDistributionFromInputs(
|
|
|
1405
1399
|
const requireBiomeOwner = (consumer, edge) => {
|
|
1406
1400
|
const owner = edge.owner;
|
|
1407
1401
|
if (owner.target !== 'server') {
|
|
1408
|
-
throw new
|
|
1402
|
+
throw new RefusalError(
|
|
1409
1403
|
`distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires service ` +
|
|
1410
1404
|
`"${edge.requiredService}" from cross-target ${owner.target} biome "${owner.biomeId}"`,
|
|
1411
1405
|
);
|
|
@@ -1421,7 +1415,7 @@ export function resolveDistributionFromInputs(
|
|
|
1421
1415
|
candidate.services.includes(edge.requiredService),
|
|
1422
1416
|
);
|
|
1423
1417
|
if (matches.length !== 1) {
|
|
1424
|
-
throw new
|
|
1418
|
+
throw new RefusalError(
|
|
1425
1419
|
`distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires service ` +
|
|
1426
1420
|
`"${edge.requiredService}", but its exact biome owner ${owner.biomeId} ` +
|
|
1427
1421
|
`(${owner.target}@${owner.repo}/${owner.path}, ${owner.version}, ${owner.sourceManifestHash}) ` +
|
|
@@ -1443,7 +1437,7 @@ export function resolveDistributionFromInputs(
|
|
|
1443
1437
|
const requirePlatformOwner = (consumer, edge) => {
|
|
1444
1438
|
const source = edge.owner.source;
|
|
1445
1439
|
if (source.name !== edge.requiredService) {
|
|
1446
|
-
throw new
|
|
1440
|
+
throw new RefusalError(
|
|
1447
1441
|
`distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" service edge ` +
|
|
1448
1442
|
`"${edge.requiredService}" carries mismatched platform owner "${source.name}"`,
|
|
1449
1443
|
);
|
|
@@ -1456,7 +1450,7 @@ export function resolveDistributionFromInputs(
|
|
|
1456
1450
|
);
|
|
1457
1451
|
}
|
|
1458
1452
|
if (serializeJson(canonical) !== serializeJson(source)) {
|
|
1459
|
-
throw new
|
|
1453
|
+
throw new RefusalError(
|
|
1460
1454
|
`distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires platform service ` +
|
|
1461
1455
|
`"${edge.requiredService}", but its exact committed source identity is absent or mismatched`,
|
|
1462
1456
|
);
|
|
@@ -1475,7 +1469,7 @@ export function resolveDistributionFromInputs(
|
|
|
1475
1469
|
}
|
|
1476
1470
|
const declared = shippedPlatformServices.get(edge.requiredService);
|
|
1477
1471
|
if (!declared) {
|
|
1478
|
-
throw new
|
|
1472
|
+
throw new RefusalError(
|
|
1479
1473
|
`distribution "${id}": biome "${consumer.id}" API "${edge.consumerService}" requires platform service ` +
|
|
1480
1474
|
`"${edge.requiredService}", but the distribution explicitly excludes it`,
|
|
1481
1475
|
);
|
|
@@ -1529,7 +1523,7 @@ export function resolveDistributionFromInputs(
|
|
|
1529
1523
|
pullWebBiomesOf(entry);
|
|
1530
1524
|
for (const req of [...entry.requires].sort()) {
|
|
1531
1525
|
if (excludedBiomes.has(req)) {
|
|
1532
|
-
throw new
|
|
1526
|
+
throw new RefusalError(
|
|
1533
1527
|
`distribution "${id}": biome "${entry.id}" (${entry.repo}) requires "${req}", ` +
|
|
1534
1528
|
`but the distribution explicitly excludes it`,
|
|
1535
1529
|
);
|
|
@@ -1548,7 +1542,7 @@ export function resolveDistributionFromInputs(
|
|
|
1548
1542
|
if (
|
|
1549
1543
|
dependencies.some((dependency) => dependency.audience === 'operator')
|
|
1550
1544
|
) {
|
|
1551
|
-
throw new
|
|
1545
|
+
throw new RefusalError(
|
|
1552
1546
|
`distribution "${id}": biome "${entry.id}" (${entry.repo}) requires operator biome "${req}", ` +
|
|
1553
1547
|
`which must be selected explicitly by id`,
|
|
1554
1548
|
);
|
|
@@ -1568,7 +1562,7 @@ export function resolveDistributionFromInputs(
|
|
|
1568
1562
|
);
|
|
1569
1563
|
if (foreignDeps.length > 0 && foreignDeps.length === dependencies.length) {
|
|
1570
1564
|
const names = [...new Set(foreignDeps.map((d) => d.source))].sort();
|
|
1571
|
-
throw new
|
|
1565
|
+
throw new RefusalError(
|
|
1572
1566
|
`distribution "${id}": biome "${entry.id}" (${entry.repo}) requires "${req}", ` +
|
|
1573
1567
|
`which is drawn from source(s) ${names.map((name) => `"${name}"`).join(', ')} ` +
|
|
1574
1568
|
`that this edition does not declare in sources[]. Refusing rather than ` +
|
|
@@ -1592,14 +1586,14 @@ export function resolveDistributionFromInputs(
|
|
|
1592
1586
|
if (entry.dependencyContract !== undefined) {
|
|
1593
1587
|
for (const target of schemas.contractRequiredServiceTargets(entry.dependencyContract)) {
|
|
1594
1588
|
if (excludedPlatformServices.has(target)) {
|
|
1595
|
-
throw new
|
|
1589
|
+
throw new RefusalError(
|
|
1596
1590
|
`distribution "${id}": biome "${entry.id}" dependency contract requires service ` +
|
|
1597
1591
|
`"${target}", but the distribution explicitly excludes it`,
|
|
1598
1592
|
);
|
|
1599
1593
|
}
|
|
1600
1594
|
for (const owner of serverOwnersOfService.get(target) ?? []) {
|
|
1601
1595
|
if (excludedBiomes.has(owner.id)) {
|
|
1602
|
-
throw new
|
|
1596
|
+
throw new RefusalError(
|
|
1603
1597
|
`distribution "${id}": biome "${entry.id}" dependency contract requires service ` +
|
|
1604
1598
|
`"${target}" owned by explicitly excluded biome "${owner.id}"`,
|
|
1605
1599
|
);
|
|
@@ -1609,7 +1603,7 @@ export function resolveDistributionFromInputs(
|
|
|
1609
1603
|
}
|
|
1610
1604
|
for (const edge of entry.serviceDependencies ?? []) {
|
|
1611
1605
|
if (!entry.services.includes(edge.consumerService)) {
|
|
1612
|
-
throw new
|
|
1606
|
+
throw new RefusalError(
|
|
1613
1607
|
`distribution "${id}": biome "${entry.id}" service dependency names unknown consumer API ` +
|
|
1614
1608
|
`"${edge.consumerService}"`,
|
|
1615
1609
|
);
|
|
@@ -1620,7 +1614,7 @@ export function resolveDistributionFromInputs(
|
|
|
1620
1614
|
}
|
|
1621
1615
|
const dependency = requireBiomeOwner(entry, edge);
|
|
1622
1616
|
if (excludedBiomes.has(dependency.id)) {
|
|
1623
|
-
throw new
|
|
1617
|
+
throw new RefusalError(
|
|
1624
1618
|
`distribution "${id}": biome "${entry.id}" API "${edge.consumerService}" requires service ` +
|
|
1625
1619
|
`"${edge.requiredService}" owned by explicitly excluded biome "${dependency.id}"`,
|
|
1626
1620
|
);
|
|
@@ -1629,7 +1623,7 @@ export function resolveDistributionFromInputs(
|
|
|
1629
1623
|
dependency.audience === 'operator' &&
|
|
1630
1624
|
!includedIds.has(dependency.id)
|
|
1631
1625
|
) {
|
|
1632
|
-
throw new
|
|
1626
|
+
throw new RefusalError(
|
|
1633
1627
|
`distribution "${id}": biome "${entry.id}" API "${edge.consumerService}" requires service ` +
|
|
1634
1628
|
`"${edge.requiredService}" owned by operator biome "${dependency.id}", which must be selected explicitly`,
|
|
1635
1629
|
);
|
|
@@ -1659,7 +1653,7 @@ export function resolveDistributionFromInputs(
|
|
|
1659
1653
|
});
|
|
1660
1654
|
if (provider.id === entry.id) {continue;}
|
|
1661
1655
|
if (excludedBiomes.has(provider.id)) {
|
|
1662
|
-
throw new
|
|
1656
|
+
throw new RefusalError(
|
|
1663
1657
|
`distribution "${id}": biome "${entry.id}" service "${requirement.consumerService}" requires ` +
|
|
1664
1658
|
`${requirement.kind} "${requirement.logicalName}" provided by explicitly excluded biome "${provider.id}"`,
|
|
1665
1659
|
);
|
|
@@ -1720,7 +1714,7 @@ export function resolveDistributionFromInputs(
|
|
|
1720
1714
|
}
|
|
1721
1715
|
}
|
|
1722
1716
|
const refuseWidening = (consumer, via, pulledClass) => {
|
|
1723
|
-
throw new
|
|
1717
|
+
throw new RefusalError(
|
|
1724
1718
|
`distribution "${id}": biome "${consumer.id}" ${via}, which declares ` +
|
|
1725
1719
|
`installationClass "${pulledClass}" — "${consumer.id}" is in this edition only ` +
|
|
1726
1720
|
`through the class selector(s) [${[...admittedClasses].sort().join(', ')}], and what a ` +
|
|
@@ -1763,7 +1757,7 @@ export function resolveDistributionFromInputs(
|
|
|
1763
1757
|
const allow = new Set(trustPolicy.allow);
|
|
1764
1758
|
for (const e of selected.values()) {
|
|
1765
1759
|
if (!allow.has(e.origin)) {
|
|
1766
|
-
throw new
|
|
1760
|
+
throw new RefusalError(
|
|
1767
1761
|
`distribution "${id}": biome "${e.id}" (${e.repo}) has origin "${e.origin}", not in trustPolicy.allow [${[...allow].join(', ')}]`,
|
|
1768
1762
|
);
|
|
1769
1763
|
}
|
|
@@ -1796,6 +1790,7 @@ export function resolveDistributionFromInputs(
|
|
|
1796
1790
|
|
|
1797
1791
|
// (5c') Bundle signers — every OCI bundle row (a biome's, a host module's) states the signer the data root's
|
|
1798
1792
|
// trust policy selects for it. See `locked-bundle-signer.mjs`.
|
|
1793
|
+
const bundleSignerRefusals = [];
|
|
1799
1794
|
const bundleSignerOf = (e, bundleSource, componentKey) => {
|
|
1800
1795
|
const subject = { biomeId: e.id, ...(componentKey === undefined ? {} : { componentKey }), origin: e.origin, repo: e.repo, bundleSource };
|
|
1801
1796
|
if (composed?.bundleSigners === undefined) {
|
|
@@ -1803,7 +1798,8 @@ export function resolveDistributionFromInputs(
|
|
|
1803
1798
|
`${componentKey === undefined ? `biome "${e.id}"` : `host module "${componentKey}" of biome "${e.id}"`} is an OCI bundle, but this resolve was given no bundle signer selection`,
|
|
1804
1799
|
);
|
|
1805
1800
|
}
|
|
1806
|
-
|
|
1801
|
+
// Every row is selected before any is refused, so one run names them all; the lock is not built from a refused row.
|
|
1802
|
+
return captureRefusal(bundleSignerRefusals, () => composed.bundleSigners.signerFor(subject), isRefusal);
|
|
1807
1803
|
};
|
|
1808
1804
|
// A row drawn from a GIT source carries no OCI bundle (biome-level; an OCI host-module bundle is refused below), hence no bundleSigner: it is built
|
|
1809
1805
|
// from the pinned source, and the manifest's `bundleSource` names a release artifact not built from this commit.
|
|
@@ -2310,6 +2306,9 @@ export function resolveDistributionFromInputs(
|
|
|
2310
2306
|
// one (the kernel refuses an empty list), carried unchanged.
|
|
2311
2307
|
if (storeEventCeiling !== undefined) {lock.storeEventCeiling = storeEventCeiling;}
|
|
2312
2308
|
|
|
2309
|
+
// (6b) Every bundle signer refusal found while building the rows, together: none, and the lock is whole.
|
|
2310
|
+
throwCollectedRefusals(bundleSignerRefusals, BundleSignerRefusedError);
|
|
2311
|
+
|
|
2313
2312
|
// (7) Re-derive the ownership index through the KERNEL so the table this
|
|
2314
2313
|
// resolver emits is validated by the exact code every consumer will read
|
|
2315
2314
|
// it with (`capability-registry-api`, `xema-capability-router`). Catches
|
|
@@ -2321,21 +2320,24 @@ export function resolveDistributionFromInputs(
|
|
|
2321
2320
|
schemas.resolveFirstPartyCapabilityDomains(lock);
|
|
2322
2321
|
|
|
2323
2322
|
// (8) Validate against the BUILT kernel schema — fail-fast.
|
|
2324
|
-
|
|
2325
|
-
// (9) Admit every declared service-to-service operation requirement; a
|
|
2326
|
-
// refusal throws, so a lock with one is never returned to be written.
|
|
2327
|
-
// An optional operation left ungranted is reported, one line each, and
|
|
2328
|
-
// never fails (the resolve and `edition check` both run through here).
|
|
2329
|
-
assertOperationGrantsAdmitted(schemas, parsed, { report: (line) => console.log(`[operations] ${line}`) });
|
|
2330
|
-
assertOrganizationSweepsAdmitted(schemas, parsed);
|
|
2331
|
-
assertExactAudiencesAdmitted(schemas, parsed);
|
|
2332
|
-
assertOrgContextRelaysAdmitted(schemas, parsed);
|
|
2333
|
-
assertEventSubscriptionsAdmitted(schemas, parsed);
|
|
2334
|
-
assertInstalledOperationCeilingAdmitted(schemas, parsed);
|
|
2335
|
-
assertStoreWorkloadCeilingAdmitted(schemas, parsed);
|
|
2323
|
+
schemas.parseUnsealedDistributionLock(lock);
|
|
2336
2324
|
return validateImmutableDistributionLock(lock);
|
|
2337
2325
|
}
|
|
2338
2326
|
|
|
2327
|
+
/**
|
|
2328
|
+
* Admit every declared service-to-service operation requirement of a composed lock; a refusal throws, so a lock with
|
|
2329
|
+
* one is never returned to be written. An optional operation left ungranted is reported, one line each, and never
|
|
2330
|
+
* fails (the resolve and `edition check` both run through here). Admission only reads the lock.
|
|
2331
|
+
*/
|
|
2332
|
+
export const admitResolvedLock = (schemas, lock) => admitDistributionLock(schemas, lock, { report: (line) => console.log(`[operations] ${line}`) });
|
|
2333
|
+
|
|
2334
|
+
/** The lock composed over the inputs, admitted: what every release caller gets. */
|
|
2335
|
+
export function resolveDistributionFromInputs(id, index, schemas, composed, resolvedAt, platformServiceIndex) {
|
|
2336
|
+
const lock = resolveUnadmittedDistributionFromInputs(id, index, schemas, composed, resolvedAt, platformServiceIndex);
|
|
2337
|
+
admitResolvedLock(schemas, lock);
|
|
2338
|
+
return lock;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2339
2341
|
/**
|
|
2340
2342
|
* Produce a lock object for a distribution id (used by both the CLI and
|
|
2341
2343
|
* dist-check). `resolvedAt` is freshly stamped each call; callers that diff
|
|
@@ -2359,7 +2361,18 @@ export async function resolveDistribution(id) {
|
|
|
2359
2361
|
* `materialize` is the walk's one network seam, injectable for the contract
|
|
2360
2362
|
* suite exactly as it is on `walkDeclaredSources`.
|
|
2361
2363
|
*/
|
|
2362
|
-
export async function resolveDistributionWithIndex(id,
|
|
2364
|
+
export async function resolveDistributionWithIndex(id, options = {}) {
|
|
2365
|
+
const { lock, index, schemas } = await prepareDistributionWithIndex(id, options);
|
|
2366
|
+
admitResolvedLock(schemas, lock);
|
|
2367
|
+
return { lock, index };
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* `resolveDistributionWithIndex` up to, and not including, admission: the composed lock, its index and the kernel
|
|
2372
|
+
* schemas it was composed with. A develop resolve admits it itself, beside its other checks, so every refusal is
|
|
2373
|
+
* reported in one run; the lock bytes are the ones the admitting variant returns.
|
|
2374
|
+
*/
|
|
2375
|
+
export async function prepareDistributionWithIndex(id, { materialize, inputs, readManifest } = {}) {
|
|
2363
2376
|
const schemas = await loadKernelDistributionSchemas();
|
|
2364
2377
|
// The manifest is composed FIRST, because it is what declares the sources and
|
|
2365
2378
|
// what refuses a malformed definition before any other work. The walk then
|
|
@@ -2403,7 +2416,7 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs, re
|
|
|
2403
2416
|
inputs !== undefined && Object.hasOwn(inputs, 'storePublisherTrust')
|
|
2404
2417
|
? inputs.storePublisherTrust
|
|
2405
2418
|
: readStorePublisherTrust();
|
|
2406
|
-
const lock =
|
|
2419
|
+
const lock = resolveUnadmittedDistributionFromInputs(
|
|
2407
2420
|
id,
|
|
2408
2421
|
index,
|
|
2409
2422
|
schemas,
|
|
@@ -2416,7 +2429,7 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs, re
|
|
|
2416
2429
|
new Date().toISOString(),
|
|
2417
2430
|
platformServiceIndex,
|
|
2418
2431
|
);
|
|
2419
|
-
return { lock, index };
|
|
2432
|
+
return { lock, index, schemas };
|
|
2420
2433
|
}
|
|
2421
2434
|
|
|
2422
2435
|
/**
|
|
@@ -2434,7 +2447,7 @@ export function assertReleaseGitSourcesDeclareNoBundle(lock, index) {
|
|
|
2434
2447
|
(bundleSource) => bundleSource?.kind === 'oci',
|
|
2435
2448
|
);
|
|
2436
2449
|
if (declared !== undefined) {
|
|
2437
|
-
throw new
|
|
2450
|
+
throw new RefusalError(
|
|
2438
2451
|
`biome "${biome.id}" is drawn from git source "${biome.source.name}" and declares an OCI bundleSource (${declared.ociRef}); ` +
|
|
2439
2452
|
'a release does not carry it: declare the biome as an `oci` source with a third-party source pin instead',
|
|
2440
2453
|
);
|
|
@@ -2462,7 +2475,7 @@ export async function writeResolvedDistribution(id, { log = true, materialize, f
|
|
|
2462
2475
|
// edition; resolve-chart-set.mjs keeps every copy on the pin thereafter.
|
|
2463
2476
|
const pinPath = fleetChartSetLockPath();
|
|
2464
2477
|
if (!existsSync(pinPath)) {
|
|
2465
|
-
throw new
|
|
2478
|
+
throw new RefusalError(`${pinPath} is missing — run resolve-chart-set.mjs; an edition is not resolved without its chart set`);
|
|
2466
2479
|
}
|
|
2467
2480
|
const pinText = readFileSync(pinPath, 'utf8');
|
|
2468
2481
|
writeEditionChartSet(join(editionsRoot(), id), pinText);
|
|
@@ -2513,7 +2526,7 @@ export async function sealAuthorizationBase(id, lock, index, { log = true } = {}
|
|
|
2513
2526
|
*/
|
|
2514
2527
|
export async function composeAuthorizationBase(id, lock, index) {
|
|
2515
2528
|
if (!Array.isArray(index)) {
|
|
2516
|
-
throw new
|
|
2529
|
+
throw new RefusalError(
|
|
2517
2530
|
`${id}: sealAuthorizationBase needs the index the lock was resolved from ` +
|
|
2518
2531
|
'(resolveDistributionWithIndex). The committed platform index alone does not hold a source-walked biome.',
|
|
2519
2532
|
);
|