@xemahq/xema 0.29.0 → 0.30.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 (40) hide show
  1. package/README.md +60 -389
  2. package/dist/commands/edition.js +3 -3
  3. package/dist/commands/edition.js.map +1 -1
  4. package/dist/edition/verify-image-evidence.bundle.mjs +8715 -807
  5. package/dist/edition/verify-image-evidence.bundle.provenance.json +873 -405
  6. package/edition/README.md +53 -273
  7. package/edition/adapter-bundled-versions.mjs +107 -0
  8. package/edition/adapter-host-versions.mjs +41 -0
  9. package/edition/authorization-base.mjs +18 -9
  10. package/edition/build-runtime-image-source-index.mjs +77 -4
  11. package/edition/catalogue.mjs +15 -12
  12. package/edition/customer-definition.mjs +3 -20
  13. package/edition/develop-customer-edition.mjs +2 -2
  14. package/edition/develop-edition-resolve.mjs +10 -2
  15. package/edition/develop-edition.mjs +56 -62
  16. package/edition/develop-freeze.mjs +14 -3
  17. package/edition/dist-check.mjs +1 -0
  18. package/edition/distribution-evidence-set.mjs +32 -5
  19. package/edition/edition-channel.mjs +15 -1
  20. package/edition/edition-locks.mjs +16 -6
  21. package/edition/edition-protocol.mjs +30 -8
  22. package/edition/edition-workspace.mjs +8 -0
  23. package/edition/image-evidence.mjs +133 -25
  24. package/edition/kernel-package.mjs +193 -0
  25. package/edition/lib.mjs +73 -1
  26. package/edition/oci-evidence-verbs.mjs +2 -2
  27. package/edition/oras.mjs +32 -10
  28. package/edition/plan-image-evidence-refresh.mjs +2 -2
  29. package/edition/produce-distribution-completeness-evidence.mjs +5 -0
  30. package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
  31. package/edition/release-catalogue.mjs +10 -3
  32. package/edition/resolve-chart-set.mjs +1 -1
  33. package/edition/resolve-customer-edition.mjs +29 -33
  34. package/edition/resolve-distribution.mjs +5 -1
  35. package/edition/resolve-edition.mjs +3 -2
  36. package/edition/seal-image-digests.mjs +16 -4
  37. package/edition/trust-policy-arming-verb.mjs +2 -10
  38. package/edition/verify-image-evidence.mjs +1 -0
  39. package/examples/README.md +3 -2
  40. package/package.json +3 -3
