@xemahq/xema 0.29.1 → 0.31.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/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/edition.js +3 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/infra-preflight.js +1 -1
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +10030 -1491
- package/dist/edition/verify-image-evidence.bundle.provenance.json +953 -419
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/store-submit.d.ts.map +1 -1
- package/dist/runtime/store-submit.js.map +1 -1
- package/edition/README.md +50 -271
- package/edition/adapter-bundled-versions.mjs +107 -0
- package/edition/adapter-host-versions.mjs +41 -0
- package/edition/assemble-producer-input.mjs +1 -1
- package/edition/authorization-base.mjs +18 -9
- package/edition/biome-content.mjs +6 -0
- package/edition/build-platform-service-index.mjs +2 -0
- package/edition/build-runtime-image-source-index.mjs +97 -9
- package/edition/catalogue.mjs +20 -13
- package/edition/checkout-indexed-source-repositories.mjs +41 -6
- 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/dispatched-runtime-components.mjs +82 -0
- 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/external-source-walk.mjs +1 -1
- package/edition/image-evidence.mjs +134 -26
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-package.mjs +193 -0
- package/edition/lib.mjs +192 -4
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/operation-grant-admission.mjs +70 -0
- package/edition/operation-requirements.mjs +128 -0
- package/edition/plan-image-evidence-refresh.mjs +2 -2
- package/edition/platform-lock-readability.mjs +2 -2
- package/edition/platform-service-class.mjs +42 -0
- package/edition/platform-service-ownership.mjs +78 -0
- 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/remedy-command.mjs +2 -2
- package/edition/resolve-chart-set.mjs +1 -1
- package/edition/resolve-customer-edition.mjs +33 -34
- package/edition/resolve-distribution.mjs +70 -16
- package/edition/resolve-edition.mjs +3 -2
- package/edition/resolve-images.mjs +65 -4
- package/edition/seal-image-digests.mjs +17 -5
- 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 +4 -4
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
// xema-deploy charts' registry manifest digests, in the release roster shape. Nothing here writes to the editions
|
|
7
7
|
// trees; the caller writes the artifact (edition-artifact.mjs).
|
|
8
8
|
|
|
9
|
+
import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest } from './adapter-bundled-versions.mjs';
|
|
9
10
|
import { biomeSourceTreeIds, sealLock } from './biome-content.mjs';
|
|
10
11
|
import { buildCatalogue } from './catalogue.mjs';
|
|
11
12
|
import { sealDevelopImages } from './develop-seal.mjs';
|
|
12
13
|
import { assertFrozenCheckouts, buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
|
|
13
14
|
import { EditionChannel } from './edition-constants.mjs';
|
|
14
|
-
import { deployRoot } from './edition-workspace.mjs';
|
|
15
|
+
import { deployRoot, kernelPackageDir } from './edition-workspace.mjs';
|
|
16
|
+
import { kernelPackageOfAuthority } from './kernel-package.mjs';
|
|
15
17
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
16
18
|
import { deployPinKey } from './pin-source.mjs';
|
|
17
19
|
import { resolveDevelopChartSet } from './resolve-chart-set.mjs';
|
|
@@ -19,11 +21,12 @@ import { composeAuthorizationBase, resolveDistributionWithIndex } from './resolv
|
|
|
19
21
|
import { externalSourceTrees, resolveImages, sourceTreeLocator } from './resolve-images.mjs';
|
|
20
22
|
import { createPinnedSourceAuthority } from './source-authority.mjs';
|
|
21
23
|
|
|
22
|
-
export async function resolveDevelopEdition({ id, pins, env = process.env, sealOptions = {}, chartDigestLookup }) {
|
|
24
|
+
export async function resolveDevelopEdition({ id, pins, env = process.env, sealOptions = {}, chartDigestLookup, fetchKernelTarball, fetchManifest = fetchRegistryManifest }) {
|
|
23
25
|
const schemas = await loadKernelDistributionSchemas();
|
|
24
26
|
assertFrozenCheckouts(pins);
|
|
25
27
|
const inputs = await buildSnapshotInputs({ id, pins, schemas });
|
|
26
28
|
const { lock, index } = await resolveDistributionWithIndex(id, { inputs });
|
|
29
|
+
await assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(index), fetchManifest });
|
|
27
30
|
const authorizationLock = await composeAuthorizationBase(id, lock, index);
|
|
28
31
|
const { imageLock } = await resolveImages(id, { inputs, lock });
|
|
29
32
|
const sealedImageLock = await sealDevelopImages({ imageLock, env, ...sealOptions });
|
|
@@ -59,6 +62,11 @@ export async function resolveDevelopEdition({ id, pins, env = process.env, sealO
|
|
|
59
62
|
biomeIndex: inputs.biomeIndex,
|
|
60
63
|
platformServiceIndex: inputs.platformServiceIndex,
|
|
61
64
|
contractAuthority: inputs.runtimeImageSourceIndex.contractAuthority,
|
|
65
|
+
kernel: await kernelPackageOfAuthority({
|
|
66
|
+
kernelPackageDir: kernelPackageDir(),
|
|
67
|
+
contractAuthority: inputs.runtimeImageSourceIndex.contractAuthority,
|
|
68
|
+
...(fetchKernelTarball === undefined ? {} : { fetchTarball: fetchKernelTarball }),
|
|
69
|
+
}),
|
|
62
70
|
leaf: inputs.composed.leaf,
|
|
63
71
|
});
|
|
64
72
|
return { distributionLock: sealedLock, imageLock: sealedImageLock, authorizationLock, chartSetLock, catalogue };
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// customer's own pins. --pins holds exactly one `sources/<name>` commit per source the definition
|
|
16
16
|
// declares (`freeze-sources --definition` writes it); the catalogue digest is frozen into the descriptor's
|
|
17
17
|
// provenance pins as `catalogue`. The id is the definition's. Takes --data-root, --editions-root,
|
|
18
|
-
// --sources-root, --
|
|
18
|
+
// --sources-root, --cache-dir (no --deploy-root, no --kernel: the kernel is the catalogue's, from npm).
|
|
19
19
|
// target <id> --pins <file> --registry <root>
|
|
20
20
|
// print <root>/<develop segment>/xema-edition-<id>:<snapshot hash>. The root is always the caller's
|
|
21
21
|
// registry root (e.g. registry.example.com/acme); the develop namespace segment and the repository
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
// check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
32
32
|
// refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
|
|
33
33
|
// check-existing --definition <file> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
34
|
+
// [--catalogue-username <name> --catalogue-token-env <VAR>]
|
|
34
35
|
// the same for a customer artifact; its descriptor's catalogue provenance pin must also equal the
|
|
35
|
-
// pinned develop catalogue digest.
|
|
36
|
+
// pinned develop catalogue digest. It fetches that catalogue (as `resolve --definition` does) to take the
|
|
37
|
+
// kernel from it. Takes --data-root, --cache-dir.
|
|
36
38
|
// check-overrides --overrides <json>
|
|
37
39
|
// refuse a pins override that would select the tooling this run executes
|
|
38
40
|
// (the edition tooling's own repository); the workflow runs it before any other checkout.
|
|
@@ -42,9 +44,6 @@
|
|
|
42
44
|
// freeze-sources --definition <file> --out <file>
|
|
43
45
|
// a CUSTOMER edition: one `sources/<name>` pin per source the definition declares, each the head of
|
|
44
46
|
// refs/heads/develop of that source's repository, resolved once; written exclusively (wx).
|
|
45
|
-
// kernel-commit --definition <file> [--catalogue-username <name> --catalogue-token-env <VAR>]
|
|
46
|
-
// fetch the pinned develop catalogue and print its contractAuthority.sourceCommit (one 40-hex line):
|
|
47
|
-
// the commit the kernel contracts are checked out at. Takes --data-root only (no --kernel).
|
|
48
47
|
//
|
|
49
48
|
// Tokens come from the environment (EVIDENCE_REPOSITORIES_TOKEN, XEMA_GHCR_PULL_TOKEN),
|
|
50
49
|
// never argv. Nothing here writes git or publishes a release edition.
|
|
@@ -54,7 +53,7 @@ import { resolve } from 'node:path';
|
|
|
54
53
|
|
|
55
54
|
import { fetchCatalogue } from './catalogue-fetch.mjs';
|
|
56
55
|
import { readCataloguePin } from './catalogue-pin.mjs';
|
|
57
|
-
import {
|
|
56
|
+
import { writeCatalogue } from './catalogue.mjs';
|
|
58
57
|
import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG, SOURCE_IMAGE_FLAG } from './customer-flags.mjs';
|
|
59
58
|
import { customerDevelopFrame, resolveCustomerDevelopEdition } from './develop-customer-edition.mjs';
|
|
60
59
|
import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
|
|
@@ -65,28 +64,36 @@ import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
|
65
64
|
import { EditionChannel } from './edition-constants.mjs';
|
|
66
65
|
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
|
|
67
66
|
import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
|
|
67
|
+
import { holdSource } from './git-source-auth.mjs';
|
|
68
|
+
import { adoptCatalogueKernel } from './kernel-package.mjs';
|
|
68
69
|
import { loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
69
70
|
import { snapshotPinSource } from './pin-source.mjs';
|
|
70
71
|
import { parseSourceImages } from './resolve-customer-edition.mjs';
|
|
71
72
|
import { composeEdition } from './resolve-distribution.mjs';
|
|
72
73
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
73
74
|
|
|
74
|
-
// The roots a verb takes. A customer form (it names --definition) takes fewer
|
|
75
|
-
//
|
|
76
|
-
//
|
|
75
|
+
// The roots a verb takes. A customer form (it names --definition) takes fewer, and never the kernel (the
|
|
76
|
+
// catalogue's kernel is downloaded from npm into the cache root): `target` none, `check-existing` the data
|
|
77
|
+
// root and the cache, `freeze-sources` the data root only, `resolve` all but the deploy root. `check-overrides`
|
|
78
|
+
// takes only the data root in either form: it judges its --overrides JSON against the pin roles the data
|
|
79
|
+
// root declares, so it reads nothing else and refuses every other root flag.
|
|
77
80
|
const VENDOR_ROOTS = Object.freeze([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache]);
|
|
78
81
|
const CUSTOMER_ROOTS = Object.freeze({
|
|
79
|
-
resolve: [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.
|
|
82
|
+
resolve: [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Cache],
|
|
80
83
|
target: [],
|
|
81
|
-
'check-existing': [WorkspaceRoot.Data, WorkspaceRoot.
|
|
84
|
+
'check-existing': [WorkspaceRoot.Data, WorkspaceRoot.Cache],
|
|
82
85
|
'freeze-sources': [WorkspaceRoot.Data],
|
|
83
|
-
'kernel-commit': [WorkspaceRoot.Data],
|
|
84
86
|
});
|
|
87
|
+
const DATA_ROOT_ONLY_COMMANDS = Object.freeze(['check-overrides']);
|
|
85
88
|
|
|
86
89
|
if (isEntryModule(import.meta.url)) {
|
|
87
90
|
const [command] = process.argv.slice(2);
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
if (DATA_ROOT_ONLY_COMMANDS.includes(command)) {
|
|
92
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data], { refuseOthers: true });
|
|
93
|
+
} else {
|
|
94
|
+
const customerForm = process.argv.includes(DEFINITION_FLAG);
|
|
95
|
+
configureEditionWorkspaceFromProcess(customerForm ? (CUSTOMER_ROOTS[command] ?? VENDOR_ROOTS) : VENDOR_ROOTS, { refuseOthers: customerForm && CUSTOMER_ROOTS[command] !== undefined });
|
|
96
|
+
}
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
// Per verb: the vendor form (`positional` id, `values`, `switches`) and, where the verb has one, the customer
|
|
@@ -128,15 +135,7 @@ const COMMANDS = Object.freeze({
|
|
|
128
135
|
values: ['--pins', '--artifact', '--engine-image'],
|
|
129
136
|
switches: [],
|
|
130
137
|
required: ['--pins', '--artifact', '--engine-image'],
|
|
131
|
-
customer: { values: [DEFINITION_FLAG], required: ['--pins', '--artifact', '--engine-image'], refused: [] },
|
|
132
|
-
},
|
|
133
|
-
'kernel-commit': {
|
|
134
|
-
positional: false,
|
|
135
|
-
values: [],
|
|
136
|
-
switches: [],
|
|
137
|
-
required: [],
|
|
138
|
-
customerOnly: true,
|
|
139
|
-
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS], required: [], refused: [] },
|
|
138
|
+
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS], required: ['--pins', '--artifact', '--engine-image'], refused: [] },
|
|
140
139
|
},
|
|
141
140
|
});
|
|
142
141
|
|
|
@@ -173,7 +172,7 @@ export function parseDevelopEditionArgs(argv) {
|
|
|
173
172
|
positional.push(arg);
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
|
-
const customer = spec.customer !== undefined &&
|
|
175
|
+
const customer = spec.customer !== undefined && customerValues.some((flag) => options[flag] !== undefined);
|
|
177
176
|
if (customer) {
|
|
178
177
|
if (options[DEFINITION_FLAG] === undefined) {throw new Error(`a customer ${command} requires ${DEFINITION_FLAG}`);}
|
|
179
178
|
for (const refused of spec.customer.refused) {
|
|
@@ -205,21 +204,27 @@ function writeOnce(path, value) {
|
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
function headResolver(env) {
|
|
208
|
-
|
|
207
|
+
const workspace = editionWorkspaceConfig();
|
|
208
|
+
return { resolveHead: (source) => lsRemoteDevelopHead(source, { workspace, env }), holdSource: (source) => holdSource(source, workspace, env) };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
ref,
|
|
211
|
+
/** The pinned develop catalogue of a customer definition: fetched by its digest, as every customer develop verb does. */
|
|
212
|
+
function fetchPinnedDevelopCatalogue(options, env) {
|
|
213
|
+
return fetchCatalogue({
|
|
214
|
+
ref: readCataloguePin({ definitionFile: options[DEFINITION_FLAG], channel: EditionChannel.Develop }),
|
|
216
215
|
channel: EditionChannel.Develop,
|
|
217
216
|
username: options[CATALOGUE_USERNAME_FLAG],
|
|
218
217
|
tokenEnv: options[CATALOGUE_TOKEN_ENV_FLAG],
|
|
219
218
|
env,
|
|
220
219
|
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A customer develop resolve: the artifact (descriptor and four locks, no catalogue: it is not an offering) goes to --out. */
|
|
223
|
+
async function resolveCustomer(options, env) {
|
|
224
|
+
const { catalogue, digest } = await fetchPinnedDevelopCatalogue(options, env);
|
|
221
225
|
const rawDefinition = readStrictJsonFile(options[DEFINITION_FLAG]);
|
|
222
|
-
|
|
226
|
+
// The catalogue's kernel is adopted inside the resolve, before any kernel schema is loaded; the evidence trust
|
|
227
|
+
// state loads kernel schemas, so it is read after.
|
|
223
228
|
const { id, pins, ...resolved } = await resolveCustomerDevelopEdition({
|
|
224
229
|
rawDefinition,
|
|
225
230
|
catalogue,
|
|
@@ -228,6 +233,7 @@ async function resolveCustomer(options, env) {
|
|
|
228
233
|
sourceImages: parseSourceImages(options[SOURCE_IMAGE_FLAG] ?? []),
|
|
229
234
|
env,
|
|
230
235
|
});
|
|
236
|
+
const evidenceTrustPolicy = await descriptorEvidenceTrust({ channel: EditionChannel.Develop });
|
|
231
237
|
const target = developEditionTarget(id, pins, { registry: options['--registry'] });
|
|
232
238
|
writeEditionArtifact({
|
|
233
239
|
outDir: resolve(options['--out']),
|
|
@@ -245,21 +251,23 @@ async function resolveCustomer(options, env) {
|
|
|
245
251
|
console.log(target);
|
|
246
252
|
}
|
|
247
253
|
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
/** A customer check-existing: the pinned catalogue is fetched to take its kernel; the digest fetched is the digest the artifact is compared with. */
|
|
255
|
+
export async function checkExistingCustomer(options, env, { adopt = adoptCatalogueKernel } = {}) {
|
|
256
|
+
const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
257
|
+
const { catalogue, digest } = await fetchPinnedDevelopCatalogue(options, env);
|
|
258
|
+
if (digest !== frame.catalogueDigest) {
|
|
259
|
+
throw new Error(`the fetched catalogue is ${digest}, not the pinned ${frame.catalogueDigest}`);
|
|
260
|
+
}
|
|
261
|
+
await adopt(catalogue);
|
|
262
|
+
assertExistingArtifactMatches({
|
|
263
|
+
artifactDir: resolve(options['--artifact']),
|
|
264
|
+
distributionId: frame.id,
|
|
257
265
|
channel: EditionChannel.Develop,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
evidenceTrustPolicy: await descriptorEvidenceTrust({ channel: EditionChannel.Develop }),
|
|
267
|
+
pins: frame.pins,
|
|
268
|
+
engineImage: options['--engine-image'],
|
|
269
|
+
catalogueDigest: frame.catalogueDigest,
|
|
261
270
|
});
|
|
262
|
-
console.log(contractAuthoritySourceCommit(catalogue));
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
async function run(argv, env) {
|
|
@@ -297,19 +305,7 @@ async function run(argv, env) {
|
|
|
297
305
|
return;
|
|
298
306
|
}
|
|
299
307
|
case 'check-existing': {
|
|
300
|
-
if (customer) {
|
|
301
|
-
const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
302
|
-
assertExistingArtifactMatches({
|
|
303
|
-
artifactDir: resolve(options['--artifact']),
|
|
304
|
-
distributionId: frame.id,
|
|
305
|
-
channel: EditionChannel.Develop,
|
|
306
|
-
evidenceTrustPolicy: await descriptorEvidenceTrust({ channel: EditionChannel.Develop }),
|
|
307
|
-
pins: frame.pins,
|
|
308
|
-
engineImage: options['--engine-image'],
|
|
309
|
-
catalogueDigest: frame.catalogueDigest,
|
|
310
|
-
});
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
308
|
+
if (customer) {return checkExistingCustomer(options, env);}
|
|
313
309
|
assertExistingArtifactMatches({
|
|
314
310
|
artifactDir: resolve(options['--artifact']),
|
|
315
311
|
distributionId: id,
|
|
@@ -332,7 +328,7 @@ async function run(argv, env) {
|
|
|
332
328
|
deploySha: options['--deploy-sha'],
|
|
333
329
|
editionsRootUrl: options['--editions-root-url'],
|
|
334
330
|
overrides: parseOverrides(options['--overrides']),
|
|
335
|
-
|
|
331
|
+
...headResolver(env),
|
|
336
332
|
});
|
|
337
333
|
writeOnce(options['--out'], pins);
|
|
338
334
|
return;
|
|
@@ -343,7 +339,7 @@ async function run(argv, env) {
|
|
|
343
339
|
options['--out'],
|
|
344
340
|
freezeCustomerSources({
|
|
345
341
|
definition: readStrictJsonFile(options[DEFINITION_FLAG]),
|
|
346
|
-
|
|
342
|
+
...headResolver(env),
|
|
347
343
|
sourceRepositoryUrlPrefix: editionWorkspaceConfig().sourceRepositoryUrlPrefix,
|
|
348
344
|
}),
|
|
349
345
|
);
|
|
@@ -354,12 +350,10 @@ async function run(argv, env) {
|
|
|
354
350
|
const declaredSources = composeEdition(parseDistribution, id, snapshotPinSource(pins)).declaredSources;
|
|
355
351
|
writeOnce(
|
|
356
352
|
options['--out'],
|
|
357
|
-
freezeSources({ pins, declaredSources, overrides: parseOverrides(options['--overrides']),
|
|
353
|
+
freezeSources({ pins, declaredSources, overrides: parseOverrides(options['--overrides']), ...headResolver(env) }),
|
|
358
354
|
);
|
|
359
355
|
return;
|
|
360
356
|
}
|
|
361
|
-
case 'kernel-commit':
|
|
362
|
-
return customerKernelCommit(options, env);
|
|
363
357
|
default:
|
|
364
358
|
throw new Error(`unhandled command ${command}`);
|
|
365
359
|
}
|
|
@@ -112,6 +112,7 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
|
|
|
112
112
|
* @param {string} request.kernelSha the commit the kernel checkout is at
|
|
113
113
|
* @param {string} request.deploySha the commit the deploy checkout is at
|
|
114
114
|
* @param {string} [request.editionsRootUrl] https URL of the editions root; absent = this repository
|
|
115
|
+
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source; runs for ALL graph sources before the first head
|
|
115
116
|
*/
|
|
116
117
|
export function freezeGraph({
|
|
117
118
|
runtimeSourceIndex,
|
|
@@ -121,6 +122,7 @@ export function freezeGraph({
|
|
|
121
122
|
editionsRootUrl,
|
|
122
123
|
overrides = {},
|
|
123
124
|
resolveHead,
|
|
125
|
+
holdSource,
|
|
124
126
|
}) {
|
|
125
127
|
assertNoToolingOverride(overrides);
|
|
126
128
|
assertOverrides(overrides, GRAPH_PHASE);
|
|
@@ -137,6 +139,12 @@ export function freezeGraph({
|
|
|
137
139
|
{ key: deployPinKey(), sha: deploySha, flag: '--deploy-sha', what: 'the deploy checkout' },
|
|
138
140
|
];
|
|
139
141
|
for (const { key, sha, flag, what } of checkedOut) {assertCheckedOutPin({ key, sha, flag, what, overrides });}
|
|
142
|
+
const toResolve = runtimeSourceIndex.repositoryPins.map((pin) => ({ name: pin.repo, url: pin.repositoryUrl }));
|
|
143
|
+
if (editionsRootUrl !== undefined) {
|
|
144
|
+
toResolve.push({ name: EDITIONS_ROOT_PIN_KEY, url: canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url') });
|
|
145
|
+
}
|
|
146
|
+
// Every source is held before the first head is asked: a refusal of the last leaves no git call behind.
|
|
147
|
+
for (const source of toResolve) {holdSource(source);}
|
|
140
148
|
if (editionsRootUrl === undefined && overrides[EDITIONS_ROOT_PIN_KEY] !== undefined) {
|
|
141
149
|
throw new Error(`override names "${EDITIONS_ROOT_PIN_KEY}", but the editions root is this repository`);
|
|
142
150
|
}
|
|
@@ -155,8 +163,9 @@ export function freezeGraph({
|
|
|
155
163
|
* @param {object} request
|
|
156
164
|
* @param {Record<string,string>} request.pins the graph phase's pins
|
|
157
165
|
* @param {object[]} request.declaredSources every `sources[]` entry in the chain (`{ name, url, ... }`)
|
|
166
|
+
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source (`holdSource` of git-source-auth with the workspace and env); runs for ALL sources before the first head
|
|
158
167
|
*/
|
|
159
|
-
export function freezeSources({ pins, declaredSources, overrides = {}, resolveHead }) {
|
|
168
|
+
export function freezeSources({ pins, declaredSources, overrides = {}, resolveHead, holdSource }) {
|
|
160
169
|
assertOverrides(overrides, SOURCE_PHASE);
|
|
161
170
|
const head = memoizedHeads(resolveHead);
|
|
162
171
|
const sourceByName = new Map();
|
|
@@ -172,6 +181,8 @@ export function freezeSources({ pins, declaredSources, overrides = {}, resolveHe
|
|
|
172
181
|
throw new Error(`override names "${key}", but the chain declares no such source`);
|
|
173
182
|
}
|
|
174
183
|
}
|
|
184
|
+
// Every source is held before the first head is asked: a refusal of the last leaves no git call behind.
|
|
185
|
+
for (const source of sourceByName.values()) {holdSource(source);}
|
|
175
186
|
const frozen = { ...pins };
|
|
176
187
|
for (const [name, source] of sourceByName) {
|
|
177
188
|
frozen[sourcePinKey(name)] = overrides[sourcePinKey(name)] ?? head(source);
|
|
@@ -207,11 +218,11 @@ export function assertCustomerSourceUrls(sources, sourceRepositoryUrlPrefix) {
|
|
|
207
218
|
* each the head of refs/heads/develop of that source's repository, resolved once. Every URL is held by
|
|
208
219
|
* `assertCustomerSourceUrls` before the first head is asked.
|
|
209
220
|
*/
|
|
210
|
-
export function freezeCustomerSources({ definition, resolveHead, sourceRepositoryUrlPrefix }) {
|
|
221
|
+
export function freezeCustomerSources({ definition, resolveHead, holdSource, sourceRepositoryUrlPrefix }) {
|
|
211
222
|
const sources = definition?.sources ?? [];
|
|
212
223
|
if (!Array.isArray(sources) || sources.some((source) => typeof source?.name !== 'string' || typeof source?.url !== 'string')) {
|
|
213
224
|
throw new Error('the definition\'s sources must be an array of {name, url}');
|
|
214
225
|
}
|
|
215
226
|
assertCustomerSourceUrls(sources, sourceRepositoryUrlPrefix);
|
|
216
|
-
return freezeSources({ pins: {}, declaredSources: sources, resolveHead });
|
|
227
|
+
return freezeSources({ pins: {}, declaredSources: sources, resolveHead, holdSource });
|
|
217
228
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { compareStrings } from './string-order.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The runtime components an edition must carry because a biome it rosters
|
|
5
|
+
* DISPATCHES them: the kernel's `runtime-image` edges, projected per biome into
|
|
6
|
+
* the index as `dispatchesRuntimeComponents` and followed here against the
|
|
7
|
+
* index entry of each target biome.
|
|
8
|
+
*
|
|
9
|
+
* "Needs that image" and nothing else. The target biome is NOT rostered — it is
|
|
10
|
+
* not installed, booted or required — only its runtime component's image
|
|
11
|
+
* enters the edition, as a runtime-component row of the image lock.
|
|
12
|
+
*
|
|
13
|
+
* The closure is one step by construction: the kernel admits
|
|
14
|
+
* `dispatchesRuntimeComponents` on a `service` component only, and a target is
|
|
15
|
+
* a runtime component, which is never a standing service — so a dispatched
|
|
16
|
+
* image dispatches nothing further, and an unrostered target biome's own
|
|
17
|
+
* services (and their edges) are not in the edition at all.
|
|
18
|
+
*
|
|
19
|
+
* Targets are resolved against FIRST-PARTY server entries only: the image plane
|
|
20
|
+
* reads a target's coordinates from the first-party biome index, which carries
|
|
21
|
+
* no biome drawn from a declared source. A biome drawn from a declared source
|
|
22
|
+
* that dispatches is refused for the same reason.
|
|
23
|
+
*
|
|
24
|
+
* Returns one row per distinct (target biome, component key), sorted, with the
|
|
25
|
+
* rostered biomes that dispatch it. Every unresolvable edge is refused, naming
|
|
26
|
+
* the consumer and the target.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} context the edition the refusal names, e.g. `distribution "core"`
|
|
29
|
+
* @param {object[]} consumers the rostered index entries
|
|
30
|
+
* @param {object[]} index the index the targets are resolved against
|
|
31
|
+
*/
|
|
32
|
+
export function resolveDispatchedRuntimeComponents(context, consumers, index) {
|
|
33
|
+
const rows = new Map();
|
|
34
|
+
for (const consumer of [...consumers].sort((a, b) => compareStrings(a.id, b.id))) {
|
|
35
|
+
const edges = consumer.dispatchesRuntimeComponents ?? [];
|
|
36
|
+
if (edges.length === 0) {continue;}
|
|
37
|
+
if (consumer.source !== undefined) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${context}: biome "${consumer.id}", drawn from declared source "${consumer.source}", dispatches ` +
|
|
40
|
+
`runtime components (${edges.map((edge) => `${edge.biomeId}/${edge.componentKey}`).join(', ')}). ` +
|
|
41
|
+
'A dispatched image is resolved from the first-party biome index, which carries no biome from a ' +
|
|
42
|
+
'declared source — refusing rather than shipping the dispatcher without the images it launches.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
for (const edge of edges) {
|
|
46
|
+
const edgeText = `biome "${consumer.id}" dispatches runtime component "${edge.componentKey}" of biome "${edge.biomeId}"`;
|
|
47
|
+
const candidates = index.filter(
|
|
48
|
+
(entry) => entry.id === edge.biomeId && entry.target === 'server' && entry.source === undefined,
|
|
49
|
+
);
|
|
50
|
+
if (candidates.length === 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`${context}: ${edgeText}, but the biome index has no first-party server biome "${edge.biomeId}" — ` +
|
|
53
|
+
'refusing rather than shipping the dispatcher without the image it launches.',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (candidates.length > 1) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${context}: ${edgeText}, but the biome index has ${candidates.length} first-party server biomes ` +
|
|
59
|
+
`"${edge.biomeId}" (${candidates.map((entry) => entry.repo).join(', ')}), so the target is ambiguous.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const [target] = candidates;
|
|
63
|
+
const image = (target.runtimeComponentKeys ?? {})[edge.componentKey];
|
|
64
|
+
if (image === undefined) {
|
|
65
|
+
const keys = Object.keys(target.runtimeComponentKeys ?? {});
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${context}: ${edgeText}, but "${edge.biomeId}" declares no runtime component keyed ` +
|
|
68
|
+
`"${edge.componentKey}" (its runtime components: ${keys.length > 0 ? keys.join(', ') : 'none'}). ` +
|
|
69
|
+
'Either no component has that key, or it is a standing service or worker — a dispatched ' +
|
|
70
|
+
'component must be one its own biome never schedules.',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const key = `${target.id}\0${edge.componentKey}`;
|
|
74
|
+
const row = rows.get(key) ?? { target, componentKey: edge.componentKey, image, consumers: [] };
|
|
75
|
+
if (!row.consumers.includes(consumer.id)) {row.consumers.push(consumer.id);}
|
|
76
|
+
rows.set(key, row);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [...rows.entries()]
|
|
80
|
+
.sort(([a], [b]) => compareStrings(a, b))
|
|
81
|
+
.map(([, row]) => row);
|
|
82
|
+
}
|
package/edition/dist-check.mjs
CHANGED
|
@@ -194,6 +194,7 @@ async function checkAuthorizationLock(id, kernel, index) {
|
|
|
194
194
|
distributionId: id,
|
|
195
195
|
editionDir,
|
|
196
196
|
lockBiomes: lock.biomes,
|
|
197
|
+
biomeOrigins: (await loadKernelDistributionSchemas()).biomeOriginsOfLock(lock),
|
|
197
198
|
index,
|
|
198
199
|
kernel,
|
|
199
200
|
serialize: serializeJson,
|
|
@@ -464,6 +464,7 @@ export async function resolveProducerEvidence({
|
|
|
464
464
|
schemas,
|
|
465
465
|
expectedContractAuthority,
|
|
466
466
|
registryClientFactory,
|
|
467
|
+
channel,
|
|
467
468
|
}) {
|
|
468
469
|
assertCanonicalDistributionId(distributionId);
|
|
469
470
|
assertAbsolutePath(dataRoot, 'data root');
|
|
@@ -472,7 +473,7 @@ export async function resolveProducerEvidence({
|
|
|
472
473
|
throw new Error('the expected kernel contract authority is required');
|
|
473
474
|
}
|
|
474
475
|
const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex } =
|
|
475
|
-
await loadValidatedLocks(distributionId, dataRoot, schemas, { editionsRoot });
|
|
476
|
+
await loadValidatedLocks(distributionId, dataRoot, schemas, { editionsRoot, channel });
|
|
476
477
|
assertKernelContractAuthorityEqual(
|
|
477
478
|
runtimeSourceIndex.contractAuthority,
|
|
478
479
|
expectedContractAuthority,
|
|
@@ -581,6 +582,22 @@ export async function resolveProducerEvidence({
|
|
|
581
582
|
// Statement shapes
|
|
582
583
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
583
584
|
|
|
585
|
+
/**
|
|
586
|
+
* The evidence channel a signer signs for: the one whose kernel-assigned ref (`EVIDENCE_CHANNEL_REFS`) is the
|
|
587
|
+
* signer's workflow ref. The kernel's policy schema ties each policy row to exactly that ref, so the signer's ref
|
|
588
|
+
* IS the channel the verifying row will state. A ref no channel owns is refused.
|
|
589
|
+
*/
|
|
590
|
+
export function evidenceChannelOfSigner(signer, schemas) {
|
|
591
|
+
const ref = signer?.githubWorkflow?.ref;
|
|
592
|
+
const channel = schemas.EVIDENCE_CHANNELS.find((candidate) => schemas.EVIDENCE_CHANNEL_REFS[candidate] === ref);
|
|
593
|
+
if (channel === undefined) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`the signer workflow ref "${String(ref)}" is the ref of no evidence channel (${schemas.EVIDENCE_CHANNELS.map((candidate) => `${candidate}: ${schemas.EVIDENCE_CHANNEL_REFS[candidate]}`).join(', ')})`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return channel;
|
|
599
|
+
}
|
|
600
|
+
|
|
584
601
|
/**
|
|
585
602
|
* The distribution-completeness statement: "these EXACT images, produced by
|
|
586
603
|
* these EXACT bundles, are the complete image set of this edition at this
|
|
@@ -590,6 +607,7 @@ export function buildDistributionImagesStatement({
|
|
|
590
607
|
distributionLock,
|
|
591
608
|
imageLock,
|
|
592
609
|
images,
|
|
610
|
+
channel,
|
|
593
611
|
releaseSequence,
|
|
594
612
|
releaseSourceCommit,
|
|
595
613
|
runId,
|
|
@@ -599,7 +617,7 @@ export function buildDistributionImagesStatement({
|
|
|
599
617
|
maxTtlSeconds,
|
|
600
618
|
schemas,
|
|
601
619
|
}) {
|
|
602
|
-
if (!schemas?.parseDistributionImagesStatement) {
|
|
620
|
+
if (!schemas?.parseDistributionImagesStatement || typeof schemas.distributionLockDigest !== 'function') {
|
|
603
621
|
throw new Error('trusted kernel distribution schemas are required');
|
|
604
622
|
}
|
|
605
623
|
if (
|
|
@@ -664,7 +682,9 @@ export function buildDistributionImagesStatement({
|
|
|
664
682
|
],
|
|
665
683
|
predicateType: schemas.DISTRIBUTION_IMAGES_PREDICATE_TYPE,
|
|
666
684
|
predicate: {
|
|
667
|
-
schemaVersion:
|
|
685
|
+
schemaVersion: 2,
|
|
686
|
+
channel,
|
|
687
|
+
lockDigest: schemas.distributionLockDigest(distributionLock),
|
|
668
688
|
distributionId: imageLock.distributionId,
|
|
669
689
|
distributionInputHash: distributionLock.inputHash,
|
|
670
690
|
normalizedImageLockDigest: normalizedLockDigest,
|
|
@@ -706,6 +726,7 @@ export function buildDistributionImagesStatement({
|
|
|
706
726
|
* decision that already has an owner.
|
|
707
727
|
*/
|
|
708
728
|
export function buildImageDeliveryEntitlementStatement({
|
|
729
|
+
distributionLock,
|
|
709
730
|
distributionId,
|
|
710
731
|
completenessStatementDigest,
|
|
711
732
|
customerId,
|
|
@@ -719,10 +740,13 @@ export function buildImageDeliveryEntitlementStatement({
|
|
|
719
740
|
maxTtlSeconds,
|
|
720
741
|
schemas,
|
|
721
742
|
}) {
|
|
722
|
-
if (!schemas?.parseImageDeliveryEntitlementStatement) {
|
|
743
|
+
if (!schemas?.parseImageDeliveryEntitlementStatement || typeof schemas.distributionLockDigest !== 'function') {
|
|
723
744
|
throw new Error('trusted kernel distribution schemas are required');
|
|
724
745
|
}
|
|
725
746
|
assertCanonicalDistributionId(distributionId);
|
|
747
|
+
if (distributionLock?.distributionId !== distributionId) {
|
|
748
|
+
throw new Error(`delivery entitlement for ${String(customerId)} requires the distribution lock of ${distributionId}`);
|
|
749
|
+
}
|
|
726
750
|
const label = `delivery entitlement for ${String(customerId)}`;
|
|
727
751
|
if (!SHA256_DIGEST_PATTERN.test(completenessStatementDigest ?? '')) {
|
|
728
752
|
throw new Error(
|
|
@@ -751,9 +775,12 @@ export function buildImageDeliveryEntitlementStatement({
|
|
|
751
775
|
],
|
|
752
776
|
predicateType: schemas.IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE,
|
|
753
777
|
predicate: {
|
|
754
|
-
schemaVersion:
|
|
778
|
+
schemaVersion: 2,
|
|
755
779
|
customerId,
|
|
756
780
|
distributionId,
|
|
781
|
+
lockDigest: schemas.distributionLockDigest(distributionLock),
|
|
782
|
+
// Present-and-nullable: null states the lock carries no catalogue; the lock digest binds the lock either way.
|
|
783
|
+
catalogueDigest: distributionLock.catalogueDigest ?? null,
|
|
757
784
|
distributionStatementDigest: completenessStatementDigest,
|
|
758
785
|
mode,
|
|
759
786
|
targetRegistry,
|
|
@@ -2,5 +2,19 @@
|
|
|
2
2
|
// validator (which a channel selects) and edition-constants (which imports the
|
|
3
3
|
// chart-set file name) do not import each other.
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Where an edition was produced. Recorded in the descriptor's provenance. verify-evidence selects its policy row by the
|
|
7
|
+
* descriptor's channel (a predicate channel mismatch is still refused after the signature check); lock readers require one.
|
|
8
|
+
*/
|
|
6
9
|
export const EditionChannel = Object.freeze({ Develop: 'develop', Release: 'release' });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The channel of `--channel <channel>`, which must be all of `rest`; anything
|
|
13
|
+
* else is a usage error. Shared by every verb that operates on one channel.
|
|
14
|
+
*/
|
|
15
|
+
export function parseChannelArgs(rest) {
|
|
16
|
+
if (rest.length !== 2 || rest[0] !== '--channel' || !Object.values(EditionChannel).includes(rest[1])) {
|
|
17
|
+
throw new Error(`--channel <${Object.values(EditionChannel).join('|')}> is required and nothing else is accepted (got ${rest.length === 0 ? 'nothing' : JSON.stringify(rest.join(' '))})`);
|
|
18
|
+
}
|
|
19
|
+
return { channel: rest[1] };
|
|
20
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `xema edition locks <id>`: one edition's validated locks, as JSON.
|
|
3
3
|
//
|
|
4
|
-
// usage: xema edition locks <id> --data-root <dir> --editions-root <dir> --kernel <dir>
|
|
4
|
+
// usage: xema edition locks <id> --channel <release|develop> --data-root <dir> --editions-root <dir> --kernel <dir>
|
|
5
5
|
//
|
|
6
6
|
// The locks are read and validated exactly as the evidence verifier reads
|
|
7
7
|
// them: `images.lock.json` and `distribution.lock.json` under the kernel
|
|
8
8
|
// schema, immutable coordinates only, and the image lock's kernel contract
|
|
9
|
-
// authority equal to the data root's runtime image source index.
|
|
9
|
+
// authority equal to the data root's runtime image source index. The
|
|
10
|
+
// distribution lock is read for `--channel`: an unsealed lock on develop only,
|
|
11
|
+
// a sealed one on every other channel. A consumer
|
|
10
12
|
// reads this output instead of parsing or validating a lock itself.
|
|
11
13
|
|
|
12
14
|
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
15
|
+
import { EditionChannel, parseChannelArgs } from './edition-channel.mjs';
|
|
13
16
|
import {
|
|
14
17
|
WorkspaceRoot,
|
|
15
18
|
configureEditionWorkspaceFromArgv,
|
|
@@ -21,12 +24,12 @@ import { loadValidatedLocks } from './image-evidence.mjs';
|
|
|
21
24
|
import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
22
25
|
|
|
23
26
|
const USAGE =
|
|
24
|
-
|
|
27
|
+
`usage: xema edition locks <id> --channel <${Object.values(EditionChannel).join('|')}> --data-root <dir> --editions-root <dir> --kernel <dir>`;
|
|
25
28
|
|
|
26
|
-
export async function readEditionLocks({ distributionId, dataRoot: data, editionsRoot: editions, schemas }) {
|
|
29
|
+
export async function readEditionLocks({ distributionId, channel, dataRoot: data, editionsRoot: editions, schemas }) {
|
|
27
30
|
assertCanonicalDistributionId(distributionId);
|
|
28
31
|
const { distributionLock, imageLock, repositoryPins, runtimeSourceIndex } =
|
|
29
|
-
await loadValidatedLocks(distributionId, data, schemas, { editionsRoot: editions });
|
|
32
|
+
await loadValidatedLocks(distributionId, data, schemas, { editionsRoot: editions, channel });
|
|
30
33
|
return {
|
|
31
34
|
schemaVersion: 1,
|
|
32
35
|
distributionId,
|
|
@@ -45,14 +48,21 @@ async function main() {
|
|
|
45
48
|
WorkspaceRoot.Editions,
|
|
46
49
|
WorkspaceRoot.Kernel,
|
|
47
50
|
]);
|
|
48
|
-
if (rest.length
|
|
51
|
+
if (rest.length === 0 || rest[0].startsWith('--')) {
|
|
49
52
|
throw new Error(USAGE);
|
|
50
53
|
}
|
|
54
|
+
let channel;
|
|
55
|
+
try {
|
|
56
|
+
({ channel } = parseChannelArgs(rest.slice(1)));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`${error.message}\n${USAGE}`);
|
|
59
|
+
}
|
|
51
60
|
const schemas = await loadKernelDistributionSchemas();
|
|
52
61
|
process.stdout.write(
|
|
53
62
|
serializeJson(
|
|
54
63
|
await readEditionLocks({
|
|
55
64
|
distributionId: rest[0],
|
|
65
|
+
channel,
|
|
56
66
|
dataRoot: dataRoot(),
|
|
57
67
|
editionsRoot: editionsRoot(),
|
|
58
68
|
schemas,
|