@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.
- package/README.md +60 -389
- package/dist/commands/edition.js +3 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +8715 -807
- package/dist/edition/verify-image-evidence.bundle.provenance.json +873 -405
- package/edition/README.md +53 -273
- package/edition/adapter-bundled-versions.mjs +107 -0
- package/edition/adapter-host-versions.mjs +41 -0
- package/edition/authorization-base.mjs +18 -9
- package/edition/build-runtime-image-source-index.mjs +77 -4
- package/edition/catalogue.mjs +15 -12
- package/edition/customer-definition.mjs +3 -20
- package/edition/develop-customer-edition.mjs +2 -2
- package/edition/develop-edition-resolve.mjs +10 -2
- package/edition/develop-edition.mjs +56 -62
- package/edition/develop-freeze.mjs +14 -3
- package/edition/dist-check.mjs +1 -0
- package/edition/distribution-evidence-set.mjs +32 -5
- package/edition/edition-channel.mjs +15 -1
- package/edition/edition-locks.mjs +16 -6
- package/edition/edition-protocol.mjs +30 -8
- package/edition/edition-workspace.mjs +8 -0
- package/edition/image-evidence.mjs +133 -25
- package/edition/kernel-package.mjs +193 -0
- package/edition/lib.mjs +73 -1
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/oras.mjs +32 -10
- package/edition/plan-image-evidence-refresh.mjs +2 -2
- package/edition/produce-distribution-completeness-evidence.mjs +5 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
- package/edition/release-catalogue.mjs +10 -3
- package/edition/resolve-chart-set.mjs +1 -1
- package/edition/resolve-customer-edition.mjs +29 -33
- package/edition/resolve-distribution.mjs +5 -1
- package/edition/resolve-edition.mjs +3 -2
- package/edition/seal-image-digests.mjs +16 -4
- package/edition/trust-policy-arming-verb.mjs +2 -10
- package/edition/verify-image-evidence.mjs +1 -0
- package/examples/README.md +3 -2
- 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
|
-
|
|
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,
|
|
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
|
|
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
|
|
24
|
-
//
|
|
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
|
-
*
|
|
558
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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';
|