@@ -0,0 +1,193 @@
1
+ // kernel-package — the customer edition's kernel: `@xemahq/contracts` taken from public npm.
2
+ //
3
+ // A catalogue names its kernel package (`kernel: {name, version, integrity}`, catalogue.mjs). This module is
4
+ // the ONE place that touches npm: it downloads that exact tarball (public, no credential, no .npmrc, no
5
+ // npm/pnpm process), holds it by the catalogue's integrity BEFORE anything is extracted, extracts it with the
6
+ // system `tar`, links its runtime dependencies to the copies this CLI itself ships, and proves the package is
7
+ // the contract authority the catalogue was resolved under: first by its edition protocol identity (data, no
8
+ // code executed), and only then, by loading its schemas, by the constants the authority was derived with.
9
+
10
+ import { spawnSync } from 'node:child_process';
11
+ import { createHash } from 'node:crypto';
12
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import { tmpdir } from 'node:os';
15
+ import { dirname, join } from 'node:path';
16
+ import { isDeepStrictEqual } from 'node:util';
17
+
18
+ import { readKernelEditionProtocol } from './edition-protocol.mjs';
19
+ import { cacheDir, stateKernelPackageDir } from './edition-workspace.mjs';
20
+ import { loadKernelDistributionSchemas } from './lib.mjs';
21
+
22
+ export const KERNEL_PACKAGE_NAME = '@xemahq/contracts';
23
+ const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
24
+ const DEPENDENCY_NAME = /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/;
25
+ const EXACT = /^(\d+)\.(\d+)\.(\d+)$/;
26
+ const CARET = /^\^(\d+)\.(\d+)\.(\d+)$/;
27
+
28
+ export class KernelPackageError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = 'KernelPackageError';
32
+ }
33
+ }
34
+
35
+ export const kernelTarballUrl = (version) => `https://registry.npmjs.org/${KERNEL_PACKAGE_NAME}/-/contracts-${version}.tgz`;
36
+
37
+ /** The npm integrity string of `bytes`: `sha512-<base64>`. */
38
+ export const integrityOf = (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
39
+
40
+ /** The published tarball of `@xemahq/contracts@version`, bounded in size. */
41
+ export async function fetchKernelTarball(version, { fetch = globalThis.fetch } = {}) {
42
+ const url = kernelTarballUrl(version);
43
+ const response = await fetch(url);
44
+ if (response.status === 404) {
45
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME}@${version} is not published on npm (${url}): a catalogue names a published kernel`);
46
+ }
47
+ if (!response.ok) {throw new KernelPackageError(`${url} answered HTTP ${response.status}`);}
48
+ const declared = Number(response.headers.get('content-length') ?? 0);
49
+ if (declared > MAX_TARBALL_BYTES) {throw new KernelPackageError(`${url} declares ${declared} bytes, over the ${MAX_TARBALL_BYTES} byte bound`);}
50
+ const chunks = [];
51
+ let size = 0;
52
+ for await (const chunk of response.body) {
53
+ size += chunk.length;
54
+ if (size > MAX_TARBALL_BYTES) {throw new KernelPackageError(`${url} is over the ${MAX_TARBALL_BYTES} byte bound`);}
55
+ chunks.push(chunk);
56
+ }
57
+ return Buffer.concat(chunks);
58
+ }
59
+
60
+ /**
61
+ * Does `version` (exactly x.y.z) satisfy `range`? Exactly two range forms are supported: `x.y.z`, and
62
+ * `^x.y.z` (x > 0: >=x.y.z <(x+1).0.0; 0.y.z: >=0.y.z <0.(y+1).0). Any other form is refused.
63
+ */
64
+ export function satisfiesDeclaredRange(version, range) {
65
+ const have = EXACT.exec(version)?.slice(1).map(Number);
66
+ if (have === undefined) {throw new KernelPackageError(`version ${JSON.stringify(version)} is not an exact x.y.z version`);}
67
+ const exact = EXACT.exec(range)?.slice(1).map(Number);
68
+ if (exact !== undefined) {return isDeepStrictEqual(have, exact);}
69
+ const caret = CARET.exec(range)?.slice(1).map(Number);
70
+ if (caret === undefined) {throw new KernelPackageError(`range ${JSON.stringify(range)} is not x.y.z or ^x.y.z, the only forms a kernel dependency may declare`);}
71
+ const compare = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
72
+ const upper = caret[0] > 0 ? [caret[0] + 1, 0, 0] : [0, caret[1] + 1, 0];
73
+ return compare(have, caret) >= 0 && compare(have, upper) < 0;
74
+ }
75
+
76
+ /** The directory of `name` as this CLI itself ships it: its own resolution first, then its installed kernel's. */
77
+ function shippedPackageDir(name) {
78
+ const own = createRequire(import.meta.url);
79
+ const attempts = [() => own, () => createRequire(realpathSync(own.resolve(`${KERNEL_PACKAGE_NAME}/package.json`)))];
80
+ for (const requireFrom of attempts) {
81
+ try {
82
+ return dirname(realpathSync(requireFrom().resolve(`${name}/package.json`)));
83
+ } catch (error) {
84
+ if (error?.code !== 'MODULE_NOT_FOUND' && error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {throw error;}
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function linkRuntimeDependencies(packageDir) {
91
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
92
+ const nodeModules = dirname(manifest.name.startsWith('@') ? dirname(packageDir) : packageDir);
93
+ for (const [name, range] of Object.entries(manifest.dependencies ?? {})) {
94
+ if (!DEPENDENCY_NAME.test(name)) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} declares a dependency named ${JSON.stringify(name)}, which is not a package name`);}
95
+ const shipped = shippedPackageDir(name);
96
+ if (shipped === undefined) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} depends on ${name}@${range}, which this CLI does not ship`);}
97
+ const version = JSON.parse(readFileSync(join(shipped, 'package.json'), 'utf8')).version;
98
+ let ok;
99
+ try {
100
+ ok = satisfiesDeclaredRange(version, range);
101
+ } catch (error) {
102
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} dependency ${name}: ${error.message} (this CLI ships ${version})`);
103
+ }
104
+ if (!ok) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} depends on ${name}@${range}, but this CLI ships ${name}@${version}`);}
105
+ const link = join(nodeModules, name);
106
+ mkdirSync(dirname(link), { recursive: true });
107
+ symlinkSync(shipped, link, 'dir');
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Download the catalogue's kernel, hold it to `kernel.integrity` (refused before anything is written), extract it
113
+ * under `<cacheDir>/kernel-npm/<version>-<16 hex of the sha512>/node_modules/@xemahq/contracts` (fresh every run)
114
+ * and link its runtime dependencies. Returns the package directory.
115
+ */
116
+ export async function materializeKernelPackage({ kernel, cacheDir, fetchTarball = fetchKernelTarball }) {
117
+ const bytes = await fetchTarball(kernel.version);
118
+ const actual = integrityOf(bytes);
119
+ if (actual !== kernel.integrity) {
120
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME}@${kernel.version} integrity ${actual} is not the catalogue's kernel.integrity ${kernel.integrity}`);
121
+ }
122
+ const hex = createHash('sha512').update(bytes).digest('hex').slice(0, 16);
123
+ const root = join(cacheDir, 'kernel-npm', `${kernel.version}-${hex}`);
124
+ const packageDir = join(root, 'node_modules', ...KERNEL_PACKAGE_NAME.split('/'));
125
+ rmSync(root, { recursive: true, force: true });
126
+ mkdirSync(packageDir, { recursive: true });
127
+ const extracted = spawnSync('tar', ['-xzf', '-', '-C', packageDir, '--strip-components', '1'], { input: bytes, stdio: ['pipe', 'ignore', 'pipe'] });
128
+ if (extracted.error !== undefined) {throw new KernelPackageError(`tar could not run: ${extracted.error.message}`);}
129
+ if (extracted.status !== 0) {throw new KernelPackageError(`tar failed extracting ${KERNEL_PACKAGE_NAME}@${kernel.version}: ${extracted.stderr.toString().trim()}`);}
130
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
131
+ if (manifest.name !== KERNEL_PACKAGE_NAME || manifest.version !== kernel.version) {
132
+ throw new KernelPackageError(`the tarball holds ${manifest.name}@${manifest.version}, not ${KERNEL_PACKAGE_NAME}@${kernel.version}`);
133
+ }
134
+ linkRuntimeDependencies(packageDir);
135
+ return packageDir;
136
+ }
137
+
138
+ const refuse = (field, wanted, got) => new KernelPackageError(`the kernel package is not the catalogue's contract authority: ${field} is ${JSON.stringify(got)}, the catalogue states ${JSON.stringify(wanted)}`);
139
+
140
+ /**
141
+ * The kernel package at `kernelPackageDir` is the contract authority of `catalogue`. First its identity, read as
142
+ * data (final, which the protocol read refuses otherwise, and the catalogue's digest); only then are its schemas loaded and held to the authority's constants.
143
+ * `sourceCommit` is provenance and is not compared. Returns the loaded schemas.
144
+ */
145
+ export async function assertKernelPackageIsCatalogueAuthority({ catalogue, kernelPackageDir, loadSchemas }) {
146
+ const authority = catalogue.contractAuthority;
147
+ const identity = readKernelEditionProtocol(kernelPackageDir);
148
+ if (identity.contentDigest !== authority.contentDigest) {throw refuse('contentDigest', authority.contentDigest, identity.contentDigest);}
149
+ const schemas = await loadSchemas();
150
+ if (!isDeepStrictEqual(authority.sourcePaths, schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS)) {
151
+ throw refuse('sourcePaths', authority.sourcePaths, schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS);
152
+ }
153
+ const constants = [
154
+ ['generatorIdentity', schemas.XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR],
155
+ ['repositoryUrl', schemas.XEMA_KERNEL_CONTRACT_REPOSITORY_URL],
156
+ ['contentHashAlgorithm', schemas.KernelContractContentHashAlgorithm.LengthFramedGitPathModeBlobV1],
157
+ ];
158
+ for (const [field, kernelValue] of constants) {
159
+ if (authority[field] !== kernelValue) {throw refuse(field, authority[field], kernelValue);}
160
+ }
161
+ return schemas;
162
+ }
163
+
164
+ /**
165
+ * The `kernel` row a VENDOR catalogue carries: the published `@xemahq/contracts` the vendor's own kernel
166
+ * (`kernelPackageDir`) is, taken from npm, proved to be `contractAuthority` and held by its integrity.
167
+ * An unpublished version is refused. The downloaded package is extracted to a temporary directory and removed.
168
+ */
169
+ export async function kernelPackageOfAuthority({ kernelPackageDir, contractAuthority, fetchTarball = fetchKernelTarball, loadSchemas = loadKernelDistributionSchemas }) {
170
+ const manifest = JSON.parse(readFileSync(join(kernelPackageDir, 'package.json'), 'utf8'));
171
+ if (manifest.name !== KERNEL_PACKAGE_NAME) {throw new KernelPackageError(`the vendor kernel is ${manifest.name}, not ${KERNEL_PACKAGE_NAME}`);}
172
+ const bytes = await fetchTarball(manifest.version);
173
+ const kernel = { name: KERNEL_PACKAGE_NAME, version: manifest.version, integrity: integrityOf(bytes) };
174
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-kernel-npm-'));
175
+ try {
176
+ const packageDir = await materializeKernelPackage({ kernel, cacheDir: scratch, fetchTarball: async () => bytes });
177
+ await assertKernelPackageIsCatalogueAuthority({ catalogue: { contractAuthority }, kernelPackageDir: packageDir, loadSchemas });
178
+ } finally {
179
+ rmSync(scratch, { recursive: true, force: true });
180
+ }
181
+ return kernel;
182
+ }
183
+
184
+ /**
185
+ * A customer verb's one step between fetching its catalogue and using any kernel schema: the catalogue's kernel is
186
+ * downloaded into the cache root, stated as the workspace's Kernel root, and proved to be the catalogue's contract
187
+ * authority. Returns the kernel's distribution schemas.
188
+ */
189
+ export async function adoptCatalogueKernel(catalogue) {
190
+ const kernelPackageDir = await materializeKernelPackage({ kernel: catalogue.kernel, cacheDir: cacheDir() });
191
+ stateKernelPackageDir(kernelPackageDir);
192
+ return assertKernelPackageIsCatalogueAuthority({ catalogue, kernelPackageDir, loadSchemas: loadKernelDistributionSchemas });
193
+ }
package/edition/lib.mjs CHANGED
@@ -14,6 +14,9 @@ import { createRequire } from 'node:module';
14
14
  import { isIP } from 'node:net';
