@xemahq/xema 0.45.0 → 0.46.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/commands/biome.d.ts.map +1 -1
- package/dist/commands/biome.js +3 -0
- package/dist/commands/biome.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +1613 -846
- package/dist/edition/verify-image-evidence.bundle.provenance.json +516 -474
- package/dist/runtime/biome-publish-image-layers.d.ts +20 -0
- package/dist/runtime/biome-publish-image-layers.d.ts.map +1 -0
- package/dist/runtime/biome-publish-image-layers.js +64 -0
- package/dist/runtime/biome-publish-image-layers.js.map +1 -0
- package/dist/runtime/biome-publish.d.ts +4 -0
- package/dist/runtime/biome-publish.d.ts.map +1 -1
- package/dist/runtime/biome-publish.js +25 -5
- package/dist/runtime/biome-publish.js.map +1 -1
- package/dist/runtime/config/schema.d.ts +4 -3
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/config/schema.js +8 -2
- package/dist/runtime/config/schema.js.map +1 -1
- package/dist/runtime/infra/infra-catalog.js +1 -1
- package/dist/runtime/installation/contract.d.ts +2 -2
- package/dist/runtime/installation/contract.d.ts.map +1 -1
- package/dist/runtime/installation/contract.js +1 -1
- package/dist/runtime/installation/contract.js.map +1 -1
- package/edition/README.md +2 -1
- package/edition/assemble-producer-input.mjs +2 -0
- package/edition/biome-content.mjs +6 -0
- package/edition/build-platform-service-index.mjs +2 -0
- package/edition/canonical-repository-url.mjs +12 -0
- package/edition/cosign-certificate-constraints.mjs +40 -0
- package/edition/customer-edition-verbs.mjs +6 -0
- package/edition/develop-coverage-readers.mjs +9 -4
- package/edition/develop-coverage-run.mjs +2 -1
- package/edition/develop-edition.mjs +20 -0
- package/edition/develop-record-ancestry.mjs +121 -0
- package/edition/develop-select-records.mjs +365 -0
- package/edition/distribution-evidence-set.mjs +15 -0
- package/edition/edition-config.mjs +27 -1
- package/edition/edition-protocol.mjs +8 -1
- package/edition/image-evidence.mjs +2 -12
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +104 -2
- package/edition/oci-evidence-verbs.mjs +7 -1
- package/edition/oci-image-source-evidence.mjs +129 -21
- package/edition/operation-grant-admission.mjs +98 -4
- package/edition/operation-requirements.mjs +118 -4
- package/edition/plan-image-evidence-refresh.mjs +21 -4
- package/edition/platform-service-class.mjs +23 -0
- package/edition/platform-service-ownership.mjs +23 -13
- package/edition/produce-distribution-completeness-evidence.mjs +2 -0
- package/edition/produce-image-source-evidence.mjs +9 -11
- package/edition/producer-evidence-bundle.mjs +43 -1
- package/edition/record-refused.mjs +11 -0
- package/edition/resolve-distribution.mjs +43 -1
- package/package.json +3 -3
package/edition/lib.mjs
CHANGED
|
@@ -40,9 +40,13 @@ import { isKernelCapabilityDomain } from './kernel-capability-domain.mjs';
|
|
|
40
40
|
import {
|
|
41
41
|
PRINCIPAL_PRIVILEGE_FIELDS,
|
|
42
42
|
assertPrivilegesHavePrincipals,
|
|
43
|
+
componentAudienceRequirements,
|
|
43
44
|
componentInstalledOperationRequests,
|
|
45
|
+
componentEventSubscriptionRequirements,
|
|
46
|
+
componentRelaysOrgContext,
|
|
44
47
|
componentOperationRequirements,
|
|
45
48
|
componentOptionalOperationRequirements,
|
|
49
|
+
componentOwnedAudienceNamespaces,
|
|
46
50
|
componentOrganizationSweepRequirements,
|
|
47
51
|
} from './operation-requirements.mjs';
|
|
48
52
|
import {
|
|
@@ -849,9 +853,12 @@ export async function loadKernelDistributionSchemas() {
|
|
|
849
853
|
distributionImagesPredicateChannelRefusal,
|
|
850
854
|
admitOperationGrants,
|
|
851
855
|
admitOrganizationSweeps,
|
|
856
|
+
admitExactAudiences,
|
|
852
857
|
contractRequiredServiceTargets,
|
|
853
858
|
admitInstalledOperationCeiling,
|
|
854
859
|
admitStoreWorkloadCeiling,
|
|
860
|
+
admitOrgContextRelays,
|
|
861
|
+
admitEventSubscriptions,
|
|
855
862
|
} = mod;
|
|
856
863
|
let topology;
|
|
857
864
|
try {
|
|
@@ -948,12 +955,15 @@ export async function loadKernelDistributionSchemas() {
|
|
|
948
955
|
// the resolve refuse through it (`assertOperationGrantsAdmitted`).
|
|
949
956
|
typeof admitOperationGrants !== 'function' ||
|
|
950
957
|
typeof admitOrganizationSweeps !== 'function' ||
|
|
958
|
+
typeof admitExactAudiences !== 'function' ||
|
|
951
959
|
typeof contractRequiredServiceTargets !== 'function' ||
|
|
952
960
|
// The installed-operation ceiling and the edition's store-workload ceiling,
|
|
953
961
|
// both admitted at the lock (`assertInstalledOperationCeilingAdmitted`,
|
|
954
962
|
// `assertStoreWorkloadCeilingAdmitted`).
|
|
955
963
|
typeof admitInstalledOperationCeiling !== 'function' ||
|
|
956
964
|
typeof admitStoreWorkloadCeiling !== 'function' ||
|
|
965
|
+
typeof admitOrgContextRelays !== 'function' ||
|
|
966
|
+
typeof admitEventSubscriptions !== 'function' ||
|
|
957
967
|
typeof distributionImagesPredicateLockRefusal !== 'function' ||
|
|
958
968
|
typeof distributionLockDigest !== 'function' ||
|
|
959
969
|
typeof imageDeliveryEntitlementRefusal !== 'function' ||
|
|
@@ -975,14 +985,23 @@ export async function loadKernelDistributionSchemas() {
|
|
|
975
985
|
`resolveRuntimeImagePlatforms/RuntimeImagePlatformKey/` +
|
|
976
986
|
`kernel contract-authority digest SSOT/compareLockedServiceResources/resolveNexusBindings/describeNexusBindingRefusal/canonicalNexusFacts/` +
|
|
977
987
|
`positive closure (resolveBiomeClosure/platformInvariantRoots/DistributionResolutionMode/provenance)/` +
|
|
978
|
-
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/admitInstalledOperationCeiling/admitStoreWorkloadCeiling/` +
|
|
988
|
+
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/admitExactAudiences/admitInstalledOperationCeiling/admitStoreWorkloadCeiling/admitOrgContextRelays/admitEventSubscriptions/` +
|
|
979
989
|
`defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/LockedStorePublisherSchema/` +
|
|
980
990
|
`resolveFirstPartyCapabilityDomains/DistributionVisibility/` +
|
|
981
991
|
`BiomeInstallPolicy/BiomeLaunchMode/ImageSourceHashAlgorithm/runtime-image enums — ` +
|
|
982
992
|
`the dist is stale; rebuild kernel-contracts.`,
|
|
983
993
|
);
|
|
984
994
|
}
|
|
995
|
+
// The build-record exports (absent from a kernel before the build-record contract): carried as-is, NOT required
|
|
996
|
+
// here, so every other verb still loads an older kernel. `select-records` refuses a kernel that lacks them.
|
|
997
|
+
const { BUILD_RECORD_MEDIA_TYPE, BuildRecordSchema, buildRecordTag, parseBuildRecordTag, compareBuildRecordRuns, parseBuildRecord } = mod;
|
|
985
998
|
return {
|
|
999
|
+
BUILD_RECORD_MEDIA_TYPE,
|
|
1000
|
+
BuildRecordSchema,
|
|
1001
|
+
buildRecordTag,
|
|
1002
|
+
parseBuildRecordTag,
|
|
1003
|
+
compareBuildRecordRuns,
|
|
1004
|
+
parseBuildRecord,
|
|
986
1005
|
parseDistribution,
|
|
987
1006
|
parseDistributionLock,
|
|
988
1007
|
parseDistributionLockForReading,
|
|
@@ -1061,9 +1080,12 @@ export async function loadKernelDistributionSchemas() {
|
|
|
1061
1080
|
distributionImagesPredicateChannelRefusal,
|
|
1062
1081
|
admitOperationGrants,
|
|
1063
1082
|
admitOrganizationSweeps,
|
|
1083
|
+
admitExactAudiences,
|
|
1064
1084
|
contractRequiredServiceTargets,
|
|
1065
1085
|
admitInstalledOperationCeiling,
|
|
1066
1086
|
admitStoreWorkloadCeiling,
|
|
1087
|
+
admitOrgContextRelays,
|
|
1088
|
+
admitEventSubscriptions,
|
|
1067
1089
|
distributionImagesPredicateLockRefusal,
|
|
1068
1090
|
imageDeliveryEntitlementRefusal,
|
|
1069
1091
|
producerWorkflowPinRefusal,
|
|
@@ -1695,7 +1717,7 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1695
1717
|
'resolve no worker image: a worker declares artifact.kind "oci-image" or "shared-oci-image"',
|
|
1696
1718
|
);
|
|
1697
1719
|
}
|
|
1698
|
-
// A privilege (`requiresOperations`, `optionalOperations`, `requestsInstalledOperations`, `requiresOrganizationSweeps`) is admitted for a lock workload
|
|
1720
|
+
// A privilege (`requiresOperations`, `optionalOperations`, `requestsInstalledOperations`, `requiresOrganizationSweeps`, `requiresAudiences`, `ownsAudienceNamespaces`) is admitted for a lock workload
|
|
1699
1721
|
// principal only; on any other component it is refused, never dropped.
|
|
1700
1722
|
assertPrivilegesHavePrincipals(components, imageComponents, rel);
|
|
1701
1723
|
// A biome must declare its images in EXACTLY ONE shape. Declaring both a
|
|
@@ -1750,10 +1772,22 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1750
1772
|
// organization` (`requestsInstalledOperations`), keyed like
|
|
1751
1773
|
// `operationRequirements`. Declared intent only: the lock admits the ceiling.
|
|
1752
1774
|
const installedOperationRequests = {};
|
|
1775
|
+
// The workload components that declare `relaysOrgContext`, in manifest order
|
|
1776
|
+
// (sorted at the write). Declared intent only: the lock admits it.
|
|
1777
|
+
const orgContextRelayers = [];
|
|
1778
|
+
// `component → the event subscription clauses it declares` (`subscribesEvents`),
|
|
1779
|
+
// keyed like `operationRequirements`. Declared intent only: the lock admits them.
|
|
1780
|
+
const eventSubscriptionRequirements = {};
|
|
1753
1781
|
// `component → the organization sweeps it may hold`
|
|
1754
1782
|
// (`requiresOrganizationSweeps`), keyed like `operationRequirements`.
|
|
1755
1783
|
// Declared intent only: the seal admits or refuses each one.
|
|
1756
1784
|
const organizationSweepRequirements = {};
|
|
1785
|
+
// `component → the exact audiences it requires` (`requiresAudiences`), keyed
|
|
1786
|
+
// like `operationRequirements`. Declared intent only: `admitExactAudiences`.
|
|
1787
|
+
const audienceRequirements = {};
|
|
1788
|
+
// The audience namespaces the biome's components own (`ownsAudienceNamespaces`),
|
|
1789
|
+
// ONE entry per namespace however many components list it.
|
|
1790
|
+
const ownedAudienceNamespaces = new Set();
|
|
1757
1791
|
// Nexus services this biome's workers PROVIDE and REQUIRE (worker protocol
|
|
1758
1792
|
// v2). Declared intent only, exactly like resources: the edition resolver
|
|
1759
1793
|
// binds each requirement to the ONE shipped provider of the same
|
|
@@ -1945,8 +1979,16 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1945
1979
|
if (optionalOperations !== null) {optionalOperationRequirements[name] = optionalOperations;}
|
|
1946
1980
|
const installed = componentInstalledOperationRequests(component, where);
|
|
1947
1981
|
if (installed !== null) {installedOperationRequests[name] = installed;}
|
|
1982
|
+
if (componentRelaysOrgContext(component, where)) {orgContextRelayers.push(name);}
|
|
1983
|
+
const subscriptions = componentEventSubscriptionRequirements(component, where);
|
|
1984
|
+
if (subscriptions !== null) {eventSubscriptionRequirements[name] = subscriptions;}
|
|
1948
1985
|
const sweeps = componentOrganizationSweepRequirements(component, where);
|
|
1949
1986
|
if (sweeps !== null) {organizationSweepRequirements[name] = sweeps;}
|
|
1987
|
+
const audiences = componentAudienceRequirements(component, where);
|
|
1988
|
+
if (audiences !== null) {audienceRequirements[name] = audiences;}
|
|
1989
|
+
for (const namespace of componentOwnedAudienceNamespaces(component, where) ?? []) {
|
|
1990
|
+
ownedAudienceNamespaces.add(namespace);
|
|
1991
|
+
}
|
|
1950
1992
|
const queues = componentWorkerTaskQueues(component, protocol, where);
|
|
1951
1993
|
if (queues !== null) {workerTaskQueues[name] = queues;}
|
|
1952
1994
|
const clients = componentWorkflowClients(protocol, where);
|
|
@@ -2400,6 +2442,22 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
2400
2442
|
),
|
|
2401
2443
|
}
|
|
2402
2444
|
: {}),
|
|
2445
|
+
// The workload components that relay an organization context; omitted when
|
|
2446
|
+
// none does.
|
|
2447
|
+
...(orgContextRelayers.length > 0
|
|
2448
|
+
? { orgContextRelayers: [...orgContextRelayers].sort(compareStrings) }
|
|
2449
|
+
: {}),
|
|
2450
|
+
// The event subscription clauses each workload component declares, in the
|
|
2451
|
+
// kernel's canonical order; omitted when none declares any.
|
|
2452
|
+
...(Object.keys(eventSubscriptionRequirements).length > 0
|
|
2453
|
+
? {
|
|
2454
|
+
eventSubscriptionRequirements: Object.fromEntries(
|
|
2455
|
+
Object.entries(eventSubscriptionRequirements).sort(([a], [b]) =>
|
|
2456
|
+
compareStrings(a, b),
|
|
2457
|
+
),
|
|
2458
|
+
),
|
|
2459
|
+
}
|
|
2460
|
+
: {}),
|
|
2403
2461
|
// The organization sweeps each workload component may hold; omitted when
|
|
2404
2462
|
// none declares any.
|
|
2405
2463
|
...(Object.keys(organizationSweepRequirements).length > 0
|
|
@@ -2411,6 +2469,21 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
2411
2469
|
),
|
|
2412
2470
|
}
|
|
2413
2471
|
: {}),
|
|
2472
|
+
// The exact audiences each workload component requires; omitted when none
|
|
2473
|
+
// declares any.
|
|
2474
|
+
...(Object.keys(audienceRequirements).length > 0
|
|
2475
|
+
? {
|
|
2476
|
+
audienceRequirements: Object.fromEntries(
|
|
2477
|
+
Object.entries(audienceRequirements).sort(([a], [b]) =>
|
|
2478
|
+
compareStrings(a, b),
|
|
2479
|
+
),
|
|
2480
|
+
),
|
|
2481
|
+
}
|
|
2482
|
+
: {}),
|
|
2483
|
+
// The audience namespaces this biome owns; omitted when it owns none.
|
|
2484
|
+
...(ownedAudienceNamespaces.size > 0
|
|
2485
|
+
? { ownsAudienceNamespaces: [...ownedAudienceNamespaces].sort(compareStrings) }
|
|
2486
|
+
: {}),
|
|
2414
2487
|
// The biome's signed dependency contract — DERIVED by the builder from the
|
|
2415
2488
|
// SDK seams its code calls, read from the committed generated index and
|
|
2416
2489
|
// validated there by the kernel schema. The edge set positive closure
|
|
@@ -2695,6 +2768,35 @@ export function buildCapabilityDomainOwners(index) {
|
|
|
2695
2768
|
.sort((a, b) => compareStrings(a.domain, b.domain));
|
|
2696
2769
|
}
|
|
2697
2770
|
|
|
2771
|
+
/**
|
|
2772
|
+
* The audience-namespace ownership table of the lock: namespace -> owning
|
|
2773
|
+
* biome, from every index entry's `ownsAudienceNamespaces`, like
|
|
2774
|
+
* {@link buildCapabilityDomainOwners} (the whole index, so a trimmed edition
|
|
2775
|
+
* names an unserved owner rather than an unowned audience). One row per
|
|
2776
|
+
* (namespace, biome): an entry repeated for one biome id (server and web
|
|
2777
|
+
* targets) adds nothing. Sorted by namespace.
|
|
2778
|
+
*
|
|
2779
|
+
* @throws when two biomes claim the same namespace.
|
|
2780
|
+
*/
|
|
2781
|
+
export function buildAudienceNamespaceOwners(index) {
|
|
2782
|
+
const owners = new Map();
|
|
2783
|
+
for (const entry of index) {
|
|
2784
|
+
for (const namespace of entry.ownsAudienceNamespaces ?? []) {
|
|
2785
|
+
const incumbent = owners.get(namespace);
|
|
2786
|
+
if (incumbent !== undefined && incumbent !== entry.id) {
|
|
2787
|
+
throw new Error(
|
|
2788
|
+
`audience namespace "${namespace}" is claimed by BOTH biome "${incumbent}" and biome "${entry.id}". ` +
|
|
2789
|
+
'A namespace has exactly one owner, so an ambiguous claim is refused.',
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
owners.set(namespace, entry.id);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return [...owners.entries()]
|
|
2796
|
+
.map(([namespace, biomeId]) => ({ namespace, biomeId }))
|
|
2797
|
+
.sort((a, b) => compareStrings(a.namespace, b.namespace));
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2698
2800
|
/** Deterministic ordering used for both the index and the lock. */
|
|
2699
2801
|
export function compareIndexEntries(a, b) {
|
|
2700
2802
|
const order = biomeTierOrder();
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
createRegistryClientResolver,
|
|
40
40
|
resolveImageSourceEvidenceReferrer,
|
|
41
41
|
validateMaterializedImageSourceEvidence,
|
|
42
|
+
writeInvalidReferrerReport,
|
|
42
43
|
} from './oci-image-source-evidence.mjs';
|
|
43
44
|
import { REFERRERS_DISTRIBUTION_SPEC } from './oras.mjs';
|
|
44
45
|
import { assertProducerInput, buildImageSourceStatements, cosignVerifyAttestationArgs, createCosignVerifier, readPinnedContractAuthority } from './produce-image-source-evidence.mjs';
|
|
@@ -216,6 +217,7 @@ export async function planOciEvidenceBackfill({
|
|
|
216
217
|
editionsRoot: editions,
|
|
217
218
|
producerRepositoryRoots,
|
|
218
219
|
schemas,
|
|
220
|
+
expectedContractAuthority,
|
|
219
221
|
registryClientFactory,
|
|
220
222
|
}) {
|
|
221
223
|
const gitIndex = indexCommittedProducerEvidence({
|
|
@@ -281,6 +283,8 @@ export async function planOciEvidenceBackfill({
|
|
|
281
283
|
imageRepository: item.imageRepository,
|
|
282
284
|
subjectDigest: item.subjectDigest,
|
|
283
285
|
schemas,
|
|
286
|
+
expectedContractAuthority,
|
|
287
|
+
onInvalidReferrer: writeInvalidReferrerReport,
|
|
284
288
|
});
|
|
285
289
|
const reported = {
|
|
286
290
|
subjectKind: item.subject.kind,
|
|
@@ -367,12 +371,14 @@ async function main() {
|
|
|
367
371
|
WorkspaceRoot.Kernel,
|
|
368
372
|
]);
|
|
369
373
|
const flags = parseEvidenceCliFlags(rest, { repeated: ['distribution', 'producer-repository-root'] });
|
|
374
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
370
375
|
const plan = await planOciEvidenceBackfill({
|
|
371
376
|
distributionIds: flags.requireMany('distribution'),
|
|
372
377
|
dataRoot: dataRoot(),
|
|
373
378
|
editionsRoot: editionsRoot(),
|
|
374
379
|
producerRepositoryRoots: flags.requireMany('producer-repository-root').map((root) => resolve(root)),
|
|
375
|
-
schemas
|
|
380
|
+
schemas,
|
|
381
|
+
expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
|
|
376
382
|
});
|
|
377
383
|
process.stdout.write(serializeJson(plan));
|
|
378
384
|
// 2: something is still to be produced or attached.
|
|
@@ -8,11 +8,13 @@ import { compareStrings } from './lib.mjs';
|
|
|
8
8
|
import { OrasRegistry } from './oras.mjs';
|
|
9
9
|
import { BUNDLE_MEDIA_TYPE } from './produce-image-source-evidence.mjs';
|
|
10
10
|
import {
|
|
11
|
+
EvidenceIntegrityError,
|
|
11
12
|
PRODUCER_EVIDENCE_INDEX_FILE,
|
|
12
13
|
PRODUCER_EVIDENCE_INDEX_MEDIA_TYPE,
|
|
13
14
|
PRODUCER_STATEMENT_MEDIA_TYPE,
|
|
14
15
|
producerEvidenceLayerFiles,
|
|
15
16
|
readProducerEvidenceDirectory,
|
|
17
|
+
readRawStatementAuthorityCommits,
|
|
16
18
|
} from './producer-evidence-bundle.mjs';
|
|
17
19
|
import {
|
|
18
20
|
loadRegistryCredentialDeclarations,
|
|
@@ -156,9 +158,55 @@ export function validateMaterializedImageSourceEvidence({
|
|
|
156
158
|
return { ...evidence, created };
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
function assertExpectedAuthorityCommit(value) {
|
|
162
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
163
|
+
throw new Error('the expected kernel contract authority commit is required');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The referrer is not usable evidence under the expected authority: it is skipped for its subject and reported, never thrown past the resolver. */
|
|
168
|
+
export class InvalidEvidenceReferrerError extends Error {
|
|
169
|
+
constructor({ artifactDigest, reason, sourceCommit = null, cause }) {
|
|
170
|
+
super(`evidence referrer ${artifactDigest} is invalid: ${reason}`, { cause });
|
|
171
|
+
this.name = 'InvalidEvidenceReferrerError';
|
|
172
|
+
this.artifactDigest = artifactDigest;
|
|
173
|
+
this.reason = reason;
|
|
174
|
+
this.sourceCommit = sourceCommit;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The artifact states a kernel contract authority other than the one evidence is read under. */
|
|
179
|
+
export class ForeignAuthorityReferrerError extends InvalidEvidenceReferrerError {
|
|
180
|
+
constructor({ artifactDigest, sourceCommit, expectedAuthorityCommit }) {
|
|
181
|
+
super({
|
|
182
|
+
artifactDigest,
|
|
183
|
+
sourceCommit,
|
|
184
|
+
reason: `stated under kernel contract authority ${sourceCommit}, not ${expectedAuthorityCommit}`,
|
|
185
|
+
});
|
|
186
|
+
this.name = 'ForeignAuthorityReferrerError';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** What a skipped referrer is reported as: the one sentence naming it and why it is not evidence. */
|
|
191
|
+
export function describeInvalidReferrer(skipped) {
|
|
192
|
+
return `referrer ${skipped.artifactDigest}: ${skipped.reason}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The one stderr line that names a skipped invalid referrer. */
|
|
196
|
+
export function writeInvalidReferrerReport(skipped) {
|
|
197
|
+
process.stderr.write(
|
|
198
|
+
`[evidence] skipped ${skipped.imageRepository}@${skipped.subjectDigest} ${describeInvalidReferrer(skipped)}\n`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
159
202
|
/**
|
|
160
203
|
* Fetch, content-address validate and decode one OCI evidence artifact.
|
|
161
204
|
* `getBlob` verifies every digest again, independently of the manifest.
|
|
205
|
+
* An artifact that is not valid evidence under `expectedAuthorityCommit`
|
|
206
|
+
* (a manifest that is not the exact subject's, RAW statements stating another authority commit, a
|
|
207
|
+
* failed strict parse) is refused with `InvalidEvidenceReferrerError`; a
|
|
208
|
+
* registry that cannot answer, or bytes that do not match their digests
|
|
209
|
+
* (`EvidenceIntegrityError`, a tampering signal), still throw.
|
|
162
210
|
*/
|
|
163
211
|
export async function readImageSourceEvidenceArtifact({
|
|
164
212
|
client,
|
|
@@ -166,43 +214,89 @@ export async function readImageSourceEvidenceArtifact({
|
|
|
166
214
|
subjectDigest,
|
|
167
215
|
artifactDigest,
|
|
168
216
|
schemas,
|
|
217
|
+
expectedAuthorityCommit,
|
|
169
218
|
}) {
|
|
219
|
+
assertExpectedAuthorityCommit(expectedAuthorityCommit);
|
|
170
220
|
const fetched = await client.getManifestByDigest(repository, artifactDigest);
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
221
|
+
const invalid = (error) =>
|
|
222
|
+
new InvalidEvidenceReferrerError({
|
|
223
|
+
artifactDigest,
|
|
224
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
225
|
+
cause: error,
|
|
226
|
+
});
|
|
227
|
+
let validated;
|
|
228
|
+
try {
|
|
229
|
+
validated = validateImageSourceEvidenceManifest({
|
|
230
|
+
manifest: fetched.document,
|
|
231
|
+
subjectDigest,
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw invalid(error);
|
|
235
|
+
}
|
|
236
|
+
const { created, layers } = validated;
|
|
175
237
|
const directory = mkdtempSync(join(tmpdir(), 'xema-oci-evidence-'));
|
|
176
238
|
try {
|
|
177
239
|
for (const layer of layers) {
|
|
178
240
|
const title = layer.annotations[OCI_TITLE_ANNOTATION];
|
|
179
241
|
const bytes = await client.getBlob(repository, layer.digest);
|
|
180
242
|
if (bytes.length !== layer.size || sha256Digest(bytes) !== layer.digest) {
|
|
181
|
-
throw new
|
|
243
|
+
throw new EvidenceIntegrityError(
|
|
244
|
+
`OCI evidence layer "${title}" does not match its descriptor`,
|
|
245
|
+
);
|
|
182
246
|
}
|
|
183
247
|
const path = join(directory, title);
|
|
184
248
|
mkdirSync(dirname(path), { recursive: true });
|
|
185
249
|
writeFileSync(path, bytes, { flag: 'wx', mode: 0o600 });
|
|
186
250
|
}
|
|
187
|
-
|
|
188
|
-
directory
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
251
|
+
try {
|
|
252
|
+
for (const sourceCommit of readRawStatementAuthorityCommits({ directory })) {
|
|
253
|
+
if (sourceCommit !== expectedAuthorityCommit) {
|
|
254
|
+
throw new ForeignAuthorityReferrerError({
|
|
255
|
+
artifactDigest,
|
|
256
|
+
sourceCommit,
|
|
257
|
+
expectedAuthorityCommit,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const evidence = validateMaterializedImageSourceEvidence({
|
|
262
|
+
directory,
|
|
263
|
+
manifest: fetched.document,
|
|
264
|
+
subjectDigest,
|
|
265
|
+
schemas,
|
|
266
|
+
});
|
|
267
|
+
return { ...evidence, artifactDigest, created };
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error instanceof InvalidEvidenceReferrerError || error instanceof EvidenceIntegrityError) {
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
throw invalid(error);
|
|
273
|
+
}
|
|
194
274
|
} finally {
|
|
195
275
|
rmSync(directory, { recursive: true, force: true });
|
|
196
276
|
}
|
|
197
277
|
}
|
|
198
278
|
|
|
199
|
-
/**
|
|
279
|
+
/**
|
|
280
|
+
* Resolve the newest valid evidence referrer deterministically. Every
|
|
281
|
+
* referrer of the subject is evaluated; one that is not valid evidence under
|
|
282
|
+
* `expectedContractAuthority` (foreign authority, bad statement, bad layers)
|
|
283
|
+
* is reported to `onInvalidReferrer` and skipped. It is never a fallback: an
|
|
284
|
+
* invalid referrer is not read as evidence, however old or new. Returns null
|
|
285
|
+
* when no referrer is valid; the handler has then named each invalid one.
|
|
286
|
+
*/
|
|
200
287
|
export async function resolveImageSourceEvidenceReferrer({
|
|
201
288
|
client,
|
|
202
289
|
imageRepository,
|
|
203
290
|
subjectDigest,
|
|
204
291
|
schemas,
|
|
292
|
+
expectedContractAuthority,
|
|
293
|
+
onInvalidReferrer,
|
|
205
294
|
}) {
|
|
295
|
+
const expectedAuthorityCommit = expectedContractAuthority?.sourceCommit;
|
|
296
|
+
assertExpectedAuthorityCommit(expectedAuthorityCommit);
|
|
297
|
+
if (typeof onInvalidReferrer !== 'function') {
|
|
298
|
+
throw new Error('an invalid-referrer report handler is required');
|
|
299
|
+
}
|
|
206
300
|
const { authority, repository } = splitImageRef(imageRepository);
|
|
207
301
|
if (client.authority !== authority) {
|
|
208
302
|
throw new Error(
|
|
@@ -224,16 +318,30 @@ export async function resolveImageSourceEvidenceReferrer({
|
|
|
224
318
|
if (!SHA256_DIGEST_PATTERN.test(descriptor.digest ?? '')) {
|
|
225
319
|
throw new Error('registry returned a non-canonical evidence referrer digest');
|
|
226
320
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
321
|
+
try {
|
|
322
|
+
candidates.push(
|
|
323
|
+
await readImageSourceEvidenceArtifact({
|
|
324
|
+
client,
|
|
325
|
+
repository,
|
|
326
|
+
subjectDigest,
|
|
327
|
+
artifactDigest: descriptor.digest,
|
|
328
|
+
schemas,
|
|
329
|
+
expectedAuthorityCommit,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (!(error instanceof InvalidEvidenceReferrerError)) {throw error;}
|
|
334
|
+
onInvalidReferrer({
|
|
335
|
+
imageRepository,
|
|
231
336
|
subjectDigest,
|
|
232
|
-
artifactDigest:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
337
|
+
artifactDigest: error.artifactDigest,
|
|
338
|
+
reason: error.reason,
|
|
339
|
+
sourceCommit: error.sourceCommit,
|
|
340
|
+
expectedAuthorityCommit,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
236
343
|
}
|
|
344
|
+
if (candidates.length === 0) {return null;}
|
|
237
345
|
candidates.sort((left, right) => {
|
|
238
346
|
const byCreated = compareStrings(right.created, left.created);
|
|
239
347
|
return byCreated || compareStrings(left.artifactDigest, right.artifactDigest);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export class OperationGrantAdmissionError extends Error {
|
|
15
15
|
/**
|
|
16
16
|
* @param {string} distributionId
|
|
17
|
-
* @param {ReadonlyArray<{code: string,
|
|
17
|
+
* @param {ReadonlyArray<{code: string, operation: string, message: string}>} refusals
|
|
18
18
|
*/
|
|
19
19
|
constructor(distributionId, refusals) {
|
|
20
20
|
super(
|
|
@@ -45,9 +45,11 @@ export function ungrantedOptionalOperationLines(distributionId, ungrantedOptiona
|
|
|
45
45
|
* {@link ungrantedOptionalOperationLines}; an ungranted optional operation never fails.
|
|
46
46
|
*/
|
|
47
47
|
export function assertOperationGrantsAdmitted(schemas, lock, { report } = {}) {
|
|
48
|
-
const { grants, refusals, ungrantedOptional } = schemas.admitOperationGrants(lock);
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const { grants, refusals, ungrantedOptional, platformServiceRefusals } = schemas.admitOperationGrants(lock);
|
|
49
|
+
// A platform service's refused requirement fails exactly like a biome component's, through the same error.
|
|
50
|
+
const allRefusals = [...refusals, ...platformServiceRefusals];
|
|
51
|
+
if (allRefusals.length > 0) {
|
|
52
|
+
throw new OperationGrantAdmissionError(lock.distributionId, allRefusals);
|
|
51
53
|
}
|
|
52
54
|
if (report !== undefined) {
|
|
53
55
|
for (const line of ungrantedOptionalOperationLines(lock.distributionId, ungrantedOptional)) {report(line);}
|
|
@@ -86,6 +88,37 @@ export function assertOrganizationSweepsAdmitted(schemas, lock) {
|
|
|
86
88
|
return grants;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
/** Thrown when one or more declared exact audiences are not admitted. */
|
|
92
|
+
export class ExactAudienceAdmissionError extends Error {
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} distributionId
|
|
95
|
+
* @param {ReadonlyArray<{code: string, message: string}>} refusals
|
|
96
|
+
*/
|
|
97
|
+
constructor(distributionId, refusals) {
|
|
98
|
+
super(
|
|
99
|
+
`distribution "${distributionId}" declares audience requirements that are not admitted:\n` +
|
|
100
|
+
refusals.map((refusal) => `[${refusal.code}] ${refusal.message}`).join('\n'),
|
|
101
|
+
);
|
|
102
|
+
this.name = 'ExactAudienceAdmissionError';
|
|
103
|
+
this.distributionId = distributionId;
|
|
104
|
+
this.refusals = refusals;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Admit every exact-audience requirement of `lock` through the kernel
|
|
110
|
+
* (`admitExactAudiences`), or throw {@link ExactAudienceAdmissionError} listing
|
|
111
|
+
* every refusal. Returns the admitted grants. The resolve and the seal call it
|
|
112
|
+
* beside {@link assertOperationGrantsAdmitted}.
|
|
113
|
+
*/
|
|
114
|
+
export function assertExactAudiencesAdmitted(schemas, lock) {
|
|
115
|
+
const { grants, refusals } = schemas.admitExactAudiences(lock);
|
|
116
|
+
if (refusals.length > 0) {
|
|
117
|
+
throw new ExactAudienceAdmissionError(lock.distributionId, refusals);
|
|
118
|
+
}
|
|
119
|
+
return grants;
|
|
120
|
+
}
|
|
121
|
+
|
|
89
122
|
/** Thrown when one or more requested installed operations are not admitted into the ceiling. */
|
|
90
123
|
export class InstalledOperationCeilingAdmissionError extends Error {
|
|
91
124
|
/**
|
|
@@ -147,3 +180,64 @@ export function assertStoreWorkloadCeilingAdmitted(schemas, lock) {
|
|
|
147
180
|
}
|
|
148
181
|
return ceiling;
|
|
149
182
|
}
|
|
183
|
+
|
|
184
|
+
/** Thrown when one or more declared organization-context relays are not admitted. */
|
|
185
|
+
export class OrgContextRelayAdmissionError extends Error {
|
|
186
|
+
/**
|
|
187
|
+
* @param {string} distributionId
|
|
188
|
+
* @param {ReadonlyArray<{code: string, biome: string, component: string, message: string}>} refusals
|
|
189
|
+
*/
|
|
190
|
+
constructor(distributionId, refusals) {
|
|
191
|
+
super(
|
|
192
|
+
`distribution "${distributionId}" declares organization-context relays that are not admitted:\n` +
|
|
193
|
+
refusals.map((refusal) => `[${refusal.code}] ${refusal.message}`).join('\n'),
|
|
194
|
+
);
|
|
195
|
+
this.name = 'OrgContextRelayAdmissionError';
|
|
196
|
+
this.distributionId = distributionId;
|
|
197
|
+
this.refusals = refusals;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Admit every `orgContextRelayers` entry of `lock` through the kernel
|
|
203
|
+
* (`admitOrgContextRelays`), or throw {@link OrgContextRelayAdmissionError}
|
|
204
|
+
* listing every refusal. Returns the admitted relay rows.
|
|
205
|
+
*/
|
|
206
|
+
export function assertOrgContextRelaysAdmitted(schemas, lock) {
|
|
207
|
+
const { relays, refusals } = schemas.admitOrgContextRelays(lock);
|
|
208
|
+
if (refusals.length > 0) {
|
|
209
|
+
throw new OrgContextRelayAdmissionError(lock.distributionId, refusals);
|
|
210
|
+
}
|
|
211
|
+
return relays;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Thrown when one or more declared event subscription clauses are not admitted. */
|
|
215
|
+
export class EventSubscriptionAdmissionError extends Error {
|
|
216
|
+
/**
|
|
217
|
+
* @param {string} distributionId
|
|
218
|
+
* @param {ReadonlyArray<{code: string, biome: string, component: string, message: string}>} refusals
|
|
219
|
+
*/
|
|
220
|
+
constructor(distributionId, refusals) {
|
|
221
|
+
super(
|
|
222
|
+
`distribution "${distributionId}" declares event subscriptions that are not admitted:\n` +
|
|
223
|
+
refusals.map((refusal) => `[${refusal.code}] ${refusal.message}`).join('\n'),
|
|
224
|
+
);
|
|
225
|
+
this.name = 'EventSubscriptionAdmissionError';
|
|
226
|
+
this.distributionId = distributionId;
|
|
227
|
+
this.refusals = refusals;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Admit every `eventSubscriptionRequirements` clause of `lock` through the
|
|
233
|
+
* kernel (`admitEventSubscriptions`), or throw
|
|
234
|
+
* {@link EventSubscriptionAdmissionError} listing every refusal. Returns the
|
|
235
|
+
* admission: the first-party grants and the installed ceilings.
|
|
236
|
+
*/
|
|
237
|
+
export function assertEventSubscriptionsAdmitted(schemas, lock) {
|
|
238
|
+
const admission = schemas.admitEventSubscriptions(lock);
|
|
239
|
+
if (admission.refusals.length > 0) {
|
|
240
|
+
throw new EventSubscriptionAdmissionError(lock.distributionId, admission.refusals);
|
|
241
|
+
}
|
|
242
|
+
return admission;
|
|
243
|
+
}
|