@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
@@ -6,6 +6,7 @@ import { compareStrings } from './string-order.mjs';
6
6
  const requireFromHere = createRequire(import.meta.url);
7
7
 
8
8
  let kernelPrivilegeGrammars;
9
+ let kernelAudienceGrammars;
9
10
 
10
11
  /**
11
12
  * The kernel's ONE operation-id grammar (`OperationIdSchema`) and closed
@@ -15,16 +16,54 @@ let kernelPrivilegeGrammars;
15
16
  function loadKernelPrivilegeGrammars() {
16
17
  if (kernelPrivilegeGrammars) {return kernelPrivilegeGrammars;}
17
18
  const mod = requireFromHere(kernelDistPath('distribution', 'index.js'));
18
- if (typeof mod.OperationIdSchema?.safeParse !== 'function' || typeof mod.PlatformSweepSchema?.safeParse !== 'function') {
19
+ if (
20
+ typeof mod.OperationIdSchema?.safeParse !== 'function' ||
21
+ typeof mod.PlatformSweepSchema?.safeParse !== 'function' ||
22
+ typeof mod.DeclaredEventSubscriptionClauseSchema?.safeParse !== 'function' ||
23
+ typeof mod.canonicalEventSubscriptionClauses !== 'function'
24
+ ) {
19
25
  throw new Error(
20
26
  `Built kernel distribution module at ${kernelDistPath('distribution', 'index.js')} does not export ` +
21
- 'OperationIdSchema and PlatformSweepSchema — the dist is stale; rebuild kernel-contracts at a commit that carries them.',
27
+ 'OperationIdSchema, PlatformSweepSchema, DeclaredEventSubscriptionClauseSchema and canonicalEventSubscriptionClauses — the dist is stale; rebuild kernel-contracts at a commit that carries them.',
22
28
  );
23
29
  }
24
30
  kernelPrivilegeGrammars = mod;
25
31
  return mod;
26
32
  }
27
33
 
34
+ /**
35
+ * The kernel's exact-audience and audience-namespace grammars, as ELEMENT schemas.
36
+ * The edition protocol exposes no subpath that exports the element grammars, so each is read
37
+ * from the one list schema the distribution subpath does export
38
+ * (`PlatformServiceSourceIdentitySchema.shape.requiresAudiences` / `.ownsAudienceNamespaces`):
39
+ * a value is graded as a one-element list, and the parsed element is returned.
40
+ */
41
+ function loadKernelAudienceGrammars() {
42
+ if (kernelAudienceGrammars) {return kernelAudienceGrammars;}
43
+ const mod = requireFromHere(kernelDistPath('distribution', 'index.js'));
44
+ const shape = mod.PlatformServiceSourceIdentitySchema?.shape;
45
+ const elementSchema = (listField) => {
46
+ const list = shape?.[listField]?.unwrap?.();
47
+ if (typeof list?.safeParse !== 'function') {
48
+ throw new Error(
49
+ `Built kernel distribution module at ${kernelDistPath('distribution', 'index.js')} has no PlatformServiceSourceIdentitySchema.${listField} — ` +
50
+ 'the dist is stale; rebuild kernel-contracts at a commit that carries the index-row audience fields.',
51
+ );
52
+ }
53
+ return {
54
+ safeParse: (value) => {
55
+ const parsed = list.safeParse([value]);
56
+ return parsed.success ? { success: true, data: parsed.data[0] } : parsed;
57
+ },
58
+ };
59
+ };
60
+ kernelAudienceGrammars = {
61
+ ExactAudienceSchema: elementSchema('requiresAudiences'),
62
+ AudienceNamespaceSchema: elementSchema('ownsAudienceNamespaces'),
63
+ };
64
+ return kernelAudienceGrammars;
65
+ }
66
+
28
67
  /**
29
68
  * One component's declared privilege list, as the index carries it: sorted,
30
69
  * unique, every entry graded by the kernel schema. `null` when undeclared. An
@@ -32,7 +71,7 @@ function loadKernelPrivilegeGrammars() {
32
71
  * reads the RAW manifest, and the kernel's component schema is not applied on
33
72
  * this path.
34
73
  */