15
15
  import { join } from 'node:path';
16
16
 
17
+ import { parse as parseYaml } from 'yaml';
18
+
19
+ import { ADAPTER_HOST_BIOME_ID, readHostPackageVersions } from './adapter-host-versions.mjs';
17
20
  import { readBiomeAuthorizationModuleAt } from './authorization-base.mjs';
18
21
  import {
19
22
  isAuthoritativeProductionBiomeSource,
@@ -688,6 +691,18 @@ export async function loadKernelDistributionSchemas() {
688
691
  `Underlying error: ${err.message}`,
689
692
  );
690
693
  }
694
+ // The Node-only half of the distribution contracts (`distribution/server`): the lock digest and the
695
+ // predicate checks that recompute it.
696
+ let server;
697
+ try {
698
+ server = await import(`file://${kernelDistPath('distribution', 'server', 'index.js')}`);
699
+ } catch (err) {
700
+ throw new Error(
701
+ `Cannot load the built kernel distribution/server module from ${kernelDistPath('distribution', 'server', 'index.js')}. ` +
702
+ `Rebuild the kernel checkout (${kernelRepoDir()}). Underlying error: ${err.message}`,
703
+ );
704
+ }
705
+ const { distributionLockDigest, distributionImagesPredicateLockRefusal, imageDeliveryEntitlementRefusal } = server;
691
706
  const {
692
707
  parseDistribution,
693
708
  parseDistributionLock,
@@ -695,6 +710,8 @@ export async function loadKernelDistributionSchemas() {
695
710
  parseUnsealedDistributionLock,
696
711
  parseCustomerEditionDefinition,
697
712
  customerTrustCeilingIssues,
713
+ customerLockCeilingIssues,
714
+ biomeOriginsOfLock,
698
715
  sealDistributionLock,
699
716
  unsealedProjection,
700
717
  UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
@@ -714,6 +731,7 @@ export async function loadKernelDistributionSchemas() {
714
731
  SigstoreEvidenceDescriptorSchema,
715
732
  parsePlatformServiceSourceIndex,
716
733
  parseRuntimeImageSourceIndex,
734
+ producerWorkflowPinRefusal,
717
735
  resolveRuntimeImagePlatforms,
718
736
  BiomeInstallPolicy,
719
737
  BiomeLaunchMode,
@@ -749,6 +767,7 @@ export async function loadKernelDistributionSchemas() {
749
767
  describeProvenance,
750
768
  describeHop,
751
769
  deriveWorkflowSubjects,
770
+ distributionImagesPredicateChannelRefusal,
752
771
  } = mod;
753
772
  let topology;
754
773
  try {
@@ -791,6 +810,7 @@ export async function loadKernelDistributionSchemas() {
791
810
  typeof SigstoreEvidenceDescriptorSchema?.parse !== 'function' ||
792
811
  typeof parsePlatformServiceSourceIndex !== 'function' ||
793
812
  typeof parseRuntimeImageSourceIndex !== 'function' ||
813
+ typeof producerWorkflowPinRefusal !== 'function' ||
794
814
  typeof resolveRuntimeImagePlatforms !== 'function' ||
795
815
  typeof resolveFirstPartyCapabilityDomains !== 'function' ||
796
816
  typeof DistributionVisibility !== 'object' ||
@@ -837,6 +857,12 @@ export async function loadKernelDistributionSchemas() {
837
857
  // A biome's workflow subjects and its edition trust ceiling, each written
838
858
  // through the kernel's one derivation (`@xemahq/contracts` >= 0.71.0).
839
859
  typeof deriveWorkflowSubjects !== 'function' ||
860
+ // Predicate v2 checks (`@xemahq/contracts` >= 0.78.0): the channel the verifying policy row states and
861
+ // the lock digest the predicates bind.
862
+ typeof distributionImagesPredicateChannelRefusal !== 'function' ||
863
+ typeof distributionImagesPredicateLockRefusal !== 'function' ||
864
+ typeof distributionLockDigest !== 'function' ||
865
+ typeof imageDeliveryEntitlementRefusal !== 'function' ||
840
866
  typeof deriveTrustCeiling !== 'function' ||
841
867
  // The trust-domain vocabulary a customer edition is bounded by: the vendor's
842
868
  // own domain, a biome's default domain, and the two ceiling grammars a
@@ -867,6 +893,8 @@ export async function loadKernelDistributionSchemas() {
867
893
  parseUnsealedDistributionLock,
868
894
  parseCustomerEditionDefinition,
869
895
  customerTrustCeilingIssues,
896
+ customerLockCeilingIssues,
897
+ biomeOriginsOfLock,
870
898
  sealDistributionLock,
871
899
  unsealedProjection,
872
900
  UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
@@ -926,6 +954,11 @@ export async function loadKernelDistributionSchemas() {
926
954
  FIRST_PARTY_TRUST_DOMAIN,
927
955
  ProcessTrustPolicySchema,
928
956
  DistributionTrustPolicySchema,
957
+ distributionLockDigest,
958
+ distributionImagesPredicateChannelRefusal,
959
+ distributionImagesPredicateLockRefusal,
960
+ imageDeliveryEntitlementRefusal,
961
+ producerWorkflowPinRefusal,
929
962
  };
930
963
  }
931
964
 
@@ -1086,6 +1119,10 @@ function listBiomeManifestsAt(directory, repo, commit) {
1086
1119
  rel,
1087
1120
  repo,
1088
1121
  path,
1122
+ // The manifest's directory in its repository, and a reader of any file of
1123
+ // that repository at the SAME pinned commit (the host's pnpm-lock.yaml).
1124
+ manifestDirectory,
1125
+ readRepositoryText: (repoRelativePath) => readGitTextAtCommitOrNull(directory, repoRelativePath, commit),
1089
1126
  ...(authorizationModule === null ? {} : { authorizationModule }),
1090
1127
  });
1091
1128
  }
@@ -2058,6 +2095,16 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
2058
2095
  .filter((name) => typeof name === 'string' && name.length > 0),
2059
2096
  ),
2060
2097
  ].sort(compareStrings),
2098
+ // The capabilities the signed biome declares (a delegation
2099
+ // origination may lend only these): the manifest's
2100
+ // `xema.exposesCapabilities`; `requires*` are what it asks for.
2101
+ declaredCapabilities: [
2102
+ ...new Set(
2103
+ (Array.isArray(xema.exposesCapabilities) ? xema.exposesCapabilities : []).filter(
2104
+ (ref) => typeof ref === 'string' && ref.length > 0,
2105
+ ),
2106
+ ),
2107
+ ].sort(compareStrings),
2061
2108
  },
2062
2109
  },
