@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.
Files changed (55) 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 +1664 -846
  5. package/dist/edition/verify-image-evidence.bundle.provenance.json +522 -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/dist/runtime/lock-loader.d.ts.map +1 -1
  23. package/dist/runtime/lock-loader.js +3 -3
  24. package/dist/runtime/lock-loader.js.map +1 -1
  25. package/edition/README.md +2 -1
  26. package/edition/assemble-producer-input.mjs +2 -0
  27. package/edition/biome-content.mjs +6 -0
  28. package/edition/build-platform-service-index.mjs +2 -0
  29. package/edition/canonical-repository-url.mjs +12 -0
  30. package/edition/cosign-certificate-constraints.mjs +40 -0
  31. package/edition/customer-edition-verbs.mjs +6 -0
  32. package/edition/develop-coverage-readers.mjs +6 -4
  33. package/edition/develop-edition.mjs +20 -0
  34. package/edition/develop-record-ancestry.mjs +121 -0
  35. package/edition/develop-select-records.mjs +365 -0
  36. package/edition/distribution-evidence-set.mjs +14 -2
  37. package/edition/edition-config.mjs +27 -1
  38. package/edition/edition-protocol.mjs +9 -1
  39. package/edition/image-evidence.mjs +2 -12
  40. package/edition/image-lock-service-rows.mjs +7 -5
  41. package/edition/kernel-signer-kind.mjs +23 -0
  42. package/edition/lib.mjs +107 -2
  43. package/edition/oci-evidence-verbs.mjs +2 -2
  44. package/edition/oci-image-source-evidence.mjs +84 -39
  45. package/edition/operation-grant-admission.mjs +98 -4
  46. package/edition/operation-requirements.mjs +118 -4
  47. package/edition/plan-image-evidence-refresh.mjs +20 -6
  48. package/edition/platform-service-class.mjs +23 -0
  49. package/edition/platform-service-ownership.mjs +23 -13
  50. package/edition/produce-distribution-completeness-evidence.mjs +2 -0
  51. package/edition/produce-image-source-evidence.mjs +9 -11
  52. package/edition/producer-evidence-bundle.mjs +12 -1
  53. package/edition/record-refused.mjs +11 -0
  54. package/edition/resolve-distribution.mjs +43 -1
  55. 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
- 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
  });
@@ -14,7 +14,7 @@
14
14
  export class OperationGrantAdmissionError extends Error {
15
15
  /**
16
16
  * @param {string} distributionId
17
- * @param {ReadonlyArray<{code: string, biome: string, component: string, operation: string, message: string}>} refusals
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
- if (refusals.length > 0) {
50
- throw new OperationGrantAdmissionError(lock.distributionId, refusals);
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
+ }