@xemahq/xema 0.42.0 → 0.44.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/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +1 -0
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +75 -6
- package/dist/edition/verify-image-evidence.bundle.provenance.json +71 -47
- package/dist/runtime/edition/verb-map.d.ts +1 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +2 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/edition/README.md +42 -0
- package/edition/authored-source-check.mjs +135 -0
- package/edition/build-platform-service-index.mjs +1 -1
- package/edition/build-runtime-image-source-index.mjs +25 -1
- package/edition/canonical-repository-url.mjs +35 -0
- package/edition/checkout-indexed-source-repositories.mjs +2 -1
- package/edition/develop-edition-invariants.mjs +141 -0
- package/edition/develop-edition-resolve.mjs +7 -1
- package/edition/develop-edition.mjs +3 -4
- package/edition/develop-freeze.mjs +8 -7
- package/edition/develop-graph.mjs +26 -0
- package/edition/develop-snapshot-inputs.mjs +0 -1
- package/edition/dist-check.mjs +108 -63
- package/edition/edition-config.mjs +27 -1
- package/edition/first-party-source-ids.mjs +15 -12
- package/edition/git-source-auth.mjs +1 -1
- package/edition/lib.mjs +0 -32
- package/edition/pin-source.mjs +21 -14
- package/edition/workspace-sections.mjs +35 -5
- package/package.json +2 -2
package/edition/dist-check.mjs
CHANGED
|
@@ -370,7 +370,7 @@ export async function checkImagesLock(id, checkSourceEvidence) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
async function checkCommittedRuntimeImageSourceIndex(checkSources) {
|
|
373
|
+
export async function checkCommittedRuntimeImageSourceIndex(checkSources) {
|
|
374
374
|
try {
|
|
375
375
|
const schemas = await loadKernelDistributionSchemas();
|
|
376
376
|
schemas.parseRuntimeImageSourceIndex(
|
|
@@ -394,7 +394,7 @@ async function checkCommittedRuntimeImageSourceIndex(checkSources) {
|
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
function checkCommittedIndex() {
|
|
397
|
+
export function checkCommittedIndex() {
|
|
398
398
|
try {
|
|
399
399
|
const index = validateImmutableBiomeIndex(
|
|
400
400
|
JSON.parse(readFileSync(biomeIndexPath(), 'utf8')),
|
|
@@ -464,7 +464,7 @@ export function checkCommittedDataRootReaders() {
|
|
|
464
464
|
return green;
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
async function checkCommittedPlatformServiceIndex() {
|
|
467
|
+
export async function checkCommittedPlatformServiceIndex() {
|
|
468
468
|
try {
|
|
469
469
|
const schemas = await loadKernelDistributionSchemas();
|
|
470
470
|
const index = schemas.parsePlatformServiceSourceIndex(
|
|
@@ -493,7 +493,7 @@ async function checkCommittedPlatformServiceIndex() {
|
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
function discoverDistributionIds() {
|
|
496
|
+
export function discoverDistributionIds() {
|
|
497
497
|
if (!existsSync(editionsRoot())) {
|
|
498
498
|
console.error(`[dist:check] ${editionsRoot()} does not exist.`);
|
|
499
499
|
process.exit(1);
|
|
@@ -514,37 +514,16 @@ function discoverDistributionIds() {
|
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* THE INVARIANT THAT MATTERS: **every edition carries the IDENTICAL table.**
|
|
517
|
+
* The lock-free half of the ownership guard: the kernel's reader, exercised
|
|
518
|
+
* against synthetic tables. (a) two biomes claiming one domain is refused,
|
|
519
|
+
* naming it; (b) a listed domain resolves to its owner and an unlisted one is
|
|
520
|
+
* not first-party; (c) an invalid domain is refused. Reads no lock, so the
|
|
521
|
+
* authored check runs it too.
|
|
524
522
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* edition (`appliance-lean`) leave `kb`/`agent`/… unowned — so an
|
|
528
|
-
* org admin could install the owning biome from the store and THEN bind
|
|
529
|
-
* `kb:page.read@1` to a URL they control, silently shadowing the platform
|
|
530
|
-
* data plane. The table is now built from the FULL biome index and reserves
|
|
531
|
-
* the domain even when its owning biome is absent from that edition — which
|
|
532
|
-
* is exactly the case the reservation exists to cover.
|
|
533
|
-
*
|
|
534
|
-
* Also re-asserts the two structural rules, through the KERNEL's own reader:
|
|
535
|
-
* (a) two biomes claiming the SAME domain is refused (ambiguous ownership has
|
|
536
|
-
* no safe resolution — fail the build, never pick a winner);
|
|
537
|
-
* (b) a domain in NO table entry is not first-party (so a claim that never
|
|
538
|
-
* made it into a resolved, committed, drift-checked lock carries no
|
|
539
|
-
* weight), and an invalid domain is refused rather than silently carried.
|
|
540
|
-
*
|
|
541
|
-
* @param {string[]} ids every distribution id discovered in this repo.
|
|
542
|
-
* @returns {Promise<boolean>} true when green.
|
|
523
|
+
* @param {(lock: object) => Map<string, string>} resolveFirstPartyCapabilityDomains the kernel's reader
|
|
524
|
+
* @returns {string[]} the failures, empty when green
|
|
543
525
|
*/
|
|
544
|
-
|
|
545
|
-
const { resolveFirstPartyCapabilityDomains } =
|
|
546
|
-
await loadKernelDistributionSchemas();
|
|
547
|
-
|
|
526
|
+
export function ownershipSelfTestFailures(resolveFirstPartyCapabilityDomains) {
|
|
548
527
|
const lockOf = (capabilityDomainOwners) => ({
|
|
549
528
|
schemaVersion: 1,
|
|
550
529
|
distributionId: 'dist-check-selftest',
|
|
@@ -599,6 +578,43 @@ async function checkOwnershipInvariant(ids) {
|
|
|
599
578
|
/* expected */
|
|
600
579
|
}
|
|
601
580
|
|
|
581
|
+
return failures;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Guard for the capability-domain OWNERSHIP (reservation) table.
|
|
586
|
+
*
|
|
587
|
+
* This repo has no test runner (no package.json, no CI workflow of its own) —
|
|
588
|
+
* `dist:check` IS its gate — so the invariant is asserted here, where it
|
|
589
|
+
* actually runs, rather than in a spec file nothing would execute.
|
|
590
|
+
*
|
|
591
|
+
* THE INVARIANT THAT MATTERS: **every edition carries the IDENTICAL table.**
|
|
592
|
+
*
|
|
593
|
+
* Ownership is a property of the platform, not of what an edition preloads.
|
|
594
|
+
* An earlier design put the claim on each `LockedBiome`, which made a trimmed
|
|
595
|
+
* edition (`appliance-lean`) leave `kb`/`agent`/… unowned — so an
|
|
596
|
+
* org admin could install the owning biome from the store and THEN bind
|
|
597
|
+
* `kb:page.read@1` to a URL they control, silently shadowing the platform
|
|
598
|
+
* data plane. The table is now built from the FULL biome index and reserves
|
|
599
|
+
* the domain even when its owning biome is absent from that edition — which
|
|
600
|
+
* is exactly the case the reservation exists to cover.
|
|
601
|
+
*
|
|
602
|
+
* Also re-asserts the two structural rules, through the KERNEL's own reader:
|
|
603
|
+
* (a) two biomes claiming the SAME domain is refused (ambiguous ownership has
|
|
604
|
+
* no safe resolution — fail the build, never pick a winner);
|
|
605
|
+
* (b) a domain in NO table entry is not first-party (so a claim that never
|
|
606
|
+
* made it into a resolved, committed, drift-checked lock carries no
|
|
607
|
+
* weight), and an invalid domain is refused rather than silently carried.
|
|
608
|
+
*
|
|
609
|
+
* @param {string[]} ids every distribution id discovered in this repo.
|
|
610
|
+
* @returns {Promise<boolean>} true when green.
|
|
611
|
+
*/
|
|
612
|
+
async function checkOwnershipInvariant(ids) {
|
|
613
|
+
const { resolveFirstPartyCapabilityDomains } =
|
|
614
|
+
await loadKernelDistributionSchemas();
|
|
615
|
+
|
|
616
|
+
const failures = ownershipSelfTestFailures(resolveFirstPartyCapabilityDomains);
|
|
617
|
+
|
|
602
618
|
// (d) THE headline invariant — every committed edition carries the same,
|
|
603
619
|
// non-empty table. An edition-specific table is the fail-open bug.
|
|
604
620
|
let reference;
|
|
@@ -919,13 +935,16 @@ export async function checkBiomeContentHashes(id) {
|
|
|
919
935
|
}
|
|
920
936
|
}
|
|
921
937
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
938
|
+
/**
|
|
939
|
+
* The data-root plane both checks grade before any edition: the kernel speaks
|
|
940
|
+
* a supported edition protocol (a refusal throws), the committed biome,
|
|
941
|
+
* platform-service and runtime-image-source indexes validate offline, the
|
|
942
|
+
* evidence trust policy and the data-root readers are sound. `checkSources`
|
|
943
|
+
* adds the trusted live-source audit of the indexes (release path only).
|
|
944
|
+
* Returns true when green.
|
|
945
|
+
*/
|
|
946
|
+
export async function checkDataRootPlane({ checkSources }) {
|
|
947
|
+
let green = true;
|
|
929
948
|
// 0. The pinned kernel speaks an edition protocol this CLI supports. This is
|
|
930
949
|
// the ONE comparison between the CLI and the kernel; a refusal throws
|
|
931
950
|
// EditionProtocolMismatchError and stops the check before anything is read.
|
|
@@ -938,13 +957,13 @@ async function main() {
|
|
|
938
957
|
// A live sibling-source scan is an explicit trusted regeneration audit only;
|
|
939
958
|
// CI/shared working trees must not make unrelated uncommitted sources into
|
|
940
959
|
// packaging inputs.
|
|
941
|
-
if (!checkCommittedIndex()) {
|
|
942
|
-
if (!(await checkCommittedPlatformServiceIndex())) {
|
|
960
|
+
if (!checkCommittedIndex()) {green = false;}
|
|
961
|
+
if (!(await checkCommittedPlatformServiceIndex())) {green = false;}
|
|
943
962
|
if (!(await checkCommittedRuntimeImageSourceIndex(checkSources))) {
|
|
944
|
-
|
|
963
|
+
green = false;
|
|
945
964
|
}
|
|
946
|
-
if (!(await checkEvidenceTrustPolicy())) {
|
|
947
|
-
if (!checkCommittedDataRootReaders()) {
|
|
965
|
+
if (!(await checkEvidenceTrustPolicy())) {green = false;}
|
|
966
|
+
if (!checkCommittedDataRootReaders()) {green = false;}
|
|
948
967
|
if (checkSources) {
|
|
949
968
|
try {
|
|
950
969
|
execFileSync(
|
|
@@ -958,9 +977,48 @@ async function main() {
|
|
|
958
977
|
{ stdio: 'inherit' },
|
|
959
978
|
);
|
|
960
979
|
} catch {
|
|
961
|
-
|
|
980
|
+
green = false;
|
|
962
981
|
}
|
|
963
982
|
}
|
|
983
|
+
return green;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* The pinned kernel's authorization composer (`kernel` null when the pin
|
|
988
|
+
* predates it). `green` is false when the composer could not be loaded.
|
|
989
|
+
*/
|
|
990
|
+
export async function loadPlaneAuthorizationKernel() {
|
|
991
|
+
try {
|
|
992
|
+
return { green: true, kernel: await loadKernelAuthorizationComposer({ kernelPackageDir: kernelPackageDir() }) };
|
|
993
|
+
} catch (err) {
|
|
994
|
+
console.error(`[dist:check] ${err.message}`);
|
|
995
|
+
return { green: false, kernel: undefined };
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* The fleet chart-set pin every edition must carry (edition-chart-set.mjs), as
|
|
1001
|
+
* committed text, or null when it is absent or not a rostered chart-set lock.
|
|
1002
|
+
*/
|
|
1003
|
+
export function readFleetChartSetPin() {
|
|
1004
|
+
try {
|
|
1005
|
+
const text = readFileSync(fleetChartSetLockPath(), 'utf8');
|
|
1006
|
+
assertRosteredChartSetLock(JSON.parse(text), EDITION_CHART_SET_FILE);
|
|
1007
|
+
return text;
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
console.error(`[dist:check] chart-set pin: ${err.message}. Run \`${commandFor('resolve-chart-set.mjs')}\`.`);
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function main() {
|
|
1015
|
+
let failed = false;
|
|
1016
|
+
// The tests spawned below resolve the editions root from THEIR OWN argv and
|
|
1017
|
+
// env; a root stated here by --editions-root must reach them, or they
|
|
1018
|
+
// would grade the vendor's editions under a customer's dist-check.
|
|
1019
|
+
const checkSources = process.argv.includes('--check-sources');
|
|
1020
|
+
|
|
1021
|
+
if (!(await checkDataRootPlane({ checkSources }))) {failed = true;}
|
|
964
1022
|
|
|
965
1023
|
const distributionIds = discoverDistributionIds();
|
|
966
1024
|
|
|
@@ -972,25 +1030,12 @@ async function main() {
|
|
|
972
1030
|
if (!(await checkOwnershipInvariant(distributionIds))) {failed = true;}
|
|
973
1031
|
|
|
974
1032
|
// The pinned kernel's authorization composer (null when the pin predates it).
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
authorizationKernel = await loadKernelAuthorizationComposer({
|
|
978
|
-
kernelPackageDir: kernelPackageDir(),
|
|
979
|
-
});
|
|
980
|
-
} catch (err) {
|
|
981
|
-
console.error(`[dist:check] ${err.message}`);
|
|
982
|
-
failed = true;
|
|
983
|
-
}
|
|
1033
|
+
const { green: authorizationKernelGreen, kernel: authorizationKernel } = await loadPlaneAuthorizationKernel();
|
|
1034
|
+
if (!authorizationKernelGreen) {failed = true;}
|
|
984
1035
|
|
|
985
1036
|
// The fleet chart-set pin every edition must carry (edition-chart-set.mjs).
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
chartSetPinText = readFileSync(fleetChartSetLockPath(), 'utf8');
|
|
989
|
-
assertRosteredChartSetLock(JSON.parse(chartSetPinText), EDITION_CHART_SET_FILE);
|
|
990
|
-
} catch (err) {
|
|
991
|
-
console.error(`[dist:check] chart-set pin: ${err.message}. Run \`${commandFor('resolve-chart-set.mjs')}\`.`);
|
|
992
|
-
failed = true;
|
|
993
|
-
}
|
|
1037
|
+
const chartSetPinText = readFleetChartSetPin();
|
|
1038
|
+
if (chartSetPinText === null) {failed = true;}
|
|
994
1039
|
|
|
995
1040
|
// 3 + 4 + 5 + 6 + 7. Every distribution: distribution.lock, images.lock,
|
|
996
1041
|
// authorization.lock, chart-set.lock.
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
10
|
+
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
10
11
|
import { dataRoot } from './edition-workspace.mjs';
|
|
12
|
+
import { isUnderSourcePrefix } from './git-source-auth.mjs';
|
|
11
13
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
12
14
|
import {
|
|
13
15
|
parseConfinedBiomes,
|
|
@@ -163,6 +165,28 @@ function requireEvidenceTrustPolicy(config) {
|
|
|
163
165
|
return Object.freeze({ policy: value.policy, pin: value.pin });
|
|
164
166
|
}
|
|
165
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Every first-party repository URL is canonical and under the workspace's
|
|
170
|
+
* `sourceRepositoryUrlPrefix`: the develop graph sends the source token there.
|
|
171
|
+
*/
|
|
172
|
+
function assertFirstPartyUrls(firstPartySources, prefix) {
|
|
173
|
+
for (const [repo, { repositoryUrl }] of Object.entries(firstPartySources)) {
|
|
174
|
+
const where = `${EDITION_WORKSPACE_CONFIG_FILE}: "firstPartySources"["${repo}"].repositoryUrl`;
|
|
175
|
+
let canonical;
|
|
176
|
+
try {
|
|
177
|
+
canonical = canonicalPublicHttpsRepositoryUrl(repositoryUrl, where);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw new EditionWorkspaceConfigError(err.message);
|
|
180
|
+
}
|
|
181
|
+
if (canonical !== repositoryUrl) {
|
|
182
|
+
throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(repositoryUrl)} is not in its canonical form (${canonical})`);
|
|
183
|
+
}
|
|
184
|
+
if (!isUnderSourcePrefix(canonical, prefix)) {
|
|
185
|
+
throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(repositoryUrl)} is not under sourceRepositoryUrlPrefix ${prefix}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
166
190
|
/** Validate a parsed config document; returns the frozen, typed config. */
|
|
167
191
|
export function parseEditionWorkspaceConfig(document) {
|
|
168
192
|
if (document === null || typeof document !== 'object' || Array.isArray(document)) {
|
|
@@ -179,11 +203,13 @@ export function parseEditionWorkspaceConfig(document) {
|
|
|
179
203
|
.filter(([key]) => document[key] !== undefined)
|
|
180
204
|
.map(([key, parse]) => [key, parse(document[key])]),
|
|
181
205
|
);
|
|
206
|
+
const sourceRepositoryUrlPrefix = requireString(document, 'sourceRepositoryUrlPrefix', SOURCE_URL_PREFIX, 'https://<host>/<organisation>/ with a trailing slash');
|
|
207
|
+
if (sections.firstPartySources !== undefined) {assertFirstPartyUrls(sections.firstPartySources, sourceRepositoryUrlPrefix);}
|
|
182
208
|
return Object.freeze({
|
|
183
209
|
...sections,
|
|
184
210
|
imageRegistry: requireString(document, 'imageRegistry', IMAGE_REGISTRY, '<host>/<namespace> with no scheme or trailing slash'),
|
|
185
211
|
...optionalChartSet(document),
|
|
186
|
-
sourceRepositoryUrlPrefix
|
|
212
|
+
sourceRepositoryUrlPrefix,
|
|
187
213
|
sourceAuthUsername: requireSourceAuthUsername(document),
|
|
188
214
|
evidenceTrustPolicy: requireEvidenceTrustPolicy(document),
|
|
189
215
|
});
|
|
@@ -15,20 +15,23 @@ import { workspaceSection } from './edition-config.mjs';
|
|
|
15
15
|
// `sources[]` entry, so its id is declared in the data root's
|
|
16
16
|
// `edition-workspace.json` (`firstPartySources`), keyed by the `repo` handle the
|
|
17
17
|
// runtime image source index pins it under. The key is a lookup handle and may
|
|
18
|
-
// move with the aggregator layout; the
|
|
18
|
+
// move with the aggregator layout; the entry's `sourceId` is the identity and
|
|
19
|
+
// must not. The entry's `repositoryUrl` is where the repository lives
|
|
20
|
+
// (provenance, never identity): the develop graph resolves heads there.
|
|
19
21
|
//
|
|
20
|
-
// EVERY repository `runtime-image-source-index.json` pins is declared there
|
|
21
|
-
//
|
|
22
|
-
// refused at once rather than the day it first declares a
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// EVERY repository `runtime-image-source-index.json` pins is declared there, at
|
|
23
|
+
// the same URL (the offline index check refuses otherwise), so a newly pinned
|
|
24
|
+
// repository is refused at once rather than the day it first declares a
|
|
25
|
+
// composed image. A declared repository need not be pinned yet: the develop
|
|
26
|
+
// graph carries it before any release does. A lookup of an undeclared
|
|
27
|
+
// repository is refused (`firstPartySourceId`), never named by a fallback.
|
|
25
28
|
|
|
26
29
|
/** The kernel's source-handle grammar (`BiomeSourceNameSchema`): lowercase kebab-case. */
|
|
27
30
|
export const DECLARED_SOURCE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
28
31
|
|
|
29
|
-
/** The data root's
|
|
30
|
-
export const
|
|
31
|
-
workspaceSection('firstPartySources',
|
|
32
|
+
/** The data root's first-party repositories (`firstPartySources`): repo handle -> { sourceId, repositoryUrl }. */
|
|
33
|
+
export const firstPartySources = (needed = 'naming a runtime subject after its first-party source') =>
|
|
34
|
+
workspaceSection('firstPartySources', needed);
|
|
32
35
|
|
|
33
36
|
export class UndeclaredSourceIdError extends Error {
|
|
34
37
|
constructor(message) {
|
|
@@ -48,12 +51,12 @@ export function assertDeclaredSourceId(sourceId, where) {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export function firstPartySourceId(repo) {
|
|
51
|
-
const
|
|
52
|
-
if (!Object.hasOwn(
|
|
54
|
+
const sources = firstPartySources();
|
|
55
|
+
if (!Object.hasOwn(sources, repo)) {
|
|
53
56
|
throw new UndeclaredSourceIdError(
|
|
54
57
|
`first-party repository "${repo}" has no declared source id — declare it in ` +
|
|
55
58
|
'edition-workspace.json under "firstPartySources"; a runtime identity is never derived from a repository name or URL',
|
|
56
59
|
);
|
|
57
60
|
}
|
|
58
|
-
return assertDeclaredSourceId(
|
|
61
|
+
return assertDeclaredSourceId(sources[repo].sourceId, `first-party repository "${repo}"`);
|
|
59
62
|
}
|
|
@@ -25,7 +25,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
25
25
|
import { tmpdir } from 'node:os';
|
|
26
26
|
import { join } from 'node:path';
|
|
27
27
|
|
|
28
|
-
import { canonicalPublicHttpsRepositoryUrl } from './
|
|
28
|
+
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
29
29
|
|
|
30
30
|
/** The fixed environment variable holding the source read token. */
|
|
31
31
|
export const SOURCE_READ_TOKEN_ENV = 'EVIDENCE_REPOSITORIES_TOKEN';
|
package/edition/lib.mjs
CHANGED
|
@@ -3075,38 +3075,6 @@ export function validateImmutableImageLock(lock, schemas) {
|
|
|
3075
3075
|
return lock;
|
|
3076
3076
|
}
|
|
3077
3077
|
|
|
3078
|
-
export function canonicalPublicHttpsRepositoryUrl(raw, context) {
|
|
3079
|
-
if (typeof raw !== 'string' || raw.length === 0 || raw.trim() !== raw) {
|
|
3080
|
-
throw new Error(`${context} must be a non-empty canonical HTTPS URL`);
|
|
3081
|
-
}
|
|
3082
|
-
let parsed;
|
|
3083
|
-
try {
|
|
3084
|
-
parsed = new URL(raw);
|
|
3085
|
-
} catch {
|
|
3086
|
-
throw new Error(`${context} is not a valid URL`);
|
|
3087
|
-
}
|
|
3088
|
-
if (parsed.protocol !== 'https:') {
|
|
3089
|
-
throw new Error(`${context} must use HTTPS`);
|
|
3090
|
-
}
|
|
3091
|
-
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
3092
|
-
throw new Error(
|
|
3093
|
-
`${context} must not contain username/password credentials`,
|
|
3094
|
-
);
|
|
3095
|
-
}
|
|
3096
|
-
if (
|
|
3097
|
-
parsed.search.length > 0 ||
|
|
3098
|
-
parsed.hash.length > 0 ||
|
|
3099
|
-
raw.includes('?') ||
|
|
3100
|
-
raw.includes('#')
|
|
3101
|
-
) {
|
|
3102
|
-
throw new Error(`${context} must not contain a query or fragment`);
|
|
3103
|
-
}
|
|
3104
|
-
if (parsed.hostname.length === 0) {
|
|
3105
|
-
throw new Error(`${context} must identify a repository host`);
|
|
3106
|
-
}
|
|
3107
|
-
return parsed.toString();
|
|
3108
|
-
}
|
|
3109
|
-
|
|
3110
3078
|
/**
|
|
3111
3079
|
* Assert that an exact 40-hex commit exists in a source repository. Delegates
|
|
3112
3080
|
* to `@xemahq/distribution-source-hash` — the tooling holds no second copy of
|
package/edition/pin-source.mjs
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// published pin. This module owns the SHAPE of that map: a flat object whose
|
|
11
11
|
// keys live in six closed namespaces and whose values are exact 40-hex SHAs.
|
|
12
12
|
//
|
|
13
|
-
// repos/<x> | submodules/<x> every
|
|
14
|
-
//
|
|
13
|
+
// repos/<x> | submodules/<x> every repository of the develop graph (the
|
|
14
|
+
// data root's firstPartySources, develop-graph.mjs),
|
|
15
|
+
// no more, no fewer
|
|
15
16
|
// <pinRoles.data> the commit of the data root checkout (editions and indices)
|
|
16
17
|
// <pinRoles.kernel> the commit of the kernel the authorization lock is sealed
|
|
17
18
|
// with (may match repos/<x>, but is NOT in the graph)
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
// only when that root is NOT this repository
|
|
21
22
|
// sources/<name> one per `sources[]` entry declared in the chain
|
|
22
23
|
|
|
24
|
+
import { developGraphMembers } from './develop-graph.mjs';
|
|
23
25
|
import { workspaceSection } from './edition-config.mjs';
|
|
24
26
|
import { compareStrings } from './lib.mjs';
|
|
25
27
|
import { PinRole } from './workspace-sections.mjs';
|
|
@@ -109,19 +111,20 @@ function graphPinsOf(pins) {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
|
-
* Validate a COMPLETE snapshot against the graph and the composed
|
|
113
|
-
* `declaredSourceNames` are the `sources[].name` entries the chain
|
|
114
|
-
* `editionsRootExternal` is whether the editions root is another
|
|
115
|
-
* Every refusal names the offending key; nothing falls back to a
|
|
114
|
+
* Validate a COMPLETE snapshot against the develop graph and the composed
|
|
115
|
+
* chain. `declaredSourceNames` are the `sources[].name` entries the chain
|
|
116
|
+
* declares; `editionsRootExternal` is whether the editions root is another
|
|
117
|
+
* repository. Every refusal names the offending key; nothing falls back to a
|
|
118
|
+
* published pin.
|
|
116
119
|
*/
|
|
117
120
|
export function validateSnapshotPins(
|
|
118
121
|
pins,
|
|
119
|
-
{
|
|
122
|
+
{ declaredSourceNames, editionsRootExternal },
|
|
120
123
|
) {
|
|
121
124
|
assertPinsObject(pins);
|
|
122
125
|
assertExactShas(pins);
|
|
123
126
|
for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
|
|
124
|
-
checkGraphPins(
|
|
127
|
+
checkGraphPins(developGraphMembers(), pins);
|
|
125
128
|
for (const [key, what] of [
|
|
126
129
|
[dataPinKey(), 'the commit of the data root checkout'],
|
|
127
130
|
[kernelPinKey(), 'the commit of the kernel the authorization lock is sealed with'],
|
|
@@ -175,7 +178,10 @@ export function snapshotPinSource(pins) {
|
|
|
175
178
|
/**
|
|
176
179
|
* Apply a pin source to a parsed runtime image source index. Published returns
|
|
177
180
|
* the SAME object (inert, byte for byte). Snapshot returns a copy whose
|
|
178
|
-
* `repositoryPins`
|
|
181
|
+
* `repositoryPins` are the DEVELOP GRAPH (develop-graph.mjs) at the snapshot
|
|
182
|
+
* SHAs: the committed index's own pins are not read, so a first-party
|
|
183
|
+
* repository no release pins yet is in it, and a pinned one that is not
|
|
184
|
+
* first-party is not.
|
|
179
185
|
*/
|
|
180
186
|
export function applyPinSource(runtimeSourceIndex, pinSource = PUBLISHED_PIN_SOURCE) {
|
|
181
187
|
switch (pinSource?.kind) {
|
|
@@ -211,12 +217,13 @@ function applySnapshot(runtimeSourceIndex, pins) {
|
|
|
211
217
|
assertPinsObject(pins);
|
|
212
218
|
assertExactShas(pins);
|
|
213
219
|
for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
|
|
214
|
-
const graph =
|
|
220
|
+
const graph = developGraphMembers();
|
|
215
221
|
checkGraphPins(graph, pins);
|
|
216
222
|
return {
|
|
217
223
|
...runtimeSourceIndex,
|
|
218
|
-
repositoryPins: graph.map((
|
|
219
|
-
|
|
224
|
+
repositoryPins: graph.map(({ repo, repositoryUrl }) => ({
|
|
225
|
+
repo,
|
|
226
|
+
repositoryUrl,
|
|
220
227
|
// A develop snapshot pins every repository at its develop head, so that one
|
|
221
228
|
// SHA is both the built commit (image axis, `sourceCommit`) and the source
|
|
222
229
|
// tip (`vendoredSourceCommit`): the image pin lagging the source tip cannot
|
|
@@ -226,8 +233,8 @@ function applySnapshot(runtimeSourceIndex, pins) {
|
|
|
226
233
|
// The COMMITTED index keeps the axes apart (release channel): see
|
|
227
234
|
// `source-authority.mjs#publishedSourceTip` and
|
|
228
235
|
// `build-runtime-image-source-index.mjs#repositoryIdentity`.
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
sourceCommit: pins[repo],
|
|
237
|
+
vendoredSourceCommit: pins[repo],
|
|
231
238
|
})),
|
|
232
239
|
};
|
|
233
240
|
}
|
|
@@ -91,16 +91,46 @@ function supplementalLauncher(value, where) {
|
|
|
91
91
|
return Object.freeze({ kind: value.kind, biomeId: text(value.biomeId, `${where}.biomeId`) });
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The retired `{repo handle: source id}` string form of a `firstPartySources`
|
|
96
|
+
* entry. Refused, never read: the entry also declares where the repository
|
|
97
|
+
* lives, which the develop graph resolves its head from.
|
|
98
|
+
*/
|
|
99
|
+
export class RetiredFirstPartySourceShapeError extends Error {
|
|
100
|
+
constructor(repo, id) {
|
|
101
|
+
super(
|
|
102
|
+
`${FILE}: "firstPartySources"["${repo}"] is the retired source-id string ${JSON.stringify(id)}. ` +
|
|
103
|
+
`Migrate it to { "sourceId": ${JSON.stringify(id)}, "repositoryUrl": "<the repository's canonical https URL>" }.`,
|
|
104
|
+
);
|
|
105
|
+
this.name = 'RetiredFirstPartySourceShapeError';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `firstPartySources`: every first-party repository, keyed by its source
|
|
111
|
+
* checkout handle, with its declared source id (the identity a runtime subject
|
|
112
|
+
* is named after) and its repository URL (where it lives: the develop graph
|
|
113
|
+
* resolves its head there, and a release pin must name the same URL). The URL
|
|
114
|
+
* is checked canonical and under `sourceRepositoryUrlPrefix` with the whole
|
|
115
|
+
* config (edition-config.mjs).
|
|
116
|
+
*/
|
|
94
117
|
export function parseFirstPartySources(value) {
|
|
95
118
|
const where = '"firstPartySources"';
|
|
96
|
-
if (!isObject(value) || Object.keys(value).length === 0) {
|
|
97
|
-
|
|
119
|
+
if (!isObject(value) || Object.keys(value).length === 0) {
|
|
120
|
+
fail(where, 'must be a non-empty {repo handle: { sourceId, repositoryUrl }} object');
|
|
121
|
+
}
|
|
122
|
+
const entries = Object.entries(value).map(([repo, entry]) => {
|
|
98
123
|
repoHandle(repo, `${where} key`);
|
|
99
|
-
if (typeof
|
|
100
|
-
|
|
124
|
+
if (typeof entry === 'string') {throw new RetiredFirstPartySourceShapeError(repo, entry);}
|
|
125
|
+
const at = `${where}["${repo}"]`;
|
|
126
|
+
exactObject(entry, at, ['sourceId', 'repositoryUrl']);
|
|
127
|
+
if (typeof entry.sourceId !== 'string' || !SOURCE_ID.test(entry.sourceId)) {
|
|
128
|
+
fail(`${at}.sourceId`, `must be a lowercase kebab-case source id; got ${JSON.stringify(entry.sourceId)}`);
|
|
101
129
|
}
|
|
102
|
-
return [repo,
|
|
130
|
+
return [repo, Object.freeze({ sourceId: entry.sourceId, repositoryUrl: text(entry.repositoryUrl, `${at}.repositoryUrl`) })];
|
|
103
131
|
});
|
|
132
|
+
uniqueBy(entries, `${where} sourceId`, ([, entry]) => entry.sourceId);
|
|
133
|
+
uniqueBy(entries, `${where} repositoryUrl`, ([, entry]) => entry.repositoryUrl);
|
|
104
134
|
return Object.freeze(Object.fromEntries(entries));
|
|
105
135
|
}
|
|
106
136
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/xema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/xema-dev/xema-cli.git",
|
|
@@ -92,6 +92,6 @@
|
|
|
92
92
|
"typecheck": "tsc --noEmit"
|
|
93
93
|
},
|
|
94
94
|
"xemaRelease": {
|
|
95
|
-
"sourceDigest": "sha256:
|
|
95
|
+
"sourceDigest": "sha256:72008b3d37f2cbcef3c5ec25e2d85989198ebc1f9bc1f481b8b767965813d5aa"
|
|
96
96
|
}
|
|
97
97
|
}
|