@xemahq/xema 0.45.0 → 0.46.0

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