@xemahq/xema 0.55.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.
@@ -0,0 +1,201 @@
1
+ // develop-git-bundles — the OCI bundle of a git-sourced biome, in a VENDOR DEVELOP edition only (XEMA-2456).
2
+ //
3
+ // A release and a customer edition never carry an OCI bundle on a git-sourced row: the manifest's `bundleSource` names
4
+ // an artifact the release lane builds, not one built from the pinned commit (resolve-distribution.mjs strips it, the
5
+ // release refuses it). A develop edition freezes a git source at its develop head F, and the source's own develop lane
6
+ // publishes the bundle the manifest names, signed keyless by its develop workflow. This module proves that bundle is
7
+ // the one F describes, and only then does the resolve keep it on the row, by DIGEST, with its develop bundle signer:
8
+ //
9
+ // 1. the bundle: the manifest's own `bundleSource.ociRef` at F (`<repository>:<version>`; the kernel holds the tag to
10
+ // the manifest version) resolved to its digest at the registry. The lock records `<repository>@sha256:<hex>`, never
11
+ // the tag, so the edition names immutable bytes;
12
+ // 2. the signer: the ONE `develop` row of the source's repository in the data root's trust policy
13
+ // (`bundleSignerRowFor`). cosign verifies the digest's signature under that row's identity, issuer, workflow
14
+ // repository and ref, and states the ONE commit P its certificate was issued at (`verifiedBundleSignerCommit`);
15
+ // `verifyBundleOnRegistry` then holds the signature AND the SLSA provenance to exactly the row and P. P is never
16
+ // read from bytes cosign has not verified;
17
+ // 3. the content binding of P to F. A version that is already published is never re-attested by the publisher
18
+ // (`xema biome publish` "already current"), so P is the commit that last published the version, and F may be a
19
+ // later commit of the same version. Two checks, refused on either, each naming P, F and the mismatching pair:
20
+ // (a) the kernel's `computeBiomeSourceManifestHash` of the bundle's own `xema-biome.json` equals the
21
+ // `sourceManifestHash` of the biome at F (the hash the seal and verify hold to the git source at F);
22
+ // (b) the git tree id of the biome directory at P equals its tree id at F (the catalogue's `sourceTreeIds`
23
+ // notion), so the bundle was built from the same biome source F holds.
24
+ // THE GAP, develop only: build inputs outside the biome directory (shared packages, the lockfile, the publisher's
25
+ // toolchain) are not bound. A change there between P and F reaches no check here. A release never relies on this:
26
+ // it carries a third-party bundle only through an `oci` source with a third-party pin, whose evidence is bound to
27
+ // the pinned commit itself.
28
+ //
29
+ // Every failure is a `DevelopGitBundleError` naming the biome; all subjects are verified before one is refused.
30
+
31
+ import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
32
+ import { RefusalError } from './collected-refusals.mjs';
33
+ import { EditionChannel } from './edition-channel.mjs';
34
+ import { editionWorkspaceConfig } from './edition-config.mjs';
35
+ import { loadPinnedDeclaredTrustPolicy } from './evidence-trust-pin.mjs';
36
+ import { sourceTreeIdAt } from './external-source-walk.mjs';
37
+ import { prepareTrustedCosignBinary } from './image-evidence.mjs';
38
+ import { BundleSignerRefusedError, DevelopGitBundleError } from './locked-bundle-signer.mjs';
39
+ import { createRegistryClientResolver } from './oci-image-source-evidence.mjs';
40
+ import { bundleDigestRef, fetchBundleManifest, verifiedBundleSignerCommit, verifyBundleOnRegistry } from './third-party-bundle-registry.mjs';
41
+ import { bundleSignerRowFor } from './third-party-source-evidence.mjs';
42
+ import { OCI_BUNDLE_SOURCE_KIND } from './third-party-subjects.mjs';
43
+
44
+ /** The key a verified bundle is looked up by: the source name and the biome id of its index entry. */
45
+ export const developGitBundleKey = (sourceName, biomeId) => `${sourceName}\u0000${biomeId}`;
46
+
47
+ /**
48
+ * The rows a develop resolve must verify a bundle for: each git-sourced biome of `lock` (a row with a `source`) whose
49
+ * index entry declares a biome-level OCI `bundleSource`.
50
+ *
51
+ * @returns {{biomeId: string, source: {name: string, url: string, commit: string}, entry: object}[]}
52
+ */
53
+ export function developGitBundleSubjects({ lock, index }) {
54
+ const entries = new Map(index.filter((entry) => entry.source !== undefined).map((entry) => [developGitBundleKey(entry.source, entry.id), entry]));
55
+ return lock.biomes.flatMap((biome) => {
56
+ if (biome.source === undefined) {return [];}
57
+ const entry = entries.get(developGitBundleKey(biome.source.name, biome.id));
58
+ return entry?.bundleSource?.kind === OCI_BUNDLE_SOURCE_KIND ? [{ biomeId: biome.id, source: biome.source, entry }] : [];
59
+ });
60
+ }
61
+
62
+ const subjectLabel = ({ biomeId, source }) => `biome "${biomeId}" (git source "${source.name}" at ${source.commit})`;
63
+
64
+ /** The biome directory relative to its source repository: the index path is `<source name>/<directory>`. */
65
+ function repositoryPathOf({ source, entry }) {
66
+ const prefix = `${source.name}/`;
67
+ if (!entry.path.startsWith(prefix)) {
68
+ throw new Error(`${subjectLabel({ biomeId: entry.id, source })}: index path ${JSON.stringify(entry.path)} is not under the source "${source.name}"`);
69
+ }
70
+ return entry.path.slice(prefix.length);
71
+ }
72
+
73
+ /** Verify every subject, then refuse them all at once: one refusal is rethrown unchanged, several are one error. */
74
+ async function verifyEach(subjects, verifyOne) {
75
+ const verified = new Map();
76
+ const refused = [];
77
+ for (const subject of subjects) {
78
+ try {
79
+ verified.set(developGitBundleKey(subject.source.name, subject.biomeId), await verifyOne(subject));
80
+ } catch (error) {
81
+ // A refusal, or a plain Error of the registry, cosign or git: this subject is refused. A defect (TypeError, ...) is not.
82
+ const refusal = error instanceof RefusalError || error?.constructor === Error;
83
+ if (!refusal) {throw error;}
84
+ refused.push(error instanceof DevelopGitBundleError ? error : new DevelopGitBundleError(`${subjectLabel(subject)}: ${error.message}`));
85
+ }
86
+ }
87
+ if (refused.length === 1) {throw refused[0];}
88
+ if (refused.length > 1) {throw new BundleSignerRefusedError(refused);}
89
+ return verified;
90
+ }
91
+
92
+ /**
93
+ * One subject, verified (steps 1 to 3 of this file's header). `context` is `{policy, policyRoot, schemas, trustedCosign}`.
94
+ *
95
+ * @returns {Promise<{bundleSource: object}>} the bundle source the lock row carries: the declared one, by digest
96
+ */
97
+ export async function verifyDevelopGitBundle({
98
+ subject,
99
+ checkout,
100
+ context,
101
+ clientFor,
102
+ signerCommit = verifiedBundleSignerCommit,
103
+ verifyOnRegistry = verifyBundleOnRegistry,
104
+ treeIdAt = sourceTreeIdAt,
105
+ env = process.env,
106
+ }) {
107
+ const { biomeId, source, entry } = subject;
108
+ const label = subjectLabel(subject);
109
+ if (checkout === undefined) {
110
+ throw new Error(`${label}: the source walk holds no checkout of "${source.name}"`);
111
+ }
112
+ const repository = repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(source.url, `${label} source url`), `${label} source url`);
113
+ const row = bundleSignerRowFor({ policy: context.policy, repository, channel: EditionChannel.Develop, schemas: context.schemas, label: `${label} develop bundle signer` });
114
+
115
+ // 1 + 2: the digest the manifest's tag serves; the commit P cosign verified the develop signature at; signature and
116
+ // provenance held to the row at P.
117
+ const ociRef = await bundleDigestRef({ clientFor, ociRef: entry.bundleSource.ociRef });
118
+ const cosign = { trustedCosign: context.trustedCosign, policy: context.policy, policyRoot: context.policyRoot, row, ociRef, env };
119
+ const attestedCommit = signerCommit(cosign);
120
+ verifyOnRegistry({ ...cosign, workflowSha: attestedCommit });
121
+
122
+ // 3: the content binding of P to F (see the header for the gap it leaves: build inputs outside the biome directory).
123
+ const binding = `the bundle ${ociRef} attested at ${attestedCommit} (P) against the source at ${source.commit} (F)`;
124
+ const { manifest } = await fetchBundleManifest({ clientFor, ociRef });
125
+ if (manifest?.xema?.id !== biomeId) {
126
+ throw new Error(`${label}: ${ociRef} declares xema.id ${JSON.stringify(manifest?.xema?.id)}, not "${biomeId}"`);
127
+ }
128
+ const bundleManifestHash = await context.schemas.computeBiomeSourceManifestHash(manifest);
129
+ if (bundleManifestHash !== entry.sourceManifestHash) {
130
+ throw new DevelopGitBundleError(
131
+ `${label}: ${binding}: the bundle's xema-biome.json hashes to sourceManifestHash ${bundleManifestHash}, but the manifest at F hashes to ${entry.sourceManifestHash}`,
132
+ );
133
+ }
134
+ const path = repositoryPathOf(subject);
135
+ const treeAtAttested = treeIdAt({ checkout, commit: attestedCommit, path, env });
136
+ const treeAtFrozen = treeIdAt({ checkout, commit: source.commit, path, env });
137
+ if (treeAtAttested !== treeAtFrozen) {
138
+ throw new DevelopGitBundleError(
139
+ `${label}: ${binding}: the biome directory ${path} is tree ${treeAtAttested} at P but tree ${treeAtFrozen} at F; publish a new version of the biome from F`,
140
+ );
141
+ }
142
+ return { bundleSource: { ...entry.bundleSource, ociRef } };
143
+ }
144
+
145
+ /**
146
+ * The verifier a vendor develop resolve hands `prepareDistributionWithIndex` (`inputs.developGitBundles`). Nothing is
147
+ * read (no trust policy, no cosign) unless a subject exists.
148
+ *
149
+ * @param {object} options
150
+ * @param {string|undefined} options.cosignBinary the cosign the trust policy pins by digest (`--cosign`)
151
+ * @param {string} options.dataRoot
152
+ */
153
+ export function createDevelopGitBundleVerifier({ cosignBinary, dataRoot, env = process.env, clientFor, treeIdAt }) {
154
+ return Object.freeze({
155
+ async verify({ subjects, checkouts, schemas }) {
156
+ if (subjects.length === 0) {return new Map();}
157
+ if (cosignBinary === undefined) {
158
+ throw new DevelopGitBundleError(
159
+ `${subjects.map(subjectLabel).join(', ')} declare an OCI bundle; verifying it needs a cosign binary (--cosign), and none was given`,
160
+ );
161
+ }
162
+ const policy = loadPinnedDeclaredTrustPolicy({ dataRoot, declaration: editionWorkspaceConfig().evidenceTrustPolicy, schemas });
163
+ const trustedCosign = prepareTrustedCosignBinary({ cosignBinary, policy });
164
+ const context = { policy, policyRoot: dataRoot, schemas, trustedCosign };
165
+ const registry = clientFor ?? createRegistryClientResolver({ dataRoot });
166
+ try {
167
+ return await verifyEach(subjects, (subject) =>
168
+ verifyDevelopGitBundle({ subject, checkout: checkouts.get(subject.source.name), context, clientFor: registry, env, ...(treeIdAt === undefined ? {} : { treeIdAt }) }),
169
+ );
170
+ } finally {
171
+ trustedCosign.dispose();
172
+ }
173
+ },
174
+ });
175
+ }
176
+
177
+ /**
178
+ * The OFFLINE verifier of a candidate re-seal: the bundle the pinned develop edition already verified for the SAME
179
+ * source commit, carried forward unchanged, with the signer it was verified under (`pinnedBundleSigner`): the resolve
180
+ * refuses the row when the current trust policy selects any other signer (a rotated develop row). A subject at another commit, or one the pinned edition carried no bundle
181
+ * for, is refused: verifying a new bundle reads the registry, which an offline re-seal does not.
182
+ *
183
+ * @param {object} pinnedLock the pinned edition's parsed distribution lock
184
+ */
185
+ export function carriedDevelopGitBundles(pinnedLock) {
186
+ return Object.freeze({
187
+ async verify({ subjects }) {
188
+ return verifyEach(subjects, async (subject) => {
189
+ const row = pinnedLock.biomes.find(
190
+ (biome) => biome.id === subject.biomeId && biome.source?.name === subject.source.name && biome.source.commit === subject.source.commit,
191
+ );
192
+ if (row?.bundleSource?.kind !== OCI_BUNDLE_SOURCE_KIND || row.bundleSigner === undefined) {
193
+ throw new DevelopGitBundleError(
194
+ `${subjectLabel(subject)}: the pinned edition carries no verified bundle at this commit, and an offline re-seal cannot verify one; resolve the develop edition instead`,
195
+ );
196
+ }
197
+ return { bundleSource: row.bundleSource, pinnedBundleSigner: row.bundleSigner };
198
+ });
199
+ },
200
+ });
201
+ }
@@ -180,6 +180,24 @@ function resolveRemoteCommit(source, gitEnv, exec = execFileSync) {
180
180
  return commit;
181
181
  }
