@xemahq/edition-tooling 0.0.0-stage → 0.1.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/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- package/README.md +0 -3
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The committed build ledger (`build-ledger.json`): which commits of each source
|
|
2
|
+
// repository published images, and (schema v2) which `imageRef:contentHash`
|
|
3
|
+
// coordinates are published with which platforms.
|
|
4
|
+
//
|
|
5
|
+
// `node:` builtins plus the workspace locator only, so `source-authority.mjs`
|
|
6
|
+
// (which resolves from a bare checkout) can read it.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { dataRoot } from './edition-workspace.mjs';
|
|
11
|
+
|
|
12
|
+
export const BUILD_LEDGER_FILE = 'build-ledger.json';
|
|
13
|
+
|
|
14
|
+
export class BuildLedgerError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(`${BUILD_LEDGER_FILE} is malformed: ${message}`);
|
|
17
|
+
this.name = 'BuildLedgerError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SHA1_PATTERN = /^[0-9a-f]{40}$/u;
|
|
22
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
23
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
24
|
+
const PLATFORM_PATTERN = /^[a-z0-9]+\/[a-z0-9]+(\/[a-z0-9]+)?$/u;
|
|
25
|
+
|
|
26
|
+
const isRecord = (value) =>
|
|
27
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
function parseImages(repo, images) {
|
|
30
|
+
if (!isRecord(images)) {
|
|
31
|
+
throw new BuildLedgerError(`repositories["${repo}"].images must be an object`);
|
|
32
|
+
}
|
|
33
|
+
for (const [imageRef, image] of Object.entries(images)) {
|
|
34
|
+
const where = `repositories["${repo}"].images["${imageRef}"]`;
|
|
35
|
+
if (!isRecord(image) || !isRecord(image.contentHashes)) {
|
|
36
|
+
throw new BuildLedgerError(`${where}.contentHashes must be an object`);
|
|
37
|
+
}
|
|
38
|
+
for (const [hash, entry] of Object.entries(image.contentHashes)) {
|
|
39
|
+
const at = `${where}.contentHashes["${hash}"]`;
|
|
40
|
+
if (!SHA256_PATTERN.test(hash)) {
|
|
41
|
+
throw new BuildLedgerError(`${at}: key is not 64 lowercase hex characters`);
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
!isRecord(entry) ||
|
|
45
|
+
!Array.isArray(entry.platforms) ||
|
|
46
|
+
entry.platforms.length === 0 ||
|
|
47
|
+
entry.platforms.some(
|
|
48
|
+
(platform) => typeof platform !== 'string' || !PLATFORM_PATTERN.test(platform),
|
|
49
|
+
) ||
|
|
50
|
+
new Set(entry.platforms).size !== entry.platforms.length
|
|
51
|
+
) {
|
|
52
|
+
throw new BuildLedgerError(
|
|
53
|
+
`${at}.platforms must be a non-empty list of distinct "os/arch" strings`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (typeof entry.digest !== 'string' || !DIGEST_PATTERN.test(entry.digest)) {
|
|
57
|
+
throw new BuildLedgerError(`${at}.digest must be "sha256:<64 hex>"`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse a ledger document strictly. Returns `{ schemaVersion, repositories }`
|
|
65
|
+
* where each repository is `{ commits, images }`; `images` is null for v1.
|
|
66
|
+
*/
|
|
67
|
+
export function parseBuildLedger(raw) {
|
|
68
|
+
if (
|
|
69
|
+
!isRecord(raw) ||
|
|
70
|
+
(raw.schemaVersion !== 1 && raw.schemaVersion !== 2) ||
|
|
71
|
+
!isRecord(raw.repositories)
|
|
72
|
+
) {
|
|
73
|
+
throw new BuildLedgerError('expected schemaVersion 1 or 2 with a repositories object');
|
|
74
|
+
}
|
|
75
|
+
const repositories = {};
|
|
76
|
+
for (const [repo, value] of Object.entries(raw.repositories)) {
|
|
77
|
+
// v1 is removed once the data root is re-projected at v2.
|
|
78
|
+
if (raw.schemaVersion === 1) {
|
|
79
|
+
if (!Array.isArray(value)) {
|
|
80
|
+
throw new BuildLedgerError(`repositories["${repo}"] must be a commit list`);
|
|
81
|
+
}
|
|
82
|
+
repositories[repo] = { commits: value, images: null };
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!isRecord(value) || !Array.isArray(value.commits)) {
|
|
86
|
+
throw new BuildLedgerError(`repositories["${repo}"].commits must be a list`);
|
|
87
|
+
}
|
|
88
|
+
if (value.commits.some((commit) => typeof commit !== 'string' || !SHA1_PATTERN.test(commit))) {
|
|
89
|
+
throw new BuildLedgerError(`repositories["${repo}"].commits holds a non-40-hex entry`);
|
|
90
|
+
}
|
|
91
|
+
parseImages(repo, value.images);
|
|
92
|
+
repositories[repo] = { commits: value.commits, images: value.images };
|
|
93
|
+
}
|
|
94
|
+
return { schemaVersion: raw.schemaVersion, repositories };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The parsed ledger of a data root, or null when none is projected. Malformed THROWS. */
|
|
98
|
+
export function readBuildLedger(distributionsDir = dataRoot()) {
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = JSON.parse(readFileSync(join(distributionsDir, BUILD_LEDGER_FILE), 'utf8'));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.code === 'ENOENT') {return null;}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
return parseBuildLedger(raw);
|
|
107
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
7
|
+
import { platformServiceIndexPath , WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath } from './edition-workspace.mjs';
|
|
8
|
+
import {
|
|
9
|
+
loadKernelDistributionSchemas,
|
|
10
|
+
readGitJsonAtCommit,
|
|
11
|
+
serializeJson,
|
|
12
|
+
sha256Hex,
|
|
13
|
+
} from './lib.mjs';
|
|
14
|
+
import {
|
|
15
|
+
comparablePlatformService,
|
|
16
|
+
declaredInstallationClass,
|
|
17
|
+
sourceDeclaredRequirements,
|
|
18
|
+
sourceOwnedCapabilityDomains,
|
|
19
|
+
} from './platform-service-class.mjs';
|
|
20
|
+
import {
|
|
21
|
+
platformServiceSourceDeclaration,
|
|
22
|
+
platformServiceSourceDeclarations,
|
|
23
|
+
platformServiceSourceRepositories,
|
|
24
|
+
} from './platform-service-source-authority.mjs';
|
|
25
|
+
import {
|
|
26
|
+
DistributionSourceMode,
|
|
27
|
+
committedRepositoryPins,
|
|
28
|
+
createPinnedSourceAuthority,
|
|
29
|
+
createSourceAuthority,
|
|
30
|
+
parseSourceModeArgs,
|
|
31
|
+
} from './source-authority.mjs';
|
|
32
|
+
import { readStrictJsonFile } from './strict-json.mjs';
|
|
33
|
+
|
|
34
|
+
if (isEntryModule(import.meta.url)) {
|
|
35
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TRUSTED_WRITE_FLAG = '--trusted-write';
|
|
39
|
+
const CHECK_FLAG = '--check';
|
|
40
|
+
const CHECK_SOURCES_FLAG = '--check-sources';
|
|
41
|
+
const UNPINNED_FLAG = '--unpinned';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* THE LAST INDEX IN THIS PLANE THAT COULD NOT BE SCOPED, and the one that
|
|
45
|
+
* measurably fell behind because of it.
|
|
46
|
+
*
|
|
47
|
+
* `build-biome-index.mjs` and `build-runtime-image-source-index.mjs` could both
|
|
48
|
+
* be narrowed to one repository. This one could not: every run resolved all
|
|
49
|
+
* eight platform-service source repositories live. So advancing the two services
|
|
50
|
+
* sourced from `repos/xema-base` REQUIRED moving `event-hub-api`,
|
|
51
|
+
* `identity-api`, `llm-gateway-api` and `xema-host-web` with them — measured
|
|
52
|
+
* 2026-09-23, four collateral repositories that then disagreed with the runtime
|
|
53
|
+
* pins a scoped refresh had just held.
|
|
54
|
+
*
|
|
55
|
+
* It did not stay theoretical. Measured on `origin/main` the same day, the two
|
|
56
|
+
* committed indexes pinned `repos/xema-base` at commits 164 APART —
|
|
57
|
+
* `platform-service-index.json` at `e1987a11` (2026-09-17) against
|
|
58
|
+
* `runtime-image-source-index.json` at `fcde2a83` (2026-09-20) — because the
|
|
59
|
+
* pinnable index had been scope-refreshed and this one could only move by
|
|
60
|
+
* moving everything. Two committed artifacts, one repository, two commits.
|
|
61
|
+
*
|
|
62
|
+
* The fix is not a `--repo` flag here. A flag would have made this index a
|
|
63
|
+
* SECOND place that decides what commit a repository is at, which is the defect
|
|
64
|
+
* wearing a narrower costume. It reads the pins instead, like every other
|
|
65
|
+
* consumer, so a scoped refresh reaches it for free and there is nothing to
|
|
66
|
+
* keep in step.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
function readJson(path) {
|
|
70
|
+
return readStrictJsonFile(path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function git(repo, args) {
|
|
74
|
+
return execFileSync('git', ['-C', repo, ...args], {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
|
+
}).trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Every platform service the data root declares (`platformServiceSources`) — the
|
|
82
|
+
* platform floor. NOT filtered by tier: the kernel distribution-lock schema
|
|
83
|
+
* requires an exact source identity on EVERY `platformServices[]` entry, so an
|
|
84
|
+
* index that describes only one tier makes every edition unresolvable. It did:
|
|
85
|
+
* filtering to `infra` left `xema-kernel-server`, `docs-api`,
|
|
86
|
+
* `opencode-image-prepuller` and `xema-host-web` with no identity at all.
|
|
87
|
+
*
|
|
88
|
+
* The index is derived from the declarations alone: no edition is read, so a
|
|
89
|
+
* data root with any set of editions (or none) builds and checks it the same.
|
|
90
|
+
*/
|
|
91
|
+
function loadPlatformServiceDeclarations() {
|
|
92
|
+
return platformServiceSourceDeclarations()
|
|
93
|
+
.slice()
|
|
94
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function trustedSourceAuthority(sourceMode, unpinned) {
|
|
98
|
+
return unpinned
|
|
99
|
+
? createSourceAuthority({
|
|
100
|
+
mode: sourceMode,
|
|
101
|
+
knownRepositories: platformServiceSourceRepositories(),
|
|
102
|
+
})
|
|
103
|
+
: // No delegate: this artifact decides no commits, so it must not be able
|
|
104
|
+
// to ask for one. A platform-service repository with no committed pin
|
|
105
|
+
// refuses by name rather than resolving to its tip.
|
|
106
|
+
createPinnedSourceAuthority({ pins: committedRepositoryPins() });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildTrustedIndex(sourceAuthority) {
|
|
110
|
+
const declarations = loadPlatformServiceDeclarations();
|
|
111
|
+
const services = [];
|
|
112
|
+
for (const declaration of declarations) {
|
|
113
|
+
const binding = platformServiceSourceDeclaration(declaration.name);
|
|
114
|
+
const repoDirectory = sourcePath(binding.repo);
|
|
115
|
+
// Read through the one explicit source authority. Published mode names a
|
|
116
|
+
// fetchable origin/main commit; workspace mode names a deterministic local
|
|
117
|
+
// snapshot commit containing tracked and untracked non-ignored files.
|
|
118
|
+
const before = sourceAuthority.commitForRepository(binding.repo);
|
|
119
|
+
const sourceManifest = readGitJsonAtCommit(
|
|
120
|
+
repoDirectory,
|
|
121
|
+
binding.manifestPath,
|
|
122
|
+
before,
|
|
123
|
+
);
|
|
124
|
+
if (sourceManifest.name !== binding.manifestName) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`${binding.path} at ${before} declares name "${String(sourceManifest.name)}", ` +
|
|
127
|
+
`but platform service "${declaration.name}" is declared to be sourced from ` +
|
|
128
|
+
`"${binding.manifestName}"`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
typeof sourceManifest.version !== 'string' ||
|
|
133
|
+
sourceManifest.version.length === 0
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${binding.path} has no exact version at ${before}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const repositoryUrl = canonicalPublicHttpsRepositoryUrl(
|
|
140
|
+
git(repoDirectory, ['remote', 'get-url', 'origin']),
|
|
141
|
+
`${binding.repo} origin`,
|
|
142
|
+
);
|
|
143
|
+
services.push({
|
|
144
|
+
name: declaration.name,
|
|
145
|
+
tier: declaration.tier,
|
|
146
|
+
...declaredInstallationClass(declaration),
|
|
147
|
+
...sourceOwnedCapabilityDomains(sourceManifest, `${binding.path} at ${before}`),
|
|
148
|
+
...sourceDeclaredRequirements(sourceManifest, `${binding.path} at ${before}`),
|
|
149
|
+
repositoryUrl,
|
|
150
|
+
repo: binding.repo,
|
|
151
|
+
path: binding.path,
|
|
152
|
+
sourceCommit: before,
|
|
153
|
+
version: sourceManifest.version,
|
|
154
|
+
sourceManifestHash: sha256Hex(serializeJson(sourceManifest)),
|
|
155
|
+
imageKey: binding.imageKey,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return { schemaVersion: 1, services };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateOfflineIndex(index) {
|
|
162
|
+
const declarations = loadPlatformServiceDeclarations();
|
|
163
|
+
const expected = declarations.map(comparablePlatformService);
|
|
164
|
+
const actual = index.services.map(comparablePlatformService);
|
|
165
|
+
if (serializeJson(actual) !== serializeJson(expected)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
'platform-service-index.json does not exactly match the platformServiceSources declared in edition-workspace.json',
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
for (const service of index.services) {
|
|
171
|
+
const canonicalRepositoryUrl = canonicalPublicHttpsRepositoryUrl(
|
|
172
|
+
service.repositoryUrl,
|
|
173
|
+
`platform service "${service.name}" repositoryUrl`,
|
|
174
|
+
);
|
|
175
|
+
if (canonicalRepositoryUrl !== service.repositoryUrl) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`platform service "${service.name}" repositoryUrl is not canonical`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const expectedRepoName = service.repo.split('/').at(-1);
|
|
181
|
+
// Offline, and still exact: the declared source location is the authority,
|
|
182
|
+
// so a committed index that names a different repo, manifest or image key
|
|
183
|
+
// than the declaration fails here without touching Git.
|
|
184
|
+
const binding = platformServiceSourceDeclaration(service.name);
|
|
185
|
+
if (service.repo !== binding.repo || service.path !== binding.path) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`platform service "${service.name}" source ${service.repo}:${service.path} ` +
|
|
188
|
+
`does not match its declared source ${binding.repo}:${binding.path}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (service.imageKey !== binding.imageKey) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`platform service "${service.name}" imageKey "${service.imageKey}" ` +
|
|
194
|
+
`does not match its declared image key "${binding.imageKey}"`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (!service.path.startsWith(`${service.repo}/`)) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`platform service "${service.name}" source path is outside its repo`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
!service.repositoryUrl.endsWith(`/${expectedRepoName}.git`) &&
|
|
204
|
+
!service.repositoryUrl.endsWith(`/${expectedRepoName}`)
|
|
205
|
+
) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`platform service "${service.name}" repositoryUrl does not identify ${service.repo}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return index;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The index as `--trusted-write` produces and validates it, over an explicit
|
|
216
|
+
* source authority. The develop edition builds its snapshot index here, with a
|
|
217
|
+
* pinned authority whose pins are the snapshot's, so the release and the
|
|
218
|
+
* develop lane share one builder and one validation.
|
|
219
|
+
*/
|
|
220
|
+
export function buildValidatedPlatformServiceIndex(schemas, sourceAuthority) {
|
|
221
|
+
const parsed = schemas.parsePlatformServiceSourceIndex(
|
|
222
|
+
buildTrustedIndex(sourceAuthority),
|
|
223
|
+
);
|
|
224
|
+
return validateOfflineIndex(parsed);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function main() {
|
|
228
|
+
const { mode, remaining } = parseSourceModeArgs(process.argv.slice(2));
|
|
229
|
+
const flags = new Set(remaining);
|
|
230
|
+
const unpinnedFlag = flags.delete(UNPINNED_FLAG);
|
|
231
|
+
const unknown = [...flags].filter(
|
|
232
|
+
(flag) =>
|
|
233
|
+
flag !== TRUSTED_WRITE_FLAG &&
|
|
234
|
+
flag !== CHECK_FLAG &&
|
|
235
|
+
flag !== CHECK_SOURCES_FLAG,
|
|
236
|
+
);
|
|
237
|
+
if (unknown.length > 0 || flags.size !== 1) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`usage: node build-platform-service-index.mjs (${CHECK_FLAG} | ${CHECK_SOURCES_FLAG} | ${TRUSTED_WRITE_FLAG}) [--source published|workspace] [${UNPINNED_FLAG}]`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
// A workspace snapshot has no committed pin by construction, so it IMPLIES
|
|
243
|
+
// the live authority rather than requiring the operator to state both.
|
|
244
|
+
const unpinned = unpinnedFlag || mode === DistributionSourceMode.Workspace;
|
|
245
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
246
|
+
if (flags.has(CHECK_FLAG)) {
|
|
247
|
+
if (!existsSync(platformServiceIndexPath())) {
|
|
248
|
+
throw new Error(`${platformServiceIndexPath()} does not exist`);
|
|
249
|
+
}
|
|
250
|
+
const parsed = schemas.parsePlatformServiceSourceIndex(
|
|
251
|
+
readJson(platformServiceIndexPath()),
|
|
252
|
+
);
|
|
253
|
+
validateOfflineIndex(parsed);
|
|
254
|
+
console.log(
|
|
255
|
+
`[platform-service-index] offline check passed for ${parsed.services.length} services`,
|
|
256
|
+
);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const parsed = buildValidatedPlatformServiceIndex(schemas, trustedSourceAuthority(mode, unpinned));
|
|
260
|
+
if (flags.has(CHECK_SOURCES_FLAG)) {
|
|
261
|
+
if (!existsSync(platformServiceIndexPath())) {
|
|
262
|
+
throw new Error(`${platformServiceIndexPath()} does not exist`);
|
|
263
|
+
}
|
|
264
|
+
const committed = readFileSync(platformServiceIndexPath(), 'utf8');
|
|
265
|
+
if (committed !== serializeJson(parsed)) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`platform-service-index.json does not match the exact trusted clean sources ` +
|
|
268
|
+
`(authority: ${
|
|
269
|
+
unpinned
|
|
270
|
+
? 'UNPINNED — every repository resolved LIVE, so this names the ' +
|
|
271
|
+
'fleet as it is now and regenerating will move pins'
|
|
272
|
+
: 'PINNED — the committed repositoryPins, no live resolution'
|
|
273
|
+
}).`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
console.log(
|
|
277
|
+
`[platform-service-index] trusted source check passed for ${parsed.services.length} services`,
|
|
278
|
+
);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
writeFileSync(platformServiceIndexPath(), serializeJson(parsed));
|
|
282
|
+
console.log(
|
|
283
|
+
`[platform-service-index] wrote ${platformServiceIndexPath()} from ${parsed.services.length} clean trusted sources`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (isEntryModule(import.meta.url)) {
|
|
288
|
+
main().catch((error) => {
|
|
289
|
+
console.error(`[platform-service-index] ${error.message}`);
|
|
290
|
+
process.exitCode = 1;
|
|
291
|
+
});
|
|
292
|
+
}
|