@xemahq/xema 0.25.1 → 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.
Files changed (239) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/appliance-base/images.lock.json +126 -126
  3. package/dist/assets/appliance-lean/images.lock.json +123 -123
  4. package/dist/assets/cloud/images.lock.json +126 -126
  5. package/dist/assets/core/images.lock.json +120 -120
  6. package/dist/assets/distribution-assets.manifest.json +15 -16
  7. package/dist/assets/minimal/images.lock.json +108 -108
  8. package/dist/assets/oss/images.lock.json +126 -126
  9. package/dist/assets/starter/images.lock.json +123 -123
  10. package/dist/cli.js +2 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/dev.js +1 -0
  13. package/dist/commands/dev.js.map +1 -1
  14. package/dist/commands/down.d.ts.map +1 -1
  15. package/dist/commands/down.js +7 -1
  16. package/dist/commands/down.js.map +1 -1
  17. package/dist/commands/edition.d.ts +3 -0
  18. package/dist/commands/edition.d.ts.map +1 -0
  19. package/dist/commands/edition.js +46 -0
  20. package/dist/commands/edition.js.map +1 -0
  21. package/dist/commands/init.js +2 -2
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/serve.js +1 -0
  24. package/dist/commands/serve.js.map +1 -1
  25. package/dist/commands/up.js +1 -0
  26. package/dist/commands/up.js.map +1 -1
  27. package/dist/dev/env-defaults.d.ts.map +1 -1
  28. package/dist/dev/env-defaults.js +6 -1
  29. package/dist/dev/env-defaults.js.map +1 -1
  30. package/dist/dev/infra-preflight.d.ts +3 -2
  31. package/dist/dev/infra-preflight.d.ts.map +1 -1
  32. package/dist/dev/infra-preflight.js +18 -8
  33. package/dist/dev/infra-preflight.js.map +1 -1
  34. package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
  35. package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
  36. package/dist/runtime/backing-registration.d.ts.map +1 -1
  37. package/dist/runtime/backing-registration.js +7 -2
  38. package/dist/runtime/backing-registration.js.map +1 -1
  39. package/dist/runtime/config/schema.d.ts +1 -0
  40. package/dist/runtime/config/schema.d.ts.map +1 -1
  41. package/dist/runtime/custody/local-custody.d.ts +12 -0
  42. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  43. package/dist/runtime/custody/local-custody.js +22 -0
  44. package/dist/runtime/custody/local-custody.js.map +1 -0
  45. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  46. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  47. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  48. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  49. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  50. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  51. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  52. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  53. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  54. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  55. package/dist/runtime/custody/openbao-http-client.js +154 -0
  56. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  57. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  58. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  59. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  60. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  61. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  62. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  63. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  64. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  65. package/dist/runtime/edition/run-entry.d.ts +9 -0
  66. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  67. package/dist/runtime/edition/run-entry.js +36 -0
  68. package/dist/runtime/edition/run-entry.js.map +1 -0
  69. package/dist/runtime/edition/verb-map.d.ts +60 -0
  70. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  71. package/dist/runtime/edition/verb-map.js +146 -0
  72. package/dist/runtime/edition/verb-map.js.map +1 -0
  73. package/dist/runtime/env-seeder.d.ts +3 -0
  74. package/dist/runtime/env-seeder.d.ts.map +1 -1
  75. package/dist/runtime/env-seeder.js +20 -3
  76. package/dist/runtime/env-seeder.js.map +1 -1
  77. package/dist/runtime/launch.d.ts +3 -1
  78. package/dist/runtime/launch.d.ts.map +1 -1
  79. package/dist/runtime/launch.js +37 -8
  80. package/dist/runtime/launch.js.map +1 -1
  81. package/dist/runtime/paths.d.ts +1 -2
  82. package/dist/runtime/paths.d.ts.map +1 -1
  83. package/dist/runtime/paths.js +0 -10
  84. package/dist/runtime/paths.js.map +1 -1
  85. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  86. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  87. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  88. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  89. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  90. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  91. package/dist/runtime/runner/chart-preflight.js +30 -69
  92. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  93. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  94. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  95. package/dist/runtime/runner/chart-pull.js +96 -0
  96. package/dist/runtime/runner/chart-pull.js.map +1 -0
  97. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  98. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  99. package/dist/runtime/runner/chart-resolver.js +16 -62
  100. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  101. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  102. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  103. package/dist/runtime/runner/chart-set-lock.js +55 -99
  104. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  105. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  106. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  107. package/dist/runtime/runner/helm-cli.js +9 -7
  108. package/dist/runtime/runner/helm-cli.js.map +1 -1
  109. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  110. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  111. package/dist/runtime/runner/helm-runner.js +18 -16
  112. package/dist/runtime/runner/helm-runner.js.map +1 -1
  113. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  114. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  115. package/dist/runtime/sources/kinds/git.js +8 -5
  116. package/dist/runtime/sources/kinds/git.js.map +1 -1
  117. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  118. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  119. package/dist/runtime/sources/manifest-walk.js +8 -3
  120. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  121. package/dist/runtime/sources/schema.d.ts +3 -0
  122. package/dist/runtime/sources/schema.d.ts.map +1 -1
  123. package/dist/runtime/sources/schema.js +6 -1
  124. package/dist/runtime/sources/schema.js.map +1 -1
  125. package/dist/runtime/supervisor.d.ts +3 -0
  126. package/dist/runtime/supervisor.d.ts.map +1 -1
  127. package/dist/runtime/supervisor.js +2 -0
  128. package/dist/runtime/supervisor.js.map +1 -1
  129. package/edition/README.md +315 -0
  130. package/edition/assemble-producer-input.mjs +1251 -0
  131. package/edition/authorization-base.mjs +347 -0
  132. package/edition/biome-content.mjs +268 -0
  133. package/edition/build-biome-index.mjs +175 -0
  134. package/edition/build-platform-service-index.mjs +288 -0
  135. package/edition/build-runtime-image-source-index.mjs +821 -0
  136. package/edition/built-verifier.mjs +55 -0
  137. package/edition/catalogue-edition.mjs +49 -0
  138. package/edition/catalogue-fetch.mjs +64 -0
  139. package/edition/catalogue.mjs +135 -0
  140. package/edition/chart-set-source-commit.mjs +92 -0
  141. package/edition/check-committed-image-evidence.mjs +383 -0
  142. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  143. package/edition/comment-blanker.mjs +75 -0
  144. package/edition/connector-source-authority.mjs +60 -0
  145. package/edition/customer-definition.mjs +183 -0
  146. package/edition/customer-flags.mjs +7 -0
  147. package/edition/develop-customer-edition.mjs +99 -0
  148. package/edition/develop-edition-resolve.mjs +65 -0
  149. package/edition/develop-edition.mjs +274 -0
  150. package/edition/develop-freeze.mjs +173 -0
  151. package/edition/develop-image-coordinate.mjs +54 -0
  152. package/edition/develop-seal.mjs +77 -0
  153. package/edition/develop-snapshot-inputs.mjs +104 -0
  154. package/edition/dist-check.mjs +986 -0
  155. package/edition/distribution-evidence-set.mjs +987 -0
  156. package/edition/distribution-paths.mjs +191 -0
  157. package/edition/edition-artifact.mjs +270 -0
  158. package/edition/edition-biome-manifests.mjs +85 -0
  159. package/edition/edition-channel.mjs +6 -0
  160. package/edition/edition-chart-set.mjs +93 -0
  161. package/edition/edition-config.mjs +207 -0
  162. package/edition/edition-constants.mjs +57 -0
  163. package/edition/edition-contract.json +23 -0
  164. package/edition/edition-list.mjs +35 -0
  165. package/edition/edition-locks.mjs +69 -0
  166. package/edition/edition-protocol.mjs +228 -0
  167. package/edition/edition-workspace.mjs +176 -0
  168. package/edition/evidence-checkout.mjs +418 -0
  169. package/edition/evidence-grammars.mjs +16 -0
  170. package/edition/evidence-trust-pin.mjs +304 -0
  171. package/edition/evidence-trust-policy.mjs +256 -0
  172. package/edition/external-source-walk.mjs +463 -0
  173. package/edition/first-party-source-ids.mjs +59 -0
  174. package/edition/fleet-coordinate-finality.mjs +350 -0
  175. package/edition/git-source-auth.mjs +52 -0
  176. package/edition/image-evidence-replay-state.mjs +242 -0
  177. package/edition/image-evidence.mjs +1225 -0
  178. package/edition/kernel-contract-authority.mjs +443 -0
  179. package/edition/lib.mjs +3018 -0
  180. package/edition/no-follow-file.mjs +94 -0
  181. package/edition/oci-evidence-verbs.mjs +392 -0
  182. package/edition/oci-image-source-evidence.mjs +239 -0
  183. package/edition/oras.mjs +648 -0
  184. package/edition/pin-source.mjs +227 -0
  185. package/edition/plan-image-evidence-refresh.mjs +408 -0
  186. package/edition/platform-lock-readability.mjs +892 -0
  187. package/edition/platform-service-class.mjs +18 -0
  188. package/edition/platform-service-source-authority.mjs +105 -0
  189. package/edition/positive-closure.mjs +162 -0
  190. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  191. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  192. package/edition/produce-image-source-evidence.mjs +765 -0
  193. package/edition/producer-evidence-bundle.mjs +157 -0
  194. package/edition/registry-credentials.mjs +193 -0
  195. package/edition/release-catalogue.mjs +39 -0
  196. package/edition/release-channel-guard.mjs +99 -0
  197. package/edition/release-edition-artifact.mjs +158 -0
  198. package/edition/remedy-command.mjs +89 -0
  199. package/edition/repin-evidence-trust.mjs +101 -0
  200. package/edition/required-resource-provider.mjs +58 -0
  201. package/edition/resolve-chart-set.mjs +415 -0
  202. package/edition/resolve-customer-edition.mjs +288 -0
  203. package/edition/resolve-distribution.mjs +2318 -0
  204. package/edition/resolve-edition.mjs +186 -0
  205. package/edition/resolve-images.mjs +1606 -0
  206. package/edition/seal-image-digests.mjs +855 -0
  207. package/edition/sealed-evidence-accept.mjs +82 -0
  208. package/edition/source-authority.mjs +725 -0
  209. package/edition/source-closure.mjs +116 -0
  210. package/edition/strict-json.mjs +121 -0
  211. package/edition/string-order.mjs +5 -0
  212. package/edition/trust-policy-arming.mjs +120 -0
  213. package/edition/trusted-executable.mjs +166 -0
  214. package/edition/verifier-binding.mjs +45 -0
  215. package/edition/verifier-files.mjs +8 -0
  216. package/edition/verifier-kernel-schemas.mjs +22 -0
  217. package/edition/verifier-provenance.mjs +64 -0
  218. package/edition/verify-image-evidence.mjs +430 -0
  219. package/edition/verify-image-platforms.mjs +495 -0
  220. package/edition/why.mjs +56 -0
  221. package/edition/workspace-sections.mjs +195 -0
  222. package/examples/06-biome-sources.yaml +1 -0
  223. package/package.json +18 -7
  224. package/dist/commands/dist.d.ts +0 -16
  225. package/dist/commands/dist.d.ts.map +0 -1
  226. package/dist/commands/dist.js +0 -182
  227. package/dist/commands/dist.js.map +0 -1
  228. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  229. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  230. package/dist/runtime/distribution/available-biome.js +0 -154
  231. package/dist/runtime/distribution/available-biome.js.map +0 -1
  232. package/dist/runtime/distribution/compose.d.ts +0 -23
  233. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  234. package/dist/runtime/distribution/compose.js +0 -48
  235. package/dist/runtime/distribution/compose.js.map +0 -1
  236. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  237. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  238. package/dist/runtime/runner/chart-trust.js +0 -270
  239. 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
+ }