182
182
 
183
+ /** Fetch `commit` by id, depth 1, into the checkout at `directory` unless it already holds it. */
184
+ function fetchCommitUnlessHeld(directory, commit, gitEnv, exec) {
185
+ const have = (() => {
186
+ try {
187
+ return git(directory, ['cat-file', '-e', `${commit}^{commit}`], gitEnv, exec) === '';
188
+ } catch {
189
+ return false;
190
+ }
191
+ })();
192
+ if (!have) {
193
+ exec(
194
+ 'git',
195
+ ['-C', directory, 'fetch', '--quiet', '--depth=1', 'origin', commit],
196
+ { env: gitEnv, stdio: ['ignore', 'pipe', 'pipe'] },
197
+ );
198
+ }
199
+ }
200
+
183
201
  /**
184
202
  * Fetch a declared git source and return the directory + the EXACT commit its
185
203
  * manifests will be read at.
@@ -209,20 +227,7 @@ export function materializeGitSource({
209
227
  // reuses it: fetch from where the declaration says the source lives NOW.
210
228
  git(directory, ['remote', 'set-url', 'origin', source.url], gitEnv, exec);
211
229
  }
212
- const have = (() => {
213
- try {
214
- return git(directory, ['cat-file', '-e', `${commit}^{commit}`], gitEnv, exec) === '';
215
- } catch {
216
- return false;
217
- }
218
- })();
219
- if (!have) {
220
- exec(
221
- 'git',
222
- ['-C', directory, 'fetch', '--quiet', '--depth=1', 'origin', commit],
223
- { env: gitEnv, stdio: ['ignore', 'pipe', 'pipe'] },
224
- );
225
- }
230
+ fetchCommitUnlessHeld(directory, commit, gitEnv, exec);
226
231
  git(directory, ['checkout', '--quiet', '--detach', commit], gitEnv, exec);
227
232
  return { directory, commit: pinExternalSourceCommit({ name: source.name, directory, commit }) };
228
233
  });
@@ -405,6 +410,7 @@ export async function walkDeclaredSources({
405
410
  } = {}) {
406
411
  const entries = [];
407
412
  const sourceRecords = new Map();
413
+ const checkouts = new Map();
408
414
  // Every source is held (kind, then credential and URL) before the first is materialised.
409
415
  const heldWorkspace = sources.length === 0 ? undefined : (workspace ?? editionWorkspaceConfig());
410
416
  for (const source of sources) {
@@ -446,6 +452,7 @@ export async function walkDeclaredSources({
446
452
  entries.push(stampEntry(assertSourceWalkedOrigin(await manifestToIndexEntry(manifest), source.name), source.name));
447
453
  }
448
454
  sourceRecords.set(source.name, lockedSourceRecord(source, commit));
455
+ checkouts.set(source.name, { source, directory });
449
456
  }
450
457
  // The last point at which an unstamped entry can be caught before it reaches
451
458
  // the shared index, where absent-is-the-identity makes it admissible
@@ -459,5 +466,20 @@ export async function walkDeclaredSources({
459
466
  'open-source edition. Refusing.',
460
467
  );
461
468
  }
462
- return { entries, sourceRecords };
469
+ return { entries, sourceRecords, checkouts };
470
+ }
471
+
472
+ /**
473
+ * The git tree id of `path` (repository-relative, `.` for the root) of a declared git source at `commit`, read in the
474
+ * walk's checkout of that source (`checkouts` of `walkDeclaredSources`). A commit the checkout does not hold is fetched
475
+ * by id, depth 1, from the declared URL under the source's own credential hold: no history is needed.
476
+ */
477
+ export function sourceTreeIdAt({ checkout, commit, path, env = process.env, workspace = editionWorkspaceConfig(), exec = execFileSync }) {
478
+ if (!EXACT_COMMIT.test(String(commit))) {
479
+ throw new Error(`source "${checkout.source.name}": tree of ${JSON.stringify(commit)} asked, which is not an exact 40-hex commit`);
480
+ }
481
+ return withSourceGitEnv(checkout.source, { workspace, env }, (gitEnv) => {
482
+ fetchCommitUnlessHeld(checkout.directory, commit, gitEnv, exec);
483
+ return git(checkout.directory, ['rev-parse', '--verify', path === '.' ? `${commit}^{tree}` : `${commit}:${path}`], gitEnv, exec);
484
+ });
463
485
  }
