@xemahq/xema 0.53.0 → 0.54.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 (47) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +2 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/auth.d.ts +9 -0
  6. package/dist/commands/auth.d.ts.map +1 -0
  7. package/dist/commands/auth.js +66 -0
  8. package/dist/commands/auth.js.map +1 -0
  9. package/dist/commands/edition.d.ts.map +1 -1
  10. package/dist/commands/edition.js +1 -0
  11. package/dist/commands/edition.js.map +1 -1
  12. package/dist/edition/verify-image-evidence.bundle.mjs +916 -860
  13. package/dist/edition/verify-image-evidence.bundle.provenance.json +495 -489
  14. package/dist/runtime/edition/verb-map.d.ts +2 -1
  15. package/dist/runtime/edition/verb-map.d.ts.map +1 -1
  16. package/dist/runtime/edition/verb-map.js +2 -0
  17. package/dist/runtime/edition/verb-map.js.map +1 -1
  18. package/edition/README.md +54 -2
  19. package/edition/adapter-bundled-versions.mjs +2 -1
  20. package/edition/authorization-base.mjs +9 -8
  21. package/edition/biome-content.mjs +23 -29
  22. package/edition/candidate-edition.mjs +692 -0
  23. package/edition/checkout-indexed-source-repositories.mjs +6 -8
  24. package/edition/collected-refusals.mjs +59 -0
  25. package/edition/contract-authority.mjs +122 -0
  26. package/edition/cosign-certificate-constraints.mjs +18 -8
  27. package/edition/customer-edition-verbs.mjs +30 -8
  28. package/edition/develop-chart-set.mjs +7 -6
  29. package/edition/develop-edition-invariants.mjs +2 -1
  30. package/edition/develop-edition-resolve.mjs +109 -31
  31. package/edition/develop-edition.mjs +49 -8
  32. package/edition/develop-engine-image.mjs +95 -0
  33. package/edition/develop-image-probe.mjs +2 -1
  34. package/edition/develop-resolve-report.mjs +159 -0
  35. package/edition/develop-snapshot-inputs.mjs +4 -3
  36. package/edition/develop-verify-image-record.mjs +23 -4
  37. package/edition/edition-artifact.mjs +17 -7
  38. package/edition/edition-protocol.mjs +5 -2
  39. package/edition/images-from-record.mjs +362 -0
  40. package/edition/lib.mjs +1 -19
  41. package/edition/locked-bundle-signer.mjs +15 -3
  42. package/edition/operation-grant-admission.mjs +51 -9
  43. package/edition/resolve-distribution.mjs +95 -82
  44. package/edition/resolve-images.mjs +12 -5
  45. package/edition/shallow-checkout.mjs +18 -0
  46. package/edition/workspace-sections.mjs +23 -3
  47. package/package.json +7 -3
