@xemahq/xema 0.48.0 → 0.49.1
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/dist/assets/appliance-base/distribution.lock.json +3936 -1515
- package/dist/assets/appliance-base/images.lock.json +878 -1161
- package/dist/assets/appliance-lean/distribution.lock.json +3663 -916
- package/dist/assets/appliance-lean/images.lock.json +825 -924
- package/dist/assets/boot-graph.json +398 -93
- package/dist/assets/distribution-assets.manifest.json +10 -10
- package/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +5 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +0 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +4386 -2503
- package/dist/edition/verify-image-evidence.bundle.provenance.json +590 -488
- package/dist/runtime/auth/cli-application.d.ts +18 -0
- package/dist/runtime/auth/cli-application.d.ts.map +1 -0
- package/dist/runtime/auth/cli-application.js +39 -0
- package/dist/runtime/auth/cli-application.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +3 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +5 -1
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +21 -0
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +0 -18
- package/dist/runtime/launch.js.map +1 -1
- package/edition/README.md +20 -3
- package/edition/biome-content.mjs +5 -2
- package/edition/build-runtime-image-source-index.mjs +79 -5
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/catalogue-evidence.mjs +8 -5
- package/edition/customer-edition-verbs.mjs +9 -3
- package/edition/develop-coverage-run.mjs +2 -1
- package/edition/develop-coverage.mjs +12 -1
- package/edition/develop-edition-resolve.mjs +3 -2
- package/edition/develop-edition.mjs +15 -3
- package/edition/develop-record-verify.mjs +216 -0
- package/edition/develop-select-records.mjs +24 -225
- package/edition/develop-verify-image-record.mjs +90 -0
- package/edition/dist-check.mjs +24 -3
- package/edition/distribution-evidence-set.mjs +30 -2
- package/edition/edition-artifact.mjs +110 -22
- package/edition/edition-ceiling.mjs +180 -0
- package/edition/edition-config.mjs +91 -0
- package/edition/edition-constants.mjs +21 -3
- package/edition/edition-contract-parse.mjs +88 -3
- package/edition/edition-contract.json +47 -4
- package/edition/edition-image-seal.mjs +9 -2
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-protocol.mjs +7 -1
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/evidence-grammars.mjs +2 -0
- package/edition/evidence-trust-pin.mjs +66 -10
- package/edition/evidence-trust-policy.mjs +39 -29
- package/edition/external-source-walk.mjs +16 -3
- package/edition/fleet-coordinate-finality.mjs +18 -5
- package/edition/fulcio-certificate-extensions.mjs +231 -0
- package/edition/image-evidence.mjs +34 -79
- package/edition/locked-bundle-signer.mjs +241 -0
- package/edition/oci-evidence-verbs.mjs +7 -1
- package/edition/pinned-cosign.mjs +70 -0
- package/edition/plan-image-evidence-refresh.mjs +3 -1
- package/edition/producer-evidence-bundle.mjs +3 -1
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/release-edition-artifact.mjs +3 -3
- package/edition/repin-evidence-trust.mjs +6 -1
- package/edition/resolve-distribution.mjs +90 -7
- package/edition/resolve-edition.mjs +1 -1
- package/edition/resolve-images.mjs +53 -7
- package/edition/seal-image-digests.mjs +19 -8
- package/edition/sealed-distribution-lock.mjs +2 -2
- package/edition/third-party-admission.mjs +91 -0
- package/edition/third-party-bundle-registry.mjs +147 -0
- package/edition/third-party-certificate-extensions.mjs +21 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +255 -0
- package/edition/third-party-source-flags.mjs +134 -0
- package/edition/third-party-source-pins.mjs +259 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +115 -0
- package/edition/third-party-source-writer.mjs +229 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/verify-catalogue-evidence.mjs +2 -0
- package/edition/verify-image-evidence.mjs +2 -0
- package/edition/verify-image-platforms.mjs +33 -9
- package/package.json +5 -5
- package/dist/runtime/auth/client-bootstrap.d.ts +0 -11
- package/dist/runtime/auth/client-bootstrap.d.ts.map +0 -1
- package/dist/runtime/auth/client-bootstrap.js +0 -60
- package/dist/runtime/auth/client-bootstrap.js.map +0 -1
|
@@ -78,7 +78,7 @@ import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-compone
|
|
|
78
78
|
import { fleetChartSetLockPath, writeEditionChartSet } from './edition-chart-set.mjs';
|
|
79
79
|
import { workspaceSection } from './edition-config.mjs';
|
|
80
80
|
import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
|
|
81
|
-
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, kernelPackageDir } from './edition-workspace.mjs';
|
|
81
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, dataRoot, editionsRoot, isEntryModule, kernelPackageDir } from './edition-workspace.mjs';
|
|
82
82
|
import { walkDeclaredSources } from './external-source-walk.mjs';
|
|
83
83
|
import {
|
|
84
84
|
biomeTierOrder,
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
validateImmutableDistributionLock,
|
|
96
96
|
validateServiceDependencyOwnerParity,
|
|
97
97
|
} from './lib.mjs';
|
|
98
|
+
import { BundleSignerPolicyAbsentError, GitSourcedHostModuleBundleError, loadBundleSignerSelection } from './locked-bundle-signer.mjs';
|
|
98
99
|
import {
|
|
99
100
|
assertInstalledOperationCeilingAdmitted,
|
|
100
101
|
assertOperationGrantsAdmitted,
|
|
@@ -112,6 +113,7 @@ import { commandFor } from './remedy-command.mjs';
|
|
|
112
113
|
import { soleResourceProvider } from './required-resource-provider.mjs';
|
|
113
114
|
import { readStorePublisherTrust } from './store-publisher-trust.mjs';
|
|
114
115
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
116
|
+
import { isThirdPartyOciEntry } from './third-party-subjects.mjs';
|
|
115
117
|
import { RuntimeSubjectKind } from './workspace-sections.mjs';
|
|
116
118
|
|
|
117
119
|
/**
|
|
@@ -254,7 +256,14 @@ export function loadEditionManifest(parseDistribution, id) {
|
|
|
254
256
|
if (!existsSync(path)) {
|
|
255
257
|
throw new Error(`distribution "${id}" not found: ${path} does not exist`);
|
|
256
258
|
}
|
|
257
|
-
|
|
259
|
+
return parseEditionManifest(parseDistribution, id, readStrictJsonFile(path), path);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The edition model's checks on one raw definition read from `source` (a file path, or a Git object): the refused
|
|
264
|
+
* fields, the kernel's validation and the id match. The one parse of a definition, whoever read the bytes.
|
|
265
|
+
*/
|
|
266
|
+
export function parseEditionManifest(parseDistribution, id, raw, source) {
|
|
258
267
|
if (raw !== null && typeof raw === 'object' && Object.hasOwn(raw, REFUSED_EXTENDS_FIELD)) {
|
|
259
268
|
throw new EditionModelError(
|
|
260
269
|
`edition "${id}": "${REFUSED_EXTENDS_FIELD}" is not part of the edition model; ` +
|
|
@@ -271,7 +280,7 @@ export function loadEditionManifest(parseDistribution, id) {
|
|
|
271
280
|
// `sourcePins` reaches the kernel unchanged: its Distribution schema validates it.
|
|
272
281
|
const manifest = parseDistribution(raw);
|
|
273
282
|
if (manifest.id !== id) {
|
|
274
|
-
throw new Error(`distribution at ${
|
|
283
|
+
throw new Error(`distribution at ${source} declares id "${manifest.id}", but it was addressed as "${id}"`);
|
|
275
284
|
}
|
|
276
285
|
return manifest;
|
|
277
286
|
}
|
|
@@ -618,6 +627,11 @@ export function mergeExternalSourceEntries(
|
|
|
618
627
|
* emitting `source: undefined` would make it read as first-party — so this
|
|
619
628
|
* fails rather than choosing either.
|
|
620
629
|
*/
|
|
630
|
+
/** A stamped entry of a declared OCI source: third-party, an OCI bundle source and no git record. */
|
|
631
|
+
function isThirdPartyOciSourceEntry(entry, sourceRecords) {
|
|
632
|
+
return !sourceRecords.has(entry.source) && isThirdPartyOciEntry(entry);
|
|
633
|
+
}
|
|
634
|
+
|
|
621
635
|
function requireSourceRecord(entry, sourceRecords) {
|
|
622
636
|
const record = sourceRecords.get(entry.source);
|
|
623
637
|
if (record === undefined) {
|
|
@@ -1780,6 +1794,41 @@ export function resolveDistributionFromInputs(
|
|
|
1780
1794
|
// shipped provider. See `nexusBinder`.
|
|
1781
1795
|
const nexusBindings = nexusBinder(id, [...selected.values()], schemas);
|
|
1782
1796
|
|
|
1797
|
+
// (5c') Bundle signers — every OCI bundle row (a biome's, a host module's) states the signer the data root's
|
|
1798
|
+
// trust policy selects for it. See `locked-bundle-signer.mjs`.
|
|
1799
|
+
const bundleSignerOf = (e, bundleSource, componentKey) => {
|
|
1800
|
+
const subject = { biomeId: e.id, ...(componentKey === undefined ? {} : { componentKey }), origin: e.origin, repo: e.repo, bundleSource };
|
|
1801
|
+
if (composed?.bundleSigners === undefined) {
|
|
1802
|
+
throw new BundleSignerPolicyAbsentError(
|
|
1803
|
+
`${componentKey === undefined ? `biome "${e.id}"` : `host module "${componentKey}" of biome "${e.id}"`} is an OCI bundle, but this resolve was given no bundle signer selection`,
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
return composed.bundleSigners.signerFor(subject);
|
|
1807
|
+
};
|
|
1808
|
+
// A row drawn from a GIT source carries no OCI bundle (biome-level; an OCI host-module bundle is refused below), hence no bundleSigner: it is built
|
|
1809
|
+
// from the pinned source, and the manifest's `bundleSource` names a release artifact not built from this commit.
|
|
1810
|
+
// Omitted here, at the copy point. (An entry of a declared `oci` source has a stamp but no record: not git.)
|
|
1811
|
+
const isGitSourced = (e) => e.source !== undefined && sourceRecords.has(e.source);
|
|
1812
|
+
const bundleSourceOf = (e) => (e.bundleSource === undefined || isGitSourced(e) ? {} : { bundleSource: e.bundleSource });
|
|
1813
|
+
const bundleSignerFor = (e) => (e.bundleSource?.kind === 'oci' && !isGitSourced(e) ? { bundleSigner: bundleSignerOf(e, e.bundleSource) } : {});
|
|
1814
|
+
const hostModuleOf = (e, hostModule) => {
|
|
1815
|
+
if (isGitSourced(e)) {
|
|
1816
|
+
// The lock schema requires a host module's bundleSource, so an OCI one cannot be omitted: it is refused, in every
|
|
1817
|
+
// channel. A non-OCI one (npm, ...) is untouched.
|
|
1818
|
+
if (hostModule.bundleSource?.kind === 'oci') {
|
|
1819
|
+
throw new GitSourcedHostModuleBundleError(
|
|
1820
|
+
`host module "${hostModule.componentKey}" of biome "${e.id}" (git source "${e.source}") declares an OCI bundleSource (${hostModule.bundleSource.ociRef}); ` +
|
|
1821
|
+
'such a host module bundle reaches an edition only through an `oci` source with a third-party source pin',
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
return hostModule;
|
|
1825
|
+
}
|
|
1826
|
+
return hostModule.bundleSource?.kind === 'oci'
|
|
1827
|
+
? { ...hostModule, bundleSigner: bundleSignerOf(e, hostModule.bundleSource, hostModule.componentKey) }
|
|
1828
|
+
: hostModule;
|
|
1829
|
+
};
|
|
1830
|
+
const hostModulesOf = (e) => (Array.isArray(e.hostModules) ? e.hostModules : []).map((hostModule) => hostModuleOf(e, hostModule));
|
|
1831
|
+
|
|
1783
1832
|
// (5d) Dispatched runtime components — every `runtime-image` edge of a
|
|
1784
1833
|
// shipped biome must resolve to a runtime component of a biome in the index.
|
|
1785
1834
|
// The target biome is not rostered (it enters no `biomes[]`, `requires` or
|
|
@@ -1950,7 +1999,7 @@ export function resolveDistributionFromInputs(
|
|
|
1950
1999
|
// (the connector adapters loaded by `biome-adapter-host`). Projected
|
|
1951
2000
|
// verbatim from the index, which resolved each component's bundle source
|
|
1952
2001
|
// against the biome manifest.
|
|
1953
|
-
hostModules:
|
|
2002
|
+
hostModules: hostModulesOf(e),
|
|
1954
2003
|
// The BIOME-LEVEL bundle source, for a biome whose bundle no host module
|
|
1955
2004
|
// above names — the field a `installPolicy: required` service-only
|
|
1956
2005
|
// biome needs for the runtime's required-biome reconciler to fetch,
|
|
@@ -1963,7 +2012,8 @@ export function resolveDistributionFromInputs(
|
|
|
1963
2012
|
// unconditional key would move the bytes of every committed lock for a
|
|
1964
2013
|
// fact none of their entries carries. A lock in which SOME biome carries
|
|
1965
2014
|
// it still is the one v5 grammar once sealed.
|
|
1966
|
-
...(e
|
|
2015
|
+
...bundleSourceOf(e),
|
|
2016
|
+
...bundleSignerFor(e),
|
|
1967
2017
|
serviceDependencies: Array.isArray(e.serviceDependencies)
|
|
1968
2018
|
? e.serviceDependencies
|
|
1969
2019
|
: [],
|
|
@@ -2125,7 +2175,11 @@ export function resolveDistributionFromInputs(
|
|
|
2125
2175
|
// scopes on — and the lock records the full pinned `LockedBiomeSource`,
|
|
2126
2176
|
// so a reader of the artifact never has to resolve a handle against a
|
|
2127
2177
|
// manifest it does not have.
|
|
2128
|
-
|
|
2178
|
+
//
|
|
2179
|
+
// A third-party OCI biome is stamped too (the edition fence keys on the stamp) but is NOT in `sourceRecords`:
|
|
2180
|
+
// the lock's `LockedBiomeSource` is git-only, so such a row carries no `source` and is held to its pin instead
|
|
2181
|
+
// (`third-party-subjects.mjs`).
|
|
2182
|
+
...(e.source === undefined || isThirdPartyOciSourceEntry(e, sourceRecords)
|
|
2129
2183
|
? {}
|
|
2130
2184
|
: { source: requireSourceRecord(e, sourceRecords) }),
|
|
2131
2185
|
}))
|
|
@@ -2353,13 +2407,41 @@ export async function resolveDistributionWithIndex(id, { materialize, inputs, re
|
|
|
2353
2407
|
id,
|
|
2354
2408
|
index,
|
|
2355
2409
|
schemas,
|
|
2356
|
-
{
|
|
2410
|
+
{
|
|
2411
|
+
...composed,
|
|
2412
|
+
sourceRecords,
|
|
2413
|
+
storePublisherTrust,
|
|
2414
|
+
bundleSigners: inputs?.bundleSigners ?? loadBundleSignerSelection({ dataRoot: dataRoot(), schemas }),
|
|
2415
|
+
},
|
|
2357
2416
|
new Date().toISOString(),
|
|
2358
2417
|
platformServiceIndex,
|
|
2359
2418
|
);
|
|
2360
2419
|
return { lock, index };
|
|
2361
2420
|
}
|
|
2362
2421
|
|
|
2422
|
+
/**
|
|
2423
|
+
* A release carries a third-party biome only as an `oci` source with a third-party pin. A git-sourced third-party biome
|
|
2424
|
+
* whose manifest declares an OCI bundleSource (biome-level or host module) names a main-release artifact that is not
|
|
2425
|
+
* built from the pinned commit, so a release resolve refuses it. `lock` is the resolved lock (a git row carries
|
|
2426
|
+
* `source`), `index` the index it was resolved from (which still holds the manifest's bundleSource).
|
|
2427
|
+
*/
|
|
2428
|
+
export function assertReleaseGitSourcesDeclareNoBundle(lock, index) {
|
|
2429
|
+
const byId = new Map(index.map((entry) => [entry.id, entry]));
|
|
2430
|
+
for (const biome of lock.biomes) {
|
|
2431
|
+
if (biome.source === undefined) {continue;}
|
|
2432
|
+
const entry = byId.get(biome.id);
|
|
2433
|
+
const declared = [entry?.bundleSource, ...(entry?.hostModules ?? []).map((hostModule) => hostModule.bundleSource)].find(
|
|
2434
|
+
(bundleSource) => bundleSource?.kind === 'oci',
|
|
2435
|
+
);
|
|
2436
|
+
if (declared !== undefined) {
|
|
2437
|
+
throw new Error(
|
|
2438
|
+
`biome "${biome.id}" is drawn from git source "${biome.source.name}" and declares an OCI bundleSource (${declared.ociRef}); ` +
|
|
2439
|
+
'a release does not carry it: declare the biome as an `oci` source with a third-party source pin instead',
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2363
2445
|
/**
|
|
2364
2446
|
* Resolve and persist one distribution lock. The CLI and the plane refresh use
|
|
2365
2447
|
* this exact writer so an in-process refresh cannot drift from the standalone
|
|
@@ -2369,6 +2451,7 @@ export async function writeResolvedDistribution(id, { log = true, materialize, f
|
|
|
2369
2451
|
const { lock, index } = await resolveDistributionWithIndex(id, { materialize });
|
|
2370
2452
|
await assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(index), fetchManifest });
|
|
2371
2453
|
const outPath = join(editionsRoot(), id, 'distribution.lock.json');
|
|
2454
|
+
assertReleaseGitSourcesDeclareNoBundle(lock, index);
|
|
2372
2455
|
assertReleaseContent(lock, `the distribution lock for "${id}"`);
|
|
2373
2456
|
assertReleaseEditionTree(join(editionsRoot(), id));
|
|
2374
2457
|
mkdirSync(join(editionsRoot(), id), { recursive: true });
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// node resolve-edition.mjs <root flags> <edition-id> --trusted-write-v3
|
|
8
8
|
// write the whole lock set into the editions root. The flag is the
|
|
9
9
|
// operator's explicit acknowledgement that the images lock is rewritten
|
|
10
|
-
// UNSEALED (a resolve never produces an `imageDigest`; `xema edition
|
|
10
|
+
// UNSEALED (a resolve never produces an `imageDigest`; `xema edition resolve-digests`
|
|
11
11
|
// writes them afterwards). Without it nothing is written.
|
|
12
12
|
// node resolve-edition.mjs <root flags> <edition-id> --check
|
|
13
13
|
// resolve the whole lock set into a scratch copy of the editions root and
|
|
@@ -72,6 +72,14 @@ import {
|
|
|
72
72
|
} from './lib.mjs';
|
|
73
73
|
import { assertReleaseContent } from './release-channel-guard.mjs';
|
|
74
74
|
import { composeEdition } from './resolve-distribution.mjs';
|
|
75
|
+
import { loadThirdPartySourcePins } from './third-party-source-pins.mjs';
|
|
76
|
+
import {
|
|
77
|
+
assertOciBundleSourceOrigin,
|
|
78
|
+
attestedContentHash,
|
|
79
|
+
isThirdPartyOciRow,
|
|
80
|
+
thirdPartyPinForRow,
|
|
81
|
+
thirdPartySubjectCandidates,
|
|
82
|
+
} from './third-party-subjects.mjs';
|
|
75
83
|
|
|
76
84
|
if (isEntryModule(import.meta.url)) {
|
|
77
85
|
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache]);
|
|
@@ -418,7 +426,7 @@ export function resolveImageStructure(
|
|
|
418
426
|
enums,
|
|
419
427
|
requiredPlatforms,
|
|
420
428
|
declaredSources,
|
|
421
|
-
{ biomeIndex, supplementalSources } = {},
|
|
429
|
+
{ biomeIndex, supplementalSources, thirdPartyPins } = {},
|
|
422
430
|
) {
|
|
423
431
|
if (!Array.isArray(requiredPlatforms) || requiredPlatforms.length === 0) {
|
|
424
432
|
throw new Error(
|
|
@@ -440,6 +448,9 @@ export function resolveImageStructure(
|
|
|
440
448
|
);
|
|
441
449
|
}
|
|
442
450
|
const subjects = new Map();
|
|
451
|
+
// Third-party OCI subjects come from their pin and are never built, hashed or read from a checkout, so they are kept
|
|
452
|
+
// apart from every git-backed subject (see `third-party-subjects.mjs`).
|
|
453
|
+
const thirdPartySubjects = new Map();
|
|
443
454
|
const webBiomes = [];
|
|
444
455
|
const platformServiceBindings = [];
|
|
445
456
|
const repositoryPins = new Map(
|
|
@@ -479,6 +490,15 @@ export function resolveImageStructure(
|
|
|
479
490
|
};
|
|
480
491
|
|
|
481
492
|
for (const biome of lock.biomes) {
|
|
493
|
+
const biomeLabel = `distribution "${lock.distributionId}" biome "${biome.id}"`;
|
|
494
|
+
assertOciBundleSourceOrigin(biome, biomeLabel);
|
|
495
|
+
if (isThirdPartyOciRow(biome)) {
|
|
496
|
+
const pin = thirdPartyPinForRow({ row: biome, pins: thirdPartyPins, declaredSources, label: biomeLabel });
|
|
497
|
+
for (const candidate of thirdPartySubjectCandidates({ row: biome, pin, enums, requiredPlatforms, label: biomeLabel })) {
|
|
498
|
+
addSubject(thirdPartySubjects, candidate);
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
482
502
|
// WHERE THIS BIOME'S IMAGES LIVE, and WHICH COMMIT THEY ARE BUILT FROM.
|
|
483
503
|
// Both answers move together when a biome is drawn from an external source,
|
|
484
504
|
// because both are facts about that source rather than about this fleet.
|
|
@@ -662,7 +682,7 @@ export function resolveImageStructure(
|
|
|
662
682
|
// distribution resolve if it dispatches, and the first-party index carries
|
|
663
683
|
// none, so it is not looked up here.
|
|
664
684
|
const dispatchers = lock.biomes
|
|
665
|
-
.filter((biome) => biome.source === undefined && biome.target === 'server')
|
|
685
|
+
.filter((biome) => biome.source === undefined && !isThirdPartyOciRow(biome) && biome.target === 'server')
|
|
666
686
|
.map((biome) => {
|
|
667
687
|
const entries = biomeIndex.filter(
|
|
668
688
|
(entry) =>
|
|
@@ -892,13 +912,29 @@ export function resolveImageStructure(
|
|
|
892
912
|
platformServiceBindings.sort((left, right) =>
|
|
893
913
|
compareStrings(left.platformService, right.platformService),
|
|
894
914
|
);
|
|
915
|
+
const orderedThirdPartySubjects = [...thirdPartySubjects.values()]
|
|
916
|
+
.map((subject) => ({ ...subject, owners: subject.owners.sort(compareOwner) }))
|
|
917
|
+
.sort(compareSubject);
|
|
918
|
+
for (const subject of orderedThirdPartySubjects) {
|
|
919
|
+
if (subjects.has(subjectKey(subject))) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`third-party runtime image subject "${subject.kind}:${subject.id}" has the identity of another subject of distribution "${lock.distributionId}"`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
895
925
|
return {
|
|
896
926
|
subjects: orderedSubjects,
|
|
927
|
+
thirdPartySubjects: orderedThirdPartySubjects,
|
|
897
928
|
webBiomes,
|
|
898
929
|
platformServiceBindings,
|
|
899
930
|
};
|
|
900
931
|
}
|
|
901
932
|
|
|
933
|
+
/** Every image subject of a structure, first-party and third-party, in the lock's order. */
|
|
934
|
+
export function structureSubjects(structure) {
|
|
935
|
+
return [...structure.subjects, ...structure.thirdPartySubjects].sort(compareSubject);
|
|
936
|
+
}
|
|
937
|
+
|
|
902
938
|
/**
|
|
903
939
|
* THE COMPOSED-PROCESS IMAGE OF ONE BUILD — where a producing repository
|
|
904
940
|
* declares it, and how an edition addresses it.
|
|
@@ -1775,7 +1811,11 @@ export async function resolveImageRows(id, { inputs, lock, onSubjectHashed } = {
|
|
|
1775
1811
|
schemas,
|
|
1776
1812
|
requiredPlatforms,
|
|
1777
1813
|
declaredSources,
|
|
1778
|
-
{
|
|
1814
|
+
{
|
|
1815
|
+
biomeIndex,
|
|
1816
|
+
supplementalSources: supplementalSourcesFor(runtimeSourceIndex),
|
|
1817
|
+
thirdPartyPins: loadThirdPartySourcePins(dataRoot()),
|
|
1818
|
+
},
|
|
1779
1819
|
),
|
|
1780
1820
|
schemas,
|
|
1781
1821
|
requiredPlatforms,
|
|
@@ -1909,8 +1949,11 @@ export async function resolveImageRows(id, { inputs, lock, onSubjectHashed } = {
|
|
|
1909
1949
|
'refusing image-lock write: a source repository commit changed while hashing',
|
|
1910
1950
|
);
|
|
1911
1951
|
}
|
|
1952
|
+
// After the ledger check and the hashing: a third-party subject has no ledger row and no checkout, and its
|
|
1953
|
+
// `contentHash` is the publisher's statement, so none of the above read it.
|
|
1912
1954
|
return {
|
|
1913
|
-
subjects,
|
|
1955
|
+
subjects: structure.thirdPartySubjects.length === 0 ? subjects : [...subjects, ...structure.thirdPartySubjects].sort(compareSubject),
|
|
1956
|
+
thirdPartySubjects: structure.thirdPartySubjects,
|
|
1914
1957
|
platformServiceBindings: structure.platformServiceBindings,
|
|
1915
1958
|
webBiomes,
|
|
1916
1959
|
contractAuthority: runtimeSourceIndex.contractAuthority,
|
|
@@ -1920,7 +1963,7 @@ export async function resolveImageRows(id, { inputs, lock, onSubjectHashed } = {
|
|
|
1920
1963
|
|
|
1921
1964
|
/** One edition's COMPLETE image lock: its rows, parsed and validated as the kernel's image lock. */
|
|
1922
1965
|
export async function resolveImages(id, { inputs, lock, onSubjectHashed } = {}) {
|
|
1923
|
-
const { subjects, platformServiceBindings, webBiomes, contractAuthority, commits } = await resolveImageRows(id, { inputs, lock, onSubjectHashed });
|
|
1966
|
+
const { subjects, thirdPartySubjects, platformServiceBindings, webBiomes, contractAuthority, commits } = await resolveImageRows(id, { inputs, lock, onSubjectHashed });
|
|
1924
1967
|
const schemas = await loadKernelDistributionSchemas();
|
|
1925
1968
|
const imageLock = validateImmutableImageLock(
|
|
1926
1969
|
schemas.parseImageLock({
|
|
@@ -1934,7 +1977,7 @@ export async function resolveImages(id, { inputs, lock, onSubjectHashed } = {})
|
|
|
1934
1977
|
}),
|
|
1935
1978
|
schemas,
|
|
1936
1979
|
);
|
|
1937
|
-
return { imageLock, commits };
|
|
1980
|
+
return { imageLock, commits, thirdPartySubjects };
|
|
1938
1981
|
}
|
|
1939
1982
|
|
|
1940
1983
|
/**
|
|
@@ -1954,7 +1997,7 @@ export async function writeResolvedImages(
|
|
|
1954
1997
|
}
|
|
1955
1998
|
// The RELEASE lock is published-only: a develop edition resolves through
|
|
1956
1999
|
// `develop-edition.mjs` and never reaches this writer.
|
|
1957
|
-
const { imageLock, commits } = await resolveImages(id);
|
|
2000
|
+
const { imageLock, commits, thirdPartySubjects } = await resolveImages(id);
|
|
1958
2001
|
const outPath = distributionOutputPath(
|
|
1959
2002
|
editionsRoot(),
|
|
1960
2003
|
id,
|
|
@@ -1969,6 +2012,9 @@ export async function writeResolvedImages(
|
|
|
1969
2012
|
for (const [repo, commit] of commits) {
|
|
1970
2013
|
console.log(` source ${repo}@${commit}`);
|
|
1971
2014
|
}
|
|
2015
|
+
for (const subject of thirdPartySubjects) {
|
|
2016
|
+
console.log(` third-party ${subject.kind}:${subject.id} ${subject.imageRef} ${attestedContentHash(subject)}`);
|
|
2017
|
+
}
|
|
1972
2018
|
}
|
|
1973
2019
|
return { imageLock, commits };
|
|
1974
2020
|
}
|
|
@@ -121,7 +121,7 @@ import { parseCustomerDefinition } from './customer-definition.mjs';
|
|
|
121
121
|
import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG } from './customer-flags.mjs';
|
|
122
122
|
import { EditionChannel } from './edition-constants.mjs';
|
|
123
123
|
import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
|
|
124
|
-
import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
|
|
124
|
+
import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, dataRoot, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
|
|
125
125
|
import {
|
|
126
126
|
assertCoordinatesFinal,
|
|
127
127
|
collectFleetPlatformRequirements,
|
|
@@ -137,6 +137,8 @@ import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
|
|
|
137
137
|
import { readSealedDistributionLock } from './sealed-distribution-lock.mjs';
|
|
138
138
|
import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
|
|
139
139
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
140
|
+
import { pinnedManifestDigests, thirdPartyImagePins } from './third-party-image-pins.mjs';
|
|
141
|
+
import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs';
|
|
140
142
|
import {
|
|
141
143
|
loadImageLockForVerification,
|
|
142
144
|
verifyDistributionPlatforms,
|
|
@@ -569,14 +571,14 @@ export async function sealEditionLocks({ distributionId, root, digests = new Map
|
|
|
569
571
|
* The decision, with no files: an UNSEALED lock is sealed (refused under `check`); a SEALED lock is VERIFIED -
|
|
570
572
|
* every hash recomputed, a mismatch refused naming the biome.
|
|
571
573
|
*/
|
|
572
|
-
export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, check = false, lockPath = 'distribution.lock.json' }) {
|
|
574
|
+
export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners, check = false, lockPath = 'distribution.lock.json' }) {
|
|
573
575
|
if ('unsealedSchema' in lock) {
|
|
574
576
|
if (check) {
|
|
575
|
-
throw new Error(`${lockPath} is unsealed (no biomeContentHash); run \`xema edition
|
|
577
|
+
throw new Error(`${lockPath} is unsealed (no biomeContentHash); run \`xema edition resolve-digests ${lock.distributionId}\``);
|
|
576
578
|
}
|
|
577
|
-
return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys }) };
|
|
579
|
+
return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners }) };
|
|
578
580
|
}
|
|
579
|
-
await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys });
|
|
581
|
+
await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners });
|
|
580
582
|
return { sealed: false, lock };
|
|
581
583
|
}
|
|
582
584
|
|
|
@@ -586,7 +588,7 @@ async function main() {
|
|
|
586
588
|
parseSealArgs(args);
|
|
587
589
|
if (!distributionId || rest.length > 0) {
|
|
588
590
|
throw new Error(
|
|
589
|
-
'usage: xema edition
|
|
591
|
+
'usage: xema edition resolve-digests <distributionId> ' +
|
|
590
592
|
`[--check | --if-published | --publication-verdict] ` +
|
|
591
593
|
`[--definition <file> [--catalogue-username <name> --catalogue-token-env <VAR>]]`,
|
|
592
594
|
);
|
|
@@ -700,12 +702,16 @@ async function main() {
|
|
|
700
702
|
? undefined
|
|
701
703
|
: parseCustomerDefinition(await adoptCatalogueKernel(catalogue), readStrictJsonFile(catalogueFlags.get(DEFINITION_FLAG))).definition.storePublisherAnchorKeys;
|
|
702
704
|
const imageLock = await loadImageLockForVerification(distributionId, root);
|
|
705
|
+
// A pinned third-party subject (its repository is in the data root's third-party source pins) is verified and sealed
|
|
706
|
+
// BY the pin's manifest digest: its contentHash is publisher-attested and is not a tag.
|
|
707
|
+
const thirdPartyPins = loadTrustedThirdPartySourcePins(dataRoot());
|
|
708
|
+
const pinnedImages = thirdPartyImagePins({ pins: thirdPartyPins, subjects: imageLock.subjects });
|
|
703
709
|
|
|
704
710
|
// Throws on ANY unresolvable subject or missing required platform, before a
|
|
705
711
|
// single byte of the lock is touched.
|
|
706
712
|
let report;
|
|
707
713
|
try {
|
|
708
|
-
report = await verifyDistributionPlatforms({ imageLock, distributionsRoot: root });
|
|
714
|
+
report = await verifyDistributionPlatforms({ imageLock, distributionsRoot: root, pinnedDigests: pinnedManifestDigests(pinnedImages) });
|
|
709
715
|
} catch (error) {
|
|
710
716
|
// RECONCILE: a coordinate this pass just re-pinned has not been built yet.
|
|
711
717
|
// Skip the EDITION — never some of its subjects — and leave the lock
|
|
@@ -839,14 +845,19 @@ async function main() {
|
|
|
839
845
|
// introduced and the repository holding every edition's lock is where the
|
|
840
846
|
// question is answerable at all.
|
|
841
847
|
const { editions, withoutLock } = readFleetImageLocks(root);
|
|
842
|
-
const fleetRequirements = collectFleetPlatformRequirements({ editions });
|
|
848
|
+
const fleetRequirements = collectFleetPlatformRequirements({ editions, isThirdPartySubject: (subject) => thirdPartyPins.byRepository(subject.repo) !== undefined });
|
|
843
849
|
try {
|
|
844
850
|
const finality = assertCoordinatesFinal({
|
|
845
851
|
distributionId,
|
|
846
852
|
writes,
|
|
847
853
|
verified: report.subjects,
|
|
848
854
|
fleetRequirements,
|
|
855
|
+
thirdPartyKeys: new Set(pinnedImages.keys()),
|
|
849
856
|
});
|
|
857
|
+
// Third-party subjects are NOT graded by the first-party fleet verdict, and are named here rather than skipped silently.
|
|
858
|
+
for (const key of finality.thirdParty) {
|
|
859
|
+
console.log(`[${label}] finality: ${key} third-party (pinned by digest), not a first-party fleet coordinate.`);
|
|
860
|
+
}
|
|
850
861
|
// The CORPUS, not a confirmation: how many coordinates were graded against
|
|
851
862
|
// how many editions, and which editions made no claim. Without the
|
|
852
863
|
// denominators a pass cannot be told from a pass over nothing, and the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// sealed-distribution-lock — the one predicate for "this committed distribution.lock.json is the SEALED grammar".
|
|
2
|
-
// `xema edition artifact` and the `xema edition
|
|
2
|
+
// `xema edition artifact` and the `xema edition resolve-digests --publication-verdict` gate both ask it, so the verdict can
|
|
3
3
|
// never say PUBLISHABLE for a lock the artifact verb then refuses.
|
|
4
4
|
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
@@ -15,7 +15,7 @@ export function readSealedDistributionLock({ schemas, lockPath, distributionId }
|
|
|
15
15
|
} catch (error) {
|
|
16
16
|
throw new Error(
|
|
17
17
|
`${lockPath} is not a sealed distribution lock, so nothing is published from it. ` +
|
|
18
|
-
`Run \`xema edition
|
|
18
|
+
`Run \`xema edition resolve-digests ${distributionId}\` first. (${error.message})`,
|
|
19
19
|
{ cause: error },
|
|
20
20
|
);
|
|
21
21
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Admission of third-party image subjects (XEMA-1611 design section 4), beside `externalSourceRepositoryPins` and
|
|
2
|
+
// `verifyDistributionImageEvidenceSet` of `image-evidence.mjs`. Pure over data: the pins come from the data root's
|
|
3
|
+
// `third-party-source-pins.json` (held to the evidence trust pin by the entry), the statement and bundle from a
|
|
4
|
+
// descriptor `verifySigstoreEvidenceDescriptor` has just verified.
|
|
5
|
+
|
|
6
|
+
import { assertCertificateExtensions } from './third-party-certificate-extensions.mjs';
|
|
7
|
+
|
|
8
|
+
/** The declared source kind that names a third-party pin. */
|
|
9
|
+
const OCI_SOURCE_KIND = 'oci';
|
|
10
|
+
const FIRST_PARTY_LAYOUT_PREFIX = /^(?:repos|submodules)\//u;
|
|
11
|
+
|
|
12
|
+
const repositoryPathOf = (repositoryUrl) => new URL(repositoryUrl).pathname.replace(/^\//u, '').replace(/\.git$/u, '');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The repository pins of the edition's declared OCI sources, derived from their third-party source pins:
|
|
16
|
+
* `repo` (the pinned `owner/name`), `repositoryUrl` and `sourceCommit`. Every declared OCI source must be pinned (a
|
|
17
|
+
* missing or empty pin file refuses, with the loader's reason), and the pin must not collide with an index pin or a git
|
|
18
|
+
* source, nor sit under the first-party layout (`repos/`, `submodules/`). A declared source that ships no image yields no
|
|
19
|
+
* pin; every subject of a source must agree on the pinned commit.
|
|
20
|
+
*
|
|
21
|
+
* @returns {Map<string, {repo: string, repositoryUrl: string, sourceCommit: string}>}
|
|
22
|
+
*/
|
|
23
|
+
export function thirdPartySourceRepositoryPins({ distribution, imageLock, repositoryPins, thirdPartyPins }) {
|
|
24
|
+
const sources = distribution.sources ?? [];
|
|
25
|
+
const derived = new Map();
|
|
26
|
+
for (const source of sources) {
|
|
27
|
+
if (source.kind !== OCI_SOURCE_KIND) {continue;}
|
|
28
|
+
const pin = thirdPartyPins.byName(source.name);
|
|
29
|
+
if (pin === undefined) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`edition source "${source.name}" is kind "${OCI_SOURCE_KIND}", but ${thirdPartyPins.reason ?? `the data root's third-party source pins hold no pin named "${source.name}"`}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (FIRST_PARTY_LAYOUT_PREFIX.test(pin.repository)) {
|
|
35
|
+
throw new Error(`third-party source "${pin.name}" pins repository "${pin.repository}", which is under the first-party layout (repos/ or submodules/)`);
|
|
36
|
+
}
|
|
37
|
+
if (repositoryPins.has(pin.repository)) {
|
|
38
|
+
throw new Error(`third-party source "${pin.name}" repository "${pin.repository}" shadows a repository pin the runtime image source index already commits`);
|
|
39
|
+
}
|
|
40
|
+
const shadowed = sources.find((candidate) => candidate.kind === 'git' && (candidate.name === pin.repository || candidate.name === pin.name));
|
|
41
|
+
if (shadowed !== undefined) {
|
|
42
|
+
throw new Error(`third-party source "${pin.name}" (${pin.repository}) collides with the git source "${shadowed.name}" of the same edition`);
|
|
43
|
+
}
|
|
44
|
+
const subjects = imageLock.subjects.filter((subject) => subject.repo === pin.repository);
|
|
45
|
+
if (subjects.length === 0) {continue;}
|
|
46
|
+
const wrong = subjects.filter((subject) => subject.sourceCommit !== pin.sourceCommit);
|
|
47
|
+
if (wrong.length > 0) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`third-party source "${pin.name}" is pinned at ${pin.sourceCommit}, but images.lock.json builds ${wrong.map((subject) => `${subject.kind}:${subject.id}`).join(', ')} at ${[...new Set(wrong.map((subject) => subject.sourceCommit))].join(', ')}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
derived.set(pin.repository, { repo: pin.repository, repositoryUrl: pin.repositoryUrl, sourceCommit: pin.sourceCommit });
|
|
53
|
+
}
|
|
54
|
+
return derived;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The pins behind `thirdPartySourceRepositoryPins`, by repository. */
|
|
58
|
+
export const thirdPartySourcesByRepository = (derived, thirdPartyPins) =>
|
|
59
|
+
new Map([...derived.keys()].map((repo) => [repo, thirdPartyPins.byRepository(repo)]));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What a third-party subject must satisfy beyond a first-party one, after its evidence has verified:
|
|
63
|
+
* * [d9-1] the signing row's workflow repository, the subject's `repo` and the path of the predicate's
|
|
64
|
+
* `source.repositoryUrl` are one repository;
|
|
65
|
+
* * the subject is the pinned image (reference, content hash, sealed digest when sealed);
|
|
66
|
+
* * (d) the leaf certificate states the pinned repository id, owner id and a GitHub-hosted runner.
|
|
67
|
+
*/
|
|
68
|
+
export function assertThirdPartySubjectAdmission({ subject, source, statement, identityPolicy, bundle }) {
|
|
69
|
+
const label = `third-party subject "${subject.kind}:${subject.id}" (source "${source.name}")`;
|
|
70
|
+
const claimedRepository = repositoryPathOf(statement.predicate.source.repositoryUrl);
|
|
71
|
+
const signerRepository = identityPolicy.githubWorkflow.repository;
|
|
72
|
+
if (signerRepository !== subject.repo || claimedRepository !== subject.repo) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`${label}: the signing row's workflow repository (${signerRepository}), the subject repo (${subject.repo}) and the predicate source.repositoryUrl repository (${claimedRepository}) must be one repository`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const image = source.images.find((candidate) => candidate.subjectKind === subject.kind && candidate.subjectId === subject.id);
|
|
78
|
+
if (image === undefined) {
|
|
79
|
+
throw new Error(`${label}: the pin pins no image for it`);
|
|
80
|
+
}
|
|
81
|
+
if (subject.imageRef !== image.imageRef) {
|
|
82
|
+
throw new Error(`${label}: images.lock.json names ${subject.imageRef}, but the pin pins ${image.imageRef}`);
|
|
83
|
+
}
|
|
84
|
+
if (subject.contentHash !== image.sourceTreeHash) {
|
|
85
|
+
throw new Error(`${label}: images.lock.json has contentHash ${subject.contentHash}, but the pin records sourceTreeHash ${image.sourceTreeHash}`);
|
|
86
|
+
}
|
|
87
|
+
if (subject.imageDigest !== undefined && subject.imageDigest !== image.manifestDigest) {
|
|
88
|
+
throw new Error(`${label}: images.lock.json seals ${subject.imageDigest}, but the pin records manifestDigest ${image.manifestDigest}`);
|
|
89
|
+
}
|
|
90
|
+
assertCertificateExtensions({ bundle, source, label });
|
|
91
|
+
}
|