@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,350 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // fleet-coordinate-finality — has this coordinate FINISHED MOVING?
3
+ //
4
+ // WHY THIS EXISTS
5
+ // ---------------
6
+ // A seal pins what a MUTABLE TAG currently resolves to. `imageRef:contentHash`
7
+ // is a build-input key, not a content address: two builds of one input set wear
8
+ // the same tag, and the arm64 lane REPUBLISHES that tag by design —
9
+ // `imagetools create -t :<contentHash>` writes a NEW index at the SAME
10
+ // coordinate. So sealing a coordinate that is still awaiting its arm64
11
+ // supplement pins a manifest the tag will stop serving.
12
+ //
13
+ // THE REGRESSION THIS WAS WRITTEN FOR, measured — and how it is now graded
14
+ // -----------------------------------------------------------------------
15
+ // `xema-internal` was sealed at 09:25 on 2026-09-18 while 40 of its coordinates
16
+ // were still amd64-only, awaiting the supplement. The arm64 backfill — the
17
+ // CORRECT repair — then moved those digests, and 63 of its 66 seals named a
18
+ // manifest their tag no longer served.
19
+ //
20
+ // Its own per-edition verification PASSED, and passed CORRECTLY: `xema-internal`
21
+ // declares `requiredPlatforms: ["linux/amd64"]`, and amd64-only satisfies that.
22
+ // No rule the sealer could evaluate was violated.
23
+ //
24
+ // WHY A PER-EDITION CHECK CANNOT CATCH IT — the whole point of this module
25
+ // -----------------------------------------------------------------------
26
+ // `appliance-base` and `appliance-lean` declare `linux/arm64`, and they share 63
27
+ // of those coordinates with `xema-internal` by identical `imageRef` AND
28
+ // `contentHash`. So `xema-internal`'s stability depended on a platform
29
+ // requirement declared in an edition it cannot see.
30
+ //
31
+ // > PLATFORM VERIFICATION IS PER-EDITION. FINALITY IS A FLEET PROPERTY.
32
+ //
33
+ // OWNER RULING 2026-09-29 NARROWS WHAT IS REFUSED. Finality is graded against
34
+ // the sealing edition's OWN requiredPlatforms. The xema-internal shape above —
35
+ // an amd64-only edition sealing while another edition's arm64 is pending — is
36
+ // now ALLOWED and RECORDED (`pendingElsewhere`), the residual accepted: its
37
+ // sealed digest stays resolvable and serves every platform it requires. A
38
+ // coordinate pending a platform the sealing edition itself declares is still
39
+ // refused. The fleet map is still built from every edition, because the
40
+ // recorded residual must name who is waiting.
41
+ //
42
+ // A per-edition sealer cannot express that even in principle. This module is the
43
+ // fleet-scoped half, and it lives HERE — beside the sealer, in the repository
44
+ // that holds every edition's lock — because that is where the data and the edit
45
+ // both are. An aggregator gate would grade this repository at a RECORDED
46
+ // GITLINK: it would fire late, in a repository nobody was editing, about a
47
+ // change made somewhere else.
48
+ //
49
+ // IT RESOLVES NOTHING ITSELF
50
+ // --------------------------
51
+ // `verifyDistributionPlatforms` already fetches each index, hash-checks the
52
+ // bytes, walks every child manifest BY DIGEST and reads each image CONFIG for
53
+ // its real platform — and it already returns `publishedPlatforms`, the FULL set
54
+ // the coordinate serves rather than only the subset this edition required. That
55
+ // return value is the input here. There is no second registry walk and no
56
+ // second opinion about what a coordinate serves.
57
+ //
58
+ // WHAT IT DOES NOT COVER, said out loud rather than discovered
59
+ // -----------------------------------------------------------
60
+ // The fleet is the EDITIONS ROOT IT IS GIVEN. Run against a customer's own root
61
+ // it grades that customer's editions and cannot see the vendor's, because they
62
+ // are not there to read. That is why every verdict prints its corpus — the
63
+ // number of editions and coordinates graded — so the scope of an answer is
64
+ // visible in the answer rather than assumed from the fact that it passed.
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+
67
+ import { existsSync, readFileSync } from 'node:fs';
68
+ import { join } from 'node:path';
69
+
70
+ import { discoverEditionDirectories } from './distribution-paths.mjs';
71
+ import { platformIdentity } from './verify-image-platforms.mjs';
72
+
73
+ export const IMAGE_LOCK_FILENAME = 'images.lock.json';
74
+
75
+ /**
76
+ * The coordinate a subject names, in the ONE spelling the verifier uses.
77
+ *
78
+ * `verifyDistributionPlatforms` builds `${imageRef}:${contentHash}` and returns
79
+ * it as `coordinate` on every verified subject. This function must produce the
80
+ * same string for the lock-side map, or the two halves would silently fail to
81
+ * join and every lookup would miss — which is a refusal that grades nothing
82
+ * while reporting success. The join is asserted rather than trusted: see the
83
+ * unknown-coordinate throw in {@link assertCoordinatesFinal}.
84
+ */
85
+ export function coordinateOf(subject) {
86
+ return `${subject.imageRef}:${subject.contentHash}`;
87
+ }
88
+
89
+ /**
90
+ * Pure: every platform any edition requires at each coordinate, and WHO
91
+ * requires it.
92
+ *
93
+ * `editions` is `[{ distributionId, subjects }]` — already read, so the
94
+ * decision table is testable without a filesystem or a registry.
95
+ *
96
+ * The returned shape is `Map<coordinate, Map<platformIdentity, string[]>>`,
97
+ * where the array is the sorted edition ids requiring that platform there. The
98
+ * REQUIRERS are carried rather than only the platforms because a refusal that
99
+ * cannot name who is still waiting sends the reader to look for the reason
100
+ * themselves, and the reason is in another edition's declaration.
101
+ */
102
+ export function collectFleetPlatformRequirements({ editions }) {
103
+ if (!Array.isArray(editions) || editions.length === 0) {
104
+ throw new Error(
105
+ 'fleet-coordinate-finality: no edition was supplied; a fleet requirement map ' +
106
+ 'built over zero editions would make every coordinate look final, which is ' +
107
+ 'the refusal passing while grading nothing',
108
+ );
109
+ }
110
+ const byCoordinate = new Map();
111
+ for (const edition of editions) {
112
+ const { distributionId, subjects } = edition;
113
+ if (typeof distributionId !== 'string' || distributionId.length === 0) {
114
+ throw new Error(
115
+ 'fleet-coordinate-finality: an edition was supplied with no distributionId',
116
+ );
117
+ }
118
+ if (!Array.isArray(subjects) || subjects.length === 0) {
119
+ throw new Error(
120
+ `fleet-coordinate-finality: "${distributionId}" has an image lock declaring no ` +
121
+ 'subjects. An edition that contributes no coordinate is indistinguishable ' +
122
+ 'from one this pass failed to read, so it is refused rather than skipped.',
123
+ );
124
+ }
125
+ for (const subject of subjects) {
126
+ if (
127
+ !Array.isArray(subject.requiredPlatforms) ||
128
+ subject.requiredPlatforms.length === 0
129
+ ) {
130
+ throw new Error(
131
+ `fleet-coordinate-finality: "${distributionId}" subject ${subject.kind}:${subject.id} ` +
132
+ 'declares no requiredPlatforms. It would contribute nothing to the fleet map ' +
133
+ 'and silently weaken the refusal for every edition sharing its coordinate.',
134
+ );
135
+ }
136
+ const coordinate = coordinateOf(subject);
137
+ let platforms = byCoordinate.get(coordinate);
138
+ if (!platforms) {
139
+ platforms = new Map();
140
+ byCoordinate.set(coordinate, platforms);
141
+ }
142
+ for (const platform of subject.requiredPlatforms) {
143
+ const identity = platformIdentity(platform);
144
+ let requirers = platforms.get(identity);
145
+ if (!requirers) {
146
+ requirers = new Set();
147
+ platforms.set(identity, requirers);
148
+ }
149
+ requirers.add(distributionId);
150
+ }
151
+ }
152
+ }
153
+ // Freeze the requirer sets into sorted arrays so a message built from this map
154
+ // is deterministic, and so a caller cannot mutate the map it was handed.
155
+ return new Map(
156
+ [...byCoordinate].map(([coordinate, platforms]) => [
157
+ coordinate,
158
+ new Map(
159
+ [...platforms].map(([identity, requirers]) => [
160
+ identity,
161
+ [...requirers].sort(),
162
+ ]),
163
+ ),
164
+ ]),
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Read every edition's image lock under `editionsRoot`, in BOTH tiers.
170
+ *
171
+ * A pending-merge edition is a real edition that will ship its coordinates, so
172
+ * a platform IT requires is just as much a reason a coordinate can still move.
173
+ * Measured 2026-09-19: the two pending editions carry a declaration and no lock,
174
+ * so today this widening admits nothing — which is the point at which to add it,
175
+ * rather than after a pending edition has acquired one.
176
+ *
177
+ * TWO ABSENCES THAT ARE NOT THE SAME, and conflating them is how this check
178
+ * would quietly stop working:
179
+ *
180
+ * * NO LOCK FILE — the edition makes no coordinate claim at all. It
181
+ * contributes nothing, legitimately, and is COUNTED and reported so the
182
+ * corpus line shows it was seen and skipped.
183
+ * * A LOCK THAT CANNOT BE READ — refused. An unreadable lock contributes no
184
+ * requirements either, and the difference between "requires nothing" and
185
+ * "we failed to find out" is the whole value of the refusal.
186
+ *
187
+ * `JSON.parse` rather than the kernel's `parseImageLock` is deliberate. Only
188
+ * three fields are read — `imageRef`, `contentHash`, `requiredPlatforms` — and
189
+ * requiring a BUILT KERNEL to grade the OTHER editions would make this refusal
190
+ * unrunnable in exactly the checkouts that have no kernel to build. The edition
191
+ * being sealed is still parsed by the kernel on the path that seals it.
192
+ */
193
+ export function readFleetImageLocks(editionsRoot) {
194
+ const editions = [];
195
+ const withoutLock = [];
196
+ for (const { id, directory } of discoverEditionDirectories(editionsRoot)) {
197
+ const lockPath = join(directory, IMAGE_LOCK_FILENAME);
198
+ if (!existsSync(lockPath)) {
199
+ withoutLock.push(id);
200
+ continue;
201
+ }
202
+ let raw;
203
+ try {
204
+ raw = JSON.parse(readFileSync(lockPath, 'utf8'));
205
+ } catch (error) {
206
+ throw new Error(
207
+ `fleet-coordinate-finality: cannot read ${lockPath}: ${error.message}. ` +
208
+ 'Refusing to grade coordinate finality over a fleet this pass could not ' +
209
+ 'fully read — an unreadable lock declares no requirement, which would make ' +
210
+ 'a coordinate look final because it was never asked about.',
211
+ );
212
+ }
213
+ editions.push({ distributionId: id, subjects: raw.subjects });
214
+ }
215
+ if (editions.length === 0) {
216
+ throw new Error(
217
+ `fleet-coordinate-finality: found no image lock under ${editionsRoot}; an empty ` +
218
+ 'scan is not a result',
219
+ );
220
+ }
221
+ return { editions, withoutLock: withoutLock.sort() };
222
+ }
223
+
224
+ /**
225
+ * Refuse to seal a coordinate THIS edition still awaits a platform at; record —
226
+ * and allow — one only ANOTHER edition still awaits.
227
+ *
228
+ * Owner ruling 2026-09-29: finality is TARGET-PLATFORM aware. A coordinate
229
+ * pending only a platform this edition does not declare is the accepted
230
+ * residual, returned as `pendingElsewhere` so the caller prints it. A later
231
+ * supplement for that other edition may move the tag's digest; this edition's
232
+ * seal keeps pinning the manifest it verified, which still resolves by digest
233
+ * and still carries every platform this edition requires.
234
+ *
235
+ * `verified` is `verifyDistributionPlatforms(...).subjects`, each carrying
236
+ * `coordinate` and `publishedPlatforms` — the platforms the registry ACTUALLY
237
+ * serves there, which is strictly wider than the subset this edition required.
238
+ *
239
+ * THE SUBJECT SET IS THE WRITES, NOT THE LOCK. This grades the subjects about to
240
+ * RECEIVE a digest, because that is the moment the regression is introduced. An
241
+ * already-sealed subject is past it: re-refusing one would make the daily
242
+ * reconciler permanently red with no action available to clear it, and a gate
243
+ * that cannot be cleared is one people learn to bypass.
244
+ *
245
+ * Throws with `error.notFinal === true` — a PROPERTY, never a message match, so
246
+ * the reconciler's skip cannot silently widen the day this wording changes. Same
247
+ * discipline as `error.status` on the not-published-yet path.
248
+ */
249
+ export function assertCoordinatesFinal({
250
+ distributionId,
251
+ writes,
252
+ verified,
253
+ fleetRequirements,
254
+ }) {
255
+ const graded = [];
256
+ const pendingElsewhere = [];
257
+ if (!Array.isArray(writes) || writes.length === 0) {
258
+ return { distributionId, graded, coordinateCount: 0, pendingElsewhere };
259
+ }
260
+ const byKey = new Map(
261
+ verified.map((entry) => [`${entry.subjectKind}:${entry.subjectId}`, entry]),
262
+ );
263
+ const blockers = [];
264
+ for (const write of writes) {
265
+ const entry = byKey.get(write.key);
266
+ if (!entry) {
267
+ throw new Error(
268
+ `fleet-coordinate-finality: ${write.key} is planned for sealing but has no ` +
269
+ 'verification result to read published platforms from; refusing to grade ' +
270
+ 'finality for a subject the verifier did not cover',
271
+ );
272
+ }
273
+ const required = fleetRequirements.get(entry.coordinate);
274
+ if (!required) {
275
+ // NON-VACUITY GUARD. The edition being sealed is itself one of the
276
+ // editions read into the map, so every coordinate it seals MUST be in
277
+ // there. A miss means the two halves are keyed differently and every
278
+ // lookup would return nothing — a refusal that passes while grading
279
+ // nothing, which is this tree's most-repeated defect.
280
+ throw new Error(
281
+ `fleet-coordinate-finality: coordinate ${entry.coordinate} (${write.key}) is absent ` +
282
+ 'from the fleet requirement map, which cannot happen while the edition being ' +
283
+ 'sealed is itself in that map. The lock-side and verifier-side coordinate ' +
284
+ 'spellings have diverged; refusing rather than passing over a lookup that ' +
285
+ 'silently misses.',
286
+ );
287
+ }
288
+ const served = new Set(entry.publishedPlatforms ?? []);
289
+ const unmet = [...required]
290
+ .filter(([identity]) => !served.has(identity))
291
+ .map(([identity, requirers]) => ({ platform: identity, requiredBy: requirers }));
292
+ graded.push({ key: write.key, coordinate: entry.coordinate });
293
+ // TARGET-PLATFORM AWARE (owner ruling 2026-09-29). Finality is graded
294
+ // against THIS edition's own requiredPlatforms: a platform only ANOTHER
295
+ // edition awaits here is the accepted residual — allowed, and RECORDED so
296
+ // the caller can print it. A platform this edition itself declares and the
297
+ // registry does not serve is still refused.
298
+ const own = unmet.filter((u) => u.requiredBy.includes(distributionId));
299
+ const foreign = unmet.filter((u) => !u.requiredBy.includes(distributionId));
300
+ if (foreign.length > 0) {
301
+ pendingElsewhere.push({
302
+ key: write.key,
303
+ coordinate: entry.coordinate,
304
+ unmet: foreign,
305
+ served: [...served].sort(),
306
+ });
307
+ }
308
+ if (own.length > 0) {
309
+ blockers.push({ key: write.key, coordinate: entry.coordinate, unmet: own, served: [...served].sort() });
310
+ }
311
+ }
312
+ if (blockers.length > 0) {
313
+ const detail = blockers
314
+ .slice(0, 6)
315
+ .map(
316
+ (b) =>
317
+ ` ${b.key} ${b.coordinate}\n` +
318
+ b.unmet
319
+ .map(
320
+ (u) =>
321
+ ` awaits ${u.platform}, required by ${u.requiredBy.join(', ')} ` +
322
+ `(serves: ${b.served.join(', ') || 'nothing'})`,
323
+ )
324
+ .join('\n'),
325
+ )
326
+ .join('\n');
327
+ const error = new Error(
328
+ `fleet-coordinate-finality: "${distributionId}" cannot be sealed — ${blockers.length} of ` +
329
+ `${writes.length} coordinate(s) are NOT FINAL for a platform THIS edition requires: ` +
330
+ 'the registry does not yet serve it there, so the coordinate will be supplemented ' +
331
+ '— and the arm64 lane supplements by REPUBLISHING the tag (`imagetools create -t ' +
332
+ ':<contentHash>`), which would move the very digest this seal is about to pin.\n' +
333
+ `${detail}${blockers.length > 6 ? `\n ... and ${blockers.length - 6} more` : ''}\n` +
334
+ 'Complete the supplement for this edition\'s own platforms, then seal.',
335
+ );
336
+ error.notFinal = true;
337
+ error.blockers = blockers;
338
+ throw error;
339
+ }
340
+ return { distributionId, graded, coordinateCount: graded.length, pendingElsewhere };
341
+ }
342
+
343
+ /**
344
+ * Is this failure "another edition can still supplement this coordinate"?
345
+ *
346
+ * A PROPERTY, never a message match — see {@link assertCoordinatesFinal}.
347
+ */
348
+ export function isNotFinalYet(error) {
349
+ return Boolean(error) && error.notFinal === true;
350
+ }
@@ -0,0 +1,52 @@
1
+ // git-source-auth — the ONE place the edition library turns a read token into git
2
+ // authentication for a repository URL, shared by every script that fetches or
3
+ // lists a repository (first-party sources and a customer's own git source alike).
4
+ //
5
+ // The credential reaches git through GIT_CONFIG_* environment variables, never
6
+ // argv, a file or a clone's config, and it is scoped to the ORIGIN of the URL it
7
+ // is for: `http.<origin>/.extraHeader` is applied by git only to URLs under that
8
+ // origin, so the token is not sent to any other host, nor to a host a redirect
9
+ // leads to. For a first-party source pass `sourceRepositoryUrlPrefix` (from
10
+ // edition-workspace.json); for a customer git source pass its own url.
11
+ //
12
+ // Why this is not `gitEnvWithAuth` of the CLI's runtime (src/runtime/sources/kinds/git.ts):
13
+ // the edition library is plain ESM that `node` runs straight from this directory
14
+ // and that the evidence verifier bundle is built from, so it cannot import the CLI's
15
+ // TypeScript sources or its compiled CommonJS output. The shape is the same
16
+ // on purpose (Basic `<declared username>:<token>`, `Authorization` header, origin
17
+ // scope; the username is a required argument, never a built-in default), so there is exactly ONE implementation per runtime.
18
+
19
+ /**
20
+ * `base` plus the GIT_CONFIG_* header that authenticates git against `url`'s
21
+ * origin as `username` with `token`. A token that cannot be applied is refused: an empty
22
+ * token, or a URL that is not an absolute https URL with a host. There is no
23
+ * fall-back to the ambient credential helper.
24
+ */
25
+ export function gitAuthEnv(url, username, token, base = process.env) {
26
+ if (typeof username !== 'string' || username.length === 0 || username.includes(':')) {
27
+ throw new Error(
28
+ `a git username is required to authenticate git against ${String(url)} (non-empty, no ":"); ` +
29
+ 'it is declared, never defaulted: `sourceAuthUsername` in edition-workspace.json for first-party sources, ' +
30
+ 'the source\'s own `authUsername` for a customer git source',
31
+ );
32
+ }
33
+ if (typeof token !== 'string' || token.length === 0) {
34
+ throw new Error(`a read token is required to authenticate git against ${String(url)}`);
35
+ }
36
+ let origin;
37
+ try {
38
+ const parsed = new URL(url);
39
+ if (parsed.protocol !== 'https:' || parsed.hostname === '') {throw new Error('not an https URL with a host');}
40
+ origin = parsed.origin;
41
+ } catch {
42
+ throw new Error(`cannot scope a git credential to "${String(url)}": it is not an absolute https URL with a host`);
43
+ }
44
+ const credential = Buffer.from(`${username}:${token}`, 'utf8').toString('base64');
45
+ return {
46
+ ...base,
47
+ GIT_CONFIG_COUNT: '1',
48
+ GIT_CONFIG_KEY_0: `http.${origin}/.extraHeader`,
49
+ GIT_CONFIG_VALUE_0: `Authorization: Basic ${credential}`,
50
+ GIT_TERMINAL_PROMPT: '0',
51
+ };
52
+ }
@@ -0,0 +1,242 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ closeSync,
4
+ constants as fsConstants,
5
+ fstatSync,
6
+ fsyncSync,
7
+ openSync,
8
+ readSync,
9
+ realpathSync,
10
+ renameSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from 'node:fs';
14
+ import { basename, dirname, isAbsolute, join } from 'node:path';
15
+
16
+ import {
17
+ canonicalJsonBytes,
18
+ parseCanonicalJsonBytes,
19
+ } from './image-evidence.mjs';
20
+
21
+ const MAX_REPLAY_STATE_BYTES = 64 * 1024;
22
+
23
+ function openRegularFileNoFollow(path) {
24
+ const descriptor = openSync(
25
+ path,
26
+ fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW,
27
+ );
28
+ const metadata = fstatSync(descriptor);
29
+ if (!metadata.isFile()) {
30
+ closeSync(descriptor);
31
+ throw new Error(`replay state "${path}" is not a regular file`);
32
+ }
33
+ if ((metadata.mode & 0o777) !== 0o600) {
34
+ closeSync(descriptor);
35
+ throw new Error(`replay state "${path}" must have exact mode 0600`);
36
+ }
37
+ if (
38
+ typeof process.getuid === 'function' &&
39
+ metadata.uid !== process.getuid()
40
+ ) {
41
+ closeSync(descriptor);
42
+ throw new Error(`replay state "${path}" must be owned by the verifier`);
43
+ }
44
+ return descriptor;
45
+ }
46
+
47
+ function assertTrustedStateDirectory(path) {
48
+ const descriptor = openSync(
49
+ path,
50
+ fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW,
51
+ );
52
+ try {
53
+ const metadata = fstatSync(descriptor);
54
+ if (!metadata.isDirectory()) {
55
+ throw new Error(`replay state parent "${path}" is not a directory`);
56
+ }
57
+ if ((metadata.mode & 0o777) !== 0o700) {
58
+ throw new Error(
59
+ `replay state parent "${path}" must have exact mode 0700`,
60
+ );
61
+ }
62
+ if (
63
+ typeof process.getuid === 'function' &&
64
+ metadata.uid !== process.getuid()
65
+ ) {
66
+ throw new Error(
67
+ `replay state parent "${path}" must be owned by the verifier`,
68
+ );
69
+ }
70
+ } finally {
71
+ closeSync(descriptor);
72
+ }
73
+ }
74
+
75
+ function writeDurableReplacement(path, value) {
76
+ const directory = realpathSync(dirname(path));
77
+ const temporaryPath = join(
78
+ directory,
79
+ `.${basename(path)}.${randomUUID()}.tmp`,
80
+ );
81
+ let descriptor;
82
+ try {
83
+ descriptor = openSync(
84
+ temporaryPath,
85
+ fsConstants.O_CREAT |
86
+ fsConstants.O_EXCL |
87
+ fsConstants.O_WRONLY |
88
+ fsConstants.O_NOFOLLOW,
89
+ 0o600,
90
+ );
91
+ writeFileSync(descriptor, canonicalJsonBytes(value));
92
+ fsyncSync(descriptor);
93
+ closeSync(descriptor);
94
+ descriptor = undefined;
95
+ renameSync(temporaryPath, path);
96
+ const directoryDescriptor = openSync(directory, fsConstants.O_RDONLY);
97
+ try {
98
+ fsyncSync(directoryDescriptor);
99
+ } finally {
100
+ closeSync(directoryDescriptor);
101
+ }
102
+ } finally {
103
+ if (descriptor !== undefined) {closeSync(descriptor);}
104
+ removeIfPresent(temporaryPath);
105
+ }
106
+ }
107
+
108
+ /** Remove `path`; only its absence is tolerated. */
109
+ function removeIfPresent(path) {
110
+ try {
111
+ unlinkSync(path);
112
+ } catch (error) {
113
+ if (error?.code !== 'ENOENT') {throw error;}
114
+ }
115
+ }
116
+
117
+ export async function verifyAndAdvanceReplayState({
118
+ statePath,
119
+ distributionId,
120
+ trustPolicyDigest,
121
+ parseReplayState,
122
+ verify,
123
+ }) {
124
+ if (!isAbsolute(statePath)) {
125
+ throw new Error('replay state path must be absolute');
126
+ }
127
+ const stateDirectory = realpathSync(dirname(statePath));
128
+ if (stateDirectory !== dirname(statePath)) {
129
+ throw new Error('replay state parent path must be canonical');
130
+ }
131
+ assertTrustedStateDirectory(stateDirectory);
132
+ const canonicalStatePath = join(stateDirectory, basename(statePath));
133
+ const lockPath = `${canonicalStatePath}.lock`;
134
+ let lockDescriptor;
135
+ try {
136
+ lockDescriptor = openSync(
137
+ lockPath,
138
+ fsConstants.O_CREAT |
139
+ fsConstants.O_EXCL |
140
+ fsConstants.O_WRONLY |
141
+ fsConstants.O_NOFOLLOW,
142
+ 0o600,
143
+ );
144
+ } catch (error) {
145
+ if (error?.code === 'EEXIST') {
146
+ throw new Error(
147
+ `replay state "${canonicalStatePath}" is locked by another verifier; explicit operator reconciliation is required for a stale lock`,
148
+ );
149
+ }
150
+ throw error;
151
+ }
152
+
153
+ try {
154
+ writeFileSync(
155
+ lockDescriptor,
156
+ `${process.pid} ${new Date().toISOString()}\n`,
157
+ );
158
+ fsyncSync(lockDescriptor);
159
+ const stateDescriptor = openRegularFileNoFollow(canonicalStatePath);
160
+ let state;
161
+ try {
162
+ const before = fstatSync(stateDescriptor);
163
+ if (before.size > MAX_REPLAY_STATE_BYTES) {
164
+ throw new Error(`replay state exceeds ${MAX_REPLAY_STATE_BYTES} bytes`);
165
+ }
166
+ const stateBytes = Buffer.alloc(before.size);
167
+ let offset = 0;
168
+ while (offset < stateBytes.length) {
169
+ const count = readSync(
170
+ stateDescriptor,
171
+ stateBytes,
172
+ offset,
173
+ stateBytes.length - offset,
174
+ offset,
175
+ );
176
+ if (count === 0) {
177
+ throw new Error('replay state was truncated while reading');
178
+ }
179
+ offset += count;
180
+ }
181
+ if (readSync(stateDescriptor, Buffer.alloc(1), 0, 1, offset) !== 0) {
182
+ throw new Error('replay state grew while reading');
183
+ }
184
+ const after = fstatSync(stateDescriptor);
185
+ for (const field of ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs']) {
186
+ if (after[field] !== before[field]) {
187
+ throw new Error('replay state changed while reading');
188
+ }
189
+ }
190
+ state = parseReplayState(
191
+ parseCanonicalJsonBytes(stateBytes, 'distribution image replay state'),
192
+ );
193
+ } finally {
194
+ closeSync(stateDescriptor);
195
+ }
196
+ if (
197
+ state.distributionId !== distributionId ||
198
+ state.trustPolicyDigest !== trustPolicyDigest
199
+ ) {
200
+ throw new Error(
201
+ 'replay state does not match the selected distribution and committed trust policy',
202
+ );
203
+ }
204
+
205
+ const verified = await verify(state);
206
+ const completeness = verified.completenessStatement.predicate;
207
+ if (completeness.releaseSequence <= state.lastReleaseSequence) {
208
+ throw new Error(
209
+ 'verified completeness evidence does not advance releaseSequence',
210
+ );
211
+ }
212
+ if (completeness.nonce === state.lastCompletenessNonce) {
213
+ throw new Error(
214
+ 'verified completeness evidence reuses the last accepted nonce',
215
+ );
216
+ }
217
+ if (
218
+ verified.completenessStatementDigest ===
219
+ state.lastCompletenessStatementDigest
220
+ ) {
221
+ throw new Error(
222
+ 'verified completeness evidence reuses the last accepted statement',
223
+ );
224
+ }
225
+ const nextState = parseReplayState({
226
+ schemaVersion: 1,
227
+ distributionId,
228
+ trustPolicyDigest,
229
+ lastReleaseSequence: completeness.releaseSequence,
230
+ lastCompletenessStatementDigest: verified.completenessStatementDigest,
231
+ lastCompletenessNonce: completeness.nonce,
232
+ });
233
+ writeDurableReplacement(canonicalStatePath, nextState);
234
+ return { verified, replayState: nextState };
235
+ } finally {
236
+ try {
237
+ closeSync(lockDescriptor);
238
+ } finally {
239
+ unlinkSync(lockPath);
240
+ }
241
+ }
242
+ }