@xemahq/xema 0.54.0 → 0.56.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 (46) hide show
  1. package/dist/edition/verify-image-evidence.bundle.mjs +1996 -1012
  2. package/dist/edition/verify-image-evidence.bundle.provenance.json +584 -500
  3. package/dist/runtime/lock-loader.d.ts +2 -1
  4. package/dist/runtime/lock-loader.d.ts.map +1 -1
  5. package/dist/runtime/lock-loader.js +15 -5
  6. package/dist/runtime/lock-loader.js.map +1 -1
  7. package/edition/README.md +8 -3
  8. package/edition/biome-content.mjs +4 -4
  9. package/edition/candidate-edition.mjs +8 -3
  10. package/edition/check-committed-image-evidence.mjs +44 -15
  11. package/edition/customer-edition-verbs.mjs +15 -4
  12. package/edition/develop-current-resolution.mjs +29 -0
  13. package/edition/develop-edition-resolve.mjs +18 -10
  14. package/edition/develop-edition.mjs +30 -9
  15. package/edition/develop-freeze.mjs +24 -1
  16. package/edition/develop-git-bundles.mjs +201 -0
  17. package/edition/develop-graph.mjs +5 -3
  18. package/edition/develop-image-probe.mjs +6 -4
  19. package/edition/develop-image-record.mjs +194 -0
  20. package/edition/develop-record-ancestry.mjs +23 -5
  21. package/edition/develop-select-records.mjs +29 -21
  22. package/edition/develop-selection-line.mjs +30 -0
  23. package/edition/develop-selection-pins.mjs +30 -0
  24. package/edition/dist-check.mjs +45 -9
  25. package/edition/distribution-evidence-set.mjs +14 -9
  26. package/edition/edition-image-seal.mjs +2 -1
  27. package/edition/edition-protocol.mjs +6 -20
  28. package/edition/external-source-walk.mjs +37 -15
  29. package/edition/image-evidence.mjs +6 -2
  30. package/edition/image-lock-service-rows.mjs +4 -4
  31. package/edition/image-subject-identity.mjs +24 -0
  32. package/edition/images-from-record.mjs +46 -110
  33. package/edition/lib.mjs +24 -26
  34. package/edition/locked-bundle-signer.mjs +52 -8
  35. package/edition/resolve-customer-edition.mjs +1 -1
  36. package/edition/resolve-distribution.mjs +84 -21
  37. package/edition/resolve-edition.mjs +2 -1
  38. package/edition/resolve-images.mjs +224 -12
  39. package/edition/seal-image-digests.mjs +24 -13
  40. package/edition/third-party-bundle-registry.mjs +73 -17
  41. package/edition/third-party-source-evidence.mjs +39 -14
  42. package/edition/third-party-source-writer.mjs +2 -18
  43. package/edition/trust-policy-arming.mjs +17 -6
  44. package/edition/verify-image-platforms.mjs +2 -0
  45. package/edition/workspace-sections.mjs +11 -1
  46. package/package.json +3 -3
