@xemahq/xema 0.26.0 → 0.27.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 +70 -37
- package/dist/assets/distribution-assets.manifest.json +1 -2
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +5 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +2 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +18 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +2 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +31 -7
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +1 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +315 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +270 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +207 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +23 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +176 -0
- package/edition/evidence-checkout.mjs +418 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +256 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1225 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3018 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +99 -0
- package/edition/release-edition-artifact.mjs +158 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -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-image-evidence.mjs +430 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +14 -6
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -156
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The EDITION-SCOPED external source walk.
|
|
3
|
+
*
|
|
4
|
+
* A distribution may declare `sources[]` — external repositories holding biomes
|
|
5
|
+
* that are not in the platform's own index. This module fetches one, PINS the
|
|
6
|
+
* exact commit it read, projects its biomes into index entries, and STAMPS each
|
|
7
|
+
* entry with the declaring source's name.
|
|
8
|
+
*
|
|
9
|
+
* ── THE STAMP IS THE WHOLE SECURITY PROPERTY ──
|
|
10
|
+
*
|
|
11
|
+
* `entrySourceAdmissible` (resolve-distribution.mjs) keys edition scoping on
|
|
12
|
+
* `entry.source`, and ABSENT IS THE IDENTITY: an unstamped entry is
|
|
13
|
+
* platform-default and admissible to EVERY edition. That is deliberate — it is
|
|
14
|
+
* what keeps the nine committed locks byte-identical — so an unstamped entry
|
|
15
|
+
* drawn from a customer repository is a DISCLOSURE, not a packaging slip: a
|
|
16
|
+
* `{ tier: "platform" }` selector matches on tier and audience and never on
|
|
17
|
+
* origin, so it would sweep one customer's private product into `oss` and
|
|
18
|
+
* `cloud`. Measured before the fence existed, on this corpus: 5 of 9 editions.
|
|
19
|
+
*
|
|
20
|
+
* Every entry this module emits is therefore stamped in ONE place —
|
|
21
|
+
* `stampEntry` below — and `walkDeclaredSources` asserts it before returning.
|
|
22
|
+
* That assertion is not decoration: it is the last point at which an unstamped
|
|
23
|
+
* entry can be caught before it reaches the shared index.
|
|
24
|
+
*
|
|
25
|
+
* ── WHY THIS IS A SIBLING OF `checkout-indexed-source-repositories.mjs` AND
|
|
26
|
+
* NOT A GENERALISATION OF IT ──
|
|
27
|
+
*
|
|
28
|
+
* That file looks like this fetcher and is not, in four ways, and the last one
|
|
29
|
+
* is why generalising it would REMOVE a fence rather than widen one:
|
|
30
|
+
*
|
|
31
|
+
* 1. DIFFERENT PLANE. It materialises the repositories named by
|
|
32
|
+
* `runtime-image-source-index.json` so image SOURCE EVIDENCE can be
|
|
33
|
+
* produced. This walk resolves an EDITION. Neither needs the other.
|
|
34
|
+
* 2. DIFFERENT COMMIT AUTHORITY. Its commit arrives already pinned, in the
|
|
35
|
+
* index (`pin.sourceCommit`); it only verifies. Here the commit is
|
|
36
|
+
* DERIVED, by resolving a declared ref at the remote — a different
|
|
37
|
+
* question with a different failure mode.
|
|
38
|
+
* 3. DIFFERENT REUSE POSTURE. It refuses an existing target outright
|
|
39
|
+
* ("refusing to reuse an unverified checkout"), which is right for a
|
|
40
|
+
* one-shot CI materialisation and wrong here: `dist:resolve` and
|
|
41
|
+
* `dist:check` run repeatedly over many editions, so a checkout is reused
|
|
42
|
+
* and re-verified BY COMMIT instead.
|
|
43
|
+
* 4. ITS FIRST-PARTY LOCK IS LOAD-BEARING WHERE IT SITS. It derives
|
|
44
|
+
* `<sourceRepositoryUrlPrefix><name>.git` and compares for EQUALITY, so
|
|
45
|
+
* an evidence pin cannot name a repository outside the vendor org. That is
|
|
46
|
+
* a real fence for the evidence plane. Widening it to admit a customer org
|
|
47
|
+
* would delete it for evidence at the same time — the same shape as
|
|
48
|
+
* widening `validateRepository` in `source-authority.mjs` to admit a path
|
|
49
|
+
* outside the aggregator. A customer's repository is not in the first-party organisation and
|
|
50
|
+
* must not have to be, so this walk carries its own URL from the
|
|
51
|
+
* declaration and never derives one.
|
|
52
|
+
*
|
|
53
|
+
* ── WHAT THIS MODULE DELIBERATELY DOES NOT DO ──
|
|
54
|
+
*
|
|
55
|
+
* It does not INTERPRET `source.imageRegistry` or `source.imageNamePrefix`; it
|
|
56
|
+
* CARRIES them, and the two travel together because both are facts about WHERE
|
|
57
|
+
* this source's artifacts are published rather than about where its source is
|
|
58
|
+
* fetched. The declaration
|
|
59
|
+
* is copied verbatim onto the lock's `LockedBiomeSource` and no image
|
|
60
|
+
* coordinate is derived from it here, because deriving one is the image plane's
|
|
61
|
+
* question and this is the source plane. `resolve-images.mjs` is the reader: a
|
|
62
|
+
* source-stamped biome composes its refs under its own source's registry, and a
|
|
63
|
+
* source that declares none is REFUSED there rather than falling back to the
|
|
64
|
+
* vendor namespace. The kernel says why on the field itself: *"a plane that
|
|
65
|
+
* needs one and finds it absent must REFUSE rather than substitute the
|
|
66
|
+
* first-party registry, which would publish a customer's build into the vendor
|
|
67
|
+
* org."*
|
|
68
|
+
*
|
|
69
|
+
* This paragraph said `resolve-images.mjs` "REFUSES a source-stamped biome"
|
|
70
|
+
* flatly, and that was true only while reading the field was unimplemented. It
|
|
71
|
+
* is corrected rather than deleted because the sentence it turned into is the
|
|
72
|
+
* one that matters and is easy to lose: the refusal did not go away when the
|
|
73
|
+
* reader landed — it NARROWED, to an external source that declares no registry
|
|
74
|
+
* at all. A carrier that silently dropped the field would be the same defect
|
|
75
|
+
* the `sources[]` refusal in resolve-distribution.mjs exists to prevent, one
|
|
76
|
+
* plane over, which is why the copy is verbatim and the lock schema is
|
|
77
|
+
* `.strict()`.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
import { execFileSync } from 'node:child_process';
|
|
81
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
82
|
+
import { join } from 'node:path';
|
|
83
|
+
|
|
84
|
+
import { readBiomeAuthorizationModuleAt } from './authorization-base.mjs';
|
|
85
|
+
import { cacheDir } from './edition-workspace.mjs';
|
|
86
|
+
import { gitAuthEnv } from './git-source-auth.mjs';
|
|
87
|
+
import {
|
|
88
|
+
BIOME_GENERATED_INDEX_FILENAME,
|
|
89
|
+
loadKernelBiomeContracts,
|
|
90
|
+
manifestToIndexEntry,
|
|
91
|
+
readGitJsonAtCommit,
|
|
92
|
+
readGitTextAtCommitOrNull,
|
|
93
|
+
} from './lib.mjs';
|
|
94
|
+
|
|
95
|
+
/** Where fetched sources are cached: a directory under the configured cache root. Never an input to a lock. */
|
|
96
|
+
export const externalSourceCacheDir = () => join(cacheDir(), 'external-sources');
|
|
97
|
+
|
|
98
|
+
const EXACT_COMMIT = /^[a-f0-9]{40}$/;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Directory names never walked for biome manifests in an EXTERNAL source.
|
|
102
|
+
*
|
|
103
|
+
* This is deliberately a SEPARATE list from `lib.mjs`'s `SKIP_DIR_NAMES`, and
|
|
104
|
+
* the separation is the point rather than an oversight. That one encodes the
|
|
105
|
+
* VENDOR MONOREPO's layout — `templates` is `tooling/templates`,
|
|
106
|
+
* `openapi-fixture` is an `xema-base` test convention — and those are facts
|
|
107
|
+
* about this repository's neighbours, not facts about biome discovery. A
|
|
108
|
+
* customer's repository has its own layout and must not be graded against the
|
|
109
|
+
* vendor's; equally, a future vendor-layout skip must not silently start
|
|
110
|
+
* excluding directories inside a customer's tree.
|
|
111
|
+
*
|
|
112
|
+
* What is left is the intersection that is true of any git repository: build
|
|
113
|
+
* output and vendored dependencies are not authored biomes.
|
|
114
|
+
*/
|
|
115
|
+
const EXTERNAL_SKIP_DIR_NAMES = new Set(['node_modules', '.git', 'dist']);
|
|
116
|
+
|
|
117
|
+
function git(directory, args, env = process.env) {
|
|
118
|
+
return execFileSync('git', ['-C', directory, ...args], {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
env,
|
|
121
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
|
+
}).trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Environment for a git call against one declared source.
|
|
127
|
+
*
|
|
128
|
+
* `authTokenEnv` NAMES an environment variable; the token is never inlined in
|
|
129
|
+
* the manifest (`CredentialFreeGitUrlSchema` refuses a URL carrying userinfo).
|
|
130
|
+
* A declared-but-unset variable is REFUSED rather than falling back to the
|
|
131
|
+
* ambient credential helper: a source that states it needs a credential and
|
|
132
|
+
* silently resolves without one has resolved as somebody else, and the commit
|
|
133
|
+
* it pins would be whatever that other identity can see.
|
|
134
|
+
*/
|
|
135
|
+
export function gitEnvForSource(source, env = process.env) {
|
|
136
|
+
const base = { ...env, GIT_TERMINAL_PROMPT: '0' };
|
|
137
|
+
if (source.authTokenEnv === undefined) {return base;}
|
|
138
|
+
const token = env[source.authTokenEnv];
|
|
139
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`source "${source.name}" declares authTokenEnv "${source.authTokenEnv}", but that ` +
|
|
142
|
+
'environment variable is unset or empty. Refusing to fall back to the ambient git ' +
|
|
143
|
+
'credential helper: a declared credential that silently resolves as another identity ' +
|
|
144
|
+
'would pin whatever commit that identity can see.',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return gitAuthEnv(source.url, source.authUsername, token, base);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolve a declared ref to an exact commit AT THE REMOTE.
|
|
152
|
+
*
|
|
153
|
+
* A moving branch may DISCOVER an upgrade candidate; it must never be the
|
|
154
|
+
* IDENTITY of a released member. This is the only place a ref becomes a commit,
|
|
155
|
+
* and everything downstream — the checkout, the manifest reads, the lock —
|
|
156
|
+
* carries the commit.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveRemoteCommit(source, env = process.env) {
|
|
159
|
+
const ref = source.ref ?? 'HEAD';
|
|
160
|
+
// AN EXACT COMMIT IS ALREADY THE ANSWER, and it must not be sent to
|
|
161
|
+
// `ls-remote`, which matches REFS and would find nothing for a raw sha —
|
|
162
|
+
// reporting "resolves to nothing" for the one form that needs no resolving.
|
|
163
|
+
//
|
|
164
|
+
// This is the form a RELEASED edition should declare. A branch is DISCOVERY:
|
|
165
|
+
// measured on this repository during one session, `main` moved
|
|
166
|
+
// 92edf2cf -> ab87ee09 between two calls twenty minutes apart, so an edition
|
|
167
|
+
// declaring a branch re-pins on every resolve and its committed lock drifts
|
|
168
|
+
// whenever the source's owner pushes. The artifact stays deterministic
|
|
169
|
+
// either way — the lock records the commit, never the ref — but only an
|
|
170
|
+
// exact ref makes the RESOLVE reproducible.
|
|
171
|
+
if (EXACT_COMMIT.test(ref)) {return ref;}
|
|
172
|
+
let out;
|
|
173
|
+
try {
|
|
174
|
+
out = execFileSync('git', ['ls-remote', '--exit-code', source.url, ref], {
|
|
175
|
+
encoding: 'utf8',
|
|
176
|
+
env: gitEnvForSource(source, env),
|
|
177
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
178
|
+
}).trim();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`source "${source.name}": cannot resolve ref "${ref}" at ${source.url}. ` +
|
|
182
|
+
'Source resolution never falls back to another ref or another remote. ' +
|
|
183
|
+
`Underlying error: ${error.message}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const lines = out.split('\n').filter(Boolean);
|
|
187
|
+
if (lines.length === 0) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`source "${source.name}": ref "${ref}" resolves to nothing at ${source.url}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const commit = lines[0].split('\t')[0].trim();
|
|
193
|
+
if (!EXACT_COMMIT.test(commit)) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`source "${source.name}": ref "${ref}" resolved to "${commit}", which is not an ` +
|
|
196
|
+
'exact lowercase 40-hex commit',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return commit;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Fetch a declared git source and return the directory + the EXACT commit its
|
|
204
|
+
* manifests will be read at.
|
|
205
|
+
*
|
|
206
|
+
* The checkout is reused across calls and re-verified BY COMMIT, because
|
|
207
|
+
* `dist:check` resolves every edition in one process and a per-edition clone of
|
|
208
|
+
* a customer repository would be both slow and pointless. `rev-parse HEAD` is
|
|
209
|
+
* the authority for what was actually materialised — never the ref, and never
|
|
210
|
+
* what we asked for.
|
|
211
|
+
*/
|
|
212
|
+
export function materializeGitSource({
|
|
213
|
+
source,
|
|
214
|
+
cacheRoot = externalSourceCacheDir(),
|
|
215
|
+
env = process.env,
|
|
216
|
+
}) {
|
|
217
|
+
const gitEnv = gitEnvForSource(source, env);
|
|
218
|
+
const commit = resolveRemoteCommit(source, env);
|
|
219
|
+
const directory = join(cacheRoot, source.name);
|
|
220
|
+
if (!existsSync(join(directory, '.git'))) {
|
|
221
|
+
mkdirSync(directory, { recursive: true });
|
|
222
|
+
execFileSync('git', ['init', '--quiet', directory], { env: gitEnv });
|
|
223
|
+
git(directory, ['remote', 'add', 'origin', source.url], gitEnv);
|
|
224
|
+
} else {
|
|
225
|
+
// The cache is keyed by the DECLARED name, so a renamed or moved source
|
|
226
|
+
// reuses it: fetch from where the declaration says the source lives NOW.
|
|
227
|
+
git(directory, ['remote', 'set-url', 'origin', source.url], gitEnv);
|
|
228
|
+
}
|
|
229
|
+
const have = (() => {
|
|
230
|
+
try {
|
|
231
|
+
return git(directory, ['cat-file', '-e', `${commit}^{commit}`], gitEnv) === '';
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
if (!have) {
|
|
237
|
+
execFileSync(
|
|
238
|
+
'git',
|
|
239
|
+
['-C', directory, 'fetch', '--quiet', '--depth=1', 'origin', commit],
|
|
240
|
+
{ env: gitEnv, stdio: ['ignore', 'pipe', 'pipe'] },
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
git(directory, ['checkout', '--quiet', '--detach', commit], gitEnv);
|
|
244
|
+
return { directory, commit: pinExternalSourceCommit({ name: source.name, directory, commit }) };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The EXTERNAL source authority: what commit was actually read.
|
|
249
|
+
*
|
|
250
|
+
* `source-authority.mjs` is the FIRST-PARTY authority and must not be widened
|
|
251
|
+
* into this. Its `validateRepository` refuses any repository resolving outside
|
|
252
|
+
* the aggregator, and that containment protects every first-party repository at
|
|
253
|
+
* once — admitting one external path would remove it for all of them. Its
|
|
254
|
+
* `published` mode also derives a commit from `refs/remotes/origin/main` plus
|
|
255
|
+
* PUBLISHED IMAGE EVIDENCE, which a customer source cannot have and should not
|
|
256
|
+
* need in order to resolve.
|
|
257
|
+
*
|
|
258
|
+
* So this is a second, NARROWER derivation with the same shape: the commit is
|
|
259
|
+
* whatever the checkout is actually at. `LockedBiomeSource.commit` is the exact
|
|
260
|
+
* 40-hex commit the manifest was read at, never the declared ref.
|
|
261
|
+
*/
|
|
262
|
+
export function pinExternalSourceCommit({ name, directory, commit }) {
|
|
263
|
+
const head = git(directory, ['rev-parse', 'HEAD']);
|
|
264
|
+
if (!EXACT_COMMIT.test(head)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`source "${name}": checkout at ${directory} reports HEAD "${head}", which is not an ` +
|
|
267
|
+
'exact lowercase 40-hex commit',
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (commit !== undefined && head !== commit) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`source "${name}": checkout at ${directory} is at ${head}, expected ${commit}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const dirty = git(directory, ['status', '--porcelain', '--untracked-files=no']);
|
|
276
|
+
if (dirty.length > 0) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`source "${name}": checkout at ${directory} has uncommitted changes. An external source ` +
|
|
279
|
+
'is pinned by commit, so a dirty checkout would lock a revision that exists nowhere.',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return head;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Every biome manifest in an external checkout at an exact commit.
|
|
287
|
+
*
|
|
288
|
+
* Read from the COMMIT rather than the worktree, for the reason the first-party
|
|
289
|
+
* walk reads from a commit: the manifest and its generated index must describe
|
|
290
|
+
* the same revision, and a worktree read cannot promise that.
|
|
291
|
+
*/
|
|
292
|
+
export function listExternalBiomeManifests({ source, directory, commit }) {
|
|
293
|
+
const raw = execFileSync(
|
|
294
|
+
'git',
|
|
295
|
+
['-C', directory, 'ls-tree', '-r', '-z', '--name-only', commit],
|
|
296
|
+
{ encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 },
|
|
297
|
+
);
|
|
298
|
+
const found = [];
|
|
299
|
+
for (const repoPath of raw.split('\0').filter(Boolean)) {
|
|
300
|
+
if (!repoPath.endsWith(`/${'xema-biome.json'}`)) {continue;}
|
|
301
|
+
if (repoPath.split('/').some((seg) => EXTERNAL_SKIP_DIR_NAMES.has(seg))) {continue;}
|
|
302
|
+
const manifestDirectory = repoPath.slice(0, -'/xema-biome.json'.length);
|
|
303
|
+
const rel = `${source.name}/${repoPath}`;
|
|
304
|
+
const manifest = readGitJsonAtCommit(directory, repoPath, commit);
|
|
305
|
+
const indexRepoPath = `${manifestDirectory}/${BIOME_GENERATED_INDEX_FILENAME}`;
|
|
306
|
+
let generatedIndex;
|
|
307
|
+
try {
|
|
308
|
+
generatedIndex = readGitJsonAtCommit(directory, indexRepoPath, commit);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`${rel}: ${BIOME_GENERATED_INDEX_FILENAME} is missing at ${commit}: ${error.message}. ` +
|
|
312
|
+
'Every biome carries a committed generated index beside its manifest — it is the ' +
|
|
313
|
+
'authority for which biomes this one requires, and it is read at the SAME commit so ' +
|
|
314
|
+
'it cannot describe a different revision of the biome than the one being locked.',
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
// The biome's authorization module, read at the SAME commit as the
|
|
318
|
+
// manifest and by the SAME reader the first-party index uses, so a
|
|
319
|
+
// source-walked biome contributes its module to the edition's
|
|
320
|
+
// authorization base exactly like a platform biome. Absent ⇒ no module.
|
|
321
|
+
const authorizationModule = readBiomeAuthorizationModuleAt(
|
|
322
|
+
manifestDirectory,
|
|
323
|
+
(repoRelativePath) => readGitTextAtCommitOrNull(directory, repoRelativePath, commit),
|
|
324
|
+
rel,
|
|
325
|
+
);
|
|
326
|
+
found.push({
|
|
327
|
+
raw: manifest,
|
|
328
|
+
generatedIndex,
|
|
329
|
+
rel,
|
|
330
|
+
...(authorizationModule === null ? {} : { authorizationModule }),
|
|
331
|
+
// The LOGICAL source identity, not the physical repository. The URL is a
|
|
332
|
+
// source-management fact and lives in exactly one place, the lock's
|
|
333
|
+
// `LockedBiomeSource.url`; `repo` stays a stable handle so renaming the
|
|
334
|
+
// hosting repository does not move a biome's provenance.
|
|
335
|
+
repo: source.name,
|
|
336
|
+
path: `${source.name}/${manifestDirectory}`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return found.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* THE STAMP. One place, so there is one thing to audit and one thing to mutate
|
|
344
|
+
* when proving the fence works.
|
|
345
|
+
*/
|
|
346
|
+
function stampEntry(entry, sourceName) {
|
|
347
|
+
return { ...entry, source: sourceName };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* A biome drawn from a declared source is THIRD-PARTY, and its manifest cannot
|
|
352
|
+
* say otherwise. `xema.trustTier` is the author's own statement; in the vendor's
|
|
353
|
+
* graph the vendor is the author, so the index build reads it. Here the author
|
|
354
|
+
* is whoever owns the declared repository, and a manifest that names the
|
|
355
|
+
* vendor's origin would place itself in the vendor's trust domain by default.
|
|
356
|
+
* The refusal is on this path and not in the manifest reader, which serves both.
|
|
357
|
+
*/
|
|
358
|
+
function assertSourceWalkedOrigin(entry, sourceName) {
|
|
359
|
+
const { BiomeOrigin } = loadKernelBiomeContracts();
|
|
360
|
+
if (entry.origin !== BiomeOrigin.ThirdParty) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`source "${sourceName}": biome "${entry.id}" declares xema.trustTier "${entry.origin}". A biome drawn from a ` +
|
|
363
|
+
`declared source is "${BiomeOrigin.ThirdParty}" and cannot declare another origin: "${BiomeOrigin.FirstParty}" ` +
|
|
364
|
+
'is the vendor\'s, and is never self-declared by a source the edition names.',
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return entry;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The `LockedBiomeSource` record for a declared source, pinned to a commit.
|
|
372
|
+
* Only the kernel's declared fields are carried, and `.strict()` on the lock
|
|
373
|
+
* schema is what keeps that honest.
|
|
374
|
+
*/
|
|
375
|
+
function lockedSourceRecord(source, commit) {
|
|
376
|
+
const record = {
|
|
377
|
+
kind: source.kind,
|
|
378
|
+
name: source.name,
|
|
379
|
+
url: source.url,
|
|
380
|
+
commit,
|
|
381
|
+
};
|
|
382
|
+
if (source.ref !== undefined) {record.ref = source.ref;}
|
|
383
|
+
if (source.imageRegistry !== undefined) {record.imageRegistry = source.imageRegistry;}
|
|
384
|
+
// `imageNamePrefix` IS DELIBERATELY NOT PERSISTED, and this is the producer
|
|
385
|
+
// fix for a defect that was previously repaired in the ARTIFACT.
|
|
386
|
+
//
|
|
387
|
+
// It is RESOLVER INPUT: `resolve-images.mjs` composes an image name from the
|
|
388
|
+
// DECLARED source (`<imageRegistry>/<imageNamePrefix><subject>`) and the lock
|
|
389
|
+
// then carries the fully-qualified `imageRef`, so the prefix is already spent
|
|
390
|
+
// by the time a lock exists. Measured across all 28 repositories with comment
|
|
391
|
+
// lines stripped: ZERO executable readers of `biomes[].source.imageNamePrefix`
|
|
392
|
+
// — xema-deploy's three mentions are two comments and two self-test fixtures
|
|
393
|
+
// (control, same probe: `imageRepository` returns 4 executable). Write-only.
|
|
394
|
+
//
|
|
395
|
+
// And carrying it is not merely useless, it is BREAKING. `@xemahq/contracts`'
|
|
396
|
+
// DistributionLockSchema is `.strict()` and only learned this key in 0.10.0,
|
|
397
|
+
// so any pinned image resolving `^0.9.x` REFUSES THE WHOLE LOCK at boot —
|
|
398
|
+
// `Unrecognized key: "imageNamePrefix"`, before Nest starts. That took
|
|
399
|
+
// presenca-dev's three lock consumers down on 2026-09-17 and was fixed by
|
|
400
|
+
// removing the key from the SEALED LOCK, leaving this line to re-emit it on
|
|
401
|
+
// the next resolve. A workaround standing where a fix belongs removes the
|
|
402
|
+
// signal; this is the fix.
|
|
403
|
+
return record;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Walk every declared source for one edition.
|
|
408
|
+
*
|
|
409
|
+
* Returns the stamped index entries and the `LockedBiomeSource` record per
|
|
410
|
+
* source name. `materialize` is injectable so the contract suite can drive a
|
|
411
|
+
* real local git repository instead of reaching the network — the walk under
|
|
412
|
+
* test is then the real one, not a fixture of it.
|
|
413
|
+
*/
|
|
414
|
+
export async function walkDeclaredSources({
|
|
415
|
+
sources = [],
|
|
416
|
+
materialize = materializeGitSource,
|
|
417
|
+
env = process.env,
|
|
418
|
+
} = {}) {
|
|
419
|
+
const entries = [];
|
|
420
|
+
const sourceRecords = new Map();
|
|
421
|
+
for (const source of sources) {
|
|
422
|
+
if (source.kind !== 'git') {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`source "${source.name}" declares kind "${source.kind}", which this walk does not ` +
|
|
425
|
+
'resolve. The kernel\'s LockedBiomeSourceSchema can only record a GIT source ' +
|
|
426
|
+
'(kind is z.literal(Git)), so an oci or npm source has nowhere to be pinned in the ' +
|
|
427
|
+
'lock — resolving it would produce an edition whose members cannot be traced to a ' +
|
|
428
|
+
'revision. Refusing rather than resolving.',
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const { directory, commit } = materialize({ source, env });
|
|
432
|
+
if (!EXACT_COMMIT.test(String(commit))) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`source "${source.name}": walk produced commit "${commit}", which is not exact 40-hex`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const manifests = listExternalBiomeManifests({ source, directory, commit });
|
|
438
|
+
if (manifests.length === 0) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`source "${source.name}" at ${commit} carries no biome manifests. A declared source ` +
|
|
441
|
+
'that contributes nothing is a broken declaration, not an empty one: the edition ' +
|
|
442
|
+
'naming it would lock without the biomes the declaration was for.',
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
for (const manifest of manifests) {
|
|
446
|
+
entries.push(stampEntry(assertSourceWalkedOrigin(await manifestToIndexEntry(manifest), source.name), source.name));
|
|
447
|
+
}
|
|
448
|
+
sourceRecords.set(source.name, lockedSourceRecord(source, commit));
|
|
449
|
+
}
|
|
450
|
+
// The last point at which an unstamped entry can be caught before it reaches
|
|
451
|
+
// the shared index, where absent-is-the-identity makes it admissible
|
|
452
|
+
// everywhere. See this file's header.
|
|
453
|
+
const unstamped = entries.filter((entry) => entry.source === undefined);
|
|
454
|
+
if (unstamped.length > 0) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`the source walk produced ${unstamped.length} UNSTAMPED entr(ies) ` +
|
|
457
|
+
`(${unstamped.map((e) => e.id).join(', ')}). An unstamped entry is platform-default ` +
|
|
458
|
+
'and admissible to every edition, so this would publish a private biome into the ' +
|
|
459
|
+
'open-source edition. Refusing.',
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return { entries, sourceRecords };
|
|
463
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { workspaceSection } from './edition-config.mjs';
|
|
2
|
+
|
|
3
|
+
// THE DECLARED SOURCE ID OF EACH FIRST-PARTY REPOSITORY THAT NAMES A RUNTIME
|
|
4
|
+
// SUBJECT AFTER ITS SOURCE.
|
|
5
|
+
//
|
|
6
|
+
// An identifier is stable iff it survives a re-implementation of what is behind
|
|
7
|
+
// it. A runtime subject named after its source (today: the composed-process
|
|
8
|
+
// image, `<source id>-composed-process`) must therefore take that name from a
|
|
9
|
+
// DECLARED id, never from where the repository happens to be hosted: a rename
|
|
10
|
+
// or a host move changes `repositoryUrl` — provenance — and must change no
|
|
11
|
+
// runtime identity.
|
|
12
|
+
//
|
|
13
|
+
// An external source declares its id in the edition (`sources[].name`, which
|
|
14
|
+
// the walk also stamps as the biome's `repo`). A first-party repository has no
|
|
15
|
+
// `sources[]` entry, so its id is declared in the data root's
|
|
16
|
+
// `edition-workspace.json` (`firstPartySources`), keyed by the `repo` handle the
|
|
17
|
+
// runtime image source index pins it under. The key is a lookup handle and may
|
|
18
|
+
// move with the aggregator layout; the VALUE is the identity and must not.
|
|
19
|
+
//
|
|
20
|
+
// EVERY repository `runtime-image-source-index.json` pins is declared there (a
|
|
21
|
+
// contract test holds the two sets equal), so a newly pinned repository is
|
|
22
|
+
// refused at once rather than the day it first declares a composed image. A
|
|
23
|
+
// lookup of an undeclared repository is refused (`firstPartySourceId`), never
|
|
24
|
+
// named by a fallback.
|
|
25
|
+
|
|
26
|
+
/** The kernel's source-handle grammar (`BiomeSourceNameSchema`): lowercase kebab-case. */
|
|
27
|
+
export const DECLARED_SOURCE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
28
|
+
|
|
29
|
+
/** The data root's declared id of each first-party repository (`firstPartySources`). */
|
|
30
|
+
export const firstPartySourceIds = () =>
|
|
31
|
+
workspaceSection('firstPartySources', 'naming a runtime subject after its first-party source');
|
|
32
|
+
|
|
33
|
+
export class UndeclaredSourceIdError extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'UndeclaredSourceIdError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function assertDeclaredSourceId(sourceId, where) {
|
|
41
|
+
if (typeof sourceId !== 'string' || !DECLARED_SOURCE_ID_PATTERN.test(sourceId)) {
|
|
42
|
+
throw new UndeclaredSourceIdError(
|
|
43
|
+
`${where}: declared source id ${JSON.stringify(sourceId)} is not lowercase kebab-case ` +
|
|
44
|
+
`(${DECLARED_SOURCE_ID_PATTERN})`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return sourceId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function firstPartySourceId(repo) {
|
|
51
|
+
const ids = firstPartySourceIds();
|
|
52
|
+
if (!Object.hasOwn(ids, repo)) {
|
|
53
|
+
throw new UndeclaredSourceIdError(
|
|
54
|
+
`first-party repository "${repo}" has no declared source id — declare it in ` +
|
|
55
|
+
'edition-workspace.json under "firstPartySources"; a runtime identity is never derived from a repository name or URL',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return assertDeclaredSourceId(ids[repo], `first-party repository "${repo}"`);
|
|
59
|
+
}
|