@@ -3,7 +3,10 @@
3
3
  // Every `bundleSource: {kind: 'oci'}` row of a lock (a biome's, or one of its host modules') states WHO signs its bundle:
4
4
  // `bundleSigner: {policyId, signer}`, the kernel's `lockedBundleSignerOf` projection of ONE row of the data root's evidence
5
5
  // trust policy. The row is selected, never defaulted:
6
- // - a git-sourced row (one with a lock `source`) carries no OCI bundle at all, and a lock stating one is refused;
6
+ // - a git-sourced row (one with a lock `source`) carries no OCI bundle, and a lock stating one is refused, EXCEPT in a
7
+ // selection made for a vendor develop edition (`GitSourcedBundle.DevelopVerified`, XEMA-2456): there a git-sourced
8
+ // biome row may carry the bundle the develop resolve verified (`develop-git-bundles.mjs`), signed by the ONE
9
+ // `develop` row of the source's repository;
7
10
  // - a third-party row (no `source`, `isThirdPartyOciRow`) by its pin (`third-party-source-pins.json`), whose `bundle.signerPolicyId` names an
8
11
  // `oidc-certificate` row held to the checks `thirdPartySignerRow` makes;
9
12
  // - a first-party row by the data root's `firstPartyBundleSigner.policyId`, a `public-key` row whose key is bound to