@@ -66,31 +66,56 @@ export function parseProvenanceStatement(value) {
66
66
  const readEvidenceFile = (root, path, label, maxBytes) => readBoundedNoFollowFile(root, path, label, maxBytes);
67
67
 
68
68
  /**
69
- * The trust-policy row `policyId` names, held to everything a third-party signer row must be: one row, the release
70
- * channel signed from refs/heads/main, the GitHub Actions issuer, the exact identity of its own workflow, and the
71
- * pinned repository and workflow. Throws naming the row and the field.
69
+ * A keyless signer row of `channel`, held to everything such a row must be: the channel signed from that channel's ref, the
70
+ * GitHub Actions issuer and the exact identity of its own workflow. Throws naming the row and the field.
72
71
  */
73
- export function thirdPartySignerRow({ policy, policyId, source, schemas, label }) {
74
- const rows = policy.policies.filter((row) => row.policyId === policyId);
75
- if (rows.length !== 1) {
76
- throw new Error(`${label}: the trust policy has ${rows.length} rows with policyId "${policyId}"; exactly one is required`);
77
- }
78
- const [row] = rows;
79
- const ref = schemas.EVIDENCE_CHANNEL_REFS[THIRD_PARTY_EVIDENCE_CHANNEL];
80
- if (row.channel !== THIRD_PARTY_EVIDENCE_CHANNEL || row.githubWorkflow?.ref !== ref) {
72
+ export function assertChannelSignerRow({ row, channel, schemas, label }) {
73
+ const ref = schemas.EVIDENCE_CHANNEL_REFS[channel];
74
+ if (row.channel !== channel || row.githubWorkflow?.ref !== ref) {
81
75
  throw new Error(
82
- `${label}: row "${policyId}" is not a ${THIRD_PARTY_EVIDENCE_CHANNEL} row signed from ${ref} ` +
76
+ `${label}: row "${row.policyId}" is not a ${channel} row signed from ${ref} ` +
83
77
  `(channel ${JSON.stringify(row.channel)}, ref ${JSON.stringify(row.githubWorkflow?.ref)})`,
84
78
  );
85
79
  }
86
80
  if (row.oidcIssuer !== GITHUB_ACTIONS_OIDC_ISSUER) {
87
- throw new Error(`${label}: row "${policyId}" oidcIssuer ${JSON.stringify(row.oidcIssuer)} is not ${GITHUB_ACTIONS_OIDC_ISSUER}`);
81
+ throw new Error(`${label}: row "${row.policyId}" oidcIssuer ${JSON.stringify(row.oidcIssuer)} is not ${GITHUB_ACTIONS_OIDC_ISSUER}`);
88
82
  }
89
83
  const { repository, path } = row.githubWorkflow;
90
84
  const identity = `https://github.com/${repository}/${path}@${ref}`;
91
85
  if (row.certificateIdentity !== identity) {
92
- throw new Error(`${label}: row "${policyId}" certificateIdentity ${JSON.stringify(row.certificateIdentity)} is not the exact workflow identity ${identity}`);
86
+ throw new Error(`${label}: row "${row.policyId}" certificateIdentity ${JSON.stringify(row.certificateIdentity)} is not the exact workflow identity ${identity}`);
93
87
  }
88
+ return row;
89
+ }
90
+
91
+ /**
92
+ * The ONE `channel` row that signs the SLSA provenance of `repository`'s biome bundles, held to `assertChannelSignerRow`.
93
+ * None, or several, is refused naming the repository and the channel.
94
+ */
95
+ export function bundleSignerRowFor({ policy, repository, channel, schemas, label }) {
96
+ const rows = policy.policies.filter(
97
+ (row) => row.channel === channel && row.predicateTypes?.includes(SLSA_PROVENANCE_V1) && row.githubWorkflow?.repository === repository,
98
+ );
99
+ if (rows.length !== 1) {
100
+ throw new Error(
101
+ `${label}: the trust policy has ${rows.length} ${channel} rows authorizing ${SLSA_PROVENANCE_V1} for ${repository}; exactly one bundle-signer row is required`,
102
+ );
103
+ }
104
+ return assertChannelSignerRow({ row: rows[0], channel, schemas, label });
105
+ }
106
+
107
+ /**
108
+ * The trust-policy row `policyId` names, held to everything a third-party signer row must be: one row, the release
109
+ * channel signed from refs/heads/main, the GitHub Actions issuer, the exact identity of its own workflow, and the
110
+ * pinned repository and workflow. Throws naming the row and the field.
111
+ */
112
+ export function thirdPartySignerRow({ policy, policyId, source, schemas, label }) {
113
+ const rows = policy.policies.filter((row) => row.policyId === policyId);
114
+ if (rows.length !== 1) {
115
+ throw new Error(`${label}: the trust policy has ${rows.length} rows with policyId "${policyId}"; exactly one is required`);
116
+ }
117
+ const row = assertChannelSignerRow({ row: rows[0], channel: THIRD_PARTY_EVIDENCE_CHANNEL, schemas, label });
118
+ const { repository, path } = row.githubWorkflow;
94
119
  if (repository !== source.repository) {
95
120
  throw new Error(`${label}: row "${policyId}" signs for ${repository}, but the source is pinned to ${source.repository}`);
96
121
  }
@@ -16,8 +16,8 @@ import { bundleImageNames, fetchBundleManifest, fetchBundleProvenance, verifyBun
16
16
  import {
17
17
  PROVENANCE_BUNDLE_FILE,
18
18
  PROVENANCE_STATEMENT_FILE,
19
- SLSA_PROVENANCE_V1,
20
19
  THIRD_PARTY_EVIDENCE_CHANNEL,
20
+ bundleSignerRowFor,
21
21
  digestOfOciRef,
22
22
  provenanceDirectoryForDigest,
23
23
  thirdPartySignerRow,
@@ -28,22 +28,6 @@ import { assertThirdPartyUniqueness } from './third-party-source-uniqueness.mjs'
28
28
 
29
29
  const compare = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
30
30
 
31
- /** The one release-channel row that signs the biome bundle's provenance for `repository`. */
32
- function bundleSignerRow(policy, repository) {
33
- const rows = policy.policies.filter(
34
- (row) =>
35
- row.channel === THIRD_PARTY_EVIDENCE_CHANNEL &&
36
- row.predicateTypes.includes(SLSA_PROVENANCE_V1) &&
37
- row.githubWorkflow?.repository === repository,
38
- );
39
- if (rows.length !== 1) {
40
- throw new Error(
41
- `the trust policy has ${rows.length} ${THIRD_PARTY_EVIDENCE_CHANNEL} rows authorizing ${SLSA_PROVENANCE_V1} for ${repository}; exactly one bundle-signer row is required`,
42
- );
43
- }
44
- return rows[0];
45
- }
46
-
47
31
  /** The one release-channel row whose imageRepositories lists `imageRef`. */
48
32
  function imageSignerRow(policy, schemas, imageRef) {
49
33
  const rows = policyCandidates(policy, schemas.IMAGE_PRODUCER_PREDICATE_TYPE, THIRD_PARTY_EVIDENCE_CHANNEL, imageRef);
@@ -116,7 +100,7 @@ async function collectImage({ image, request, context, clientFor, services }) {
116
100
  export async function planThirdPartySourceWrite({ request, context, clientFor, withProducerWorkflowPin, dataRoot, verifyOnRegistry = verifyBundleOnRegistry }) {
117
101
  const { policy, schemas } = context;
118
102
  const repositoryUrl = `https://github.com/${request.repository}`;
119
- const row = bundleSignerRow(policy, request.repository);
103
+ const row = bundleSignerRowFor({ policy, repository: request.repository, channel: THIRD_PARTY_EVIDENCE_CHANNEL, schemas, label: `third-party source "${request.name}" bundle signer` });
120
104
  const workflowPath = row.githubWorkflow.path;
121
105
  // The row must be exact (release channel, refs/heads/main, the GitHub issuer, its own workflow identity) before cosign runs.
122
106
  thirdPartySignerRow({ policy, policyId: row.policyId, source: { repository: request.repository, producerWorkflow: { path: workflowPath } }, schemas, label: `third-party source "${request.name}" bundle signer` });
@@ -38,6 +38,13 @@
38
38
  // channel neither arm nor disarm the other: a policy that authorizes the three
39
39
  // stages on the release channel and pins both binaries is ARMED for a release
40
40
  // edition whatever it says about develop.
41
+ //
42
+ // ONLY A STAGE ROW ARMS A CHANNEL: a row counts toward a channel's arming when it
43
+ // authorizes one of the stage predicates judged here. A bundle-signer row (SLSA
44
+ // provenance of a third-party bundle, e.g. the develop row of a git-sourced
45
+ // biome) authorizes bundle verification and arms no stage, so a channel whose
46
+ // only rows are bundle signers is UNARMED, exactly like a channel with no row.
47
+ // One stage row on the channel still makes it PARTIAL until every stage is met.
41
48
 
42
49
  export const TrustPolicyArmingState = Object.freeze({
43
50
  ARMED: 'armed',
@@ -81,8 +88,11 @@ export function assessTrustPolicyArming(policy, schemas, channel, { consumesCata
81
88
  stages.push(['edition-catalogue', schemas.EDITION_CATALOGUE_PREDICATE_TYPE]);
82
89
  }
83
90
  }
84
- const onChannel = policy.policies.filter((candidate) => candidate.channel === channel);
85
- const authorized = stages.filter(([, predicateType]) => onChannel.some((candidate) => candidate.predicateTypes.includes(predicateType)));
91
+ const stagePredicateTypes = stages.map(([, predicateType]) => predicateType);
92
+ const stageRows = policy.policies.filter(
93
+ (candidate) => candidate.channel === channel && candidate.predicateTypes.some((predicateType) => stagePredicateTypes.includes(predicateType)),
94
+ );
95
+ const authorized = stages.filter(([, predicateType]) => stageRows.some((candidate) => candidate.predicateTypes.includes(predicateType)));
86
96
 
87
97
  if (policy.policies.length === 0) {
88
98
  unmet.push(
@@ -120,12 +130,13 @@ export function assessTrustPolicyArming(policy, schemas, channel, { consumesCata
120
130
  policy.policies.length === 0 &&
121
131
  policy.trustedCosignBinaryDigests.length === 0 &&
122
132
  policy.trustedGitBinaryDigests.length === 0;
123
- // The channel states nothing while another channel does: its signers and the
124
- // binary pins are that channel's arming, not half of this one's.
125
- const anotherChannelOnly = onChannel.length === 0 && policy.policies.length > 0;
133
+ // The channel has no stage row while the policy has rows (another channel's,
134
+ // or bundle signers): those rows and the binary pins are not half of this
135
+ // channel's arming.
136
+ const noStageRow = stageRows.length === 0 && policy.policies.length > 0;
126
137
  const state = armed
127
138
  ? TrustPolicyArmingState.ARMED
128
- : bootstrap || anotherChannelOnly
139
+ : bootstrap || noStageRow
129
140
  ? TrustPolicyArmingState.UNARMED
130
141
  : TrustPolicyArmingState.PARTIAL;
131
142
 
@@ -434,6 +434,8 @@ export async function verifyDistributionPlatforms({
434
434
  return {
435
435
  subjectKind: subject.kind,
436
436
  subjectId: subject.id,
437
+ subjectImageRef: subject.imageRef,
438
+ subjectContentHash: subject.contentHash,
437
439
  coordinate,
438
440
  manifestDigest: published.indexDigest,
439
441
  multiArchIndex: published.isIndex,
@@ -118,6 +118,9 @@ export const FirstPartyImageOrigin = Object.freeze({
118
118
 
119
119
  const IMAGE_ORIGINS = Object.values(FirstPartyImageOrigin);
120
120
 
121
+ /** An issue key, `<PROJECT>-<number>`: the ticket that removes a member's current-resolution exception. */
122
+ const TICKET = /^[A-Z][A-Z0-9]*-[1-9][0-9]*$/;
123
+
121
124
  /**
122
125
  * `firstPartySources`: every first-party repository, keyed by its source
123
126
  * checkout handle, with its declared source id (the identity a runtime subject
@@ -126,6 +129,9 @@ const IMAGE_ORIGINS = Object.values(FirstPartyImageOrigin);
126
129
  * is checked canonical and under `sourceRepositoryUrlPrefix` with the whole
127
130
  * config (edition-config.mjs). `imageOrigin` (FirstPartyImageOrigin) is optional
128
131
  * here and has no default: `--images-from` refuses a source that does not declare it.
132
+ * `currentResolution` (a ticket, `<PROJECT>-<number>`) is optional and has no default: a member that declares it keeps
133
+ * the CURRENT develop resolution instead of a signed build record until that ticket removes it
134
+ * (develop-current-resolution.mjs); a member without it never does.
129
135
  */
130
136
  export function parseFirstPartySources(value) {
131
137
  const where = '"firstPartySources"';
@@ -136,17 +142,21 @@ export function parseFirstPartySources(value) {
136
142
  repoHandle(repo, `${where} key`);
137
143
  if (typeof entry === 'string') {throw new RetiredFirstPartySourceShapeError(repo, entry);}
138
144
  const at = `${where}["${repo}"]`;
139
- exactObject(entry, at, ['sourceId', 'repositoryUrl'], ['imageOrigin']);
145
+ exactObject(entry, at, ['sourceId', 'repositoryUrl'], ['imageOrigin', 'currentResolution']);
140
146
  if (typeof entry.sourceId !== 'string' || !SOURCE_ID.test(entry.sourceId)) {
141
147
  fail(`${at}.sourceId`, `must be a lowercase kebab-case source id; got ${JSON.stringify(entry.sourceId)}`);
142
148
  }
143
149
  if (Object.hasOwn(entry, 'imageOrigin') && !IMAGE_ORIGINS.includes(entry.imageOrigin)) {
144
150
  fail(`${at}.imageOrigin`, `must be one of ${IMAGE_ORIGINS.join(', ')}; got ${JSON.stringify(entry.imageOrigin)}`);
145
151
  }
152
+ if (Object.hasOwn(entry, 'currentResolution') && (typeof entry.currentResolution !== 'string' || !TICKET.test(entry.currentResolution))) {
153
+ fail(`${at}.currentResolution`, `must be the ticket that removes the exception, <PROJECT>-<number>; got ${JSON.stringify(entry.currentResolution)}`);
154
+ }
146
155
  return [repo, Object.freeze({
147
156
  sourceId: entry.sourceId,
148
157
  repositoryUrl: text(entry.repositoryUrl, `${at}.repositoryUrl`),
149
158
  ...(Object.hasOwn(entry, 'imageOrigin') ? { imageOrigin: entry.imageOrigin } : {}),
159
+ ...(Object.hasOwn(entry, 'currentResolution') ? { currentResolution: entry.currentResolution } : {}),
150
160
  })];
151
161
  });
152
162
  uniqueBy(entries, `${where} sourceId`, ([, entry]) => entry.sourceId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.54.0",
3
+ "version": "0.56.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -61,7 +61,7 @@
61
61
  "@xemahq/distribution-source-hash": "^0.2.2",
62
62
  "@xemahq/dsl": "^0.28.0",
63
63
  "@xemahq/identity-client": "^0.36.0",
64
- "@xemahq/contracts": "^0.117.0",
64
+ "@xemahq/contracts": "^0.119.0",
65
65
  "@xemahq/kernel-state-runtime": "^0.8.0",
66
66
  "@xemahq/license-verifier": "^0.2.0",
67
67
  "@xemahq/release-policy": "^17.6.0",
@@ -100,6 +100,6 @@
100
100
  "typecheck": "tsc --noEmit"
101
101
  },
102
102
  "xemaRelease": {
103
- "sourceDigest": "sha256:7bcf18b69778e4e380b22bf9c301cf08ea6234a10c98ec06940abdde0f8920c1"
103
+ "sourceDigest": "sha256:80acaaf576705b4a71ecd5e67794fea62c6fc834652a2830bbd03a2d855d2409"
104
104
  }
105
105
  }