@xemahq/xema 0.45.1 → 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/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 +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 +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 +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
|
@@ -61,8 +61,9 @@ import { compareStrings } from './lib.mjs';
|
|
|
61
61
|
import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
62
62
|
import {
|
|
63
63
|
createRegistryClientResolver,
|
|
64
|
+
describeInvalidReferrer,
|
|
64
65
|
resolveImageSourceEvidenceReferrer,
|
|
65
|
-
|
|
66
|
+
writeInvalidReferrerReport,
|
|
66
67
|
} from './oci-image-source-evidence.mjs';
|
|
67
68
|
import { BUNDLE_MEDIA_TYPE, canonicalSignedBundleBytes, cosignVerifyAttestationArgs, validateProducedBundle } from './produce-image-source-evidence.mjs';
|
|
68
69
|
import { splitImageRef } from './registry-credentials.mjs';
|
|
@@ -495,14 +496,25 @@ export async function resolveProducerEvidence({
|
|
|
495
496
|
const { authority, repository } = splitImageRef(subject.imageRef);
|
|
496
497
|
const client = clientFor(authority);
|
|
497
498
|
const image = await client.getManifestByTag(repository, subject.contentHash);
|
|
499
|
+
const invalidReferrers = [];
|
|
498
500
|
const ociEvidence = await resolveImageSourceEvidenceReferrer({
|
|
499
501
|
client,
|
|
500
502
|
imageRepository: subject.imageRef,
|
|
501
503
|
subjectDigest: image.digest,
|
|
502
504
|
schemas,
|
|
503
505
|
expectedContractAuthority,
|
|
504
|
-
|
|
506
|
+
onInvalidReferrer: (skipped) => {
|
|
507
|
+
invalidReferrers.push(skipped);
|
|
508
|
+
writeInvalidReferrerReport(skipped);
|
|
509
|
+
},
|
|
505
510
|
});
|
|
511
|
+
if (ociEvidence === null && invalidReferrers.length > 0) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`no valid OCI image-source evidence referrer exists for "${subject.kind}:${subject.id}" ` +
|
|
514
|
+
`at ${subject.imageRef}@${image.digest}: ` +
|
|
515
|
+
invalidReferrers.map(describeInvalidReferrer).join('; '),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
506
518
|
if (ociEvidence === null) {
|
|
507
519
|
throw new Error(
|
|
508
520
|
`no OCI image-source evidence referrer exists for "${subject.kind}:${subject.id}" ` +
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
10
|
-
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
10
|
+
import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
|
|
11
11
|
import { dataRoot } from './edition-workspace.mjs';
|
|
12
12
|
import { isUnderSourcePrefix } from './git-source-auth.mjs';
|
|
13
13
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
@@ -49,6 +49,7 @@ const KEYS = Object.freeze([
|
|
|
49
49
|
'chartRegistry',
|
|
50
50
|
'chartPublishers',
|
|
51
51
|
'sourceRepositoryUrlPrefix',
|
|
52
|
+
'dataRootRepositoryUrl',
|
|
52
53
|
'sourceAuthUsername',
|
|
53
54
|
'evidenceTrustPolicy',
|
|
54
55
|
...Object.keys(SECTIONS),
|
|
@@ -187,6 +188,30 @@ function assertFirstPartyUrls(firstPartySources, prefix) {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/**
|
|
192
|
+
* The OPTIONAL `dataRootRepositoryUrl`: where the data root itself lives, in the canonical public HTTPS form
|
|
193
|
+
* and under `sourceRepositoryUrlPrefix` like a first-party repository. It names the record signer
|
|
194
|
+
* (the data root's reusable workflows); a data root that does not declare it simply cannot be named as one.
|
|
195
|
+
*/
|
|
196
|
+
function optionalDataRootRepositoryUrl(document, prefix) {
|
|
197
|
+
if (document.dataRootRepositoryUrl === undefined) {return {};}
|
|
198
|
+
const where = `${EDITION_WORKSPACE_CONFIG_FILE}: "dataRootRepositoryUrl"`;
|
|
199
|
+
let canonical;
|
|
200
|
+
try {
|
|
201
|
+
canonical = canonicalPublicHttpsRepositoryUrl(document.dataRootRepositoryUrl, where);
|
|
202
|
+
repositoryOwnerName(canonical, where);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
throw new EditionWorkspaceConfigError(err.message);
|
|
205
|
+
}
|
|
206
|
+
if (canonical !== document.dataRootRepositoryUrl) {
|
|
207
|
+
throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(document.dataRootRepositoryUrl)} is not in its canonical form (${canonical})`);
|
|
208
|
+
}
|
|
209
|
+
if (!isUnderSourcePrefix(canonical, prefix)) {
|
|
210
|
+
throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(canonical)} is not under sourceRepositoryUrlPrefix ${prefix}`);
|
|
211
|
+
}
|
|
212
|
+
return { dataRootRepositoryUrl: canonical };
|
|
213
|
+
}
|
|
214
|
+
|
|
190
215
|
/** Validate a parsed config document; returns the frozen, typed config. */
|
|
191
216
|
export function parseEditionWorkspaceConfig(document) {
|
|
192
217
|
if (document === null || typeof document !== 'object' || Array.isArray(document)) {
|
|
@@ -210,6 +235,7 @@ export function parseEditionWorkspaceConfig(document) {
|
|
|
210
235
|
imageRegistry: requireString(document, 'imageRegistry', IMAGE_REGISTRY, '<host>/<namespace> with no scheme or trailing slash'),
|
|
211
236
|
...optionalChartSet(document),
|
|
212
237
|
sourceRepositoryUrlPrefix,
|
|
238
|
+
...optionalDataRootRepositoryUrl(document, sourceRepositoryUrlPrefix),
|
|
213
239
|
sourceAuthUsername: requireSourceAuthUsername(document),
|
|
214
240
|
evidenceTrustPolicy: requireEvidenceTrustPolicy(document),
|
|
215
241
|
});
|
|
@@ -28,7 +28,7 @@ import { realpathSync } from 'node:fs';
|
|
|
28
28
|
|
|
29
29
|
import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
30
30
|
|
|
31
|
-
/** The edition protocols this library interprets: version -> content digest. v10..
|
|
31
|
+
/** The edition protocols this library interprets: version -> content digest. v10..v27 grow the kernel
|
|
32
32
|
* source closure only (entry subpaths and hashing unchanged), so one library speaks every row. */
|
|
33
33
|
export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
34
34
|
Object.freeze({ version: 9, contentDigest: '7cd6d760e7e7caaec3634a08c739debbdeeeaac4f1bad6791246a12f4f248e37' }),
|
|
@@ -41,6 +41,13 @@ export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
|
41
41
|
Object.freeze({ version: 16, contentDigest: '2e65c280a399304ebb438e4e9829bcb4688f36a3c6a5e8b35b679cf69c689f55' }),
|
|
42
42
|
Object.freeze({ version: 19, contentDigest: '66dab3dcf18c3a587f073808cc91165cc513eabb237b35439ff6cfe2b312b127' }),
|
|
43
43
|
Object.freeze({ version: 20, contentDigest: '22c0e4ba6ee2771e91516ea78806117b5e3e60b76e085b9a9b98e993c1de79f8' }),
|
|
44
|
+
Object.freeze({ version: 21, contentDigest: 'f6364cf4830dcd15f82fb316ec9c0f0c11ffbe45262d97d8621242117d02f2b8' }),
|
|
45
|
+
Object.freeze({ version: 22, contentDigest: '566a0c37ee8b25c22bb9a5979190508e479c87593790b4a9e481ab1db8f7bcd1' }),
|
|
46
|
+
Object.freeze({ version: 23, contentDigest: '96735bd71236b4d0a2cef8a59c4a8d4f6919c6b5e54654c4b2b01e22b9880e04' }),
|
|
47
|
+
Object.freeze({ version: 24, contentDigest: '6ae70f55db789f95e194f623525b30ffedc21478ec17d246336b9a2783b6c0db' }),
|
|
48
|
+
Object.freeze({ version: 25, contentDigest: '79916e652836c7f6c31a7284c083c4f11249c6f21ef0b1bf79efc9dc23684d4a' }),
|
|
49
|
+
Object.freeze({ version: 26, contentDigest: '8d1dcfca5c59ff7b5eee6ea84fec3d9d62a48f3260f4ddb27b6665c25056f968' }),
|
|
50
|
+
Object.freeze({ version: 27, contentDigest: 'd4807f6464508c1abb32f7de9754f2e1f12cd7f2be5fbf939b6a5175c01dd90b' }),
|
|
44
51
|
]);
|
|
45
52
|
|
|
46
53
|
/** Where a kernel package publishes its identity and its contract, package-relative. */
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from 'node:path';
|
|
18
18
|
import { TextDecoder } from 'node:util';
|
|
19
19
|
|
|
20
|
+
import { certificateConstraintArgs } from './cosign-certificate-constraints.mjs';
|
|
20
21
|
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
21
22
|
import { effectiveRepositoryPins, manifestSourcePins } from './edition-source-pins.mjs';
|
|
22
23
|
import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
@@ -804,21 +805,10 @@ export function verifySigstoreEvidenceDescriptor({
|
|
|
804
805
|
verifiedTrustedRootPath,
|
|
805
806
|
'--bundle',
|
|
806
807
|
verifiedBundlePath,
|
|
807
|
-
|
|
808
|
-
candidate.certificateIdentity,
|
|
809
|
-
'--certificate-oidc-issuer',
|
|
810
|
-
candidate.oidcIssuer,
|
|
808
|
+
...certificateConstraintArgs(candidate, verifiedWorkflowSha),
|
|
811
809
|
'--type',
|
|
812
810
|
predicateType,
|
|
813
811
|
];
|
|
814
|
-
args.push(
|
|
815
|
-
'--certificate-github-workflow-repository',
|
|
816
|
-
candidate.githubWorkflow.repository,
|
|
817
|
-
'--certificate-github-workflow-ref',
|
|
818
|
-
candidate.githubWorkflow.ref,
|
|
819
|
-
'--certificate-github-workflow-sha',
|
|
820
|
-
verifiedWorkflowSha,
|
|
821
|
-
);
|
|
822
812
|
execFileSync(trustedCosignPath, args, {
|
|
823
813
|
env: COSIGN_ENV,
|
|
824
814
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
4
|
+
import { kernelDistPath, kernelPackageDir } from './edition-workspace.mjs';
|
|
5
|
+
|
|
6
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The kind of signer this library produces and verifies: a keyless OIDC
|
|
10
|
+
* certificate, `SignerKind.OidcCertificate` of the built kernel. The producer
|
|
11
|
+
* inputs and the trust-policy rows the evidence verbs handle carry it.
|
|
12
|
+
*/
|
|
13
|
+
export function oidcCertificateSignerKind() {
|
|
14
|
+
assertKernelEditionProtocol(kernelPackageDir());
|
|
15
|
+
const { SignerKind } = requireFromHere(kernelDistPath('distribution', 'index.js'));
|
|
16
|
+
const kind = SignerKind?.OidcCertificate;
|
|
17
|
+
if (typeof kind !== 'string' || kind.length === 0) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`The built kernel distribution module at ${kernelDistPath('distribution', 'index.js')} does not export SignerKind.OidcCertificate; rebuild the kernel checkout.`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return kind;
|
|
23
|
+
}
|
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,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
|
});
|