@@ -0,0 +1,692 @@
1
+ // candidate-edition — re-seal a pinned develop edition with ONE repository's candidate commit, for a boot test.
2
+ //
3
+ // A repository's candidate CI boots the composed member set through the production path, and that needs
4
+ // a lock triple that matches its candidate commit: a pinned develop edition is stale against it the moment
5
+ // the candidate changes a biome. This module produces that triple, in two steps:
6
+ //
7
+ // fetchPinnedEditionCheckouts the ONE network step: every checkout the develop resolve reads, at the
8
+ // pinned edition's own pins (xema-edition.json provenance.pins), cached
9
+ // under the caller's cacheDir by pin key + commit. A warm cache does no network.
10
+ // resealCandidateEdition OFFLINE: the develop resolve's distribution and authorization steps rerun
11
+ // over those frozen checkouts with the candidate repository at its HEAD, the
12
+ // images lock carried byte for byte, and the unsplit resolved deployment the
13
+ // kernel computes from exactly the bytes written.
14
+ //
15
+ // THE OUTPUT IS A PLAIN DIRECTORY, NEVER AN EDITION. It carries no xema-edition.json descriptor, it is never
16
+ // an OCI artifact and it is never signed, so no deploy admits it: admission rests on the cosign signature of
17
+ // a pushed edition artifact's digest (edition-artifact.mjs), and the resolved deployment written here is the
18
+ // kernel's UNSPLIT variant, which `verifyResolvedDeployment` refuses unless its caller opts into it.
19
+ //
20
+ // THE CANDIDATE CONTENT RECORD. The images lock is carried, so for a biome that ships images from the
21
+ // candidate repository it still says the images were built at the PINNED commit, and the content-hash rule
22
+ // (biome-content.mjs `pinnedCommit`) reads a biome's content where its images were built. The candidate's
23
+ // content is at another commit, so the output records that fact in `candidate-content.json`
24
+ // ({ repo -> commit }, bound to the distribution lock bytes by digest) and the seal and the verifier read it
25
+ // through biome-content.mjs `contentCommits`: a record naming a commit whose content is not the lock's is
26
+ // refused by the same comparison every seal makes, and a triple without a record verifies exactly as before.
27
+
28
+ import { execFileSync } from 'node:child_process';
29
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
30
+ import { tmpdir } from 'node:os';
31
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
32
+
33
+ import { sealLock, verifyBiomeContentHashes } from './biome-content.mjs';
34
+ import {
35
+ crossRepoVendoredCommits,
36
+ platformServiceExtraCommits,
37
+ sourceAxisExtraCommits,
38
+ sourceCheckoutPlan,
39
+ } from './checkout-indexed-source-repositories.mjs';
40
+ import { assertDevelopEditionInvariants } from './develop-edition-invariants.mjs';
41
+ import { buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
42
+ import { parseEditionDescriptor } from './edition-artifact.mjs';
43
+ import { resetEditionWorkspaceConfig, editionWorkspaceConfig } from './edition-config.mjs';
44
+ import { DIGEST_PIN_KEYS, EDITION_DESCRIPTOR_FILE, EditionChannel, EditionLockFile } from './edition-constants.mjs';
45
+ import {
46
+ KERNEL_PACKAGE_REPO_PATH,
47
+ WorkspaceRoot,
48
+ configureEditionWorkspace,
49
+ configuredWorkspace,
50
+ kernelDistPath,
51
+ kernelPackageDir,
52
+ platformServiceIndexPath,
53
+ runtimeImageSourceIndexPath,
54
+ } from './edition-workspace.mjs';
55
+ import { materializeGitSource, externalSourceCacheDir } from './external-source-walk.mjs';
56
+ import { withSourceGitEnv } from './git-source-auth.mjs';
57
+ import { loadKernelDistributionSchemas, serializeJson, sha256Hex } from './lib.mjs';
58
+ import { assertLockBundleSigners } from './locked-bundle-signer.mjs';
59
+ import { admitDistributionLock } from './operation-grant-admission.mjs';
60
+ import {
61
+ EDITIONS_ROOT_PIN_KEY,
62
+ SnapshotPinNamespace,
63
+ applyPinSource,
64
+ dataPinKey,
65
+ kernelPinKey,
66
+ pinnedSource,
67
+ snapshotPinNamespace,
68
+ snapshotPinSource,
69
+ } from './pin-source.mjs';
70
+ import { composeAuthorizationBase, composeEdition, prepareDistributionWithIndex } from './resolve-distribution.mjs';
71
+ import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
72
+ import { shallowCheckout, shallowFetch } from './shallow-checkout.mjs';
73
+ import { createPinnedSourceAuthority } from './source-authority.mjs';
74
+ import { readStrictJsonFile } from './strict-json.mjs';
75
+
76
+ /** The candidate content record: which repository's first-party biomes have their content at which commit. */
77
+ export const CANDIDATE_CONTENT_FILE = 'candidate-content.json';
78
+ export const CANDIDATE_CONTENT_SCHEMA = 'xema.candidate-content/v1';
79
+ /** The kernel's unsplit resolved deployment, computed from exactly the lock bytes written beside it. */
80
+ export const RESOLVED_DEPLOYMENT_FILE = 'resolved-deployment.json';
81
+ /** The coordinate a candidate triple names: a reserved `.invalid` registry, so it can never be read as a published edition's. */
82
+ export const CANDIDATE_EDITION_REGISTRY = 'candidate.invalid';
83
+ export const CANDIDATE_EDITION_VERSION = '0.0.0-candidate';
84
+ /** Every file of a candidate triple, in the order it is written. */
85
+ export const CANDIDATE_FILES = Object.freeze([
86
+ EditionLockFile.Distribution,
87
+ EditionLockFile.Images,
88
+ EditionLockFile.Authorization,
89
+ RESOLVED_DEPLOYMENT_FILE,
90
+ CANDIDATE_CONTENT_FILE,
91
+ ]);
92
+
93
+ /** A refusal of the candidate re-seal: the pinned edition, a checkout or the candidate is not what it must be. */
94
+ export class CandidateEditionError extends Error {
95
+ constructor(message) {
96
+ super(message);
97
+ this.name = 'CandidateEditionError';
98
+ }
99
+ }
100
+
101
+
102
+ function defaultRunGit(args, env) {
103
+ return execFileSync('git', args, { encoding: 'utf8', env, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
104
+ }
105
+
106
+ /**
107
+ * The environment of a LOCAL git command: PATH and HOME only. No GIT_* variable (GIT_DIR, GIT_WORK_TREE,
108
+ * GIT_INDEX_FILE, a hook's environment) reaches it, so it reads exactly the directory it names; it needs no credential.
109
+ */
110
+ function localGitEnv() {
111
+ const env = {};
112
+ for (const name of ['PATH', 'HOME']) {
113
+ if (process.env[name] !== undefined) {env[name] = process.env[name];}
114
+ }
115
+ return env;
116
+ }
117
+
118
+ function runLocalGit(args) {
119
+ return execFileSync('git', args, { encoding: 'utf8', env: localGitEnv(), stdio: ['ignore', 'pipe', 'pipe'] }).trim();
120
+ }
121
+
122
+ /** A LOCAL git read; the reseal runs only these. */
123
+ function localGit(dir, args) {
124
+ return runLocalGit(['-C', dir, ...args]);
125
+ }
126
+
127
+ function hasCommit(dir, commit) {
128
+ try {
129
+ localGit(dir, ['cat-file', '-e', `${commit}^{commit}`]);
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /** The develop build of the kernel: the same two commands develop-edition.yaml runs in its kernel checkout. */
137
+ function defaultBuildKernel(repoDir) {
138
+ for (const args of [['pnpm', 'install', '--frozen-lockfile'], ['pnpm', 'run', 'build']]) {
139
+ execFileSync('corepack', args, { cwd: repoDir, stdio: ['ignore', 'inherit', 'inherit'] });
140
+ }
141
+ }
142
+
143
+ function absoluteDir(name, value) {
144
+ if (typeof value !== 'string' || !isAbsolute(value)) {
145
+ throw new CandidateEditionError(`${name} must be an absolute directory path, got ${JSON.stringify(value)}`);
146
+ }
147
+ return resolve(value);
148
+ }
149
+
150
+ /**
151
+ * The pinned develop edition: its descriptor (strict parse), its pins, and the three lock files. A customer
152
+ * edition (digest pins), an edition of another channel, or a missing lock is refused.
153
+ */
154
+ export function readPinnedEdition(tripleDir) {
155
+ const dir = absoluteDir('tripleDir', tripleDir);
156
+ const descriptorPath = join(dir, EDITION_DESCRIPTOR_FILE);
157
+ if (!existsSync(descriptorPath)) {
158
+ throw new CandidateEditionError(`${dir} holds no ${EDITION_DESCRIPTOR_FILE}: the pinned edition's pins are read from its descriptor`);
159
+ }
160
+ const descriptor = parseEditionDescriptor(readStrictJsonFile(descriptorPath));
161
+ if (descriptor.provenance.channel !== EditionChannel.Develop) {
162
+ throw new CandidateEditionError(
163
+ `${descriptorPath}: "${descriptor.distributionId}" is a ${descriptor.provenance.channel} edition; a candidate is re-sealed only from a develop edition`,
164
+ );
165
+ }
166
+ const digestPins = DIGEST_PIN_KEYS.filter((key) => key in descriptor.provenance.pins);
167
+ if (digestPins.length > 0) {
168
+ throw new CandidateEditionError(
169
+ `${descriptorPath}: "${descriptor.distributionId}" pins ${digestPins.join(', ')}: a customer edition is resolved against a catalogue, not from checkouts`,
170
+ );
171
+ }
172
+ const bytes = {};
173
+ for (const file of [EditionLockFile.Distribution, EditionLockFile.Images, EditionLockFile.Authorization]) {
174
+ const path = join(dir, file);
175
+ if (!existsSync(path)) {throw new CandidateEditionError(`${dir} holds no ${file}`);}
176
+ bytes[file] = readFileSync(path);
177
+ }
178
+ for (const file of Object.keys(bytes)) {
179
+ const { distributionId } = JSON.parse(bytes[file].toString('utf8'));
180
+ if (distributionId !== descriptor.distributionId) {
181
+ throw new CandidateEditionError(`${join(dir, file)} is for "${distributionId}", but the descriptor names "${descriptor.distributionId}"`);
182
+ }
183
+ }
184
+ return { dir, id: descriptor.distributionId, pins: { ...descriptor.provenance.pins }, bytes };
185
+ }
186
+
187
+ /** Where a pin key's checkout at `commit` is cached. */
188
+ function cachedCheckoutDir(cacheDir, key, commit) {
189
+ return join(cacheDir, 'checkouts', key.replaceAll('/', '__'), commit);
190
+ }
191
+
192
+ /**
193
+ * A checkout that IS the frozen commit: HEAD is the pin and the tree is clean. `trackedOnly` ignores
194
+ * untracked files (the candidate, whose tracked files are what is read); a cached checkout is held to all.
195
+ */
196
+ function assertCheckoutAt({ dir, commit, what, trackedOnly = false }) {
197
+ if (!existsSync(join(dir, '.git'))) {
198
+ throw new CandidateEditionError(`${what}: ${dir} is not a git checkout`);
199
+ }
200
+ const head = localGit(dir, ['rev-parse', 'HEAD']);
201
+ if (commit !== undefined && head !== commit) {
202
+ throw new CandidateEditionError(`${what}: ${dir} is at ${head}, not the pinned ${commit}`);
203
+ }
204
+ const dirty = localGit(dir, ['status', '--porcelain', ...(trackedOnly ? ['--untracked-files=no'] : [])]);
205
+ if (dirty.length > 0) {
206
+ throw new CandidateEditionError(`${what}: ${dir} is not clean, so it is not the commit ${head}:\n${dirty}`);
207
+ }
208
+ return head;
209
+ }
210
+
211
+ /** The candidate: a pin key of the edition's develop graph, and a clean checkout whose HEAD is the candidate commit. */
212
+ function readCandidate(candidate, pins) {
213
+ const repo = candidate?.repo;
214
+ if (typeof repo !== 'string' || !(repo in pins)) {
215
+ throw new CandidateEditionError(`candidate repository ${JSON.stringify(repo)} is not pinned by the edition; it pins ${Object.keys(pins).sort().join(', ')}`);
216
+ }
217
+ if (snapshotPinNamespace(repo) !== SnapshotPinNamespace.Graph) {
218
+ throw new CandidateEditionError(
219
+ `candidate repository "${repo}" is not a develop-graph repository (repos/<x> or submodules/<x>); the data root, the kernel and the chart tree are not candidates`,
220
+ );
221
+ }
222
+ const checkoutDir = absoluteDir('candidate.checkoutDir', candidate.checkoutDir);
223
+ const commit = assertCheckoutAt({ dir: checkoutDir, what: `candidate ${repo}`, trackedOnly: true });
224
+ return { repo, checkoutDir, commit };
225
+ }
226
+
227
+ /**
228
+ * Run `fn` with the edition workspace configured on `roots`, and put the previous configuration back after it.
229
+ * The edition workspace is PROCESS-GLOBAL configuration (edition-workspace.mjs): this swaps it for the duration
230
+ * of `fn`, async `fn` included. Two calls in flight in one process (two concurrent re-seals, fetches or
231
+ * verifications) would read each other's roots, so it is not safe to run two at once in one process.
232
+ */
233
+ function withWorkspace(roots, fn) {
234
+ const previous = configuredWorkspace();
235
+ configureEditionWorkspace(roots);
236
+ resetEditionWorkspaceConfig();
237
+ const restore = () => {
238
+ configureEditionWorkspace(previous);
239
+ resetEditionWorkspaceConfig();
240
+ };
241
+ let result;
242
+ try {
243
+ result = fn();
244
+ } catch (error) {
245
+ restore();
246
+ throw error;
247
+ }
248
+ if (result instanceof Promise) {return result.finally(restore);}
249
+ restore();
250
+ return result;
251
+ }
252
+
253
+ /**
254
+ * The checkouts of the pinned edition with the candidate in place: a sources root under `scratch` whose
255
+ * `<pin key>` entries link the cached checkouts (and the candidate's own), each held to its pin first.
256
+ * Returns the workspace roots the develop resolve reads.
257
+ */
258
+ function stageWorkspace({ cacheDir, dataKey, pins, candidate, scratch }) {
259
+ const dataDir = cachedDataRoot(cacheDir, dataKey, pins);
260
+ return withWorkspace({ [WorkspaceRoot.Data]: dataDir }, () => {
261
+ if (EDITIONS_ROOT_PIN_KEY in pins) {
262
+ throw new CandidateEditionError(`the edition pins "${EDITIONS_ROOT_PIN_KEY}": an editions root apart from the data root is not supported by the candidate re-seal`);
263
+ }
264
+ const kernelKey = kernelPinKey();
265
+ const kernelRepo = cachedCheckoutDir(cacheDir, kernelKey, pins[kernelKey]);
266
+ const sources = join(scratch, 'sources');
267
+ mkdirSync(sources, { recursive: true });
268
+ const graph = Object.keys(pins).filter((key) => snapshotPinNamespace(key) === SnapshotPinNamespace.Graph);
269
+ for (const key of graph) {
270
+ const target = key === candidate.repo ? candidate.checkoutDir : cachedCheckoutDir(cacheDir, key, pins[key]);
271
+ if (key !== candidate.repo) {
272
+ if (!existsSync(target)) {
273
+ throw new CandidateEditionError(`no cached checkout of ${key} at ${pins[key]} under ${cacheDir}: run fetchPinnedEditionCheckouts first`);
274
+ }
275
+ assertCheckoutAt({ dir: target, commit: pins[key], what: `cached ${key}` });
276
+ }
277
+ mkdirSync(dirname(join(sources, key)), { recursive: true });
278
+ symlinkSync(target, join(sources, key));
279
+ }
280
+ return {
281
+ roots: {
282
+ [WorkspaceRoot.Data]: dataDir,
283
+ [WorkspaceRoot.Editions]: dataDir,
284
+ [WorkspaceRoot.Sources]: sources,
285
+ [WorkspaceRoot.Kernel]: join(kernelRepo, ...KERNEL_PACKAGE_REPO_PATH.split('/')),
286
+ [WorkspaceRoot.Cache]: join(cacheDir, 'work'),
287
+ },
288
+ kernelRepo,
289
+ kernelKey,
290
+ };
291
+ });
292
+ }
293
+
294
+ /**
295
+ * The cached data root at the edition's data pin, held to it: its HEAD is the pin, it is clean, and its own
296
+ * pinRoles name `dataKey` as the data role (so a handle cannot point the resolve at another repository).
297
+ */
298
+ function cachedDataRoot(cacheDir, dataKey, pins) {
299
+ if (typeof dataKey !== 'string' || pins[dataKey] === undefined) {
300
+ throw new CandidateEditionError(`the edition does not pin a data root ${JSON.stringify(dataKey)}; run fetchPinnedEditionCheckouts first`);
301
+ }
302
+ const dir = cachedCheckoutDir(cacheDir, dataKey, pins[dataKey]);
303
+ if (!existsSync(dir)) {
304
+ throw new CandidateEditionError(`no cached data root ${dataKey} at ${pins[dataKey]} under ${cacheDir}: run fetchPinnedEditionCheckouts first`);
305
+ }
306
+ assertCheckoutAt({ dir, commit: pins[dataKey], what: `cached ${dataKey}` });
307
+ const declared = withWorkspace({ [WorkspaceRoot.Data]: dir }, () => dataPinKey());
308
+ if (declared !== dataKey) {
309
+ throw new CandidateEditionError(`the cached data root ${dir} declares its data role as "${declared}", not "${dataKey}"`);
310
+ }
311
+ return dir;
312
+ }
313
+
314
+ /**
315
+ * Build `dir` in a staging directory beside it and rename it into place only once `build(staging)` succeeded,
316
+ * so a cached checkout that exists is a complete one: a failed or interrupted run leaves only a staging
317
+ * directory, which the next run discards.
318
+ */
319
+ function stageThenRename(dir, build) {
320
+ const staging = `${dir}.partial-${process.pid}`;
321
+ rmSync(staging, { recursive: true, force: true });
322
+ mkdirSync(dirname(staging), { recursive: true });
323
+ try {
324
+ build(staging);
325
+ } catch (error) {
326
+ rmSync(staging, { recursive: true, force: true });
327
+ throw error;
328
+ }
329
+ renameSync(staging, dir);
330
+ }
331
+
332
+ /**
333
+ * A clone at `commit` under the cache, or the cached one held to it. Network only when absent. `prepare`
334
+ * (the kernel build) runs in the staging directory, before the rename, so a cached clone is never half-prepared.
335
+ */
336
+ function ensureCachedClone({ cacheDir, key, commit, url, workspace, env, runGit, prepare }) {
337
+ const dir = cachedCheckoutDir(cacheDir, key, commit);
338
+ if (existsSync(dir)) {
339
+ assertCheckoutAt({ dir, commit, what: `cached ${key}` });
340
+ return { dir, fetched: false };
341
+ }
342
+ stageThenRename(dir, (staging) => {
343
+ withSourceGitEnv({ name: key, url }, { workspace, env }, (gitEnv) => {
344
+ shallowCheckout({ runGit, gitEnv, target: staging, url, commit });
345
+ });
346
+ assertCheckoutAt({ dir: staging, commit, what: `fetched ${key}` });
347
+ if (prepare !== undefined) {
348
+ prepare(staging);
349
+ assertCheckoutAt({ dir: staging, commit, what: `prepared ${key}` });
350
+ }
351
+ });
352
+ return { dir, fetched: true };
353
+ }
354
+
355
+ /** Fetch `commits` into a cached checkout, only the ones it does not hold. */
356
+ function ensureCommits({ dir, key, url, commits, workspace, env, runGit }) {
357
+ const absent = commits.filter((commit) => !hasCommit(dir, commit));
358
+ if (absent.length === 0) {return 0;}
359
+ withSourceGitEnv({ name: key, url }, { workspace, env }, (gitEnv) => {
360
+ for (const commit of absent) {shallowFetch({ runGit, gitEnv, target: dir, commit });}
361
+ });
362
+ return absent.length;
363
+ }
364
+
365
+ /**
366
+ * THE ONE NETWORK STEP. Every checkout the develop resolve of the pinned edition reads, at the edition's
367
+ * pins, cached under `cacheDir/checkouts/<pin key>/<commit>`:
368
+ *
369
+ * - the data root, cloned LOCALLY from `dataRootDir` (the caller's own checkout of it at its pin: the data
370
+ * root is what declares every other repository and the credential that reads them);
371
+ * - the kernel, at its pin, built the way develop-edition.yaml builds it (`buildKernel`);
372
+ * - every develop-graph repository but the candidate, plus the extra commits the resolve reads in them
373
+ * (platform-service and cross-repo vendored commits, computed with the candidate in place);
374
+ * - every declared `sources[]` entry at its pin (external-source-walk.mjs's own cache).
375
+ *
376
+ * A warm cache spawns no network git command and no build. Every cached checkout is held to its pin and
377
+ * must be clean, or the fetch refuses. Credentials follow the data root's `sourceRepositoryUrlPrefix` rule
378
+ * (git-source-auth.mjs) and are read only from `env`, which the caller passes. Local git commands (the data-root
379
+ * clone, every HEAD and status check) run with PATH and HOME alone; `buildKernel`'s default inherits the process
380
+ * environment, as `corepack pnpm` needs it.
381
+ *
382
+ * @param {object} request
383
+ * @param {string} request.tripleDir the pinned develop edition: xema-edition.json and its lock files
384
+ * @param {string} request.cacheDir absolute, writable; reused across runs
385
+ * @param {string} request.dataRootDir a checkout of the data root at the edition's data pin
386
+ * @param {{ repo: string, checkoutDir: string }} request.candidate the candidate repository's pin key and checkout
387
+ * @param {Record<string, string>} [request.env] the environment credentials are read from (default: none)
388
+ * @returns {Promise<{ cacheDir: string, dataKey: string, distributionId: string, fetched: string[] }>} the handle
389
+ * `resealCandidateEdition` takes; every checkout it names is re-held to its pin when it is used, never trusted
390
+ */
391
+ export async function fetchPinnedEditionCheckouts({ tripleDir, cacheDir, dataRootDir, candidate, env = {}, runGit = defaultRunGit, buildKernel = defaultBuildKernel }) {
392
+ const edition = readPinnedEdition(tripleDir);
393
+ const cache = absoluteDir('cacheDir', cacheDir);
394
+ mkdirSync(join(cache, 'work'), { recursive: true });
395
+ const dataSource = absoluteDir('dataRootDir', dataRootDir);
396
+ const fetched = [];
397
+ // The data root first: it names the pin roles, the graph and the credential.
398
+ const dataKey = withWorkspace({ [WorkspaceRoot.Data]: dataSource }, () => dataPinKey());
399
+ const dataCommit = edition.pins[dataKey];
400
+ if (dataCommit === undefined) {throw new CandidateEditionError(`the edition does not pin its data root "${dataKey}"`);}
401
+ const dataDir = cachedCheckoutDir(cache, dataKey, dataCommit);
402
+ if (existsSync(dataDir)) {
403
+ assertCheckoutAt({ dir: dataDir, commit: dataCommit, what: `cached ${dataKey}` });
404
+ } else {
405
+ assertCheckoutAt({ dir: dataSource, commit: dataCommit, what: `dataRootDir ${dataKey}` });
406
+ stageThenRename(dataDir, (staging) => {
407
+ runLocalGit(['clone', '--quiet', '--no-checkout', dataSource, staging]);
408
+ localGit(staging, ['-c', 'advice.detachedHead=false', 'checkout', '--quiet', '--detach', dataCommit]);
409
+ assertCheckoutAt({ dir: staging, commit: dataCommit, what: `cloned ${dataKey}` });
410
+ });
411
+ fetched.push(dataKey);
412
+ }
413
+ const workspace = withWorkspace({ [WorkspaceRoot.Data]: dataDir }, () => {
414
+ readCandidate(candidate, edition.pins);
415
+ return editionWorkspaceConfig();
416
+ });
417
+ const committedIndex = readStrictJsonFile(join(dataDir, 'runtime-image-source-index.json'));
418
+ // The kernel, at its pin, from the repository its contract authority names, built once.
419
+ const kernelKey = withWorkspace({ [WorkspaceRoot.Data]: dataDir }, () => kernelPinKey());
420
+ const kernelUrl = committedIndex?.contractAuthority?.repositoryUrl;
421
+ if (typeof kernelUrl !== 'string') {throw new CandidateEditionError(`${dataKey}: runtime-image-source-index.json names no contractAuthority.repositoryUrl`);}
422
+ const builtKernel = (repoDir) => join(repoDir, ...KERNEL_PACKAGE_REPO_PATH.split('/'), 'dist', 'distribution', 'index.js');
423
+ const kernel = ensureCachedClone({
424
+ cacheDir: cache, key: kernelKey, commit: edition.pins[kernelKey], url: kernelUrl, workspace, env, runGit,
425
+ prepare: (staging) => {
426
+ buildKernel(staging);
427
+ if (!existsSync(builtKernel(staging))) {throw new CandidateEditionError(`the ${kernelKey} build produced no ${KERNEL_PACKAGE_REPO_PATH}/dist`);}
428
+ },
429
+ });
430
+ // A cached kernel exists only once its build succeeded (built in staging, renamed after); one without its
431
+ // build output is not this code's, and is refused rather than rebuilt in place.
432
+ if (!existsSync(builtKernel(kernel.dir))) {
433
+ throw new CandidateEditionError(`the cached ${kernelKey} at ${kernel.dir} holds no build output; remove it and fetch again`);
434
+ }
435
+ if (kernel.fetched) {fetched.push(kernelKey);}
436
+ // The graph, with the candidate in place: the plan is the develop graph at the candidate's pins.
437
+ const read = readCandidate(candidate, edition.pins);
438
+ const candidatePins = { ...edition.pins, [read.repo]: read.commit };
439
+ const index = withWorkspace({ [WorkspaceRoot.Data]: dataDir }, () => applyPinSource(committedIndex, snapshotPinSource(candidatePins)));
440
+ const plan = sourceCheckoutPlan(index, workspace);
441
+ for (const entry of plan) {
442
+ if (entry.repo === read.repo) {continue;}
443
+ if (ensureCachedClone({ cacheDir: cache, key: entry.repo, commit: entry.sourceCommit, url: entry.repositoryUrl, workspace, env, runGit }).fetched) {
444
+ fetched.push(entry.repo);
445
+ }
446
+ }
447
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-candidate-fetch-'));
448
+ try {
449
+ const staged = stageWorkspace({ cacheDir: cache, dataKey, pins: candidatePins, candidate: read, scratch });
450
+ await withWorkspace(staged.roots, async () => {
451
+ const extra = new Map();
452
+ const add = (map) => {
453
+ for (const [repo, commits] of map) {extra.set(repo, [...new Set([...(extra.get(repo) ?? []), ...[commits].flat()])]);}
454
+ };
455
+ add(sourceAxisExtraCommits(plan));
456
+ add(platformServiceExtraCommits(plan, readStrictJsonFile(platformServiceIndexPath())));
457
+ const crossRepoOnly = crossRepoVendoredCommits(plan, index, staged.roots[WorkspaceRoot.Sources]);
458
+ add(crossRepoOnly);
459
+ for (const entry of plan) {
460
+ const commits = extra.get(entry.repo) ?? [];
461
+ if (commits.length === 0) {continue;}
462
+ if (entry.repo === read.repo) {
463
+ // The snapshot resolve rebuilds the platform service index AT the candidate commit, so a platform
464
+ // service's committed commit is never read in the candidate; a cross-repo vendored commit is.
465
+ const absent = (crossRepoOnly.get(entry.repo) ?? []).filter((commit) => !hasCommit(read.checkoutDir, commit));
466
+ if (absent.length > 0) {
467
+ throw new CandidateEditionError(`candidate ${read.repo}: the resolve also reads ${absent.join(', ')}, which its checkout does not hold; fetch them into it`);
468
+ }
469
+ continue;
470
+ }
471
+ const dir = cachedCheckoutDir(cache, entry.repo, entry.sourceCommit);
472
+ if (ensureCommits({ dir, key: entry.repo, url: entry.repositoryUrl, commits, workspace, env, runGit }) > 0) {fetched.push(`${entry.repo} (extra commits)`);}
473
+ }
474
+ // Every declared source at its pin, in the cache external-source-walk reads (an exact commit is never resolved remotely).
475
+ const schemas = await loadKernelDistributionSchemas();
476
+ const pinSource = snapshotPinSource(candidatePins);
477
+ const composed = composeEdition(schemas.parseDistribution, edition.id, pinSource);
478
+ for (const source of composed.declaredSources.filter((declared) => declared.kind !== 'oci')) {
479
+ materializeGitSource({ source: pinnedSource(source, pinSource), cacheRoot: externalSourceCacheDir(), env, workspace });
480
+ }
481
+ });
482
+ } finally {
483
+ rmSync(scratch, { recursive: true, force: true });
484
+ }
485
+ return { cacheDir: cache, dataKey, distributionId: edition.id, fetched };
486
+ }
487
+
488
+ /**
489
+ * The candidate content record, held to the distribution lock bytes it was written beside AND to the
490
+ * candidate it records: exactly one entry, `candidate.repo` at `candidate.commit` (the candidate checkout's
491
+ * HEAD), for the edition `distributionId`. A record for another commit (one in the candidate's history
492
+ * included), another or a second repository, or another edition is refused.
493
+ */
494
+ export function parseCandidateContentRecord(value, { distributionLockBytes, distributionId, candidate }) {
495
+ const keys = ['schema', 'distributionId', 'distributionLockSha256', 'contentCommits'];
496
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join() !== [...keys].sort().join()) {
497
+ throw new CandidateEditionError(`${CANDIDATE_CONTENT_FILE} must be exactly {${keys.join(', ')}}`);
498
+ }
499
+ if (value.schema !== CANDIDATE_CONTENT_SCHEMA) {
500
+ throw new CandidateEditionError(`${CANDIDATE_CONTENT_FILE}: schema is ${JSON.stringify(value.schema)}, expected ${CANDIDATE_CONTENT_SCHEMA}`);
501
+ }
502
+ const digest = sha256Hex(distributionLockBytes);
503
+ if (value.distributionLockSha256 !== digest) {
504
+ throw new CandidateEditionError(
505
+ `${CANDIDATE_CONTENT_FILE} is bound to the distribution lock ${value.distributionLockSha256}, but the lock beside it hashes to ${digest}`,
506
+ );
507
+ }
508
+ if (value.distributionId !== distributionId) {
509
+ throw new CandidateEditionError(
510
+ `${CANDIDATE_CONTENT_FILE} records the edition ${JSON.stringify(value.distributionId)}, not ${JSON.stringify(distributionId)}`,
511
+ );
512
+ }
513
+ const commits = value.contentCommits;
514
+ const entries = commits !== null && typeof commits === 'object' && !Array.isArray(commits) ? Object.entries(commits) : [];
515
+ if (entries.length !== 1) {
516
+ throw new CandidateEditionError(
517
+ `${CANDIDATE_CONTENT_FILE}: contentCommits must hold exactly one entry, the candidate ${candidate.repo}; it holds ${entries.length}`,
518
+ );
519
+ }
520
+ const [[repo, commit]] = entries;
521
+ if (repo !== candidate.repo) {
522
+ throw new CandidateEditionError(`${CANDIDATE_CONTENT_FILE} records ${JSON.stringify(repo)}, not the candidate ${candidate.repo}`);
523
+ }
524
+ if (commit !== candidate.commit) {
525
+ throw new CandidateEditionError(
526
+ `${CANDIDATE_CONTENT_FILE} records ${repo} at ${JSON.stringify(commit)}, but the candidate checkout is at ${candidate.commit}`,
527
+ );
528
+ }
529
+ return new Map(entries);
530
+ }
531
+
532
+ /**
533
+ * OFFLINE. Re-seal the pinned develop edition with `candidate.repo` at its checkout's HEAD:
534
+ *
535
+ * distribution lock the develop resolve's own steps (buildSnapshotInputs, prepareDistributionWithIndex,
536
+ * the bundle-signer, plane-invariant and operation admissions), sealed by the kernel's
537
+ * sealDistributionLock through biome-content.mjs `sealLock`, with the candidate content
538
+ * record as `contentCommits`;
539
+ * authorization lock the develop resolve's composeAuthorizationBase (`fga model test`, XEMA_FGA_CLI);
540
+ * images lock the pinned edition's bytes, unchanged;
541
+ * resolved deployment the kernel's resolveUnsplitDeployment over exactly the three lock bytes written.
542
+ *
543
+ * Not run, because each reads a registry: the images resolve and seal, the adapter-bundle check and the chart
544
+ * set. Every refusal throws; nothing is written unless everything passed. `outDir` must not exist yet.
545
+ *
546
+ * @param {object} request
547
+ * @param {string} request.tripleDir the pinned develop edition (as for fetchPinnedEditionCheckouts)
548
+ * @param {{ cacheDir: string, dataKey: string }} request.checkouts what fetchPinnedEditionCheckouts returned
549
+ * @param {{ repo: string, checkoutDir: string }} request.candidate
550
+ * @param {string} request.outDir absolute, absent
551
+ * @param {{ id: string, runtimeNamespace: string }} request.deployment the deployment the resolved deployment names
552
+ * @returns {Promise<{ outDir: string, distributionId: string, candidateCommit: string, files: readonly string[] }>}
553
+ */
554
+ export async function resealCandidateEdition({ tripleDir, checkouts, candidate, outDir, deployment }) {
555
+ const edition = readPinnedEdition(tripleDir);
556
+ const cacheDir = absoluteDir('checkouts.cacheDir', checkouts?.cacheDir);
557
+ const out = absoluteDir('outDir', outDir);
558
+ if (existsSync(out)) {throw new CandidateEditionError(`outDir ${out} already exists; a candidate triple is written to a fresh directory`);}
559
+ if (typeof deployment?.id !== 'string' || typeof deployment?.runtimeNamespace !== 'string') {
560
+ throw new CandidateEditionError('deployment must be { id, runtimeNamespace }: the resolved deployment names both');
561
+ }
562
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-candidate-reseal-'));
563
+ try {
564
+ const dataDir = cachedDataRoot(cacheDir, checkouts.dataKey, edition.pins);
565
+ const read = withWorkspace({ [WorkspaceRoot.Data]: dataDir }, () => readCandidate(candidate, edition.pins));
566
+ const pins = { ...edition.pins, [read.repo]: read.commit };
567
+ const staged = stageWorkspace({ cacheDir, dataKey: checkouts.dataKey, pins, candidate: read, scratch });
568
+ assertCheckoutAt({ dir: staged.kernelRepo, commit: pins[staged.kernelKey], what: `cached ${staged.kernelKey}` });
569
+ const written = await withWorkspace(staged.roots, () => resealStaged({ edition, pins, read, deployment }));
570
+ mkdirSync(out, { recursive: true });
571
+ for (const file of CANDIDATE_FILES) {writeFileSync(join(out, file), written[file]);}
572
+ return { outDir: out, distributionId: edition.id, candidateCommit: read.commit, files: CANDIDATE_FILES };
573
+ } finally {
574
+ rmSync(scratch, { recursive: true, force: true });
575
+ }
576
+
577
+ }
578
+
579
+ /** The re-seal proper, inside the staged workspace: every step of the develop resolve that reads no registry. */
580
+ async function resealStaged({ edition: pinned, pins, read, deployment }) {
581
+ const id = pinned.id;
582
+ const schemas = await loadKernelDistributionSchemas();
583
+ const inputs = await buildSnapshotInputs({ id, pins, schemas });
584
+ const { lock, index } = await prepareDistributionWithIndex(id, { inputs });
585
+ assertLockBundleSigners({ lock, schemas });
586
+ assertDevelopEditionInvariants({ id, lock, biomeIndex: inputs.biomeIndex, schemas });
587
+ admitDistributionLock(schemas, lock);
588
+ const authorizationLock = await composeAuthorizationBase(id, lock, index);
589
+ if (authorizationLock === null) {
590
+ throw new CandidateEditionError(`${id}: the pinned kernel ships no authorization composer, but the pinned edition carries an authorization lock`);
591
+ }
592
+ const imagesLockBytes = pinned.bytes[EditionLockFile.Images];
593
+ const imageLock = schemas.parseImageLock(JSON.parse(imagesLockBytes.toString('utf8')));
594
+ const contentCommits = new Map([[read.repo, read.commit]]);
595
+ const authority = createPinnedSourceAuthority({ pins: inputs.runtimeImageSourceIndex.repositoryPins });
596
+ const sealed = await sealLock({
597
+ schemas,
598
+ unsealed: lock,
599
+ imageLock,
600
+ locate: sourceTreeLocator(externalSourceTrees(lock)),
601
+ vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
602
+ contentCommits,
603
+ });
604
+ const lockBytes = Buffer.from(serializeJson(sealed), 'utf8');
605
+ const authorizationLockBytes = Buffer.from(serializeJson(authorizationLock), 'utf8');
606
+ const resolvedBytes = await resolvedDeploymentBytes({ id, deployment, lockBytes, imagesLockBytes, authorizationLockBytes });
607
+ const record = {
608
+ schema: CANDIDATE_CONTENT_SCHEMA,
609
+ distributionId: id,
610
+ distributionLockSha256: sha256Hex(lockBytes),
611
+ contentCommits: Object.fromEntries(contentCommits),
612
+ };
613
+ return {
614
+ [EditionLockFile.Distribution]: lockBytes,
615
+ [EditionLockFile.Images]: imagesLockBytes,
616
+ [EditionLockFile.Authorization]: authorizationLockBytes,
617
+ [RESOLVED_DEPLOYMENT_FILE]: resolvedBytes,
618
+ [CANDIDATE_CONTENT_FILE]: Buffer.from(serializeJson(record), 'utf8'),
619
+ };
620
+ }
621
+
622
+ /**
623
+ * The kernel's unsplit resolved deployment of the three lock bytes. It is written with the library's `serializeJson`:
624
+ * the kernel's canonical-JSON module is outside the edition protocol's entry modules, and no verifier hashes this file
625
+ * (verifyResolvedDeployment reads its fields and checks the lock digests it names).
626
+ */
627
+ async function resolvedDeploymentBytes({ id, deployment, lockBytes, imagesLockBytes, authorizationLockBytes }) {
628
+ const distribution = await import(`file://${kernelDistPath('distribution', 'index.js')}`);
629
+ if (typeof distribution.resolveUnsplitDeployment !== 'function') {
630
+ throw new CandidateEditionError(`${id}: the pinned kernel exports no resolveUnsplitDeployment; it cannot resolve a deployment`);
631
+ }
632
+ const { version } = readStrictJsonFile(join(kernelPackageDir(), 'package.json'));
633
+ const lockSha256 = sha256Hex(lockBytes);
634
+ const resolved = await distribution.resolveUnsplitDeployment({
635
+ lockBytes,
636
+ imagesLockBytes,
637
+ authorizationLockBytes,
638
+ deployment: deployment.id,
639
+ runtimeNamespace: deployment.runtimeNamespace,
640
+ edition: {
641
+ version: CANDIDATE_EDITION_VERSION,
642
+ registry: CANDIDATE_EDITION_REGISTRY,
643
+ ref: `${CANDIDATE_EDITION_REGISTRY}/candidate-${id}@sha256:${lockSha256}`,
644
+ },
645
+ resolverVersion: version,
646
+ });
647
+ return Buffer.from(serializeJson(resolved), 'utf8');
648
+ }
649
+
650
+ /**
651
+ * OFFLINE. Recompute every biomeContentHash of a candidate triple from the cached checkouts and the candidate,
652
+ * reading the candidate content record as `contentCommits` when the triple carries one, and refuse a mismatch
653
+ * (biome-content.mjs `verifyBiomeContentHashes`). A triple without a record is verified as any lock is.
654
+ */
655
+ export async function verifyCandidateContentHashes({ tripleDir, checkouts, candidate, candidateDir }) {
656
+ const edition = readPinnedEdition(tripleDir);
657
+ const cacheDir = absoluteDir('checkouts.cacheDir', checkouts?.cacheDir);
658
+ const dir = absoluteDir('candidateDir', candidateDir);
659
+ const lockBytes = readFileSync(join(dir, EditionLockFile.Distribution));
660
+ const recordPath = join(dir, CANDIDATE_CONTENT_FILE);
661
+ const dataDir = cachedDataRoot(cacheDir, checkouts.dataKey, edition.pins);
662
+ const read = withWorkspace({ [WorkspaceRoot.Data]: dataDir }, () => readCandidate(candidate, edition.pins));
663
+ const contentCommits = existsSync(recordPath)
664
+ ? parseCandidateContentRecord(readStrictJsonFile(recordPath), {
665
+ distributionLockBytes: lockBytes,
666
+ distributionId: edition.id,
667
+ candidate: { repo: read.repo, commit: read.commit },
668
+ })
669
+ : undefined;
670
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-candidate-verify-'));
671
+ try {
672
+ const pins = { ...edition.pins, [read.repo]: read.commit };
673
+ const staged = stageWorkspace({ cacheDir, dataKey: checkouts.dataKey, pins, candidate: read, scratch });
674
+ return await withWorkspace(staged.roots, async () => {
675
+ const schemas = await loadKernelDistributionSchemas();
676
+ const lock = schemas.parseDistributionLock(JSON.parse(lockBytes.toString('utf8')));
677
+ const imageLock = schemas.parseImageLock(JSON.parse(readFileSync(join(dir, EditionLockFile.Images), 'utf8')));
678
+ const index = applyPinSource(readStrictJsonFile(runtimeImageSourceIndexPath()), snapshotPinSource(pins));
679
+ const authority = createPinnedSourceAuthority({ pins: index.repositoryPins });
680
+ return verifyBiomeContentHashes({
681
+ schemas,
682
+ lock,
683
+ imageLock,
684
+ locate: sourceTreeLocator(externalSourceTrees(lock)),
685
+ vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
686
+ ...(contentCommits === undefined ? {} : { contentCommits }),
687
+ });
688
+ });
689
+ } finally {
690
+ rmSync(scratch, { recursive: true, force: true });
691
+ }
692
+ }