@xemahq/xema 0.33.6 → 0.34.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/edition/verify-image-evidence.bundle.mjs +4191 -1650
- package/dist/edition/verify-image-evidence.bundle.provenance.json +645 -465
- package/edition/README.md +2 -2
- package/edition/biome-content.mjs +11 -7
- package/edition/catalogue-edition.mjs +4 -4
- package/edition/catalogue.mjs +73 -3
- package/edition/develop-customer-edition.mjs +1 -0
- package/edition/edition-pinned-inputs.mjs +2 -2
- package/edition/edition-protocol.mjs +2 -2
- package/edition/edition-source-pins.mjs +23 -21
- package/edition/edition-workspace.mjs +1 -0
- package/edition/image-evidence.mjs +17 -8
- package/edition/lib.mjs +9 -3
- package/edition/operation-grant-admission.mjs +18 -3
- package/edition/release-edition-artifact.mjs +9 -15
- package/edition/resolve-customer-edition.mjs +25 -42
- package/edition/resolve-distribution.mjs +50 -4
- package/edition/seal-image-digests.mjs +36 -26
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/package.json +4 -4
package/edition/README.md
CHANGED
|
@@ -95,7 +95,7 @@ The edition library builds, validates and verifies evidence statements; it never
|
|
|
95
95
|
|
|
96
96
|
## Kernel protocol and pins
|
|
97
97
|
|
|
98
|
-
The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports exactly protocol v5 and REFUSES an identity with `provisional: true`; every loader that executes the kernel, `xema edition check` first, and the evidence verifier compare only that identity and refuse an unsupported one, naming the supported and the declared versions. A protocol change needs a CLI release first; only then does the data root repin its kernel authority.
|
|
98
|
+
The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports exactly protocol v8 (a data root whose pinned kernel speaks v5 stays on CLI 0.33.x) and REFUSES an identity with `provisional: true`; every loader that executes the kernel, `xema edition check` first, and the evidence verifier compare only that identity and refuse an unsupported one, naming the supported and the declared versions. A protocol change needs a CLI release first; only then does the data root repin its kernel authority.
|
|
99
99
|
|
|
100
100
|
- `xema edition evidence statement entitlement stage` writes entitlement predicate v2 (`lockDigest`, `catalogueDigest`, read from the edition's committed locks), so it takes `--editions-root` besides `--data-root` and `--kernel`.
|
|
101
101
|
- The runtime image source index is v2 and carries operator-stated `producerWorkflowPins` (the workflow commit a producer certificate must be signed from, per repository). `xema edition index runtime-image-source [--producer-workflow-pin <owner/repo>=<40-hex>]...` preserves the committed pins exactly; each flag sets one repository's pin (repeatable; a repeated or non-GitHub `owner/repo` is refused; only `--trusted-write` writes an index, so the flag with any other mode, `--check-sources` included, is refused); a bootstrap index with none writes `[]`. A v1 index is refused, never migrated: delete it and bootstrap a new one with `--kernel-commit`.
|
|
@@ -125,7 +125,7 @@ A held commit is authorized, never trusted, and nothing overrides it: the commit
|
|
|
125
125
|
|
|
126
126
|
## Customer editions
|
|
127
127
|
|
|
128
|
-
A customer edition is resolved against a catalogue pinned in `catalogue.pin.json` beside the definition file. The catalogue is the ceiling: it supplies availability, pins and digests and never adds a component. It also names the kernel: `kernel` is `{name, version, integrity}` (`@xemahq/contracts`, an exact `x.y.z`, the npm `sha512-` integrity of that tarball), and a catalogue without it is refused.
|
|
128
|
+
A customer edition is resolved against a catalogue pinned in `catalogue.pin.json` beside the definition file. The catalogue is the ceiling: it supplies availability, pins and digests and never adds a component. It also names the kernel: `kernel` is `{name, version, integrity}` (`@xemahq/contracts`, an exact `x.y.z`, the npm `sha512-` integrity of that tarball), and a catalogue without it is refused. Its `ceiling.storePublisherTrust` is the offering lock's Store publishers (or `null`); a definition selects among them with `storePublisherAnchorKeys`, the resolve copies the selected entries into the customer lock, and the kernel's `customerLockCeilingIssues` refuses, at resolve, seal and verify, an entry the definition does not select, a key the catalogue does not vouch for, or an entry that differs from the catalogue's.
|
|
129
129
|
|
|
130
130
|
```bash
|
|
131
131
|
xema edition resolve --definition <file> --channel <release|develop> \
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
import { relative, sep } from 'node:path';
|
|
21
21
|
|
|
22
|
+
import { assertCustomerLockWithinCatalogueCeiling } from './catalogue.mjs';
|
|
22
23
|
import { readGitJsonAtCommit } from './lib.mjs';
|
|
23
24
|
import {
|
|
24
25
|
assertOperationGrantsAdmitted,
|
|
@@ -191,7 +192,7 @@ async function catalogueFacts({ schemas, biome, imageLock, catalogue }) {
|
|
|
191
192
|
* the catalogue supplied. A lock without `catalogueDigest`, or with another one, was not
|
|
192
193
|
* resolved against this catalogue, so its hashes cannot be checked on this catalogue's authority.
|
|
193
194
|
*/
|
|
194
|
-
function assertLockNamesCatalogue({ lock, catalogue, catalogueDigest }) {
|
|
195
|
+
function assertLockNamesCatalogue({ schemas, lock, catalogue, catalogueDigest, storePublisherAnchorKeys }) {
|
|
195
196
|
if (catalogueDigest === undefined) {
|
|
196
197
|
throw new BiomeContentHashError(`customer mode needs the digest of catalogue "${catalogue.offeringId}"`);
|
|
197
198
|
}
|
|
@@ -205,6 +206,8 @@ function assertLockNamesCatalogue({ lock, catalogue, catalogueDigest }) {
|
|
|
205
206
|
`distribution "${lock.distributionId}": the lock was resolved against catalogue ${lock.catalogueDigest}, not the supplied ${catalogueDigest}`,
|
|
206
207
|
);
|
|
207
208
|
}
|
|
209
|
+
// The lock is within the catalogue's ceiling: the kernel's one check, its Store publishers against the definition's selection.
|
|
210
|
+
assertCustomerLockWithinCatalogueCeiling({ schemas, catalogue, lock, storePublisherAnchorKeys });
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
/**
|
|
@@ -218,10 +221,11 @@ function assertLockNamesCatalogue({ lock, catalogue, catalogueDigest }) {
|
|
|
218
221
|
* @param {(repo: string) => string} request.vendoredCommit the vendored-source pin of a first-party repo
|
|
219
222
|
* @param {object} [request.catalogue] customer mode: the digest-pinned catalogue vendor biomes are taken from
|
|
220
223
|
* @param {string} [request.catalogueDigest] customer mode: the digest of that catalogue; the lock must carry the same one
|
|
224
|
+
* @param {readonly string[]} [request.storePublisherAnchorKeys] customer mode: the definition's Store publisher selection (absent = none)
|
|
221
225
|
* @returns {Promise<Map<string, string>>} biome id -> BiomeContentHash
|
|
222
226
|
*/
|
|
223
|
-
export async function computeBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest }) {
|
|
224
|
-
if (catalogue !== undefined) {assertLockNamesCatalogue({ lock, catalogue, catalogueDigest });}
|
|
227
|
+
export async function computeBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys }) {
|
|
228
|
+
if (catalogue !== undefined) {assertLockNamesCatalogue({ schemas, lock, catalogue, catalogueDigest, storePublisherAnchorKeys });}
|
|
225
229
|
const hashes = new Map();
|
|
226
230
|
for (const biome of lock.biomes) {
|
|
227
231
|
const facts =
|
|
@@ -248,8 +252,8 @@ export async function biomeSourceTreeIds({ schemas, lock, imageLock, locate, ven
|
|
|
248
252
|
}
|
|
249
253
|
|
|
250
254
|
/** Seal: the unsealed lock + the recomputed hashes -> the kernel's `sealDistributionLock`. */
|
|
251
|
-
export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest }) {
|
|
252
|
-
const hashes = await computeBiomeContentHashes({ schemas, lock: unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest });
|
|
255
|
+
export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys }) {
|
|
256
|
+
const hashes = await computeBiomeContentHashes({ schemas, lock: unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys });
|
|
253
257
|
assertOperationGrantsAdmitted(schemas, unsealed);
|
|
254
258
|
assertOrganizationSweepsAdmitted(schemas, unsealed);
|
|
255
259
|
return schemas.sealDistributionLock(unsealed, hashes);
|
|
@@ -259,8 +263,8 @@ export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredC
|
|
|
259
263
|
* Verify: recompute every biome's hash and REFUSE a mismatch, naming the biome.
|
|
260
264
|
* The lock's own field is never trusted; a missing or extra biome is a mismatch.
|
|
261
265
|
*/
|
|
262
|
-
export async function verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest }) {
|
|
263
|
-
const expected = await computeBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest });
|
|
266
|
+
export async function verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys }) {
|
|
267
|
+
const expected = await computeBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys });
|
|
264
268
|
const mismatched = lock.biomes
|
|
265
269
|
.filter((biome) => biome.biomeContentHash !== expected.get(biome.id))
|
|
266
270
|
.map((biome) => `${biome.id} (lock ${String(biome.biomeContentHash)}, recomputed ${expected.get(biome.id)})`);
|
|
@@ -24,11 +24,11 @@ if (isEntryModule(import.meta.url)) {
|
|
|
24
24
|
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** `{ id, out }` of
|
|
27
|
+
/** `{ id, out }` of `<distribution-id> --out <dir>` (the argv AFTER the `catalogue` verb, which the edition dispatcher has already stripped); anything else is the usage error. */
|
|
28
28
|
export function parseCatalogueArgs(argv) {
|
|
29
|
-
const [
|
|
30
|
-
if (
|
|
31
|
-
throw new Error('usage: catalogue <distribution-id> --out <dir>');
|
|
29
|
+
const [id, flag, out, ...extra] = argv;
|
|
30
|
+
if (id === undefined || flag !== '--out' || out === undefined || out.startsWith('--') || extra.length > 0) {
|
|
31
|
+
throw new Error('usage: xema edition catalogue <distribution-id> --out <dir>');
|
|
32
32
|
}
|
|
33
33
|
assertCanonicalDistributionId(id);
|
|
34
34
|
return { id, out };
|
package/edition/catalogue.mjs
CHANGED
|
@@ -17,11 +17,13 @@ import { serializeJson } from './lib.mjs';
|
|
|
17
17
|
import { verifiedPlatformServiceIndex } from './platform-service-ownership.mjs';
|
|
18
18
|
|
|
19
19
|
export const CATALOGUE_FILE = EDITION_OFFERING_LAYERS.catalogue;
|
|
20
|
-
|
|
20
|
+
// v2 (edition protocol v8): `ceiling.storePublisherTrust`, the Store publishers the offering
|
|
21
|
+
// vouches for, from which a customer definition selects (`storePublisherAnchorKeys`).
|
|
22
|
+
export const CATALOGUE_SCHEMA = 'xema.edition-catalogue/v2';
|
|
21
23
|
|
|
22
24
|
const TOP_KEYS = ['schema', 'offeringId', 'channel', 'biomes', 'sourceTreeIds', 'platformServices', 'images', 'chartSet', 'contractAuthority', 'kernel', 'ceiling'];
|
|
23
25
|
const IMAGES_KEYS = ['sourceHashAlgorithm', 'subjects', 'platformServiceBindings', 'webBiomes'];
|
|
24
|
-
const CEILING_KEYS = ['trustPolicy', 'trustCeiling'];
|
|
26
|
+
const CEILING_KEYS = ['trustPolicy', 'trustCeiling', 'storePublisherTrust'];
|
|
25
27
|
const KERNEL_KEYS = ['name', 'version', 'integrity'];
|
|
26
28
|
const KERNEL_PACKAGE = '@xemahq/contracts';
|
|
27
29
|
const EXACT_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
@@ -140,8 +142,76 @@ export function buildCatalogue({
|
|
|
140
142
|
chartSet: chartSetLock,
|
|
141
143
|
contractAuthority,
|
|
142
144
|
kernel,
|
|
143
|
-
ceiling: {
|
|
145
|
+
ceiling: {
|
|
146
|
+
trustPolicy: leaf?.trustPolicy ?? null,
|
|
147
|
+
trustCeiling: leaf?.trustCeiling ?? null,
|
|
148
|
+
// The Store publishers the offering's lock vouches for: a customer edition copies only those it selects.
|
|
149
|
+
storePublisherTrust: distributionLock.storePublisherTrust ?? null,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A customer lock outside its catalogue's ceiling, or a selection or dependency the catalogue does not offer. */
|
|
155
|
+
export class CatalogueCeilingError extends Error {
|
|
156
|
+
constructor(message) {
|
|
157
|
+
super(message);
|
|
158
|
+
this.name = 'CatalogueCeilingError';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The catalogue's declared ceiling, read by the kernel's grammars: `trustPolicy` is the offering's
|
|
164
|
+
* origin allow list, `trustCeiling` its trust-domain assignments, `storePublisherTrust` the Store
|
|
165
|
+
* publishers the offering vouches for (each a kernel `LockedStorePublisher`). `null` states none.
|
|
166
|
+
*/
|
|
167
|
+
export function catalogueCeiling(catalogue, schemas) {
|
|
168
|
+
const read = (key, schema) => {
|
|
169
|
+
const value = catalogue.ceiling[key];
|
|
170
|
+
if (value === null) {return undefined;}
|
|
171
|
+
const parsed = schema.safeParse(value);
|
|
172
|
+
if (!parsed.success) {
|
|
173
|
+
throw new CatalogueError(
|
|
174
|
+
`${CATALOGUE_FILE}: ceiling.${key} of "${catalogue.offeringId}" is not the kernel's shape: ` +
|
|
175
|
+
parsed.error.issues.map((issue) => `[${issue.path.join('.')}] ${issue.message}`).join('; '),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return parsed.data;
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
trustPolicy: read('trustPolicy', schemas.DistributionTrustPolicySchema),
|
|
182
|
+
trustCeiling: read('trustCeiling', schemas.ProcessTrustPolicySchema),
|
|
183
|
+
storePublisherTrust: read('storePublisherTrust', schemas.LockedStorePublisherSchema.array()),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The Store publishers a customer lock carries: the entries of the catalogue's parsed ceiling
|
|
189
|
+
* (`catalogueCeiling(...).storePublisherTrust`) whose anchorKey the definition selects
|
|
190
|
+
* (`storePublisherAnchorKeys`), copied as they are. No selection, or none of it offered, is no
|
|
191
|
+
* field; a key the catalogue does not vouch for is refused by the kernel's ceiling check, not here.
|
|
192
|
+
*/
|
|
193
|
+
export function selectedStorePublisherTrust(base, storePublisherAnchorKeys) {
|
|
194
|
+
if (storePublisherAnchorKeys === undefined) {return undefined;}
|
|
195
|
+
const selected = new Set(storePublisherAnchorKeys);
|
|
196
|
+
const rows = (base ?? []).filter((row) => selected.has(row.anchorKey));
|
|
197
|
+
return rows.length > 0 ? rows : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The kernel's `customerLockCeilingIssues` is the one check of a customer lock against its catalogue:
|
|
202
|
+
* the biomes' trust ceilings, and the Store publishers (base = the catalogue's, selected = the
|
|
203
|
+
* definition's `storePublisherAnchorKeys`, customer = the lock's).
|
|
204
|
+
*/
|
|
205
|
+
export function assertCustomerLockWithinCatalogueCeiling({ schemas, catalogue, lock, storePublisherAnchorKeys }) {
|
|
206
|
+
const ceiling = catalogueCeiling(catalogue, schemas);
|
|
207
|
+
const issues = schemas.customerLockCeilingIssues(ceiling.trustCeiling, lock.biomes, {
|
|
208
|
+
base: ceiling.storePublisherTrust,
|
|
209
|
+
selected: storePublisherAnchorKeys,
|
|
210
|
+
customer: lock.storePublisherTrust,
|
|
144
211
|
});
|
|
212
|
+
if (issues.length > 0) {
|
|
213
|
+
throw new CatalogueCeilingError(`catalogue "${catalogue.offeringId}" (${catalogue.channel}): "${lock.distributionId}" is outside the catalogue's ceiling: ${issues.join('; ')}`);
|
|
214
|
+
}
|
|
145
215
|
}
|
|
146
216
|
|
|
147
217
|
/** Write `catalogue.json` into an artifact directory (deterministic serialization); returns its path. */
|
|
@@ -98,6 +98,7 @@ export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue,
|
|
|
98
98
|
},
|
|
99
99
|
catalogue,
|
|
100
100
|
catalogueDigest,
|
|
101
|
+
storePublisherAnchorKeys: resolved.storePublisherAnchorKeys,
|
|
101
102
|
});
|
|
102
103
|
return { ...resolved, distributionLock, pins: frozen };
|
|
103
104
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
17
|
import { existsSync, readFileSync } from 'node:fs';
|
|
18
18
|
|
|
19
|
-
import { assertEditionPinsAuthorized, effectiveRepositoryPins, EditionSourcePinsError,
|
|
19
|
+
import { assertEditionPinsAuthorized, effectiveRepositoryPins, EditionSourcePinsError, readKernelEditionSourcePins } from './edition-source-pins.mjs';
|
|
20
20
|
import { biomeIndexPath, dataRoot, editionsRoot, platformServiceIndexPath, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
|
|
21
21
|
import { buildIndexesAtPins } from './indexes-at-pins.mjs';
|
|
22
22
|
import { commandFor } from './remedy-command.mjs';
|
|
@@ -102,7 +102,7 @@ function committedInputs({ schemas, sourcePins }) {
|
|
|
102
102
|
* pins and committed runtime index, so one process pays the rebuild once.
|
|
103
103
|
*/
|
|
104
104
|
export async function editionPinnedInputs(id, { schemas }) {
|
|
105
|
-
const sourcePins =
|
|
105
|
+
const sourcePins = readKernelEditionSourcePins(id, schemas.parseDistribution);
|
|
106
106
|
if (sourcePins.length === 0) {return committedInputs({ schemas, sourcePins });}
|
|
107
107
|
const indexText = readFileSync(runtimeImageSourceIndexPath(), 'utf8');
|
|
108
108
|
const committed = schemas.parseRuntimeImageSourceIndex(readStrictJsonFile(runtimeImageSourceIndexPath()));
|
|
@@ -31,8 +31,8 @@ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
|
31
31
|
/** The edition protocols this library interprets: version -> content digest. */
|
|
32
32
|
export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
33
33
|
Object.freeze({
|
|
34
|
-
version:
|
|
35
|
-
contentDigest: '
|
|
34
|
+
version: 8,
|
|
35
|
+
contentDigest: '5f878e9eaa2cd0ab39b645e4a67259b1e39383e4931e9849c4cc746f7ae43047',
|
|
36
36
|
}),
|
|
37
37
|
]);
|
|
38
38
|
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
// `until` states the condition under which the hold ends. Entries are sparse:
|
|
9
9
|
// one exists only for a repository the edition holds back, so an entry equal to
|
|
10
10
|
// the shared pin is refused (it is not a hold). `xema edition pin|unpin` are the
|
|
11
|
-
// only supported way to change them.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// only supported way to change them. The kernel's Distribution schema models and
|
|
12
|
+
// validates the key (`DistributionSourcePinsSchema`), so the definition reaches
|
|
13
|
+
// `parseDistribution` unchanged and a reader that parses with the kernel takes
|
|
14
|
+
// `sourcePins` from the parsed manifest (`readKernelEditionSourcePins`).
|
|
15
|
+
// `parseEditionSourcePins` remains for the one reader that runs before any kernel
|
|
16
|
+
// exists (the source checkout plan, `readEditionSourcePins`) and for the pin verbs.
|
|
16
17
|
//
|
|
17
18
|
// `effectiveRepositoryPins` is the ONLY computation of an edition's pins: the
|
|
18
19
|
// committed pins untouched (the same array) when the edition holds nothing,
|
|
@@ -87,26 +88,27 @@ export function parseEditionSourcePins(raw, id) {
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
* the CLI key) and the parsed `sourcePins`. A value that is not an object is
|
|
93
|
-
* returned as it is for the kernel to refuse.
|
|
94
|
-
*/
|
|
95
|
-
export function splitEditionDefinition(raw, id) {
|
|
96
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw) || !Object.hasOwn(raw, SOURCE_PINS_KEY)) {
|
|
97
|
-
return { document: raw, sourcePins: [] };
|
|
98
|
-
}
|
|
99
|
-
const { [SOURCE_PINS_KEY]: declared, ...document } = raw;
|
|
100
|
-
return { document, sourcePins: parseEditionSourcePins(declared, id) };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** The `sourcePins` of edition `id`, read from the configured editions root. */
|
|
104
|
-
export function readEditionSourcePins(id) {
|
|
91
|
+
/** The raw definition of edition `id` in the configured editions root. */
|
|
92
|
+
function readEditionDefinition(id) {
|
|
105
93
|
const path = join(editionsRoot(), id, 'xema-distribution.json');
|
|
106
94
|
if (!existsSync(path)) {
|
|
107
95
|
throw new Error(`distribution "${id}" not found: ${path} does not exist`);
|
|
108
96
|
}
|
|
109
|
-
return
|
|
97
|
+
return readStrictJsonFile(path);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The `sourcePins` of a kernel-parsed Distribution manifest; absent is none. */
|
|
101
|
+
export const manifestSourcePins = (manifest) => manifest.sourcePins ?? [];
|
|
102
|
+
|
|
103
|
+
/** The `sourcePins` of edition `id`, parsed by the kernel's `parseDistribution` (the one validator). */
|
|
104
|
+
export function readKernelEditionSourcePins(id, parseDistribution) {
|
|
105
|
+
return manifestSourcePins(parseDistribution(readEditionDefinition(id)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The `sourcePins` of edition `id` where no kernel is loaded yet (the source checkout plan). */
|
|
109
|
+
export function readEditionSourcePins(id) {
|
|
110
|
+
const raw = readEditionDefinition(id);
|
|
111
|
+
return raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? parseEditionSourcePins(raw[SOURCE_PINS_KEY], id) : [];
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/** A repository URL without a trailing `.git`, so the two forms a pin may carry compare equal. */
|
|
@@ -108,6 +108,7 @@ export const kernelDistPath = (...segments) =>
|
|
|
108
108
|
join(kernelPackageDir(), 'dist', ...segments);
|
|
109
109
|
|
|
110
110
|
export const biomeIndexPath = () => join(dataRoot(), 'biome-index.json');
|
|
111
|
+
export const storePublisherIdentitiesPath = () => join(dataRoot(), 'store-publisher-identities.json');
|
|
111
112
|
export const platformServiceIndexPath = () =>
|
|
112
113
|
join(dataRoot(), 'platform-service-index.json');
|
|
113
114
|
export const runtimeImageSourceIndexPath = () =>
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { TextDecoder } from 'node:util';
|
|
19
19
|
|
|
20
20
|
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
21
|
-
import { effectiveRepositoryPins,
|
|
21
|
+
import { effectiveRepositoryPins, manifestSourcePins } from './edition-source-pins.mjs';
|
|
22
22
|
import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
23
23
|
import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
|
|
24
24
|
import {
|
|
@@ -40,6 +40,8 @@ const MAX_CANONICAL_JSON_BYTES = 2 * 1024 * 1024;
|
|
|
40
40
|
const MAX_STATEMENT_BYTES = 2 * 1024 * 1024;
|
|
41
41
|
const MAX_SIGSTORE_BUNDLE_BYTES = 16 * 1024 * 1024;
|
|
42
42
|
const MAX_TRUSTED_ROOT_BYTES = 8 * 1024 * 1024;
|
|
43
|
+
/** The kernel's `SignerKind.OidcCertificate`: the one signer kind this verifier runs (cosign keyless). */
|
|
44
|
+
const OIDC_CERTIFICATE_SIGNER = 'oidc-certificate';
|
|
43
45
|
const COSIGN_ENV = Object.freeze({
|
|
44
46
|
LANG: 'C',
|
|
45
47
|
LC_ALL: 'C',
|
|
@@ -631,13 +633,21 @@ function assertEvidenceChannel(channel) {
|
|
|
631
633
|
* certificate matching a row of that channel.
|
|
632
634
|
*/
|
|
633
635
|
function policyCandidates(policy, predicateType, channel, imageRepository) {
|
|
634
|
-
|
|
636
|
+
const candidates = policy.policies.filter(
|
|
635
637
|
(candidate) =>
|
|
636
638
|
candidate.channel === channel &&
|
|
637
639
|
candidate.predicateTypes.includes(predicateType) &&
|
|
638
640
|
(imageRepository === undefined ||
|
|
639
641
|
candidate.imageRepositories?.includes(imageRepository) === true),
|
|
640
642
|
);
|
|
643
|
+
// The kernel policy also admits `public-key` signers; this verifier runs cosign keyless only, so such a row is refused, never skipped.
|
|
644
|
+
const unsupported = candidates.find((candidate) => candidate.kind !== OIDC_CERTIFICATE_SIGNER);
|
|
645
|
+
if (unsupported !== undefined) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`policy row "${unsupported.policyId}" is a ${JSON.stringify(unsupported.kind)} signer; this CLI verifies ${OIDC_CERTIFICATE_SIGNER} signers only`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return candidates;
|
|
641
651
|
}
|
|
642
652
|
|
|
643
653
|
function cosignVersion(cosignBinary) {
|
|
@@ -1317,19 +1327,18 @@ export async function loadValidatedImageLocks(
|
|
|
1317
1327
|
'images.lock.json kernel contract authority does not exactly match runtime-image-source-index.json',
|
|
1318
1328
|
);
|
|
1319
1329
|
}
|
|
1320
|
-
// The definition
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
const
|
|
1330
|
+
// The definition's `sourcePins` (the repositories this edition holds at an exact commit) is parsed by the
|
|
1331
|
+
// kernel with the rest of it. Each subject is graded against the EFFECTIVE pin of THIS edition. Pure: no
|
|
1332
|
+
// git, no sources root (this runs inside the built verifier).
|
|
1333
|
+
const distribution = schemas.parseDistribution(
|
|
1324
1334
|
JSON.parse(
|
|
1325
1335
|
readFileSync(
|
|
1326
1336
|
join(editionsRoot, distributionId, 'xema-distribution.json'),
|
|
1327
1337
|
'utf8',
|
|
1328
1338
|
),
|
|
1329
1339
|
),
|
|
1330
|
-
distributionId,
|
|
1331
1340
|
);
|
|
1332
|
-
const
|
|
1341
|
+
const sourcePins = manifestSourcePins(distribution);
|
|
1333
1342
|
const repositoryPins = new Map(
|
|
1334
1343
|
effectiveRepositoryPins(runtimeSourceIndex.repositoryPins, sourcePins, { id: distributionId, contractAuthority: runtimeSourceIndex.contractAuthority }).map((repository) => [
|
|
1335
1344
|
repository.repo,
|
package/edition/lib.mjs
CHANGED
|
@@ -778,6 +778,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
778
778
|
distributionImagesPredicateChannelRefusal,
|
|
779
779
|
admitOperationGrants,
|
|
780
780
|
admitOrganizationSweeps,
|
|
781
|
+
contractRequiredServiceTargets,
|
|
781
782
|
} = mod;
|
|
782
783
|
let topology;
|
|
783
784
|
try {
|
|
@@ -789,7 +790,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
789
790
|
);
|
|
790
791
|
}
|
|
791
792
|
const { deriveTrustCeiling, defaultTrustDomain, FIRST_PARTY_TRUST_DOMAIN, ProcessTrustPolicySchema } = topology;
|
|
792
|
-
const { DistributionTrustPolicySchema } = mod;
|
|
793
|
+
const { DistributionTrustPolicySchema, LockedStorePublisherSchema } = mod;
|
|
793
794
|
if (
|
|
794
795
|
typeof parseDistribution !== 'function' ||
|
|
795
796
|
typeof parseDistributionLock !== 'function' ||
|
|
@@ -874,6 +875,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
874
875
|
// the resolve refuse through it (`assertOperationGrantsAdmitted`).
|
|
875
876
|
typeof admitOperationGrants !== 'function' ||
|
|
876
877
|
typeof admitOrganizationSweeps !== 'function' ||
|
|
878
|
+
typeof contractRequiredServiceTargets !== 'function' ||
|
|
877
879
|
typeof distributionImagesPredicateLockRefusal !== 'function' ||
|
|
878
880
|
typeof distributionLockDigest !== 'function' ||
|
|
879
881
|
typeof imageDeliveryEntitlementRefusal !== 'function' ||
|
|
@@ -884,7 +886,9 @@ export async function loadKernelDistributionSchemas() {
|
|
|
884
886
|
typeof defaultTrustDomain !== 'function' ||
|
|
885
887
|
typeof FIRST_PARTY_TRUST_DOMAIN !== 'string' ||
|
|
886
888
|
typeof ProcessTrustPolicySchema?.safeParse !== 'function' ||
|
|
887
|
-
typeof DistributionTrustPolicySchema?.safeParse !== 'function'
|
|
889
|
+
typeof DistributionTrustPolicySchema?.safeParse !== 'function' ||
|
|
890
|
+
// One Store publisher entry of a lock: the grammar a catalogue's `ceiling.storePublisherTrust` is read by.
|
|
891
|
+
typeof LockedStorePublisherSchema?.array !== 'function'
|
|
888
892
|
) {
|
|
889
893
|
throw new Error(
|
|
890
894
|
`Built kernel distribution module at ${kernelDistributionDist()} does not export ` +
|
|
@@ -894,7 +898,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
894
898
|
`kernel contract-authority digest SSOT/compareLockedServiceResources/resolveNexusBindings/describeNexusBindingRefusal/canonicalNexusFacts/` +
|
|
895
899
|
`positive closure (resolveBiomeClosure/platformInvariantRoots/DistributionResolutionMode/provenance)/` +
|
|
896
900
|
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/` +
|
|
897
|
-
`defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/` +
|
|
901
|
+
`defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/LockedStorePublisherSchema/` +
|
|
898
902
|
`resolveFirstPartyCapabilityDomains/DistributionVisibility/` +
|
|
899
903
|
`BiomeInstallPolicy/BiomeLaunchMode/ImageSourceHashAlgorithm/runtime-image enums — ` +
|
|
900
904
|
`the dist is stale; rebuild kernel-contracts.`,
|
|
@@ -968,10 +972,12 @@ export async function loadKernelDistributionSchemas() {
|
|
|
968
972
|
FIRST_PARTY_TRUST_DOMAIN,
|
|
969
973
|
ProcessTrustPolicySchema,
|
|
970
974
|
DistributionTrustPolicySchema,
|
|
975
|
+
LockedStorePublisherSchema,
|
|
971
976
|
distributionLockDigest,
|
|
972
977
|
distributionImagesPredicateChannelRefusal,
|
|
973
978
|
admitOperationGrants,
|
|
974
979
|
admitOrganizationSweeps,
|
|
980
|
+
contractRequiredServiceTargets,
|
|
975
981
|
distributionImagesPredicateLockRefusal,
|
|
976
982
|
imageDeliveryEntitlementRefusal,
|
|
977
983
|
producerWorkflowPinRefusal,
|
|
@@ -25,16 +25,31 @@ export class OperationGrantAdmissionError extends Error {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* One informational line per optional operation the kernel left ungranted
|
|
30
|
+
* (`ungrantedOptional`): the edition is valid, the caller runs without it.
|
|
31
|
+
*/
|
|
32
|
+
export function ungrantedOptionalOperationLines(distributionId, ungrantedOptional) {
|
|
33
|
+
return ungrantedOptional.map(
|
|
34
|
+
({ caller, operation, reason }) =>
|
|
35
|
+
`distribution "${distributionId}": optional operation ${operation} of ${caller.biome}/${caller.component} is not granted (${reason})`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
/**
|
|
29
40
|
* Admit every operation requirement of `lock` through the kernel, or throw
|
|
30
41
|
* {@link OperationGrantAdmissionError} listing every refusal. Returns the
|
|
31
|
-
* admitted grants.
|
|
42
|
+
* admitted grants. `report`, when given, receives each line of
|
|
43
|
+
* {@link ungrantedOptionalOperationLines}; an ungranted optional operation never fails.
|
|
32
44
|
*/
|
|
33
|
-
export function assertOperationGrantsAdmitted(schemas, lock) {
|
|
34
|
-
const { grants, refusals } = schemas.admitOperationGrants(lock);
|
|
45
|
+
export function assertOperationGrantsAdmitted(schemas, lock, { report } = {}) {
|
|
46
|
+
const { grants, refusals, ungrantedOptional } = schemas.admitOperationGrants(lock);
|
|
35
47
|
if (refusals.length > 0) {
|
|
36
48
|
throw new OperationGrantAdmissionError(lock.distributionId, refusals);
|
|
37
49
|
}
|
|
50
|
+
if (report !== undefined) {
|
|
51
|
+
for (const line of ungrantedOptionalOperationLines(lock.distributionId, ungrantedOptional)) {report(line);}
|
|
52
|
+
}
|
|
38
53
|
return grants;
|
|
39
54
|
}
|
|
40
55
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// release-edition-artifact — write the OCI artifact of a RELEASE edition from
|
|
3
3
|
// the committed, sealed locks of the editions root.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// <id> --channel release --registry <root> --coordinate <tag>
|
|
6
6
|
// --engine-image <repository>@sha256:<digest> --out <dir> [--local]
|
|
7
7
|
//
|
|
8
8
|
// The artifact has the layout of every edition (edition-artifact.mjs): the
|
|
@@ -28,6 +28,7 @@ import { loadKernelDistributionSchemas, validateImmutableImageLock } from './lib
|
|
|
28
28
|
import { releaseCatalogue } from './release-catalogue.mjs';
|
|
29
29
|
import { assertReleaseEditionTree } from './release-channel-guard.mjs';
|
|
30
30
|
import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
|
|
31
|
+
import { readSealedDistributionLock } from './sealed-distribution-lock.mjs';
|
|
31
32
|
import { createPinnedSourceAuthority } from './source-authority.mjs';
|
|
32
33
|
|
|
33
34
|
if (isEntryModule(import.meta.url)) {
|
|
@@ -38,10 +39,9 @@ const VALUE_FLAGS = Object.freeze(['--channel', '--registry', '--coordinate', '-
|
|
|
38
39
|
const REQUIRED_FLAGS = VALUE_FLAGS;
|
|
39
40
|
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
40
41
|
|
|
41
|
-
/** Parse
|
|
42
|
+
/** Parse `<id> --flag value ...` (the argv AFTER the `artifact` verb, which the edition dispatcher has already stripped); only the release channel is written by this verb. */
|
|
42
43
|
export function parseReleaseArtifactArgs(argv) {
|
|
43
|
-
const
|
|
44
|
-
if (command !== 'artifact') {throw new Error(`unknown command ${JSON.stringify(command)}; expected artifact`);}
|
|
44
|
+
const rest = argv;
|
|
45
45
|
const options = {};
|
|
46
46
|
const positional = [];
|
|
47
47
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -100,17 +100,11 @@ export async function readCommittedEdition(id, root = editionsRoot()) {
|
|
|
100
100
|
const schemas = await loadKernelDistributionSchemas();
|
|
101
101
|
const imageLock = validateImmutableImageLock(schemas.parseImageLock(read(EditionLockFile.Images)), schemas);
|
|
102
102
|
assertSealedImageLock(imageLock, join(dir, EditionLockFile.Images));
|
|
103
|
-
const distributionLock =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
throw new Error(
|
|
109
|
-
`${join(dir, EditionLockFile.Distribution)} is not a sealed distribution lock, so nothing is published from it. ` +
|
|
110
|
-
`Run \`xema edition seal ${id}\` first. (${error.message})`,
|
|
111
|
-
{ cause: error },
|
|
112
|
-
);
|
|
113
|
-
}
|
|
103
|
+
const { raw: distributionLock, sealed } = readSealedDistributionLock({
|
|
104
|
+
schemas,
|
|
105
|
+
lockPath: join(dir, EditionLockFile.Distribution),
|
|
106
|
+
distributionId: id,
|
|
107
|
+
});
|
|
114
108
|
const authority = createPinnedSourceAuthority({ pins: (await editionPinnedInputs(id, { schemas })).repositoryPins });
|
|
115
109
|
await verifyBiomeContentHashes({
|
|
116
110
|
schemas,
|
|
@@ -20,7 +20,13 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
20
20
|
|
|
21
21
|
import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest } from './adapter-bundled-versions.mjs';
|
|
22
22
|
import { AUTHORIZATION_LOCK_FILENAME, writeAuthorizationLock } from './authorization-base.mjs';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
assertCustomerLockWithinCatalogueCeiling,
|
|
25
|
+
CatalogueCeilingError,
|
|
26
|
+
CatalogueError,
|
|
27
|
+
catalogueCeiling,
|
|
28
|
+
selectedStorePublisherTrust,
|
|
29
|
+
} from './catalogue.mjs';
|
|
24
30
|
import {
|
|
25
31
|
assertTrustCeilingAssignsCustomerBiomes,
|
|
26
32
|
mirrorVendorSubjects,
|
|
@@ -36,45 +42,11 @@ import { pinnedSource, snapshotPinSource } from './pin-source.mjs';
|
|
|
36
42
|
import { composeAuthorizationBase, composeManifest, resolveDistributionWithIndex, UnknownSelectionError } from './resolve-distribution.mjs';
|
|
37
43
|
import { resolveImageRows } from './resolve-images.mjs';
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
export class CatalogueCeilingError extends Error {
|
|
41
|
-
constructor(message) {
|
|
42
|
-
super(message);
|
|
43
|
-
this.name = 'CatalogueCeilingError';
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The catalogue's declared ceiling, read by the kernel's two grammars: `trustPolicy` is the
|
|
49
|
-
* offering's origin allow list, `trustCeiling` its trust-domain assignments. `null` states none.
|
|
50
|
-
*/
|
|
51
|
-
function catalogueCeiling(catalogue, schemas) {
|
|
52
|
-
const read = (key, schema) => {
|
|
53
|
-
const value = catalogue.ceiling[key];
|
|
54
|
-
if (value === null) {return undefined;}
|
|
55
|
-
const parsed = schema.safeParse(value);
|
|
56
|
-
if (!parsed.success) {
|
|
57
|
-
throw new CatalogueError(
|
|
58
|
-
`${CATALOGUE_FILE}: ceiling.${key} of "${catalogue.offeringId}" is not the kernel's shape: ` +
|
|
59
|
-
parsed.error.issues.map((issue) => `[${issue.path.join('.')}] ${issue.message}`).join('; '),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
return parsed.data;
|
|
63
|
-
};
|
|
64
|
-
return { trustPolicy: read('trustPolicy', schemas.DistributionTrustPolicySchema), trustCeiling: read('trustCeiling', schemas.ProcessTrustPolicySchema) };
|
|
65
|
-
}
|
|
45
|
+
export { CatalogueCeilingError };
|
|
66
46
|
|
|
67
47
|
/** A catalogue whose ceiling states no trust-domain assignments reserves no domain: the kernel's required ceiling argument is the empty policy. */
|
|
68
48
|
const NO_CATALOGUE_TRUST_CEILING = Object.freeze({ assignments: [] });
|
|
69
49
|
|
|
70
|
-
/** The kernel's `customerLockCeilingIssues` is the one check of a customer lock against the catalogue's ceiling. */
|
|
71
|
-
function assertLockWithinCatalogueCeiling({ catalogue, definition, lock, schemas }) {
|
|
72
|
-
const issues = schemas.customerLockCeilingIssues(catalogueCeiling(catalogue, schemas).trustCeiling, lock.biomes);
|
|
73
|
-
if (issues.length > 0) {
|
|
74
|
-
throw new CatalogueCeilingError(`catalogue "${catalogue.offeringId}" (${catalogue.channel}): "${definition.id}" is outside the catalogue's trustCeiling: ${issues.join('; ')}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
50
|
/**
|
|
79
51
|
* The ceiling policy the customer's lock is written under: the catalogue's assignments, then
|
|
80
52
|
* the definition's for what the catalogue leaves open. An assignment of the definition that
|
|
@@ -128,7 +100,9 @@ function withSourceImages(definition, sourceImages, pins) {
|
|
|
128
100
|
/** The composition of a customer definition over the catalogue. */
|
|
129
101
|
export function composeCustomerEdition({ schemas, definition, mirror, catalogue, sourceImages, pins }) {
|
|
130
102
|
const ceiling = catalogueCeiling(catalogue, schemas);
|
|
131
|
-
|
|
103
|
+
// `storePublisherAnchorKeys` selects from the catalogue (`selectedStorePublisherTrust`); the Distribution manifest is strict and has no such key.
|
|
104
|
+
const { storePublisherAnchorKeys: _selection, ...document } = definition;
|
|
105
|
+
const manifest = schemas.parseDistribution({ ...document, ...(definition.sources === undefined ? {} : { sources: withSourceImages(definition, sourceImages, pins) }) });
|
|
132
106
|
if (definition.sources === undefined && sourceImages.size > 0) {
|
|
133
107
|
throw new Error('--source-image given, but the definition declares no sources');
|
|
134
108
|
}
|
|
@@ -295,7 +269,14 @@ export async function resolveCustomerDistribution({
|
|
|
295
269
|
// `sourcesAvailable: false`: a customer machine holds no vendor source checkouts, so the platform
|
|
296
270
|
// services' ownership cannot be re-derived here. It is verified where the catalogue is produced
|
|
297
271
|
// (`buildCatalogue`), and the catalogue is digest-pinned.
|
|
298
|
-
|
|
272
|
+
// A customer edition carries only the catalogue's Store publishers its definition selects, copied (`storePublisherTrust`).
|
|
273
|
+
const inputs = {
|
|
274
|
+
composed,
|
|
275
|
+
biomeIndex: catalogue.biomes,
|
|
276
|
+
platformServiceIndex: { schemaVersion: 1, services: catalogue.platformServices },
|
|
277
|
+
sourcesAvailable: false,
|
|
278
|
+
storePublisherTrust: selectedStorePublisherTrust(catalogueCeiling(catalogue, schemas).storePublisherTrust, definition.storePublisherAnchorKeys),
|
|
279
|
+
};
|
|
299
280
|
let resolved;
|
|
300
281
|
try {
|
|
301
282
|
resolved = await resolveDistributionWithIndex(id, { inputs, ...(materialize === undefined ? {} : { materialize }) });
|
|
@@ -312,22 +293,24 @@ export async function resolveCustomerDistribution({
|
|
|
312
293
|
lock.biomes.filter((biome) => biome.source !== undefined).map((biome) => biome.id),
|
|
313
294
|
catalogueCeiling(catalogue, schemas).trustCeiling ?? NO_CATALOGUE_TRUST_CEILING,
|
|
314
295
|
);
|
|
315
|
-
|
|
296
|
+
const { storePublisherAnchorKeys } = definition;
|
|
297
|
+
assertCustomerLockWithinCatalogueCeiling({ schemas, catalogue, lock, storePublisherAnchorKeys });
|
|
316
298
|
await assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(catalogue.biomes), fetchManifest });
|
|
317
299
|
const authorizationLock = await composeAuthorizationBase(id, lock, index);
|
|
318
300
|
// A customer lock names the catalogue it was resolved against; seal and verify refuse any other.
|
|
319
|
-
return { id, schemas, composed, inputs, lock: { ...lock, catalogueDigest }, authorizationLock };
|
|
301
|
+
return { id, schemas, composed, inputs, lock: { ...lock, catalogueDigest }, authorizationLock, storePublisherAnchorKeys };
|
|
320
302
|
}
|
|
321
303
|
|
|
322
304
|
/** Resolve the customer edition of `channel` against `catalogue`; returns the four locks. `sealOwn` seals the customer's OWN image rows only (a develop edition); vendor rows arrive sealed. */
|
|
323
305
|
export async function resolveCustomerEdition(request) {
|
|
324
|
-
const { id, schemas, composed, inputs, lock, authorizationLock } = await resolveCustomerDistribution(request);
|
|
306
|
+
const { id, schemas, composed, inputs, lock, authorizationLock, storePublisherAnchorKeys } = await resolveCustomerDistribution(request);
|
|
325
307
|
const { catalogue } = request;
|
|
326
308
|
const imageLock = await resolveCustomerImages({ id, catalogue, lock, composed, schemas, inputs, sealOwn: request.sealOwn });
|
|
327
309
|
// The chart-set is copied whole, so it carries the coordinates of the channel it was published on:
|
|
328
310
|
// the edition being written states its channel, and a chart-set of the other one is refused here.
|
|
329
311
|
const chartSetLock = assertRosteredChartSetLock(catalogue.chartSet, `the catalogue's chart-set of "${catalogue.offeringId}"`);
|
|
330
|
-
|
|
312
|
+
// `storePublisherAnchorKeys` (the definition's selection) is what the seal checks the lock's Store publishers against.
|
|
313
|
+
return { id, distributionLock: lock, imageLock, authorizationLock, chartSetLock, storePublisherAnchorKeys };
|
|
331
314
|
}
|
|
332
315
|
|
|
333
316
|
/** Write the four locks to `<out>/<id>/` (the layout of an editions root). */
|