@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.
Files changed (51) hide show
  1. package/dist/commands/biome.d.ts.map +1 -1
  2. package/dist/commands/biome.js +3 -0
  3. package/dist/commands/biome.js.map +1 -1
  4. package/dist/edition/verify-image-evidence.bundle.mjs +1613 -846
  5. package/dist/edition/verify-image-evidence.bundle.provenance.json +516 -474
  6. package/dist/runtime/biome-publish-image-layers.d.ts +20 -0
  7. package/dist/runtime/biome-publish-image-layers.d.ts.map +1 -0
  8. package/dist/runtime/biome-publish-image-layers.js +64 -0
  9. package/dist/runtime/biome-publish-image-layers.js.map +1 -0
  10. package/dist/runtime/biome-publish.d.ts +4 -0
  11. package/dist/runtime/biome-publish.d.ts.map +1 -1
  12. package/dist/runtime/biome-publish.js +25 -5
  13. package/dist/runtime/biome-publish.js.map +1 -1
  14. package/dist/runtime/config/schema.d.ts +4 -3
  15. package/dist/runtime/config/schema.d.ts.map +1 -1
  16. package/dist/runtime/config/schema.js +8 -2
  17. package/dist/runtime/config/schema.js.map +1 -1
  18. package/dist/runtime/installation/contract.d.ts +2 -2
  19. package/dist/runtime/installation/contract.d.ts.map +1 -1
  20. package/dist/runtime/installation/contract.js +1 -1
  21. package/dist/runtime/installation/contract.js.map +1 -1
  22. package/edition/README.md +2 -1
  23. package/edition/assemble-producer-input.mjs +2 -0
  24. package/edition/biome-content.mjs +6 -0
  25. package/edition/build-platform-service-index.mjs +2 -0
  26. package/edition/canonical-repository-url.mjs +12 -0
  27. package/edition/cosign-certificate-constraints.mjs +40 -0
  28. package/edition/customer-edition-verbs.mjs +6 -0
  29. package/edition/develop-coverage-readers.mjs +6 -4
  30. package/edition/develop-edition.mjs +20 -0
  31. package/edition/develop-record-ancestry.mjs +121 -0
  32. package/edition/develop-select-records.mjs +365 -0
  33. package/edition/distribution-evidence-set.mjs +14 -2
  34. package/edition/edition-config.mjs +27 -1
  35. package/edition/edition-protocol.mjs +8 -1
  36. package/edition/image-evidence.mjs +2 -12
  37. package/edition/kernel-signer-kind.mjs +23 -0
  38. package/edition/lib.mjs +104 -2
  39. package/edition/oci-evidence-verbs.mjs +2 -2
  40. package/edition/oci-image-source-evidence.mjs +84 -39
  41. package/edition/operation-grant-admission.mjs +98 -4
  42. package/edition/operation-requirements.mjs +118 -4
  43. package/edition/plan-image-evidence-refresh.mjs +20 -6
  44. package/edition/platform-service-class.mjs +23 -0
  45. package/edition/platform-service-ownership.mjs +23 -13
  46. package/edition/produce-distribution-completeness-evidence.mjs +2 -0
  47. package/edition/produce-image-source-evidence.mjs +9 -11
  48. package/edition/producer-evidence-bundle.mjs +12 -1
  49. package/edition/record-refused.mjs +11 -0
  50. package/edition/resolve-distribution.mjs +43 -1
  51. 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
- writeForeignAuthorityReferrerReport,
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
- onForeignAuthorityReferrer: writeForeignAuthorityReferrerReport,
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..v20 grow the kernel
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
- '--certificate-identity',
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
- writeForeignAuthorityReferrerReport,
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
- onForeignAuthorityReferrer: writeForeignAuthorityReferrerReport,
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 artifact states a kernel contract authority other than the one evidence is read under. */
167
- export class ForeignAuthorityReferrerError extends Error {
168
- constructor({ artifactDigest, sourceCommit }) {
169
- super(
170
- `evidence referrer ${artifactDigest} is stated under kernel contract authority ${sourceCommit}`,
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 one stderr line that names a skipped foreign-authority referrer. */
179
- export function writeForeignAuthorityReferrerReport(skipped) {
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} referrer ${skipped.artifactDigest}: ` +
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 whose RAW statements state another authority commit than
191
- * `expectedAuthorityCommit` is refused with `ForeignAuthorityReferrerError`
192
- * before the strict parse, which is bound to the expected authority.
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 { created, layers } = validateImageSourceEvidenceManifest({
205
- manifest: fetched.document,
206
- subjectDigest,
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 Error(`OCI evidence layer "${title}" does not match its descriptor`);
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
- for (const sourceCommit of readRawStatementAuthorityCommits({ directory })) {
221
- if (sourceCommit !== expectedAuthorityCommit) {
222
- throw new ForeignAuthorityReferrerError({ artifactDigest, sourceCommit });
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. A referrer
239
- * stated under a different kernel contract authority is not evidence under
240
- * `expectedContractAuthority`: it is reported to `onForeignAuthorityReferrer`
241
- * and skipped. Every other defect of a referrer still throws.
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
- onForeignAuthorityReferrer,
293
+ onInvalidReferrer,
250
294
  }) {
251
295
  const expectedAuthorityCommit = expectedContractAuthority?.sourceCommit;
252
296
  assertExpectedAuthorityCommit(expectedAuthorityCommit);
253
- if (typeof onForeignAuthorityReferrer !== 'function') {
254
- throw new Error('a foreign-authority referrer report handler is required');
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 ForeignAuthorityReferrerError)) {throw error;}
290
- onForeignAuthorityReferrer({
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
  });