@@ -15,13 +18,15 @@
15
18
 
16
19
  import { createRequire } from 'node:module';
17
20
 
21
+ import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
18
22
  import { KernelRefusalName, RefusalError, collectRefusals, isRefusal, joinRefusalMessages } from './collected-refusals.mjs';
23
+ import { EditionChannel } from './edition-channel.mjs';
19
24
  import { editionWorkspaceConfig } from './edition-config.mjs';
20
25
  import { assertKernelEditionProtocol } from './edition-protocol.mjs';
21
26
  import { dataRoot, kernelDistPath, kernelPackageDir } from './edition-workspace.mjs';
22
27
  import { loadPinnedDeclaredTrustPolicy } from './evidence-trust-pin.mjs';
23
28
  import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs';
24
- import { thirdPartySignerRow } from './third-party-source-evidence.mjs';
29
+ import { bundleSignerRowFor, thirdPartySignerRow } from './third-party-source-evidence.mjs';
25
30
  import { OCI_BUNDLE_SOURCE_KIND, isThirdPartyOciRow } from './third-party-subjects.mjs';
26
31
 
27
32
  const requireFromHere = createRequire(import.meta.url);
@@ -31,6 +36,13 @@ const PUBLIC_KEY_KIND = 'public-key';
31
36
 