35
- function declaredPrivilegeList(component, where, field, schema, what) {
74
+ export function declaredPrivilegeList(component, where, field, schema, what) {
36
75
  const declared = component[field];
37
76
  if (declared === undefined) {return null;}
38
77
  if (!Array.isArray(declared) || declared.length === 0) {
@@ -45,7 +84,7 @@ function declaredPrivilegeList(component, where, field, schema, what) {
45
84
  const parsed = schema.safeParse(value);
46
85
  if (!parsed.success) {
47
86
  throw new Error(
48
- `${where} ${field}[${position}] ${JSON.stringify(value)} is not ${what === 'operation id' ? 'an' : 'a'} ${what}: ` +
87
+ `${where} ${field}[${position}] ${JSON.stringify(value)} is not ${/^[aeiou]/.test(what) ? 'an' : 'a'} ${what}: ` +
49
88
  parsed.error.issues.map((issue) => issue.message).join('; '),
50
89
  );
51
90
  }
@@ -104,6 +143,77 @@ export function componentOrganizationSweepRequirements(component, where) {
104
143
  return declaredPrivilegeList(component, where, 'requiresOrganizationSweeps', PlatformSweepSchema, 'platform sweep');
105
144
  }
106
145
 
146
+ /**
147
+ * The exact audiences one workload component requires (`requiresAudiences`),
148
+ * graded by the kernel's exact-audience grammar (no wildcard, no prefix).
149
+ * Declared INTENT only; `admitExactAudiences` admits or refuses each.
150
+ */
151
+ export function componentAudienceRequirements(component, where) {
152
+ const { ExactAudienceSchema } = loadKernelAudienceGrammars();
153
+ return declaredPrivilegeList(component, where, 'requiresAudiences', ExactAudienceSchema, 'exact audience');
154
+ }
155
+
156
+ /**
157
+ * The audience namespaces one workload component owns (`ownsAudienceNamespaces`),
158
+ * graded by the kernel's namespace grammar.
159
+ */
160
+ export function componentOwnedAudienceNamespaces(component, where) {
161
+ const { AudienceNamespaceSchema } = loadKernelAudienceGrammars();
162
+ return declaredPrivilegeList(component, where, 'ownsAudienceNamespaces', AudienceNamespaceSchema, 'audience namespace');
163
+ }
164
+
165
+ /**
166
+ * The event subscription clauses one workload component declares
167
+ * (`subscribesEvents`), each graded by the kernel's clause grammar and returned
168
+ * in the kernel's canonical order, as the lock carries them
169
+ * (`eventSubscriptionRequirements`). An empty list, a repeated clause or an
170
+ * invalid one REFUSES naming the manifest; `null` when undeclared. Declared
171
+ * INTENT only; the lock admits it (`admitEventSubscriptions`).
172
+ */
173
+ export function componentEventSubscriptionRequirements(component, where) {
174
+ const { DeclaredEventSubscriptionClauseSchema, canonicalEventSubscriptionClauses } = loadKernelPrivilegeGrammars();
175
+ const declared = component.subscribesEvents;
176
+ if (declared === undefined) {return null;}
177
+ if (!Array.isArray(declared) || declared.length === 0) {
178
+ throw new Error(
179
+ `${where} declares subscribesEvents that is not a non-empty array of event subscription clauses — ` +
180
+ 'a list that names nothing states nothing, so omit the field',
181
+ );
182
+ }
183
+ const clauses = declared.map((value, position) => {
184
+ const parsed = DeclaredEventSubscriptionClauseSchema.safeParse(value);
185
+ if (!parsed.success) {
186
+ throw new Error(
187
+ `${where} subscribesEvents[${position}] ${JSON.stringify(value)} is not an event subscription clause: ` +
188
+ parsed.error.issues.map((issue) => issue.message).join('; '),
189
+ );
190
+ }
191
+ return parsed.data;
192
+ });
193
+ const canonical = canonicalEventSubscriptionClauses(clauses);
194
+ if (canonical.length !== clauses.length) {
195
+ throw new Error(`${where} subscribesEvents lists the same clause more than once`);
196
+ }
197
+ return canonical;
198
+ }
199
+
200
+ /**
201
+ * Whether one workload component declares `relaysOrgContext`: the privilege to
202
+ * relay an organization context its caller handed it. The only legal value is
203
+ * `true` (the kernel's component grammar); absent means it does not relay.
204
+ * Declared INTENT only; the lock admits it (`admitOrgContextRelays`).
205
+ */
206
+ export function componentRelaysOrgContext(component, where) {
207
+ const declared = component.relaysOrgContext;
208
+ if (declared === undefined) {return false;}
209
+ if (declared !== true) {
210
+ throw new Error(
211
+ `${where} declares relaysOrgContext ${JSON.stringify(declared)} — the only legal value is true; omit the field to not relay`,
212
+ );
213
+ }
214
+ return true;
215
+ }
216
+
107
217
  /**
108
218
  * The privilege declarations a component may carry only if it is a workload
109
219
  * principal in the lock.
@@ -112,7 +222,11 @@ export const PRINCIPAL_PRIVILEGE_FIELDS = Object.freeze([
112
222
  'requiresOperations',
113
223
  'optionalOperations',
114
224
  'requestsInstalledOperations',
225
+ 'relaysOrgContext',
226
+ 'subscribesEvents',
115
227
  'requiresOrganizationSweeps',
228
+ 'requiresAudiences',
229
+ 'ownsAudienceNamespaces',
116
230
  ]);
117
231
 
118
232
  /**
@@ -72,7 +72,9 @@ import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.
72
72
  import { compareStrings, loadKernelDistributionSchemas } from './lib.mjs';
73
73
  import {
74
74
  createRegistryClientResolver,
75
+ describeInvalidReferrer,
75
76
  resolveImageSourceEvidenceReferrer,
77
+ writeInvalidReferrerReport,
76
78
  } from './oci-image-source-evidence.mjs';
77
79
  import {
78
80
  loadProducerKernelSchemas,
@@ -168,7 +170,10 @@ export async function planImageEvidenceRefresh({
168
170
  // planner: measured that day, ALL 68 locked coordinates were absent from
169
171
  // ghcr, and the run died on the first of them and said nothing about the
170
172
  // other 67.
171
- let coordinateAbsence = null;
173
+ let unusableReason = null;
174
+ // Every referrer of this subject that is not valid evidence, as reported
175
+ // by the resolver. All of them invalid is a decided UNUSABLE verdict.
176
+ const invalidReferrers = [];
172
177
  {
173
178
  const { authority, repository } = splitImageRef(subject.imageRef);
174
179
  const client = clientFor(authority);
@@ -181,7 +186,7 @@ export async function planImageEvidenceRefresh({
181
186
  // that cannot decide must never report "nothing to do" — so it still
182
187
  // escapes and the run still exits 1.
183
188
  if (error?.status !== 404) {throw error;}
184
- coordinateAbsence =
189
+ unusableReason =
185
190
  `the registry has no image at the locked coordinate ` +
186
191
  `${subject.imageRef}:${subject.contentHash} — nothing was ever ` +
187
192
  `published there, so no producer evidence can attest it and the ` +
@@ -193,7 +198,19 @@ export async function planImageEvidenceRefresh({
193
198
  imageRepository: subject.imageRef,
194
199
  subjectDigest: image.digest,
195
200
  schemas,
201
+ expectedContractAuthority,
202
+ onInvalidReferrer: (skipped) => {
203
+ invalidReferrers.push(skipped);
204
+ writeInvalidReferrerReport(skipped);
205
+ },
196
206
  });
207
+ if (artifact === null && invalidReferrers.length > 0) {
208
+ unusableReason =
209
+ `every evidence referrer of ${subject.imageRef}@${image.digest} ` +
210
+ `(repo ${subject.repo}) is invalid under the required kernel contract ` +
211
+ `authority ${expectedContractAuthority.sourceCommit}: ` +
212
+ invalidReferrers.map(describeInvalidReferrer).join('; ');
213
+ }
197
214
  if (artifact !== null) {
198
215
  matches = artifact.records.filter(
199
216
  (record) =>
@@ -204,11 +221,11 @@ export async function planImageEvidenceRefresh({
204
221
  }
205
222
  }
206
223
  const verdict =
207
- coordinateAbsence !== null
224
+ unusableReason !== null
208
225
  ? {
209
226
  status: EvidenceRefreshStatus.Unusable,
210
227
  remainingSeconds: null,
211
- reason: coordinateAbsence,
228
+ reason: unusableReason,
212
229
  }
213
230
  : classifySubject({ matches, horizonSeconds, now });
214
231
  subjects.push({
@@ -1,4 +1,5 @@
1
1
  import { isKernelCapabilityDomain } from './kernel-capability-domain.mjs';
2
+ import { componentAudienceRequirements, componentOperationRequirements, componentOwnedAudienceNamespaces } from './operation-requirements.mjs';
2
3
  import { compareStrings } from './string-order.mjs';
3
4
 
4
5
  /**
@@ -54,6 +55,28 @@ export function sourceOwnedCapabilityDomains(sourceManifest, where) {
54
55
  return { ownsCapabilityDomains: sorted };
55
56
  }
56
57
 
58
+ /**
59
+ * What a platform service declares it requires and owns, as the index carries it, read from the
60
+ * same declaration as {@link sourceOwnedCapabilityDomains} (the `xema` block of its source manifest):
61
+ * `requiresOperations`, `requiresAudiences` and `ownsAudienceNamespaces`. Each is sorted, unique and
62
+ * non-empty, or no key at all. Declared INTENT: the lock admits or refuses each
63
+ * (`admitOperationGrants`, `admitExactAudiences`).
64
+ *
65
+ * @param {unknown} sourceManifest the parsed source manifest
66
+ * @param {string} where names the manifest in a refusal
67
+ */
68
+ export function sourceDeclaredRequirements(sourceManifest, where) {
69
+ const xema = sourceManifest?.xema ?? {};
70
+ const requiresOperations = componentOperationRequirements(xema, where);
71
+ const requiresAudiences = componentAudienceRequirements(xema, where);
72
+ const ownsAudienceNamespaces = componentOwnedAudienceNamespaces(xema, where);
73
+ return {
74
+ ...(requiresOperations === null ? {} : { requiresOperations }),
75
+ ...(requiresAudiences === null ? {} : { requiresAudiences }),
76
+ ...(ownsAudienceNamespaces === null ? {} : { ownsAudienceNamespaces }),
77
+ };
78
+ }
79
+
57
80
  /** The (name, tier, declared class) a declaration and an index entry must agree on. */
58
81
  export function comparablePlatformService(entry) {
59
82
  return { name: entry.name, tier: entry.tier, ...declaredInstallationClass(entry) };
@@ -1,6 +1,14 @@
1
1
  import { sourcePath } from './edition-workspace.mjs';
2
2
  import { readGitJsonAtCommit, serializeJson, sha256Hex } from './lib.mjs';
3
- import { sourceOwnedCapabilityDomains } from './platform-service-class.mjs';
3
+ import { sourceDeclaredRequirements, sourceOwnedCapabilityDomains } from './platform-service-class.mjs';
4
+
5
+ /** The index-row fields re-derived from a service's own source manifest and compared on read. */
6
+ const DERIVED_ROW_FIELDS = Object.freeze([
7
+ 'ownsCapabilityDomains',
8
+ 'requiresOperations',
9
+ 'requiresAudiences',
10
+ 'ownsAudienceNamespaces',
11
+ ]);
4
12
 
5
13
  /** Why a platform service's capability-domain ownership was refused. */
6
14
  export const PlatformServiceOwnershipFault = Object.freeze({
@@ -8,7 +16,7 @@ export const PlatformServiceOwnershipFault = Object.freeze({
8
16
  ManifestUnavailable: 'manifest-unavailable',
9
17
  /** The manifest bytes do not hash to the index entry's `sourceManifestHash`. */
10
18
  ManifestHashMismatch: 'manifest-hash-mismatch',
11
- /** The index entry's `ownsCapabilityDomains` differs from the source manifest's. */
19
+ /** An index entry's ownership or requirement field differs from the source manifest's. */
12
20
  OwnershipMismatch: 'ownership-mismatch',
13
21
  });
14
22
 
@@ -38,7 +46,7 @@ export function readPinnedSourceManifest(entry) {
38
46
  }
39
47
 
40
48
  /**
41
- * Re-derive each platform service's `ownsCapabilityDomains` from its pinned
49
+ * Re-derive each platform service's ownership and requirement fields (`DERIVED_ROW_FIELDS`) from its pinned
42
50
  * source manifest and refuse the index when an entry disagrees. The index is a
43
51
  * hand-editable file; the SERVICE's own manifest is the authority for which
44
52
  * service owns a domain, and so for where grants land.
@@ -61,17 +69,19 @@ export function verifiedPlatformServiceIndex(index, { names, readManifest = read
61
69
  `but the index pins sourceManifestHash ${entry.sourceManifestHash}`,
62
70
  );
63
71
  }
64
- const derived = sourceOwnedCapabilityDomains(manifest, `${entry.path} at ${entry.sourceCommit}`);
65
- const claimed = entry.ownsCapabilityDomains;
66
- if (serializeJson(claimed ?? null) !== serializeJson(derived.ownsCapabilityDomains ?? null)) {
67
- throw new PlatformServiceOwnershipError(
68
- PlatformServiceOwnershipFault.OwnershipMismatch,
69
- `platform service "${entry.name}": the index claims ownsCapabilityDomains ${JSON.stringify(claimed ?? [])}, ` +
70
- `its source manifest declares ${JSON.stringify(derived.ownsCapabilityDomains ?? [])} ` +
71
- `(sourceManifestHash ${entry.sourceManifestHash})`,
72
- );
72
+ const where = `${entry.path} at ${entry.sourceCommit}`;
73
+ const derived = { ...sourceOwnedCapabilityDomains(manifest, where), ...sourceDeclaredRequirements(manifest, where) };
74
+ for (const field of DERIVED_ROW_FIELDS) {
75
+ if (serializeJson(entry[field] ?? null) !== serializeJson(derived[field] ?? null)) {
76
+ throw new PlatformServiceOwnershipError(
77
+ PlatformServiceOwnershipFault.OwnershipMismatch,
78
+ `platform service "${entry.name}": the index claims ${field} ${JSON.stringify(entry[field] ?? [])}, ` +
79
+ `its source manifest declares ${JSON.stringify(derived[field] ?? [])} ` +
80
+ `(sourceManifestHash ${entry.sourceManifestHash})`,
81
+ );
82
+ }
73
83
  }
74
- const { ownsCapabilityDomains: _claimed, ...rest } = entry;
84
+ const rest = Object.fromEntries(Object.entries(entry).filter(([key]) => !DERIVED_ROW_FIELDS.includes(key)));
75
85
  return { ...rest, ...derived };
76
86
  });
77
87
  return { ...index, services };
@@ -37,6 +37,7 @@ import {
37
37
  isEntryModule,
38
38
  } from './edition-workspace.mjs';
39
39
  import { canonicalJsonBytes } from './image-evidence.mjs';
40
+ import { oidcCertificateSignerKind } from './kernel-signer-kind.mjs';
40
41
  import { serializeJson } from './lib.mjs';
41
42
  import {
42
43
  assertGitHubProducerContext,
@@ -52,6 +53,7 @@ const COMPLETENESS_BUNDLE_PATH = 'completeness/bundle.sigstore.json';
52
53
  /** The keyless signer a GitHub workflow is: its certificate identity and issuer. */
53
54
  export function githubWorkflowSigner({ repository, ref, path }) {
54
55
  return {
56
+ kind: oidcCertificateSignerKind(),
55
57
  certificateIdentity: `https://github.com/${repository}/${path}@${ref}`,
56
58
  oidcIssuer: 'https://token.actions.githubusercontent.com',
57
59
  githubWorkflow: { repository, ref, path },
@@ -25,6 +25,7 @@ import {
25
25
  import { isAbsolute, join, resolve } from 'node:path';
26
26
  import { pathToFileURL } from 'node:url';
27
27
 
28
+ import { certificateConstraintArgs } from './cosign-certificate-constraints.mjs';
28
29
  import { editionWorkspaceConfig } from './edition-config.mjs';
29
30
  import {
30
31
  WorkspaceRoot,
@@ -50,6 +51,7 @@ import {
50
51
  sha256Digest,
51
52
  } from './image-evidence.mjs';
52
53
  import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
54
+ import { oidcCertificateSignerKind } from './kernel-signer-kind.mjs';
53
55
  import { serializeJson } from './lib.mjs';
54
56
  import { assertStagedExactly, reserveSealing, sealAccepted } from './sealed-evidence-accept.mjs';
55
57
  import { BUNDLE_MEDIA_TYPE } from './sigstore-bundle-media-type.mjs';
@@ -219,7 +221,7 @@ function artifactStem(subject) {
219
221
  function assertSigner(signer) {
220
222
  exactKeys(
221
223
  signer,
222
- ['certificateIdentity', 'oidcIssuer', 'githubWorkflow'],
224
+ ['kind', 'certificateIdentity', 'oidcIssuer', 'githubWorkflow'],
223
225
  'image-source producer signer',
224
226
  );
225
227
  exactKeys(
@@ -238,6 +240,11 @@ function assertSigner(signer) {
238
240
  throw new Error('image-source producer GitHub workflow is not canonical');
239
241
  }
240
242
  const expectedIdentity = `https://github.com/${repository}/${path}@${ref}`;
243
+ if (signer.kind !== oidcCertificateSignerKind()) {
244
+ throw new Error(
245
+ `image-source producer signer kind must be "${oidcCertificateSignerKind()}"`,
246
+ );
247
+ }
241
248
  if (
242
249
  signer.certificateIdentity !== expectedIdentity ||
243
250
  signer.oidcIssuer !== 'https://token.actions.githubusercontent.com'
@@ -514,16 +521,7 @@ export function cosignVerifyAttestationArgs({ bundlePath, signer, certificateWor
514
521
  '--new-bundle-format',
515
522
  '--bundle',
516
523
  bundlePath,
517
- '--certificate-identity',
518
- signer.certificateIdentity,
519
- '--certificate-oidc-issuer',
520
- signer.oidcIssuer,
521
- '--certificate-github-workflow-repository',
522
- signer.githubWorkflow.repository,
523
- '--certificate-github-workflow-ref',
524
- signer.githubWorkflow.ref,
525
- '--certificate-github-workflow-sha',
526
- certificateWorkflowSha,
524
+ ...certificateConstraintArgs(signer, certificateWorkflowSha),
527
525
  '--type',
528
526
  predicateType,
529
527
  ];
@@ -62,6 +62,48 @@ export function producerStatementMatchesInput(statement, expectedStatement) {
62
62
  );
63
63
  }
64
64
 
65
+ /**
66
+ * The RAW `predicate.contractAuthority.sourceCommit` of every statement a
67
+ * producer evidence directory carries, read BEFORE any schema parse: the parse
68
+ * is bound to one kernel authority, so deciding whose authority a statement
69
+ * speaks under cannot go through it. A statement that states no string commit
70
+ * contributes nothing here; the strict parse refuses it later.
71
+ */
72
+ export function readRawStatementAuthorityCommits({ directory }) {
73
+ const index = JSON.parse(
74
+ decodeExactUtf8(
75
+ readBounded(directory, PRODUCER_EVIDENCE_INDEX_FILE, 'producer evidence index'),
76
+ 'producer evidence index',
77
+ ),
78
+ );
79
+ if (!Array.isArray(index?.producerEvidence)) {
80
+ throw new Error('producer evidence index is not non-empty schemaVersion 1');
81
+ }
82
+ const commits = new Set();
83
+ for (const entry of index.producerEvidence) {
84
+ const statement = JSON.parse(
85
+ decodeExactUtf8(
86
+ readBounded(directory, entry?.evidence?.statementPath, 'producer statement'),
87
+ 'producer statement',
88
+ ),
89
+ );
90
+ const commit = statement?.predicate?.contractAuthority?.sourceCommit;
91
+ if (typeof commit === 'string') {commits.add(commit);}
92
+ }
93
+ return commits;
94
+ }
95
+
96
+ /**
97
+ * Evidence bytes that do not match the digests that describe them: a tampering
98
+ * signal. It is never an invalid-but-skippable referrer; it fails closed.
99
+ */
100
+ export class EvidenceIntegrityError extends Error {
101
+ constructor(message) {
102
+ super(message);
103
+ this.name = 'EvidenceIntegrityError';
104
+ }
105
+ }
106
+
65
107
  /** Read and structurally validate one producer evidence directory. */
66
108
  export function readProducerEvidenceDirectory({ directory, schemas }) {
67
109
  if (!isAbsolute(directory)) {
@@ -119,7 +161,7 @@ export function readProducerEvidenceDirectory({ directory, schemas }) {
119
161
  sha256Digest(statementBytes) !== descriptor.statementDigest ||
120
162
  sha256Digest(bundleBytes) !== descriptor.bundleDigest
121
163
  ) {
122
- throw new Error(`${key} artifacts do not match their descriptor`);
164
+ throw new EvidenceIntegrityError(`${key} artifacts do not match their descriptor`);
123
165
  }
124
166
  validateProducedBundle(statementBytes, bundleBytes);
125
167
  const statement = schemas.parseImageProducerStatement(
@@ -0,0 +1,11 @@
1
+ // record-refused — the refusal class of `select-records`: the data says no (a record is missing, unsigned,
2
+ // contradicts its tag, or its commit is not the develop line). Leaf module, so the ancestry check and the
3
+ // selection can both subclass it. A registry, cosign or git that could not answer is a plain Error instead.
4
+
5
+ export class RecordRefused extends Error {
6
+ constructor(repo, message) {
7
+ super(`select-records: ${repo}: ${message}`);
8
+ this.name = 'RecordRefused';
9
+ this.repo = repo;
10
+ }
11
+ }
@@ -83,6 +83,7 @@ import { walkDeclaredSources } from './external-source-walk.mjs';
83
83
  import {
84
84
  biomeTierOrder,
85
85
  assertDeclaredDatabasesUnowned,
86
+ buildAudienceNamespaceOwners,
86
87
  buildCapabilityDomainOwners,
87
88
  compareIndexEntries,
88
89
  loadKernelBiomeContracts,
@@ -97,7 +98,10 @@ import {
97
98
  import {
98
99
  assertInstalledOperationCeilingAdmitted,
99
100
  assertOperationGrantsAdmitted,
101
+ assertExactAudiencesAdmitted,
100
102
  assertOrganizationSweepsAdmitted,
103
+ assertEventSubscriptionsAdmitted,
104
+ assertOrgContextRelaysAdmitted,
101
105
  assertStoreWorkloadCeilingAdmitted,
102
106
  } from './operation-grant-admission.mjs';
103
107
  import { PUBLISHED_PIN_SOURCE, pinnedSource } from './pin-source.mjs';
@@ -310,6 +314,7 @@ export function composeManifest(manifest, pinSource = PUBLISHED_PIN_SOURCE) {
310
314
  // reduces the declarations.
311
315
  declaredVisibilities: manifest.visibility === undefined ? [] : [manifest.visibility],
312
316
  storeWorkloadCeiling: manifest.storeWorkloadCeiling,
317
+ storeEventCeiling: manifest.storeEventCeiling,
313
318
  declaredSourceNames: new Set(sources.map((source) => source.name)),
314
319
  declaredSources: sources.map((source) => pinnedSource(source, pinSource)),
315
320
  owner: manifest.owner,
@@ -1163,6 +1168,7 @@ export function resolveDistributionFromInputs(
1163
1168
  trustCeiling,
1164
1169
  declaredVisibilities,
1165
1170
  storeWorkloadCeiling,
1171
+ storeEventCeiling,
1166
1172
  owner,
1167
1173
  leaf,
1168
1174
  // Absent on a selector-model composition — including every `composed` a
@@ -2058,12 +2064,29 @@ export function resolveDistributionFromInputs(
2058
2064
  Object.keys(e.installedOperationRequests).length > 0
2059
2065
  ? { installedOperationRequests: e.installedOperationRequests }
2060
2066
  : {}),
2067
+ // The workload components that relay an organization context, forwarded
2068
+ // verbatim; the lock admits them (`admitOrgContextRelays`).
2069
+ ...(e.orgContextRelayers !== undefined && e.orgContextRelayers.length > 0
2070
+ ? { orgContextRelayers: e.orgContextRelayers }
2071
+ : {}),
2072
+ // The event subscription clauses each workload component declares,
2073
+ // forwarded verbatim; the lock admits them (`admitEventSubscriptions`).
2074
+ ...(e.eventSubscriptionRequirements !== undefined &&
2075
+ Object.keys(e.eventSubscriptionRequirements).length > 0
2076
+ ? { eventSubscriptionRequirements: e.eventSubscriptionRequirements }
2077
+ : {}),
2061
2078
  // The organization sweeps each workload component may hold, forwarded
2062
2079
  // verbatim; the seal admits them (`admitOrganizationSweeps`).
2063
2080
  ...(e.organizationSweepRequirements !== undefined &&
2064
2081
  Object.keys(e.organizationSweepRequirements).length > 0
2065
2082
  ? { organizationSweepRequirements: e.organizationSweepRequirements }
2066
2083
  : {}),
2084
+ // The exact audiences each workload component requires, forwarded
2085
+ // verbatim; the seal admits them (`admitExactAudiences`).
2086
+ ...(e.audienceRequirements !== undefined &&
2087
+ Object.keys(e.audienceRequirements).length > 0
2088
+ ? { audienceRequirements: e.audienceRequirements }
2089
+ : {}),
2067
2090
  // --- Catalog projection (consumed by biome-host's DistributionCatalogSource) ---
2068
2091
  // Forwarded verbatim from the index so the lock is the SELF-CONTAINED
2069
2092
  // single source of truth for the biome catalog: biome-host builds the
@@ -2140,6 +2163,12 @@ export function resolveDistributionFromInputs(
2140
2163
  ...(source.ownsCapabilityDomains === undefined
2141
2164
  ? {}
2142
2165
  : { ownsCapabilityDomains: source.ownsCapabilityDomains }),
2166
+ // The service's own requirement and ownership declarations, likewise
2167
+ // verbatim from the index row: `admitOperationGrants` and
2168
+ // `admitExactAudiences` read them from the lock.
2169
+ ...(source.requiresOperations === undefined ? {} : { requiresOperations: source.requiresOperations }),
2170
+ ...(source.requiresAudiences === undefined ? {} : { requiresAudiences: source.requiresAudiences }),
2171
+ ...(source.ownsAudienceNamespaces === undefined ? {} : { ownsAudienceNamespaces: source.ownsAudienceNamespaces }),
2143
2172
  source,
2144
2173
  };
2145
2174
  })
@@ -2157,6 +2186,7 @@ export function resolveDistributionFromInputs(
2157
2186
  // owning biome from the store and then bind its refs to a URL they control.
2158
2187
  // See `buildCapabilityDomainOwners`. Throws on a duplicate claim.
2159
2188
  const capabilityDomainOwners = buildCapabilityDomainOwners(index);
2189
+ const audienceNamespaceOwners = buildAudienceNamespaceOwners(index);
2160
2190
 
2161
2191
  // inputHash: deterministic over the index SUBSET that fed the lock + the
2162
2192
  // reservation table + the leaf manifest, so drift in any input is detectable.
@@ -2164,8 +2194,11 @@ export function resolveDistributionFromInputs(
2164
2194
  const platform = serializeJson(platformServiceList);
2165
2195
  const owners = serializeJson(capabilityDomainOwners);
2166
2196
  const manifestImg = serializeJson(leaf);
2197
+ // The audience-namespace owners are derived from the WHOLE index, so a claim change in an unselected
2198
+ // biome is drift. Appended only when non-empty: an edition with none keeps the hash it always had.
2199
+ const audienceOwners = audienceNamespaceOwners.length > 0 ? `\0${serializeJson(audienceNamespaceOwners)}` : '';
2167
2200
  const inputHash = sha256Hex(
2168
- `${subset}\0${platform}\0${owners}\0${manifestImg}`,
2201
+ `${subset}\0${platform}\0${owners}\0${manifestImg}${audienceOwners}`,
2169
2202
  );
2170
2203
 
2171
2204
  const lock = {
@@ -2181,6 +2214,9 @@ export function resolveDistributionFromInputs(
2181
2214
  resolvedAt,
2182
2215
  inputHash,
2183
2216
  };
2217
+ // Appended after `inputHash` and only when a biome owns a namespace, like
2218
+ // `owner` below: an unconditional key would change the bytes of every lock.
2219
+ if (audienceNamespaceOwners.length > 0) {lock.audienceNamespaceOwners = audienceNamespaceOwners;}
2184
2220
 
2185
2221
  // Owner and visibility are written ONLY when something declared them, and
2186
2222
  // they are appended AFTER `inputHash` rather than woven into the literal
@@ -2216,6 +2252,9 @@ export function resolveDistributionFromInputs(
2216
2252
  // The edition's store-workload ceiling — written ONLY when the edition
2217
2253
  // declares one (the kernel refuses an empty list), carried unchanged.
2218
2254
  if (storeWorkloadCeiling !== undefined) {lock.storeWorkloadCeiling = storeWorkloadCeiling;}
2255
+ // The edition's store event ceiling — written ONLY when the edition declares
2256
+ // one (the kernel refuses an empty list), carried unchanged.
2257
+ if (storeEventCeiling !== undefined) {lock.storeEventCeiling = storeEventCeiling;}
2219
2258
 
2220
2259
  // (7) Re-derive the ownership index through the KERNEL so the table this
2221
2260
  // resolver emits is validated by the exact code every consumer will read
@@ -2235,6 +2274,9 @@ export function resolveDistributionFromInputs(
2235
2274
  // never fails (the resolve and `edition check` both run through here).
2236
2275
  assertOperationGrantsAdmitted(schemas, parsed, { report: (line) => console.log(`[operations] ${line}`) });
2237
2276
  assertOrganizationSweepsAdmitted(schemas, parsed);
2277
+ assertExactAudiencesAdmitted(schemas, parsed);
2278
+ assertOrgContextRelaysAdmitted(schemas, parsed);
2279
+ assertEventSubscriptionsAdmitted(schemas, parsed);
2238
2280
  assertInstalledOperationCeilingAdmitted(schemas, parsed);
2239
2281
  assertStoreWorkloadCeilingAdmitted(schemas, parsed);
2240
2282
  return validateImmutableDistributionLock(lock);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -54,7 +54,7 @@
54
54
  "@xemahq/distribution-source-hash": "^0.2.2",
55
55
  "@xemahq/dsl": "^0.24.0",
56
56
  "@xemahq/identity-client": "^0.16.0",
57
- "@xemahq/contracts": "^0.102.0",
57
+ "@xemahq/contracts": "^0.110.0",
58
58
  "@xemahq/kernel-state-runtime": "^0.8.0",
59
59
  "@xemahq/license-verifier": "^0.2.0",
60
60
  "@xemahq/service-registry-nest": "^0.12.2",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:97a574d113d6c30640832ec00008c3788c449ec45456db7b5d117c98460340cc"
95
+ "sourceDigest": "sha256:184f63317ecd8173e0fd3122d80aca617832209e5221bd8d984ec9c5eff5ef6c"
96
96
  }
97
97
  }