@xemahq/xema 0.45.1 → 0.47.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 +1664 -846
- package/dist/edition/verify-image-evidence.bundle.provenance.json +522 -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/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/dist/runtime/lock-loader.d.ts.map +1 -1
- package/dist/runtime/lock-loader.js +3 -3
- package/dist/runtime/lock-loader.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 +6 -4
- 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 +14 -2
- package/edition/edition-config.mjs +27 -1
- package/edition/edition-protocol.mjs +9 -1
- package/edition/image-evidence.mjs +2 -12
- package/edition/image-lock-service-rows.mjs +7 -5
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +107 -2
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/oci-image-source-evidence.mjs +84 -39
- package/edition/operation-grant-admission.mjs +98 -4
- package/edition/operation-requirements.mjs +118 -4
- package/edition/plan-image-evidence-refresh.mjs +20 -6
- 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 +12 -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 {
|
|
@@ -796,6 +800,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
796
800
|
parseOciDigest,
|
|
797
801
|
parseGitTreeId,
|
|
798
802
|
parseImageLock,
|
|
803
|
+
serviceImageSubjects,
|
|
799
804
|
IMAGE_LOCK_WRITE_SCHEMA_VERSION,
|
|
800
805
|
ImageLockSchemaVersion,
|
|
801
806
|
parseImageProducerStatement,
|
|
@@ -849,9 +854,12 @@ export async function loadKernelDistributionSchemas() {
|
|
|
849
854
|
distributionImagesPredicateChannelRefusal,
|
|
850
855
|
admitOperationGrants,
|
|
851
856
|
admitOrganizationSweeps,
|
|
857
|
+
admitExactAudiences,
|
|
852
858
|
contractRequiredServiceTargets,
|
|
853
859
|
admitInstalledOperationCeiling,
|
|
854
860
|
admitStoreWorkloadCeiling,
|
|
861
|
+
admitOrgContextRelays,
|
|
862
|
+
admitEventSubscriptions,
|
|
855
863
|
} = mod;
|
|
856
864
|
let topology;
|
|
857
865
|
try {
|
|
@@ -885,6 +893,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
885
893
|
typeof parseOciDigest !== 'function' ||
|
|
886
894
|
typeof parseGitTreeId !== 'function' ||
|
|
887
895
|
typeof parseImageLock !== 'function' ||
|
|
896
|
+
typeof serviceImageSubjects !== 'function' ||
|
|
888
897
|
typeof parseImageProducerStatement !== 'function' ||
|
|
889
898
|
typeof parseDistributionImagesStatement !== 'function' ||
|
|
890
899
|
typeof parseImageDeliveryEntitlementStatement !== 'function' ||
|
|
@@ -948,12 +957,15 @@ export async function loadKernelDistributionSchemas() {
|
|
|
948
957
|
// the resolve refuse through it (`assertOperationGrantsAdmitted`).
|
|
949
958
|
typeof admitOperationGrants !== 'function' ||
|
|
950
959
|
typeof admitOrganizationSweeps !== 'function' ||
|
|
960
|
+
typeof admitExactAudiences !== 'function' ||
|
|
951
961
|
typeof contractRequiredServiceTargets !== 'function' ||
|
|
952
962
|
// The installed-operation ceiling and the edition's store-workload ceiling,
|
|
953
963
|
// both admitted at the lock (`assertInstalledOperationCeilingAdmitted`,
|
|
954
964
|
// `assertStoreWorkloadCeilingAdmitted`).
|
|
955
965
|
typeof admitInstalledOperationCeiling !== 'function' ||
|
|
956
966
|
typeof admitStoreWorkloadCeiling !== 'function' ||
|
|
967
|
+
typeof admitOrgContextRelays !== 'function' ||
|
|
968
|
+
typeof admitEventSubscriptions !== 'function' ||
|
|
957
969
|
typeof distributionImagesPredicateLockRefusal !== 'function' ||
|
|
958
970
|
typeof distributionLockDigest !== 'function' ||
|
|
959
971
|
typeof imageDeliveryEntitlementRefusal !== 'function' ||
|
|
@@ -975,14 +987,23 @@ export async function loadKernelDistributionSchemas() {
|
|
|
975
987
|
`resolveRuntimeImagePlatforms/RuntimeImagePlatformKey/` +
|
|
976
988
|
`kernel contract-authority digest SSOT/compareLockedServiceResources/resolveNexusBindings/describeNexusBindingRefusal/canonicalNexusFacts/` +
|
|
977
989
|
`positive closure (resolveBiomeClosure/platformInvariantRoots/DistributionResolutionMode/provenance)/` +
|
|
978
|
-
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/admitInstalledOperationCeiling/admitStoreWorkloadCeiling/` +
|
|
990
|
+
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/admitExactAudiences/admitInstalledOperationCeiling/admitStoreWorkloadCeiling/admitOrgContextRelays/admitEventSubscriptions/` +
|
|
979
991
|
`defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/LockedStorePublisherSchema/` +
|
|
980
992
|
`resolveFirstPartyCapabilityDomains/DistributionVisibility/` +
|
|
981
993
|
`BiomeInstallPolicy/BiomeLaunchMode/ImageSourceHashAlgorithm/runtime-image enums — ` +
|
|
982
994
|
`the dist is stale; rebuild kernel-contracts.`,
|
|
983
995
|
);
|
|
984
996
|
}
|
|
997
|
+
// The build-record exports (absent from a kernel before the build-record contract): carried as-is, NOT required
|
|
998
|
+
// here, so every other verb still loads an older kernel. `select-records` refuses a kernel that lacks them.
|
|
999
|
+
const { BUILD_RECORD_MEDIA_TYPE, BuildRecordSchema, buildRecordTag, parseBuildRecordTag, compareBuildRecordRuns, parseBuildRecord } = mod;
|
|
985
1000
|
return {
|
|
1001
|
+
BUILD_RECORD_MEDIA_TYPE,
|
|
1002
|
+
BuildRecordSchema,
|
|
1003
|
+
buildRecordTag,
|
|
1004
|
+
parseBuildRecordTag,
|
|
1005
|
+
compareBuildRecordRuns,
|
|
1006
|
+
parseBuildRecord,
|
|
986
1007
|
parseDistribution,
|
|
987
1008
|
parseDistributionLock,
|
|
988
1009
|
parseDistributionLockForReading,
|
|
@@ -1001,6 +1022,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
1001
1022
|
parseOciDigest,
|
|
1002
1023
|
parseGitTreeId,
|
|
1003
1024
|
parseImageLock,
|
|
1025
|
+
serviceImageSubjects,
|
|
1004
1026
|
IMAGE_LOCK_WRITE_SCHEMA_VERSION,
|
|
1005
1027
|
ImageLockSchemaVersion,
|
|
1006
1028
|
parseImageProducerStatement,
|
|
@@ -1061,9 +1083,12 @@ export async function loadKernelDistributionSchemas() {
|
|
|
1061
1083
|
distributionImagesPredicateChannelRefusal,
|
|
1062
1084
|
admitOperationGrants,
|
|
1063
1085
|
admitOrganizationSweeps,
|
|
1086
|
+
admitExactAudiences,
|
|
1064
1087
|
contractRequiredServiceTargets,
|
|
1065
1088
|
admitInstalledOperationCeiling,
|
|
1066
1089
|
admitStoreWorkloadCeiling,
|
|
1090
|
+
admitOrgContextRelays,
|
|
1091
|
+
admitEventSubscriptions,
|
|
1067
1092
|
distributionImagesPredicateLockRefusal,
|
|
1068
1093
|
imageDeliveryEntitlementRefusal,
|
|
1069
1094
|
producerWorkflowPinRefusal,
|
|
@@ -1695,7 +1720,7 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1695
1720
|
'resolve no worker image: a worker declares artifact.kind "oci-image" or "shared-oci-image"',
|
|
1696
1721
|
);
|
|
1697
1722
|
}
|
|
1698
|
-
// A privilege (`requiresOperations`, `optionalOperations`, `requestsInstalledOperations`, `requiresOrganizationSweeps`) is admitted for a lock workload
|
|
1723
|
+
// A privilege (`requiresOperations`, `optionalOperations`, `requestsInstalledOperations`, `requiresOrganizationSweeps`, `requiresAudiences`, `ownsAudienceNamespaces`) is admitted for a lock workload
|
|
1699
1724
|
// principal only; on any other component it is refused, never dropped.
|
|
1700
1725
|
assertPrivilegesHavePrincipals(components, imageComponents, rel);
|
|
1701
1726
|
// A biome must declare its images in EXACTLY ONE shape. Declaring both a
|
|
@@ -1750,10 +1775,22 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1750
1775
|
// organization` (`requestsInstalledOperations`), keyed like
|
|
1751
1776
|
// `operationRequirements`. Declared intent only: the lock admits the ceiling.
|
|
1752
1777
|
const installedOperationRequests = {};
|
|
1778
|
+
// The workload components that declare `relaysOrgContext`, in manifest order
|
|
1779
|
+
// (sorted at the write). Declared intent only: the lock admits it.
|
|
1780
|
+
const orgContextRelayers = [];
|
|
1781
|
+
// `component → the event subscription clauses it declares` (`subscribesEvents`),
|
|
1782
|
+
// keyed like `operationRequirements`. Declared intent only: the lock admits them.
|
|
1783
|
+
const eventSubscriptionRequirements = {};
|
|
1753
1784
|
// `component → the organization sweeps it may hold`
|
|
1754
1785
|
// (`requiresOrganizationSweeps`), keyed like `operationRequirements`.
|
|
1755
1786
|
// Declared intent only: the seal admits or refuses each one.
|
|
1756
1787
|
const organizationSweepRequirements = {};
|
|
1788
|
+
// `component → the exact audiences it requires` (`requiresAudiences`), keyed
|
|
1789
|
+
// like `operationRequirements`. Declared intent only: `admitExactAudiences`.
|
|
1790
|
+
const audienceRequirements = {};
|
|
1791
|
+
// The audience namespaces the biome's components own (`ownsAudienceNamespaces`),
|
|
1792
|
+
// ONE entry per namespace however many components list it.
|
|
1793
|
+
const ownedAudienceNamespaces = new Set();
|
|
1757
1794
|
// Nexus services this biome's workers PROVIDE and REQUIRE (worker protocol
|
|
1758
1795
|
// v2). Declared intent only, exactly like resources: the edition resolver
|
|
1759
1796
|
// binds each requirement to the ONE shipped provider of the same
|
|
@@ -1945,8 +1982,16 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1945
1982
|
if (optionalOperations !== null) {optionalOperationRequirements[name] = optionalOperations;}
|
|
1946
1983
|
const installed = componentInstalledOperationRequests(component, where);
|
|
1947
1984
|
if (installed !== null) {installedOperationRequests[name] = installed;}
|
|
1985
|
+
if (componentRelaysOrgContext(component, where)) {orgContextRelayers.push(name);}
|
|
1986
|
+
const subscriptions = componentEventSubscriptionRequirements(component, where);
|
|
1987
|
+
if (subscriptions !== null) {eventSubscriptionRequirements[name] = subscriptions;}
|
|
1948
1988
|
const sweeps = componentOrganizationSweepRequirements(component, where);
|
|
1949
1989
|
if (sweeps !== null) {organizationSweepRequirements[name] = sweeps;}
|
|
1990
|
+
const audiences = componentAudienceRequirements(component, where);
|
|
1991
|
+
if (audiences !== null) {audienceRequirements[name] = audiences;}
|
|
1992
|
+
for (const namespace of componentOwnedAudienceNamespaces(component, where) ?? []) {
|
|
1993
|
+
ownedAudienceNamespaces.add(namespace);
|
|
1994
|
+
}
|
|
1950
1995
|
const queues = componentWorkerTaskQueues(component, protocol, where);
|
|
1951
1996
|
if (queues !== null) {workerTaskQueues[name] = queues;}
|
|
1952
1997
|
const clients = componentWorkflowClients(protocol, where);
|
|
@@ -2400,6 +2445,22 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
2400
2445
|
),
|
|
2401
2446
|
}
|
|
2402
2447
|
: {}),
|
|
2448
|
+
// The workload components that relay an organization context; omitted when
|
|
2449
|
+
// none does.
|
|
2450
|
+
...(orgContextRelayers.length > 0
|
|
2451
|
+
? { orgContextRelayers: [...orgContextRelayers].sort(compareStrings) }
|
|
2452
|
+
: {}),
|
|
2453
|
+
// The event subscription clauses each workload component declares, in the
|
|
2454
|
+
// kernel's canonical order; omitted when none declares any.
|
|
2455
|
+
...(Object.keys(eventSubscriptionRequirements).length > 0
|
|
2456
|
+
? {
|
|
2457
|
+
eventSubscriptionRequirements: Object.fromEntries(
|
|
2458
|
+
Object.entries(eventSubscriptionRequirements).sort(([a], [b]) =>
|
|
2459
|
+
compareStrings(a, b),
|
|
2460
|
+
),
|
|
2461
|
+
),
|
|
2462
|
+
}
|
|
2463
|
+
: {}),
|
|
2403
2464
|
// The organization sweeps each workload component may hold; omitted when
|
|
2404
2465
|
// none declares any.
|
|
2405
2466
|
...(Object.keys(organizationSweepRequirements).length > 0
|
|
@@ -2411,6 +2472,21 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
2411
2472
|
),
|
|
2412
2473
|
}
|
|
2413
2474
|
: {}),
|
|
2475
|
+
// The exact audiences each workload component requires; omitted when none
|
|
2476
|
+
// declares any.
|
|
2477
|
+
...(Object.keys(audienceRequirements).length > 0
|
|
2478
|
+
? {
|
|
2479
|
+
audienceRequirements: Object.fromEntries(
|
|
2480
|
+
Object.entries(audienceRequirements).sort(([a], [b]) =>
|
|
2481
|
+
compareStrings(a, b),
|
|
2482
|
+
),
|
|
2483
|
+
),
|
|
2484
|
+
}
|
|
2485
|
+
: {}),
|
|
2486
|
+
// The audience namespaces this biome owns; omitted when it owns none.
|
|
2487
|
+
...(ownedAudienceNamespaces.size > 0
|
|
2488
|
+
? { ownsAudienceNamespaces: [...ownedAudienceNamespaces].sort(compareStrings) }
|
|
2489
|
+
: {}),
|
|
2414
2490
|
// The biome's signed dependency contract — DERIVED by the builder from the
|
|
2415
2491
|
// SDK seams its code calls, read from the committed generated index and
|
|
2416
2492
|
// validated there by the kernel schema. The edge set positive closure
|
|
@@ -2695,6 +2771,35 @@ export function buildCapabilityDomainOwners(index) {
|
|
|
2695
2771
|
.sort((a, b) => compareStrings(a.domain, b.domain));
|
|
2696
2772
|
}
|
|
2697
2773
|
|
|
2774
|
+
/**
|
|
2775
|
+
* The audience-namespace ownership table of the lock: namespace -> owning
|
|
2776
|
+
* biome, from every index entry's `ownsAudienceNamespaces`, like
|
|
2777
|
+
* {@link buildCapabilityDomainOwners} (the whole index, so a trimmed edition
|
|
2778
|
+
* names an unserved owner rather than an unowned audience). One row per
|
|
2779
|
+
* (namespace, biome): an entry repeated for one biome id (server and web
|
|
2780
|
+
* targets) adds nothing. Sorted by namespace.
|
|
2781
|
+
*
|
|
2782
|
+
* @throws when two biomes claim the same namespace.
|
|
2783
|
+
*/
|
|
2784
|
+
export function buildAudienceNamespaceOwners(index) {
|
|
2785
|
+
const owners = new Map();
|
|
2786
|
+
for (const entry of index) {
|
|
2787
|
+
for (const namespace of entry.ownsAudienceNamespaces ?? []) {
|
|
2788
|
+
const incumbent = owners.get(namespace);
|
|
2789
|
+
if (incumbent !== undefined && incumbent !== entry.id) {
|
|
2790
|
+
throw new Error(
|
|
2791
|
+
`audience namespace "${namespace}" is claimed by BOTH biome "${incumbent}" and biome "${entry.id}". ` +
|
|
2792
|
+
'A namespace has exactly one owner, so an ambiguous claim is refused.',
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
owners.set(namespace, entry.id);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
return [...owners.entries()]
|
|
2799
|
+
.map(([namespace, biomeId]) => ({ namespace, biomeId }))
|
|
2800
|
+
.sort((a, b) => compareStrings(a.namespace, b.namespace));
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2698
2803
|
/** Deterministic ordering used for both the index and the lock. */
|
|
2699
2804
|
export function compareIndexEntries(a, b) {
|
|
2700
2805
|
const order = biomeTierOrder();
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
createRegistryClientResolver,
|
|
40
40
|
resolveImageSourceEvidenceReferrer,
|
|
41
41
|
validateMaterializedImageSourceEvidence,
|
|
42
|
-
|
|
42
|
+
writeInvalidReferrerReport,
|
|
43
43
|
} from './oci-image-source-evidence.mjs';
|
|
44
44
|
import { REFERRERS_DISTRIBUTION_SPEC } from './oras.mjs';
|
|
45
45
|
import { assertProducerInput, buildImageSourceStatements, cosignVerifyAttestationArgs, createCosignVerifier, readPinnedContractAuthority } from './produce-image-source-evidence.mjs';
|
|
@@ -284,7 +284,7 @@ export async function planOciEvidenceBackfill({
|
|
|
284
284
|
subjectDigest: item.subjectDigest,
|
|
285
285
|
schemas,
|
|
286
286
|
expectedContractAuthority,
|
|
287
|
-
|
|
287
|
+
onInvalidReferrer: writeInvalidReferrerReport,
|
|
288
288
|
});
|
|
289
289
|
const reported = {
|
|
290
290
|
subjectKind: item.subject.kind,
|
|
@@ -8,6 +8,7 @@ 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,
|
|
@@ -163,33 +164,49 @@ function assertExpectedAuthorityCommit(value) {
|
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
/** The
|
|
167
|
-
export class
|
|
168
|
-
constructor({ artifactDigest, sourceCommit }) {
|
|
169
|
-
super(
|
|
170
|
-
|
|
171
|
-
);
|
|
172
|
-
this.name = 'ForeignAuthorityReferrerError';
|
|
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';
|
|
173
172
|
this.artifactDigest = artifactDigest;
|
|
173
|
+
this.reason = reason;
|
|
174
174
|
this.sourceCommit = sourceCommit;
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
/** The
|
|
179
|
-
export
|
|
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) {
|
|
180
197
|
process.stderr.write(
|
|
181
|
-
`[evidence] skipped ${skipped.imageRepository}@${skipped.subjectDigest}
|
|
182
|
-
`stated under kernel contract authority ${skipped.sourceCommit}, ` +
|
|
183
|
-
`not ${skipped.expectedAuthorityCommit}\n`,
|
|
198
|
+
`[evidence] skipped ${skipped.imageRepository}@${skipped.subjectDigest} ${describeInvalidReferrer(skipped)}\n`,
|
|
184
199
|
);
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
/**
|
|
188
203
|
* Fetch, content-address validate and decode one OCI evidence artifact.
|
|
189
204
|
* `getBlob` verifies every digest again, independently of the manifest.
|
|
190
|
-
* An artifact
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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.
|
|
193
210
|
*/
|
|
194
211
|
export async function readImageSourceEvidenceArtifact({
|
|
195
212
|
client,
|
|
@@ -201,44 +218,71 @@ export async function readImageSourceEvidenceArtifact({
|
|
|
201
218
|
}) {
|
|
202
219
|
assertExpectedAuthorityCommit(expectedAuthorityCommit);
|
|
203
220
|
const fetched = await client.getManifestByDigest(repository, artifactDigest);
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
237
|
const directory = mkdtempSync(join(tmpdir(), 'xema-oci-evidence-'));
|
|
209
238
|
try {
|
|
210
239
|
for (const layer of layers) {
|
|
211
240
|
const title = layer.annotations[OCI_TITLE_ANNOTATION];
|
|
212
241
|
const bytes = await client.getBlob(repository, layer.digest);
|
|
213
242
|
if (bytes.length !== layer.size || sha256Digest(bytes) !== layer.digest) {
|
|
214
|
-
throw new
|
|
243
|
+
throw new EvidenceIntegrityError(
|
|
244
|
+
`OCI evidence layer "${title}" does not match its descriptor`,
|
|
245
|
+
);
|
|
215
246
|
}
|
|
216
247
|
const path = join(directory, title);
|
|
217
248
|
mkdirSync(dirname(path), { recursive: true });
|
|
218
249
|
writeFileSync(path, bytes, { flag: 'wx', mode: 0o600 });
|
|
219
250
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
223
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);
|
|
224
273
|
}
|
|
225
|
-
const evidence = validateMaterializedImageSourceEvidence({
|
|
226
|
-
directory,
|
|
227
|
-
manifest: fetched.document,
|
|
228
|
-
subjectDigest,
|
|
229
|
-
schemas,
|
|
230
|
-
});
|
|
231
|
-
return { ...evidence, artifactDigest, created };
|
|
232
274
|
} finally {
|
|
233
275
|
rmSync(directory, { recursive: true, force: true });
|
|
234
276
|
}
|
|
235
277
|
}
|
|
236
278
|
|
|
237
279
|
/**
|
|
238
|
-
* Resolve the newest valid evidence referrer deterministically.
|
|
239
|
-
*
|
|
240
|
-
* `expectedContractAuthority
|
|
241
|
-
* and skipped.
|
|
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.
|
|
242
286
|
*/
|
|
243
287
|
export async function resolveImageSourceEvidenceReferrer({
|
|
244
288
|
client,
|
|
@@ -246,12 +290,12 @@ export async function resolveImageSourceEvidenceReferrer({
|
|
|
246
290
|
subjectDigest,
|
|
247
291
|
schemas,
|
|
248
292
|
expectedContractAuthority,
|
|
249
|
-
|
|
293
|
+
onInvalidReferrer,
|
|
250
294
|
}) {
|
|
251
295
|
const expectedAuthorityCommit = expectedContractAuthority?.sourceCommit;
|
|
252
296
|
assertExpectedAuthorityCommit(expectedAuthorityCommit);
|
|
253
|
-
if (typeof
|
|
254
|
-
throw new Error('
|
|
297
|
+
if (typeof onInvalidReferrer !== 'function') {
|
|
298
|
+
throw new Error('an invalid-referrer report handler is required');
|
|
255
299
|
}
|
|
256
300
|
const { authority, repository } = splitImageRef(imageRepository);
|
|
257
301
|
if (client.authority !== authority) {
|
|
@@ -286,11 +330,12 @@ export async function resolveImageSourceEvidenceReferrer({
|
|
|
286
330
|
}),
|
|
287
331
|
);
|
|
288
332
|
} catch (error) {
|
|
289
|
-
if (!(error instanceof
|
|
290
|
-
|
|
333
|
+
if (!(error instanceof InvalidEvidenceReferrerError)) {throw error;}
|
|
334
|
+
onInvalidReferrer({
|
|
291
335
|
imageRepository,
|
|
292
336
|
subjectDigest,
|
|
293
337
|
artifactDigest: error.artifactDigest,
|
|
338
|
+
reason: error.reason,
|
|
294
339
|
sourceCommit: error.sourceCommit,
|
|
295
340
|
expectedAuthorityCommit,
|
|
296
341
|
});
|
|
@@ -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
|
+
}
|