32
37
  const KERNEL_EXPORTS = Object.freeze(['lockedBundleSignerOf', 'assertLockedBundleSignerMatches', 'LockedBundleSignerUnknownPolicyError', 'LockedBundleSignerAmbiguousPolicyError']);
33
38
 
39
+ /**
40
+ * Whether a selection admits an OCI bundle on a git-sourced row. `Refused` is every release, customer and dist-check
41
+ * selection, unchanged; `DevelopVerified` only a vendor develop edition's resolve, seal and verify, whose git bundles
42
+ * the develop resolve verified against the source commit (`develop-git-bundles.mjs`).
43
+ */
44
+ export const GitSourcedBundle = Object.freeze({ Refused: 'refused', DevelopVerified: 'develop-verified' });
45
+
34
46
  /** A named refusal of the bundle signer plane. */
35
47
  export class BundleSignerError extends RefusalError {
36
48
  constructor(message) {
@@ -56,6 +68,8 @@ export class BundleSignerMissingError extends BundleSignerError {}
56
68
  export class BundleSourceOnGitRowError extends BundleSignerError {}
57
69
  /** A resolve found an OCI host-module bundle on a git-sourced biome: such a bundle reaches an edition only through an `oci` source. */
58
70
  export class GitSourcedHostModuleBundleError extends BundleSignerError {}
71
+ /** A develop resolve could not verify the OCI bundle a git-sourced biome declares (no develop row, cosign, or the content binding refused it). */
72
+ export class DevelopGitBundleError extends BundleSignerError {}
59
73
  /** The loaded kernel does not export what the bundle signer plane needs. */
60
74
  export class BundleSignerKernelUnsupportedError extends BundleSignerError {}
61
75
  /** Several OCI rows were refused in one pass; `.errors` holds each row's own refusal. */
@@ -98,6 +112,13 @@ function assertNotGitSourced(subject) {
98
112
  }
99
113
  }
100
114
 
115
+ /** Refuses a git-sourced subject unless the selection admits develop git bundles and the subject is a biome row. */
116
+ function assertGitSourceAdmitted(subject, gitSourcedBundle) {
117
+ if (subject.source === undefined) {return;}
118
+ if (gitSourcedBundle === GitSourcedBundle.DevelopVerified && subject.componentKey === undefined) {return;}
119
+ assertNotGitSourced(subject);
120
+ }
121
+
101
122
  const rowsOf = (policy, policyId) => policy.policies.filter((row) => row.policyId === policyId);
102
123
 
103
124
  /**
@@ -105,7 +126,7 @@ const rowsOf = (policy, policyId) => policy.policies.filter((row) => row.policyI
105
126
  * firstPartyPolicyId: string|undefined, schemas: object, kernel?: object}} options
106
127
  * @returns {{policyIdFor: (subject: object) => string, signerFor: (subject: object) => object, assertSealed: (subject: object, stated: object|undefined) => void}}
107
128
  */
108
- export function createBundleSignerSelection({ policy, thirdPartyPins, firstPartyPolicyId, schemas, kernel }) {
129
+ export function createBundleSignerSelection({ policy, thirdPartyPins, firstPartyPolicyId, schemas, kernel, gitSourcedBundle = GitSourcedBundle.Refused }) {
109
130
  let loaded = kernel;
110
131
  const kernelExports = () => (loaded ??= loadBundleSignerKernel());
111
132
 
@@ -155,6 +176,24 @@ export function createBundleSignerSelection({ policy, thirdPartyPins, firstParty
155
176
  return row;
156
177
  }
157
178
 
179
+ // A git-sourced biome's develop bundle: the ONE develop row of the source's repository (a host module never reaches here:
180
+ // the resolve refuses an OCI host module on a git-sourced biome in every channel).
181
+ function developGitRow(subject) {
182
+ const label = `${describeSubject(subject)} develop bundle signer`;
183
+ const repository = repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(subject.source.url, `${label} source url`), `${label} source url`);
184
+ let row;
185
+ try {
186
+ row = bundleSignerRowFor({ policy, repository, channel: EditionChannel.Develop, schemas, label });
187
+ } catch (error) {
188
+ throw new BundleSignerPolicyAbsentError(error.message);
189
+ }
190
+ requireKind(subject, row, OIDC_CERTIFICATE_KIND);
191
+ return onlyRow(row.policyId);
192
+ }
193
+
194
+ /** Refuses a git-sourced subject this selection does not admit; any other subject passes. */
195
+ const assertAdmitted = (subject) => assertGitSourceAdmitted(subject, gitSourcedBundle);
196
+
158
197
  function requirePolicy(subject) {
159
198
  if (policy === null) {
160
199
  throw new BundleSignerPolicyAbsentError(
@@ -164,8 +203,9 @@ export function createBundleSignerSelection({ policy, thirdPartyPins, firstParty
164
203
  }
165
204
 
166
205
  function selectedRow(subject) {
167
- assertNotGitSourced(subject);
206
+ assertAdmitted(subject);
168
207
  requirePolicy(subject);
208
+ if (subject.source !== undefined) {return developGitRow(subject);}
169
209
  return isThirdPartyOciRow(subject) ? thirdPartyRow(subject) : firstPartyRow(subject);
170
210
  }
171
211
 
@@ -178,6 +218,7 @@ export function createBundleSignerSelection({ policy, thirdPartyPins, firstParty
178
218
  };
179
219
 
180
220
  return {
221
+ assertAdmitted,
181
222
  policyIdFor: (subject) => selectedRow(subject).policyId,
182
223
  signerFor,
183
224
  assertSealed(subject, stated) {
@@ -200,7 +241,7 @@ export function createBundleSignerSelection({ policy, thirdPartyPins, firstParty
200
241
  * The data root's selection, LAZY: nothing is read until the first OCI subject asks, so a lock with no OCI row never
201
242
  * needs a policy, a pin or a kernel that speaks v33.
202
243
  */
203
- export function loadBundleSignerSelection({ dataRoot, schemas }) {
244
+ export function loadBundleSignerSelection({ dataRoot, schemas, gitSourcedBundle = GitSourcedBundle.Refused }) {
204
245
  let selection;
205
246
  const resolved = () => {
206
247
  if (selection === undefined) {
@@ -211,11 +252,14 @@ export function loadBundleSignerSelection({ dataRoot, schemas }) {
211
252
  thirdPartyPins: loadTrustedThirdPartySourcePins(dataRoot),
212
253
  firstPartyPolicyId: config.firstPartyBundleSigner?.policyId,
213
254
  schemas,
255
+ gitSourcedBundle,
214
256
  });
215
257
  }
216
258
  return selection;
217
259
  };
218
260
  return Object.freeze({
261
+ // Refusing an unadmitted git-sourced row needs no data root, so it reads none.
262
+ assertAdmitted: (subject) => assertGitSourceAdmitted(subject, gitSourcedBundle),
219
263
  policyIdFor: (subject) => resolved().policyIdFor(subject),
220
264
  signerFor: (subject) => resolved().signerFor(subject),
221
265
  assertSealed: (subject, stated) => resolved().assertSealed(subject, stated),
@@ -242,12 +286,12 @@ export function lockBundleSubjects(lock) {
242
286
  * row is never checked and never resolves a data root: `selection` is used when given, else the data root's is loaded
243
287
  * on the first OCI subject.
244
288
  */
245
- export function assertLockBundleSigners({ lock, selection, schemas }) {
289
+ export function assertLockBundleSigners({ lock, selection, schemas, gitSourcedBundle = GitSourcedBundle.Refused }) {
246
290
  const subjects = lockBundleSubjects(lock);
247
291
  if (subjects.length === 0) {return;}
248
- const chosen = selection ?? loadBundleSignerSelection({ dataRoot: dataRoot(), schemas });
292
+ const chosen = selection ?? loadBundleSignerSelection({ dataRoot: dataRoot(), schemas, gitSourcedBundle });
249
293
  collectRefusals(subjects, ({ row, subject }) => {
250
- assertNotGitSourced(subject);
294
+ chosen.assertAdmitted(subject);
251
295
  chosen.assertSealed(subject, row.bundleSigner);
252
296
  }, isRefusal, BundleSignerRefusedError);
253
297
  }
@@ -62,6 +62,7 @@
62
62
 
63
63
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
64
64
  import { join } from 'node:path';
65
+ import { isDeepStrictEqual } from 'node:util';
65
66
 
66
67
  import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest } from './adapter-bundled-versions.mjs';
67
68
  import {
@@ -75,6 +76,7 @@ import {
75
76
  writeAuthorizationLock,
76
77
  } from './authorization-base.mjs';
77
78
  import { RefusalError, captureRefusal, isRefusal, throwCollectedRefusals } from './collected-refusals.mjs';
79
+ import { developGitBundleKey, developGitBundleSubjects } from './develop-git-bundles.mjs';
78
80
  import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-components.mjs';
79
81
  import { fleetChartSetLockPath, writeEditionChartSet } from './edition-chart-set.mjs';
80
82
  import { workspaceSection } from './edition-config.mjs';
@@ -96,7 +98,14 @@ import {
96
98
  validateImmutableDistributionLock,
97
99
  validateServiceDependencyOwnerParity,
98
100
  } from './lib.mjs';
99
- import { BundleSignerPolicyAbsentError, BundleSignerRefusedError, GitSourcedHostModuleBundleError, loadBundleSignerSelection } from './locked-bundle-signer.mjs';
101
+ import {
102
+ BundleSignerPolicyAbsentError,
103
+ BundleSignerRefusedError,
104
+ DevelopGitBundleError,
105
+ GitSourcedBundle,
106
+ GitSourcedHostModuleBundleError,
107
+ loadBundleSignerSelection,
108
+ } from './locked-bundle-signer.mjs';
100
109
  import { admitDistributionLock } from './operation-grant-admission.mjs';
101
110
  import { PUBLISHED_PIN_SOURCE, pinnedSource } from './pin-source.mjs';
102
111
  import { verifiedPlatformServiceIndex } from './platform-service-ownership.mjs';
@@ -1792,7 +1801,14 @@ export function resolveUnadmittedDistributionFromInputs(
1792
1801
  // trust policy selects for it. See `locked-bundle-signer.mjs`.
1793
1802
  const bundleSignerRefusals = [];
1794
1803
  const bundleSignerOf = (e, bundleSource, componentKey) => {
1795
- const subject = { biomeId: e.id, ...(componentKey === undefined ? {} : { componentKey }), origin: e.origin, repo: e.repo, bundleSource };
1804
+ const subject = {
1805
+ biomeId: e.id,
1806
+ ...(componentKey === undefined ? {} : { componentKey }),
1807
+ origin: e.origin,
1808
+ repo: e.repo,
1809
+ ...(isGitSourced(e) ? { source: requireSourceRecord(e, sourceRecords) } : {}),
1810
+ bundleSource,
1811
+ };
1796
1812
  if (composed?.bundleSigners === undefined) {
1797
1813
  throw new BundleSignerPolicyAbsentError(
1798
1814
  `${componentKey === undefined ? `biome "${e.id}"` : `host module "${componentKey}" of biome "${e.id}"`} is an OCI bundle, but this resolve was given no bundle signer selection`,
@@ -1801,12 +1817,45 @@ export function resolveUnadmittedDistributionFromInputs(
1801
1817
  // Every row is selected before any is refused, so one run names them all; the lock is not built from a refused row.
1802
1818
  return captureRefusal(bundleSignerRefusals, () => composed.bundleSigners.signerFor(subject), isRefusal);
1803
1819
  };
1804
- // A row drawn from a GIT source carries no OCI bundle (biome-level; an OCI host-module bundle is refused below), hence no bundleSigner: it is built
1805
- // from the pinned source, and the manifest's `bundleSource` names a release artifact not built from this commit.
1806
- // Omitted here, at the copy point. (An entry of a declared `oci` source has a stamp but no record: not git.)
1820
+ // A row drawn from a GIT source is built from the pinned source, and the manifest's biome-level `bundleSource` names an
1821
+ // artifact its release lane publishes, not one proven to be built from this commit. So, at the copy point:
1822
+ // - with `composed.developGitBundles` (a VENDOR DEVELOP resolve, XEMA-2456) the row carries the bundle the develop
1823
+ // resolve verified for it (`develop-git-bundles.mjs`: the develop signer at the attested commit, content-bound to
1824
+ // the frozen one), by digest, and its develop bundle signer. A git-sourced OCI row the resolve did not verify is
1825
+ // refused, never stripped;
1826
+ // - otherwise (release, customer, dist-check) it carries neither, as before; the release refuses the declaration.
1827
+ // An OCI host-module bundle on a git-sourced biome is refused below in every channel. (An entry of a declared `oci`
1828
+ // source has a stamp but no record: not git.)
1807
1829
  const isGitSourced = (e) => e.source !== undefined && sourceRecords.has(e.source);
1808
- const bundleSourceOf = (e) => (e.bundleSource === undefined || isGitSourced(e) ? {} : { bundleSource: e.bundleSource });
1809
- const bundleSignerFor = (e) => (e.bundleSource?.kind === 'oci' && !isGitSourced(e) ? { bundleSigner: bundleSignerOf(e, e.bundleSource) } : {});
1830
+ const developGitBundleOf = (e) => {
1831
+ if (composed?.developGitBundles === undefined || !isGitSourced(e) || e.bundleSource?.kind !== 'oci') {return undefined;}
1832
+ const verified = composed.developGitBundles.get(developGitBundleKey(e.source, e.id));
1833
+ if (verified === undefined) {
1834
+ throw new DevelopGitBundleError(`biome "${e.id}" (git source "${e.source}") declares an OCI bundle (${e.bundleSource.ociRef}) that this develop resolve did not verify`);
1835
+ }
1836
+ return verified;
1837
+ };
1838
+ const bundleSourceOf = (e) => {
1839
+ if (e.bundleSource === undefined) {return {};}
1840
+ if (!isGitSourced(e)) {return { bundleSource: e.bundleSource };}
1841
+ const verified = developGitBundleOf(e);
1842
+ return verified === undefined ? {} : { bundleSource: verified.bundleSource };
1843
+ };
1844
+ const bundleSignerFor = (e) => {
1845
+ if (e.bundleSource?.kind !== 'oci') {return {};}
1846
+ if (!isGitSourced(e)) {return { bundleSigner: bundleSignerOf(e, e.bundleSource) };}
1847
+ const verified = developGitBundleOf(e);
1848
+ if (verified === undefined) {return {};}
1849
+ const bundleSigner = bundleSignerOf(e, verified.bundleSource);
1850
+ // A bundle carried from a pinned lock (an offline re-seal) was verified under the pinned signer; a develop row rotated
1851
+ // since then would put into the lock a signer the bundle was never verified under, so any difference is refused.
1852
+ if (verified.pinnedBundleSigner !== undefined && !isDeepStrictEqual(verified.pinnedBundleSigner, bundleSigner)) {
1853
+ throw new DevelopGitBundleError(
1854
+ `biome "${e.id}" (git source "${e.source}"): the pinned edition verified ${verified.bundleSource.ociRef} under bundle signer ${JSON.stringify(verified.pinnedBundleSigner)}, but the current trust policy selects ${JSON.stringify(bundleSigner)}; resolve the develop edition to verify the bundle under the current row`,
1855
+ );
1856
+ }
1857
+ return { bundleSigner };
1858
+ };
1810
1859
  const hostModuleOf = (e, hostModule) => {
1811
1860
  if (isGitSourced(e)) {
1812
1861
  // The lock schema requires a host module's bundleSource, so an OCI one cannot be omitted: it is refused, in every
@@ -2405,7 +2454,7 @@ export async function prepareDistributionWithIndex(id, { materialize, inputs, re
2405
2454
  validateImmutableBiomeIndex(inputs?.biomeIndex ?? pinned.biomeIndex),
2406
2455
  platformServiceIndex,
2407
2456
  );
2408
- const { entries, sourceRecords } = await walkDeclaredSources({
2457
+ const { entries, sourceRecords, checkouts } = await walkDeclaredSources({
2409
2458
  sources: composed.declaredSources,
2410
2459
  ...(materialize === undefined ? {} : { materialize }),
2411
2460
  });
@@ -2416,19 +2465,33 @@ export async function prepareDistributionWithIndex(id, { materialize, inputs, re
2416
2465
  inputs !== undefined && Object.hasOwn(inputs, 'storePublisherTrust')
2417
2466
  ? inputs.storePublisherTrust
2418
2467
  : readStorePublisherTrust();
2419
- const lock = resolveUnadmittedDistributionFromInputs(
2420
- id,
2421
- index,
2422
- schemas,
2423
- {
2424
- ...composed,
2425
- sourceRecords,
2426
- storePublisherTrust,
2427
- bundleSigners: inputs?.bundleSigners ?? loadBundleSignerSelection({ dataRoot: dataRoot(), schemas }),
2428
- },
2429
- new Date().toISOString(),
2430
- platformServiceIndex,
2431
- );
2468
+ // A VENDOR DEVELOP resolve hands `inputs.developGitBundles` (develop-git-bundles.mjs); nothing else does, so a release,
2469
+ // customer or dist-check resolve keeps a git-sourced row bundle-less, exactly as before.
2470
+ const developGitBundles = inputs?.developGitBundles;
2471
+ const bundleSigners =
2472
+ inputs?.bundleSigners ??
2473
+ loadBundleSignerSelection({
2474
+ dataRoot: dataRoot(),
2475
+ schemas,
2476
+ gitSourcedBundle: developGitBundles === undefined ? GitSourcedBundle.Refused : GitSourcedBundle.DevelopVerified,
2477
+ });
2478
+ const resolvedAt = new Date().toISOString();
2479
+ const resolveWith = (verified) =>
2480
+ resolveUnadmittedDistributionFromInputs(
2481
+ id,
2482
+ index,
2483
+ schemas,
2484
+ { ...composed, sourceRecords, storePublisherTrust, bundleSigners, ...(verified === undefined ? {} : { developGitBundles: verified }) },
2485
+ resolvedAt,
2486
+ platformServiceIndex,
2487
+ );
2488
+ // Selection never reads a bundle, so the first pass selects exactly the biomes the second ships: only THEIR bundles are
2489
+ // verified (a source's unselected biome is never fetched), and the second pass refuses any git-sourced OCI row the
2490
+ // verifier did not answer for.
2491
+ const selectedOnly = resolveWith(undefined);
2492
+ if (developGitBundles === undefined) {return { lock: selectedOnly, index, schemas };}
2493
+ const subjects = developGitBundleSubjects({ lock: selectedOnly, index });
2494
+ const lock = subjects.length === 0 ? selectedOnly : resolveWith(await developGitBundles.verify({ subjects, checkouts, schemas }));
2432
2495
  return { lock, index, schemas };
2433
2496
  }
2434
2497