2063
2110
  }
@@ -2263,6 +2310,26 @@ export function validateServiceDependencyOwnerParity(
2263
2310
  return entries;
2264
2311
  }
2265
2312
 
2313
+ /**
2314
+ * The adapter host's resolved `@xemahq/contracts` / `@xemahq/biome-sdk` versions, read from its source
2315
+ * repository's pnpm-lock.yaml at the pinned commit. An INDEX field only: the distribution lock never carries it.
2316
+ */
2317
+ function adapterHostPackageVersions(manifest) {
2318
+ const services = (manifest.raw.xema.components ?? []).filter(
2319
+ (component) => component?.kind === 'service' && component.artifact?.kind === 'oci-image',
2320
+ );
2321
+ if (services.length !== 1) {
2322
+ throw new Error(`${manifest.rel}: the adapter host declares ${services.length} oci-image service components; exactly one carries its package`);
2323
+ }
2324
+ const lockfilePath = 'pnpm-lock.yaml';
2325
+ return readHostPackageVersions({
2326
+ lockfileText: manifest.readRepositoryText(lockfilePath),
2327
+ importer: `${manifest.manifestDirectory}/${services[0].artifact.path}`,
2328
+ lockfilePath: `${manifest.repo}/${lockfilePath}`,
2329
+ parseYaml,
2330
+ });
2331
+ }
2332
+
2266
2333
  export async function buildBiomeIndex(
2267
2334
  platformServiceIndex,
2268
2335
  sourceRepositories = distributionBiomeSourceRepositories(),
@@ -2275,7 +2342,12 @@ export async function buildBiomeIndex(
2275
2342
  });
2276
2343
  const entries = [];
2277
2344
  for (const manifest of await committedBiomeManifests(sourceRepositories, authority)) {
2278
- entries.push(await manifestToIndexEntry(manifest));
2345
+ const entry = await manifestToIndexEntry(manifest);
2346
+ entries.push(
2347
+ entry.id === ADAPTER_HOST_BIOME_ID
2348
+ ? { ...entry, hostPackageVersions: adapterHostPackageVersions(manifest) }
2349
+ : entry,
2350
+ );
2279
2351
  }
2280
2352
  resolveServiceDependencyOwners(entries, platformServiceIndex);
2281
2353
  assertDeclaredDatabasesUnowned(entries);
@@ -32,7 +32,7 @@ import {
32
32
  isEntryModule,
33
33
  } from './edition-workspace.mjs';
34
34
  import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
35
- import { canonicalJson, loadValidatedLocks } from './image-evidence.mjs';
35
+ import { canonicalJson, loadValidatedImageLocks } from './image-evidence.mjs';
36
36
  import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
37
37
  import {
38
38
  IMAGE_SOURCE_EVIDENCE_ARTIFACT_TYPE,
@@ -230,7 +230,7 @@ export async function planOciEvidenceBackfill({
230
230
  const missingGit = new Map();
231
231
  for (const distributionId of distributionIds) {
232
232
  assertCanonicalDistributionId(distributionId);
233
- const { imageLock, repositoryPins } = await loadValidatedLocks(distributionId, data, schemas, {
233
+ const { imageLock, repositoryPins } = await loadValidatedImageLocks(distributionId, data, schemas, {
234
234
  editionsRoot: editions,
235
235
  });
236
236
  for (const subject of imageLock.subjects) {
package/edition/oras.mjs CHANGED
@@ -20,8 +20,9 @@
20
20
  // * before oras ever receives a credential, the registry's own `/v2/`
21
21
  // challenge is probed and a token realm on another origin is refused;
22
22
  // * a digest-addressed read whose bytes do not hash to the requested digest
23
- // throws; a tag read is resolved to a descriptor first, then read by digest,
24
- // so its bytes are always hash-checked;
23
+ // throws; a tag read and a digest read are both resolved to the registry's
24
+ // descriptor first, and the manifest media type is the descriptor's, never
25
+ // the manifest body's optional `mediaType` (Helm's has none);
25
26
  // * descriptor sizes are checked BEFORE the fetch, actual lengths after it;
26
27
  // * a registry status is read only from oras's own error forms; any other
27
28
  // failure carries no status and is thrown as undecided;
@@ -554,11 +555,11 @@ export class OrasRegistry {
554
555
  }
555
556
 
556
557
  /**
557
- * Fetch a manifest by TAG: resolve the descriptor, then read by digest, so the
558
- * bytes are always hash-checked against what the registry said the tag is.
558
+ * The descriptor the registry reports for a manifest reference, refused when
559
+ * its media type is not an accepted manifest type or it declares more than
560
+ * the manifest size cap.
559
561
  */
560
- async getManifestByTag(repository, tag) {
561
- const ref = `${this.#ref(repository)}:${tag}`;
562
+ async #manifestDescriptor(ref) {
562
563
  const out = await this.#oras(['manifest', 'fetch'], ['--descriptor'], ref);
563
564
  const descriptor = this.#parseDescriptor(out, ref);
564
565
  if (!ACCEPTED_MANIFEST_MEDIA_TYPES.includes(descriptor.mediaType)) {
@@ -569,12 +570,33 @@ export class OrasRegistry {
569
570
  if (descriptor.size > MAX_MANIFEST_BYTES) {
570
571
  throw new Error(`${ref} declares ${descriptor.size} bytes, exceeding ${MAX_MANIFEST_BYTES}`);
571
572
  }
573
+ return descriptor;
574
+ }
575
+
576
+ /**
577
+ * Fetch a manifest by TAG: resolve the descriptor, then read by digest, so the
578
+ * bytes are always hash-checked against what the registry said the tag is.
579
+ */
580
+ async getManifestByTag(repository, tag) {
581
+ const descriptor = await this.#manifestDescriptor(`${this.#ref(repository)}:${tag}`);
572
582
  return this.#manifestByDigest(repository, descriptor.digest, descriptor.mediaType);
573
583
  }
574
584
 
575
- /** Fetch a manifest by DIGEST; the bytes must hash to exactly that digest. */
585
+ /**
586
+ * Fetch a manifest by DIGEST: the media type is the one the registry reports
587
+ * for that digest (a manifest body's own `mediaType` is optional, and Helm's
588
+ * has none), and the bytes must hash to exactly that digest.
589
+ */
576
590
  async getManifestByDigest(repository, digest) {
577
- return this.#manifestByDigest(repository, digest, null);
591
+ if (!SHA256_DIGEST_PATTERN.test(digest)) {
592
+ throw new Error(`"${digest}" is not a canonical lowercase sha256 digest`);
593
+ }
594
+ const ref = `${this.#ref(repository)}@${digest}`;
595
+ const descriptor = await this.#manifestDescriptor(ref);
596
+ if (descriptor.digest !== digest) {
597
+ throw new Error(`${ref} described ${descriptor.digest}, not the requested ${digest}`);
598
+ }
599
+ return this.#manifestByDigest(repository, digest, descriptor.mediaType);
578
600
  }
579
601
 
580
602
  async #manifestByDigest(repository, digest, expectedMediaType) {
@@ -629,10 +651,10 @@ function decodeManifest(bytes, expectedMediaType, ref) {
629
651
  if (typeof document !== 'object' || document === null || Array.isArray(document)) {
630
652
  throw new Error(`${ref} returned a manifest that is not a JSON object`);
631
653
  }
632
- const mediaType = expectedMediaType ?? document.mediaType;
654
+ const mediaType = expectedMediaType;
633
655
  if (!ACCEPTED_MANIFEST_MEDIA_TYPES.includes(mediaType)) {
634
656
  throw new Error(
635
- `${ref} returned unsupported manifest media type "${mediaType || '(none)'}"`,
657
+ `${ref} returned unsupported manifest media type "${mediaType}"`,
636
658
  );
637
659
  }
638
660
  if (typeof document.mediaType === 'string' && document.mediaType !== mediaType) {
@@ -66,7 +66,7 @@ import {
66
66
  } from './edition-workspace.mjs';
67
67
  import {
68
68
  canonicalJson,
69
- loadValidatedLocks,
69
+ loadValidatedImageLocks,
70
70
  } from './image-evidence.mjs';
71
71
  import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
72
72
  import { compareStrings, loadKernelDistributionSchemas } from './lib.mjs';
@@ -142,7 +142,7 @@ export async function planImageEvidenceRefresh({
142
142
 
143
143
  const subjects = [];
144
144
  for (const distributionId of [...distributionIds].sort(compareStrings)) {
145
- const { imageLock, repositoryPins, runtimeSourceIndex } = await loadValidatedLocks(
145
+ const { imageLock, repositoryPins, runtimeSourceIndex } = await loadValidatedImageLocks(
146
146
  distributionId,
147
147
  dataRootDirectory,
148
148
  schemas,
@@ -21,6 +21,7 @@ import {
21
21
  assertPinnedCosignVersion,
22
22
  buildDistributionImageEvidenceSet,
23
23
  buildDistributionImagesStatement,
24
+ evidenceChannelOfSigner,
24
25
  loadPinnedTrustPolicy,
25
26
  parseEvidenceCliFlags,
26
27
  resolveProducerEvidence,
@@ -59,6 +60,8 @@ export function githubWorkflowSigner({ repository, ref, path }) {
59
60
 
60
61
  /** The producer evidence and the completeness statement these inputs resolve to; deterministic. */
61
62
  async function deriveCompleteness(options, { outputRoot }) {
63
+ // The channel the signer signs for is the channel the lock is read on.
64
+ const channel = evidenceChannelOfSigner(options.signer, options.schemas);
62
65
  const resolution = {
63
66
  distributionId: options.distributionId,
64
67
  dataRoot: options.dataRoot,
@@ -66,6 +69,7 @@ async function deriveCompleteness(options, { outputRoot }) {
66
69
  schemas: options.schemas,
67
70
  expectedContractAuthority: options.expectedContractAuthority,
68
71
  registryClientFactory: options.registryClientFactory,
72
+ channel,
69
73
  };
70
74
  const staged = outputRoot === null
71
75
  ? await resolveProducerEvidence(resolution)
@@ -74,6 +78,7 @@ async function deriveCompleteness(options, { outputRoot }) {
74
78
  distributionLock: staged.distributionLock,
75
79
  imageLock: staged.imageLock,
76
80
  images: staged.images,
81
+ channel,
77
82
  releaseSequence: options.releaseSequence,
78
83
  releaseSourceCommit: options.releaseSourceCommit,
79
84
  runId: options.runId,
@@ -18,6 +18,7 @@ import {
18
18
  acceptSignedEvidenceStatement,
19
19
  assertPinnedCosignVersion,
20
20
  buildImageDeliveryEntitlementStatement,
21
+ evidenceChannelOfSigner,
21
22
  loadPinnedTrustPolicy,
22
23
  parseEvidenceCliFlags,
23
24
  stageEvidenceStatement,
@@ -26,10 +27,11 @@ import {
26
27
  WorkspaceRoot,
27
28
  configureEditionWorkspaceFromArgv,
28
29
  dataRoot,
30
+ editionsRoot,
29
31
  isEntryModule,
30
32
  } from './edition-workspace.mjs';
31
33
  import { ENTITLEMENT_DESCRIPTOR_FILE, entitlementDescriptorDocument } from './image-delivery-entitlement-descriptor.mjs';
32
- import { canonicalJsonBytes, readCanonicalJsonFromRoot } from './image-evidence.mjs';
34
+ import { canonicalJsonBytes, readCanonicalJsonFromRoot, readEditionDistributionLock } from './image-evidence.mjs';
33
35
  import { serializeJson } from './lib.mjs';
34
36
  import { githubWorkflowSigner } from './produce-distribution-completeness-evidence.mjs';
35
37
  import {
@@ -81,6 +83,12 @@ function deriveEntitlement(options) {
81
83
  schemas: options.schemas,
82
84
  });
83
85
  const statement = buildImageDeliveryEntitlementStatement({
86
+ distributionLock: readEditionDistributionLock(
87
+ options.editionsRoot,
88
+ options.distributionId,
89
+ options.schemas,
90
+ evidenceChannelOfSigner(options.signer, options.schemas),
91
+ ),
84
92
  distributionId: options.distributionId,
85
93
  completenessStatementDigest,
86
94
  customerId: options.customerId,
@@ -226,6 +234,7 @@ const SHARED_FLAGS = Object.freeze([
226
234
  async function main() {
227
235
  const [phase, ...args] = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
228
236
  WorkspaceRoot.Data,
237
+ WorkspaceRoot.Editions,
229
238
  WorkspaceRoot.Kernel,
230
239
  ]);
231
240
  if (phase !== 'stage' && phase !== 'accept') {
@@ -240,6 +249,7 @@ async function main() {
240
249
  const options = {
241
250
  distributionId: flags.require('distribution'),
242
251
  evidenceRoot: resolve(flags.require('evidence-root')),
252
+ editionsRoot: editionsRoot(),
243
253
  stagingDirectory: resolve(flags.require('staging')),
244
254
  schemas,
245
255
  policy: loadPinnedTrustPolicy({ dataRoot: dataRoot(), schemas }),
@@ -5,14 +5,15 @@ import { readFileSync } from 'node:fs';
5
5
  import { biomeSourceTreeIds } from './biome-content.mjs';
6
6
  import { buildCatalogue } from './catalogue.mjs';
7
7
  import { EditionChannel } from './edition-constants.mjs';
8
- import { biomeIndexPath, platformServiceIndexPath, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
8
+ import { biomeIndexPath, kernelPackageDir, platformServiceIndexPath, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
9
+ import { kernelPackageOfAuthority } from './kernel-package.mjs';
9
10
  import { loadKernelDistributionSchemas } from './lib.mjs';
10
11
  import { loadEditionManifest } from './resolve-distribution.mjs';
11
12
  import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
12
13
  import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
13
14
 
14
15
  /** The catalogue of release offering `id`, from the committed locks (`locks` as `readCommittedEdition` returns them). */
15
- export async function releaseCatalogue(id, locks) {
16
+ export async function releaseCatalogue(id, locks, { fetchKernelTarball } = {}) {
16
17
  const schemas = await loadKernelDistributionSchemas();
17
18
  const authority = createPinnedSourceAuthority({ pins: committedRepositoryPins() });
18
19
  // The WRITER of the catalogue's tree ids: the real `git rev-parse <commit>:<path>` of every biome.
@@ -23,6 +24,7 @@ export async function releaseCatalogue(id, locks) {
23
24
  locate: sourceTreeLocator(externalSourceTrees(locks.distributionLock)),
24
25
  vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
25
26
  });
27
+ const contractAuthority = schemas.parseRuntimeImageSourceIndex(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8'))).contractAuthority;
26
28
  return buildCatalogue({
27
29
  sourceTreeIds,
28
30
  offeringId: id,
@@ -32,7 +34,12 @@ export async function releaseCatalogue(id, locks) {
32
34
  chartSetLock: locks.chartSetLock,
33
35
  biomeIndex: JSON.parse(readFileSync(biomeIndexPath(), 'utf8')),
34
36
  platformServiceIndex: schemas.parsePlatformServiceSourceIndex(JSON.parse(readFileSync(platformServiceIndexPath(), 'utf8'))),
35
- contractAuthority: schemas.parseRuntimeImageSourceIndex(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8'))).contractAuthority,
37
+ contractAuthority,
38
+ kernel: await kernelPackageOfAuthority({
39
+ kernelPackageDir: kernelPackageDir(),
40
+ contractAuthority,
41
+ ...(fetchKernelTarball === undefined ? {} : { fetchTarball: fetchKernelTarball }),
42
+ }),
36
43
  leaf: loadEditionManifest(schemas.parseDistribution, id),
37
44
  });
38
45
  }
@@ -82,7 +82,7 @@ import {
82
82
  } from './registry-credentials.mjs';
83
83
 
84
84
  if (isEntryModule(import.meta.url)) {
85
- configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Deploy, WorkspaceRoot.Kernel]);
85
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Kernel], { refuseOthers: true });
86
86
  }
87
87
 
88
88
  export const CHART_SET_LOCK_FILE = 'chart-set.lock.json';