@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.
Files changed (229) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/distribution-assets.manifest.json +1 -2
  3. package/dist/cli.js +2 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/dev.js +1 -0
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/down.d.ts.map +1 -1
  8. package/dist/commands/down.js +7 -1
  9. package/dist/commands/down.js.map +1 -1
  10. package/dist/commands/edition.d.ts +3 -0
  11. package/dist/commands/edition.d.ts.map +1 -0
  12. package/dist/commands/edition.js +46 -0
  13. package/dist/commands/edition.js.map +1 -0
  14. package/dist/commands/init.js +2 -2
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/serve.js +1 -0
  17. package/dist/commands/serve.js.map +1 -1
  18. package/dist/commands/up.js +1 -0
  19. package/dist/commands/up.js.map +1 -1
  20. package/dist/dev/env-defaults.d.ts.map +1 -1
  21. package/dist/dev/env-defaults.js +5 -1
  22. package/dist/dev/env-defaults.js.map +1 -1
  23. package/dist/dev/infra-preflight.d.ts +3 -2
  24. package/dist/dev/infra-preflight.d.ts.map +1 -1
  25. package/dist/dev/infra-preflight.js +18 -8
  26. package/dist/dev/infra-preflight.js.map +1 -1
  27. package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
  28. package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
  29. package/dist/runtime/config/schema.d.ts +1 -0
  30. package/dist/runtime/config/schema.d.ts.map +1 -1
  31. package/dist/runtime/custody/local-custody.d.ts +12 -0
  32. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  33. package/dist/runtime/custody/local-custody.js +22 -0
  34. package/dist/runtime/custody/local-custody.js.map +1 -0
  35. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  36. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  37. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  38. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  39. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  40. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  41. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  42. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  43. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  44. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  45. package/dist/runtime/custody/openbao-http-client.js +154 -0
  46. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  47. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  48. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  49. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  50. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  51. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  52. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  53. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  54. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  55. package/dist/runtime/edition/run-entry.d.ts +9 -0
  56. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  57. package/dist/runtime/edition/run-entry.js +36 -0
  58. package/dist/runtime/edition/run-entry.js.map +1 -0
  59. package/dist/runtime/edition/verb-map.d.ts +60 -0
  60. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  61. package/dist/runtime/edition/verb-map.js +146 -0
  62. package/dist/runtime/edition/verb-map.js.map +1 -0
  63. package/dist/runtime/env-seeder.d.ts +2 -0
  64. package/dist/runtime/env-seeder.d.ts.map +1 -1
  65. package/dist/runtime/env-seeder.js +18 -3
  66. package/dist/runtime/env-seeder.js.map +1 -1
  67. package/dist/runtime/launch.d.ts +2 -1
  68. package/dist/runtime/launch.d.ts.map +1 -1
  69. package/dist/runtime/launch.js +31 -7
  70. package/dist/runtime/launch.js.map +1 -1
  71. package/dist/runtime/paths.d.ts +1 -2
  72. package/dist/runtime/paths.d.ts.map +1 -1
  73. package/dist/runtime/paths.js +0 -10
  74. package/dist/runtime/paths.js.map +1 -1
  75. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  76. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  77. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  78. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  79. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  80. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  81. package/dist/runtime/runner/chart-preflight.js +30 -69
  82. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  83. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  84. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  85. package/dist/runtime/runner/chart-pull.js +96 -0
  86. package/dist/runtime/runner/chart-pull.js.map +1 -0
  87. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  88. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  89. package/dist/runtime/runner/chart-resolver.js +16 -62
  90. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  91. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  92. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  93. package/dist/runtime/runner/chart-set-lock.js +55 -99
  94. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  95. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  96. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  97. package/dist/runtime/runner/helm-cli.js +9 -7
  98. package/dist/runtime/runner/helm-cli.js.map +1 -1
  99. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  100. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  101. package/dist/runtime/runner/helm-runner.js +18 -16
  102. package/dist/runtime/runner/helm-runner.js.map +1 -1
  103. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  104. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  105. package/dist/runtime/sources/kinds/git.js +8 -5
  106. package/dist/runtime/sources/kinds/git.js.map +1 -1
  107. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  108. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  109. package/dist/runtime/sources/manifest-walk.js +8 -3
  110. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  111. package/dist/runtime/sources/schema.d.ts +3 -0
  112. package/dist/runtime/sources/schema.d.ts.map +1 -1
  113. package/dist/runtime/sources/schema.js +6 -1
  114. package/dist/runtime/sources/schema.js.map +1 -1
  115. package/dist/runtime/supervisor.d.ts +2 -0
  116. package/dist/runtime/supervisor.d.ts.map +1 -1
  117. package/dist/runtime/supervisor.js +1 -0
  118. package/dist/runtime/supervisor.js.map +1 -1
  119. package/edition/README.md +315 -0
  120. package/edition/assemble-producer-input.mjs +1251 -0
  121. package/edition/authorization-base.mjs +347 -0
  122. package/edition/biome-content.mjs +268 -0
  123. package/edition/build-biome-index.mjs +175 -0
  124. package/edition/build-platform-service-index.mjs +288 -0
  125. package/edition/build-runtime-image-source-index.mjs +821 -0
  126. package/edition/built-verifier.mjs +55 -0
  127. package/edition/catalogue-edition.mjs +49 -0
  128. package/edition/catalogue-fetch.mjs +64 -0
  129. package/edition/catalogue.mjs +135 -0
  130. package/edition/chart-set-source-commit.mjs +92 -0
  131. package/edition/check-committed-image-evidence.mjs +383 -0
  132. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  133. package/edition/comment-blanker.mjs +75 -0
  134. package/edition/connector-source-authority.mjs +60 -0
  135. package/edition/customer-definition.mjs +183 -0
  136. package/edition/customer-flags.mjs +7 -0
  137. package/edition/develop-customer-edition.mjs +99 -0
  138. package/edition/develop-edition-resolve.mjs +65 -0
  139. package/edition/develop-edition.mjs +274 -0
  140. package/edition/develop-freeze.mjs +173 -0
  141. package/edition/develop-image-coordinate.mjs +54 -0
  142. package/edition/develop-seal.mjs +77 -0
  143. package/edition/develop-snapshot-inputs.mjs +104 -0
  144. package/edition/dist-check.mjs +986 -0
  145. package/edition/distribution-evidence-set.mjs +987 -0
  146. package/edition/distribution-paths.mjs +191 -0
  147. package/edition/edition-artifact.mjs +270 -0
  148. package/edition/edition-biome-manifests.mjs +85 -0
  149. package/edition/edition-channel.mjs +6 -0
  150. package/edition/edition-chart-set.mjs +93 -0
  151. package/edition/edition-config.mjs +207 -0
  152. package/edition/edition-constants.mjs +57 -0
  153. package/edition/edition-contract.json +23 -0
  154. package/edition/edition-list.mjs +35 -0
  155. package/edition/edition-locks.mjs +69 -0
  156. package/edition/edition-protocol.mjs +228 -0
  157. package/edition/edition-workspace.mjs +176 -0
  158. package/edition/evidence-checkout.mjs +418 -0
  159. package/edition/evidence-grammars.mjs +16 -0
  160. package/edition/evidence-trust-pin.mjs +304 -0
  161. package/edition/evidence-trust-policy.mjs +256 -0
  162. package/edition/external-source-walk.mjs +463 -0
  163. package/edition/first-party-source-ids.mjs +59 -0
  164. package/edition/fleet-coordinate-finality.mjs +350 -0
  165. package/edition/git-source-auth.mjs +52 -0
  166. package/edition/image-evidence-replay-state.mjs +242 -0
  167. package/edition/image-evidence.mjs +1225 -0
  168. package/edition/kernel-contract-authority.mjs +443 -0
  169. package/edition/lib.mjs +3018 -0
  170. package/edition/no-follow-file.mjs +94 -0
  171. package/edition/oci-evidence-verbs.mjs +392 -0
  172. package/edition/oci-image-source-evidence.mjs +239 -0
  173. package/edition/oras.mjs +648 -0
  174. package/edition/pin-source.mjs +227 -0
  175. package/edition/plan-image-evidence-refresh.mjs +408 -0
  176. package/edition/platform-lock-readability.mjs +892 -0
  177. package/edition/platform-service-class.mjs +18 -0
  178. package/edition/platform-service-source-authority.mjs +105 -0
  179. package/edition/positive-closure.mjs +162 -0
  180. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  181. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  182. package/edition/produce-image-source-evidence.mjs +765 -0
  183. package/edition/producer-evidence-bundle.mjs +157 -0
  184. package/edition/registry-credentials.mjs +193 -0
  185. package/edition/release-catalogue.mjs +39 -0
  186. package/edition/release-channel-guard.mjs +99 -0
  187. package/edition/release-edition-artifact.mjs +158 -0
  188. package/edition/remedy-command.mjs +89 -0
  189. package/edition/repin-evidence-trust.mjs +101 -0
  190. package/edition/required-resource-provider.mjs +58 -0
  191. package/edition/resolve-chart-set.mjs +415 -0
  192. package/edition/resolve-customer-edition.mjs +288 -0
  193. package/edition/resolve-distribution.mjs +2318 -0
  194. package/edition/resolve-edition.mjs +186 -0
  195. package/edition/resolve-images.mjs +1606 -0
  196. package/edition/seal-image-digests.mjs +855 -0
  197. package/edition/sealed-evidence-accept.mjs +82 -0
  198. package/edition/source-authority.mjs +725 -0
  199. package/edition/source-closure.mjs +116 -0
  200. package/edition/strict-json.mjs +121 -0
  201. package/edition/string-order.mjs +5 -0
  202. package/edition/trust-policy-arming.mjs +120 -0
  203. package/edition/trusted-executable.mjs +166 -0
  204. package/edition/verifier-binding.mjs +45 -0
  205. package/edition/verifier-files.mjs +8 -0
  206. package/edition/verifier-kernel-schemas.mjs +22 -0
  207. package/edition/verifier-provenance.mjs +64 -0
  208. package/edition/verify-image-evidence.mjs +430 -0
  209. package/edition/verify-image-platforms.mjs +495 -0
  210. package/edition/why.mjs +56 -0
  211. package/edition/workspace-sections.mjs +195 -0
  212. package/examples/06-biome-sources.yaml +1 -0
  213. package/package.json +14 -6
  214. package/dist/commands/dist.d.ts +0 -16
  215. package/dist/commands/dist.d.ts.map +0 -1
  216. package/dist/commands/dist.js +0 -182
  217. package/dist/commands/dist.js.map +0 -1
  218. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  219. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  220. package/dist/runtime/distribution/available-biome.js +0 -156
  221. package/dist/runtime/distribution/available-biome.js.map +0 -1
  222. package/dist/runtime/distribution/compose.d.ts +0 -23
  223. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  224. package/dist/runtime/distribution/compose.js +0 -48
  225. package/dist/runtime/distribution/compose.js.map +0 -1
  226. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  227. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  228. package/dist/runtime/runner/chart-trust.js +0 -270
  229. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,855 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // seal-image-digests — write the PUBLISHED ARTIFACT's digest into the lock.
3
+ //
4
+ // WHY THIS EXISTS
5
+ // ---------------
6
+ // An `images.lock.json` subject records `contentHash`, `sourceCommit` and
7
+ // `sourceManifestHash`. Every one of those is a BUILD INPUT. None of them names
8
+ // the artifact that was actually produced, so the lock said what was INTENDED
9
+ // and never what SHIPPED.
10
+ //
11
+ // The kernel has carried the field for this the whole time —
12
+ // `RuntimeImageSubjectSchema.imageDigest`, optional, documented down to "this is
13
+ // the INDEX digest, not a per-platform child" and "it belongs to the DELIVERY
14
+ // step". Measured 2026-09-14: `imageDigest` occurs in NONE of the nine committed
15
+ // locks and in NO .mjs in this repository. The contract was declared and the
16
+ // producer was never built. This is the producer.
17
+ //
18
+ // WHAT IT COST TO BE MISSING. `chartlib.imageRef` refuses a 64-hex tag, and a
19
+ // contentHash IS 64-hex — so with no digest to install from, NO Distribution
20
+ // lock could render through the canonical chart library at all. Every fresh
21
+ // customer install, every appliance and every air-gap bundle was blocked on a
22
+ // field nothing wrote.
23
+ //
24
+ // IT RESOLVES NOTHING ITSELF, and that is deliberate
25
+ // --------------------------------------------------
26
+ // `verifyDistributionPlatforms` already fetches the index, hash-checks the bytes
27
+ // against `Docker-Content-Digest` rather than trusting the header, walks every
28
+ // child manifest BY DIGEST, reads each image CONFIG for its real platform, and
29
+ // fails closed when a required platform is absent. It already returns
30
+ // `manifestDigest` per subject and then throws it away. Re-implementing any of
31
+ // that here would be a second resolver that can disagree with the verifier about
32
+ // what was published — so this script performs the verification and records its
33
+ // answer, and there is no path through it that writes a digest the verifier did
34
+ // not prove.
35
+ //
36
+ // FOUR REFUSALS, none of which may be softened:
37
+ //
38
+ // * A subject the verifier cannot resolve aborts the WHOLE run before
39
+ // anything is written. A partially sealed lock is worse than an unsealed
40
+ // one: it installs some services by digest and silently leaves the rest
41
+ // un-pinned.
42
+ // * A subject whose lock ALREADY carries a different `imageDigest` is a hard
43
+ // error, never an update. The tag is immutable by convention in this fleet;
44
+ // if the same contentHash now resolves to different bytes, either the tag
45
+ // moved or the registry is lying, and both are supply-chain incidents rather
46
+ // than drift to reconcile.
47
+ // * Nothing is invented. There is no fallback that derives a digest from the
48
+ // contentHash — they are different hashes of different things, and the whole
49
+ // defect this closes came from treating one as the other.
50
+ // * A run over zero subjects fails. An empty seal that reports success is the
51
+ // "reports green while executing nothing" shape.
52
+ //
53
+ // It is IDEMPOTENT: re-running against an already-sealed lock re-verifies every
54
+ // subject, finds the same digests, and writes nothing.
55
+ //
56
+ // node repos-infra/xema-distributions/seal-image-digests.mjs <distributionId>
57
+ // node repos-infra/xema-distributions/seal-image-digests.mjs <distributionId> --check
58
+ // node repos-infra/xema-distributions/seal-image-digests.mjs <distributionId> --if-published
59
+ // node repos-infra/xema-distributions/seal-image-digests.mjs <distributionId> --publication-verdict
60
+ //
61
+ // `--check` verifies and reports without writing — the CI shape, so a lock that
62
+ // has drifted from the registry fails a pull request instead of a deploy.
63
+ //
64
+ // FOUR MODES, BECAUSE THEY ASK QUESTIONS AT DIFFERENT MOMENTS
65
+ // -----------------------------------------------------------
66
+ // The two added below exist because a lock is WRITTEN before the images it
67
+ // names are built. `resolve-images.mjs` computes `contentHash` from git build
68
+ // inputs, so it runs — correctly — against a registry that does not yet serve
69
+ // the coordinate it just predicted. Sealing therefore cannot happen at lock
70
+ // production, and a single mode cannot serve both the pass that re-pins and the
71
+ // pass that publishes.
72
+ //
73
+ // `--if-published` RECONCILE. Seal every subject, or, if the coordinate is
74
+ // not published YET, skip THE WHOLE EDITION and exit 0.
75
+ //
76
+ // This does NOT soften the partial-seal refusal above, and
77
+ // the distinction is the reason it is safe: that refusal is
78
+ // about writing SOME subjects of one lock. Skipping leaves
79
+ // the lock ENTIRELY unsealed, which is the state the
80
+ // publication gate then refuses. An edition is still all or
81
+ // nothing; what changed is that "nothing" became reachable
82
+ // without failing a scheduled job that re-pins every 20
83
+ // minutes. Without it, every pin move reddens reconcile
84
+ // fleet-wide for the interval between the pin landing and
85
+ // its producers building — which is the normal case, not an
86
+ // exceptional one.
87
+ //
88
+ // ONLY a 404 skips. The discriminator is `error.status`,
89
+ // set by `manifestHttpError` and measured to survive
90
+ // `verifyDistributionPlatforms` intact — never a match on
91
+ // message text, which would silently widen the day a
92
+ // registry reworded its error body. A 401, a 403, a
93
+ // malformed index or an absent required platform all still
94
+ // abort.
95
+ //
96
+ // `--publication-verdict` PUBLICATION. Classify the lock PUBLISHABLE
97
+ // (fully sealed) or WITHHELD (wholly unsealed — the resting
98
+ // state a pin advance leaves, owner ruling 2026-09-29), as
99
+ // ONE line on stdout, exit 0. A malformed, empty or
100
+ // PARTIALLY sealed lock exits 1: a broken artifact.
101
+ // OFFLINE and structural: it reads the lock and nothing
102
+ // else. That is deliberate rather than a shortcut — a
103
+ // publication gate that needs a registry credential can
104
+ // fail for reasons that have nothing to do with the artifact
105
+ // being published, and a gate people learn to re-run is a
106
+ // gate that stops being read. `--check` answers the STRONGER
107
+ // question (does the lock still AGREE with the registry) and
108
+ // needs the network to do it; this one answers the question
109
+ // publication actually turns on.
110
+ // ─────────────────────────────────────────────────────────────────────────────
111
+
112
+ import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
113
+ import { join } from 'node:path';
114
+
115
+ import { sealLock, verifyBiomeContentHashes } from './biome-content.mjs';
116
+ import { fetchCatalogue } from './catalogue-fetch.mjs';
117
+ import { CATALOGUE_FLAG, CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG } from './customer-flags.mjs';
118
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
119
+ import {
120
+ assertCoordinatesFinal,
121
+ collectFleetPlatformRequirements,
122
+ isNotFinalYet,
123
+ readFleetImageLocks,
124
+ } from './fleet-coordinate-finality.mjs';
125
+ import { loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
126
+ import { assertPlatformCanParseLock } from './platform-lock-readability.mjs';
127
+ import { assertReleaseEditionTree } from './release-channel-guard.mjs';
128
+ import { commandForSelf } from './remedy-command.mjs';
129
+ import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
130
+ import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
131
+ import {
132
+ loadImageLockForVerification,
133
+ verifyDistributionPlatforms,
134
+ } from './verify-image-platforms.mjs';
135
+
136
+ if (isEntryModule(import.meta.url)) {
137
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache, WorkspaceRoot.SourceHash]);
138
+ }
139
+
140
+ const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/u;
141
+
142
+ // This file's own directory is the distributions root in BOTH layouts the fleet
143
+ // runs — the aggregator's `repos-infra/xema-distributions/` and a FLAT checkout
144
+ // of this repository alone. It is the FALLBACK only: `--editions-root` and
145
+ // `XEMA_DISTRIBUTIONS_ROOT` still win, through the plane's own resolver, so an
146
+ // external editions root is honoured here exactly as it is everywhere else.
147
+ //
148
+ // The module-level `editionsRoot()` in `lib.mjs` is deliberately NOT used: it
149
+ // applies the resolver with a fallback that assumes the aggregator layout, and
150
+ // measured from a flat checkout it answers a path that does not exist.
151
+
152
+ /**
153
+ * The remedy, as a command the reader can actually run FROM WHERE THEY ARE.
154
+ *
155
+ * Built from this file's real path rather than printed as `pnpm dist:seal-digests`,
156
+ * because that script is declared in the AGGREGATOR's `package.json` and this
157
+ * repository has no `scripts` block at all — so the pnpm spelling is unrunnable
158
+ * in a flat checkout, which is exactly where the publication gate fails. A
159
+ * remedy that cannot be run where it is printed is a dead end.
160
+ *
161
+ * This was the only site in the repository that got it right, and the reasoning
162
+ * above is why. It now DELEGATES to `remedy-command.mjs`, which generalises it
163
+ * to the other ten diagnostics rather than leaving each to re-derive it.
164
+ */
165
+ export function sealCommandFor(distributionId) {
166
+ return commandForSelf(import.meta.url, distributionId);
167
+ }
168
+
169
+ /**
170
+ * The lock as the RESOLVER sees it: every subject with its sealed `imageDigest`
171
+ * removed, everything else byte-for-byte as committed.
172
+ *
173
+ * `resolve-images.mjs` computes a lock from git build inputs and can never know
174
+ * the digest — the digest is what this module writes AFTER the registry has
175
+ * served the coordinate. So a comparison of a committed lock against a fresh
176
+ * resolve has to subtract exactly what the seal added, or a sealed lock reads
177
+ * as drifted from a resolver that is in fact in full agreement with it.
178
+ *
179
+ * Measured 2026-09-16: `dist-check.mjs` stripped `contentHash` and
180
+ * `sourceManifestPath` from its structural view and NOT `imageDigest`, so every
181
+ * lock this module had sealed (a customer edition, 63/63) was reported
182
+ * "structurally out of date" and the printed remedy — re-resolve — would have
183
+ * produced an UNSEALED lock that the publication gate then refuses. Sealing
184
+ * must never make the drift check red: the digest is a fact about what
185
+ * SHIPPED, layered over a lock whose structure and hashes it does not touch.
186
+ *
187
+ * Pure, and owned HERE rather than in the checker because this is the module
188
+ * that decides what a seal writes; a second list of "fields the seal adds" in
189
+ * `dist-check.mjs` is the pair that can only agree by hand.
190
+ */
191
+ export function withoutSealedDigests(lock) {
192
+ return {
193
+ ...lock,
194
+ subjects: (lock.subjects ?? []).map(
195
+ ({ imageDigest: _imageDigest, ...subject }) => subject,
196
+ ),
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Pure: decide what each subject's `imageDigest` should become.
202
+ *
203
+ * Separated from the I/O so the decision table is testable without a registry,
204
+ * and so the refusals are assertable rather than reachable only through a
205
+ * network round trip.
206
+ */
207
+ export function planDigestSeal({ subjects, verified }) {
208
+ if (!Array.isArray(subjects) || subjects.length === 0) {
209
+ throw new Error(
210
+ 'seal-image-digests: the lock declares no subjects; sealing nothing and ' +
211
+ 'reporting success would be a verification that executed nothing',
212
+ );
213
+ }
214
+ const byId = new Map(
215
+ verified.map((entry) => [`${entry.subjectKind}:${entry.subjectId}`, entry]),
216
+ );
217
+ const writes = [];
218
+ const unchanged = [];
219
+ for (const subject of subjects) {
220
+ const key = `${subject.kind}:${subject.id}`;
221
+ const entry = byId.get(key);
222
+ if (!entry) {
223
+ throw new Error(
224
+ `seal-image-digests: ${key} has no verification result; refusing to seal a ` +
225
+ 'lock the verifier did not fully cover',
226
+ );
227
+ }
228
+ const digest = entry.manifestDigest;
229
+ if (typeof digest !== 'string' || !SHA256_DIGEST.test(digest)) {
230
+ throw new Error(
231
+ `seal-image-digests: ${key} resolved to "${digest}", which is not a canonical ` +
232
+ 'sha256 digest',
233
+ );
234
+ }
235
+ if (subject.imageDigest === digest) {
236
+ unchanged.push({ key, digest });
237
+ continue;
238
+ }
239
+ if (typeof subject.imageDigest === 'string' && subject.imageDigest.length > 0) {
240
+ throw new Error(
241
+ `seal-image-digests: ${key} is already sealed at ${subject.imageDigest} but ` +
242
+ `${entry.coordinate} now resolves to ${digest}. The tag MOVED, or the registry ` +
243
+ 'is serving different bytes for the same coordinate. This is not drift to ' +
244
+ 'reconcile — nothing is rewritten, and the cause must be established first.',
245
+ );
246
+ }
247
+ writes.push({ key, digest, coordinate: entry.coordinate });
248
+ }
249
+ return { writes, unchanged };
250
+ }
251
+
252
+ /** The two RESTING STATES a lock may be published from — or withheld in. */
253
+ export const PublicationVerdict = Object.freeze({
254
+ Publishable: 'PUBLISHABLE',
255
+ Withheld: 'WITHHELD',
256
+ });
257
+
258
+ /**
259
+ * Pure: is this lock PUBLISHABLE, WITHHELD, or BROKEN?
260
+ *
261
+ * The publication gate. OFFLINE — it reads the parsed lock and nothing else — so
262
+ * it cannot fail for a reason unrelated to the artifact being published, and
263
+ * cannot be re-run into passing.
264
+ *
265
+ * THREE ANSWERS, NOT TWO. Owner ruling 2026-09-29: an unsealed lock blocks
266
+ * PUBLICATION and nothing else. A pin advance is committed UNSEALED — the
267
+ * resolver strips every `imageDigest` when it re-pins — and only the edition
268
+ * that must publish is sealed. So a wholly unsealed lock is a RESTING STATE of
269
+ * `main`, not a defect, and it gets the npm release lane's shape: the edition is
270
+ * WITHHELD, said out loud, and nothing is uploaded for it. It is never
271
+ * published, because an unsealed lock addresses its images by a MUTABLE tag and
272
+ * `chartlib.imageRef` refuses a bare 64-hex contentHash in the tag slot, so it
273
+ * could not be installed by the canonical chart library at all.
274
+ *
275
+ * What still THROWS is a lock no resting state produces — a BROKEN artifact:
276
+ *
277
+ * * NO SUBJECTS — a gate passing over an empty roster has verified nothing;
278
+ * * a NON-CANONICAL digest — a bare 64-hex contentHash laundered into the
279
+ * digest slot, an uppercased or truncated one;
280
+ * * a PARTIAL seal — some subjects carry a digest and others do not. The
281
+ * sealer refuses to write one (it seals all or nothing) and the resolver
282
+ * strips all or nothing, so no correct path produces it. It would install
283
+ * some services by digest and silently leave the rest un-pinned.
284
+ *
285
+ * IT CHECKS SHAPE AS WELL AS PRESENCE. This gate reads the lock with
286
+ * `JSON.parse` because the publication workflow checks out this repository
287
+ * ALONE, with no built kernel to parse through, so the kernel's digest regex is
288
+ * not upstream of it and this file's own `SHA256_DIGEST` is the only rule on
289
+ * this path.
290
+ */
291
+ export function classifyPublicationState({
292
+ distributionId,
293
+ subjects,
294
+ remedy = sealCommandFor(distributionId),
295
+ }) {
296
+ if (!Array.isArray(subjects) || subjects.length === 0) {
297
+ throw new Error(
298
+ `seal-image-digests: "${distributionId}" declares no subjects; a publication gate ` +
299
+ 'that passes over an empty roster has verified nothing',
300
+ );
301
+ }
302
+ // MALFORMED is reported before anything else: one verdict, not a cascade.
303
+ const malformed = subjects
304
+ .filter(
305
+ (subject) =>
306
+ typeof subject.imageDigest === 'string' &&
307
+ subject.imageDigest.length > 0 &&
308
+ !SHA256_DIGEST.test(subject.imageDigest),
309
+ )
310
+ .map((subject) => `${subject.kind}:${subject.id}=${subject.imageDigest}`);
311
+ if (malformed.length > 0) {
312
+ throw new Error(
313
+ `seal-image-digests: "${distributionId}" carries ${malformed.length} NON-CANONICAL ` +
314
+ `imageDigest value(s): ${malformed.slice(0, 4).join(', ')}. A digest is ` +
315
+ '`sha256:` followed by 64 lowercase hex characters. A BARE 64-hex value is a ' +
316
+ "contentHash — a hash of BUILD INPUTS, not of the artifact — and `chartlib.imageRef` " +
317
+ 'refuses it in the tag slot for exactly this reason. Refusing to publish.',
318
+ );
319
+ }
320
+ const unsealed = subjects
321
+ .filter((subject) => typeof subject.imageDigest !== 'string' || subject.imageDigest.length === 0)
322
+ .map((subject) => `${subject.kind}:${subject.id}`);
323
+ if (unsealed.length === 0) {
324
+ return {
325
+ verdict: PublicationVerdict.Publishable,
326
+ distributionId,
327
+ subjectCount: subjects.length,
328
+ unsealedCount: 0,
329
+ };
330
+ }
331
+ if (unsealed.length < subjects.length) {
332
+ throw new Error(
333
+ `seal-image-digests: "${distributionId}" is PARTIALLY sealed — ${unsealed.length} of ` +
334
+ `${subjects.length} subject(s) carry no imageDigest: ${unsealed.slice(0, 8).join(', ')}` +
335
+ `${unsealed.length > 8 ? `, and ${unsealed.length - 8} more` : ''}. ` +
336
+ 'No correct path produces this: the sealer writes all or nothing and the resolver ' +
337
+ 'strips all or nothing. It would install some services by digest and silently ' +
338
+ 'leave the rest un-pinned. This is a broken artifact, not a resting state; ' +
339
+ `refusing to publish. Re-seal the whole edition:\n ${remedy}`,
340
+ );
341
+ }
342
+ return {
343
+ verdict: PublicationVerdict.Withheld,
344
+ distributionId,
345
+ subjectCount: subjects.length,
346
+ unsealedCount: unsealed.length,
347
+ remedy,
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Is this failure "the coordinate is not published YET"?
353
+ *
354
+ * `manifestHttpError` stamps `error.status` from the HTTP response, and that
355
+ * property was measured surviving `verifyDistributionPlatforms` intact against
356
+ * the live registry (a fabricated tag on a real repository returns
357
+ * `status === 404`, body `MANIFEST_UNKNOWN`). Matching the MESSAGE instead would
358
+ * widen silently the day ghcr rewords its error body — and widening this
359
+ * predicate turns a hard failure into a skip, which is the one direction that
360
+ * must never happen by accident.
361
+ */
362
+ export function isNotPublishedYet(error) {
363
+ return Boolean(error) && error.status === 404;
364
+ }
365
+
366
+ /**
367
+ * Pure: split argv into the mode flags and the ONE positional distribution id.
368
+ *
369
+ * WHY THIS EXISTS AS A FUNCTION
370
+ * -----------------------------
371
+ * The previous form was `args.filter((a) => !FLAGS.has(a))` over a set holding
372
+ * only the three MODE flags, and everything surviving that filter was treated as
373
+ * positional. `--editions-root <path>` therefore contributed TWO unexpected
374
+ * positionals and the usage error fired — so the editions-root flag was
375
+ * REJECTED by the very script that threads argv into the resolver
376
+ * (`editionsRoot()` below passes `args` to `resolveEditionsRootWithSource`,
377
+ * which reads exactly this flag).
378
+ *
379
+ * That is the "written and never wired" shape pointed at a CLI: the capability
380
+ * was reachable in code and unreachable from the command line, while
381
+ * `XEMA_DISTRIBUTIONS_ROOT` — the env half of the same precedence chain — worked.
382
+ * A customer edition resolved from a root this repository does not own is the
383
+ * case the flag exists for, and it is exactly the case that could not be spelled.
384
+ *
385
+ * The flag NAME is imported from `lib.mjs` rather than restated here, so this
386
+ * parser cannot drift from the resolver that consumes it.
387
+ *
388
+ * VALUE HANDLING IS DELIBERATELY NOT DUPLICATED. The space form's value is
389
+ * skipped without being validated, because `resolveEditionsRootWithSource`
390
+ * already refuses a missing value, an empty one, and one that swallows the next
391
+ * flag. Re-checking here would be a second implementation of a rule that owns
392
+ * itself — and the two could disagree about which is authoritative.
393
+ */
394
+ export function parseSealArgs(args) {
395
+ const MODES = new Set(['--check', '--if-published', '--publication-verdict']);
396
+ const CATALOGUE_VALUE_FLAGS = new Set([CATALOGUE_FLAG, CATALOGUE_USERNAME_FLAG, CATALOGUE_TOKEN_ENV_FLAG]);
397
+ const positional = [];
398
+ const catalogueFlags = new Map();
399
+ for (let i = 0; i < args.length; i += 1) {
400
+ const arg = args[i];
401
+ if (MODES.has(arg)) {continue;}
402
+ if (CATALOGUE_VALUE_FLAGS.has(arg)) {
403
+ if (args[i + 1] === undefined || args[i + 1].startsWith('--')) {throw new Error(`${arg} takes a value`);}
404
+ catalogueFlags.set(arg, args[i + 1]);
405
+ i += 1;
406
+ continue;
407
+ }
408
+ positional.push(arg);
409
+ }
410
+ const [distributionId, ...rest] = positional;
411
+ return {
412
+ catalogueFlags,
413
+ distributionId,
414
+ rest,
415
+ check: args.includes('--check'),
416
+ ifPublished: args.includes('--if-published'),
417
+ publicationVerdict: args.includes('--publication-verdict'),
418
+ };
419
+ }
420
+
421
+ /** Where a lock is staged before it replaces `path`: beside it, so the rename never crosses a filesystem. */
422
+ export const sealStagingPath = (path) => `${path}.sealing`;
423
+
424
+ /**
425
+ * Replace the locks of one seal together. Every document is first written to its
426
+ * staging file; only when all of them are on disk is each renamed over its lock,
427
+ * in the order given. A failure while staging removes what was staged and leaves
428
+ * every committed lock byte-identical.
429
+ */
430
+ function replaceLocksTogether(documents) {
431
+ const staged = [];
432
+ try {
433
+ for (const { path, text } of documents) {
434
+ writeFileSync(sealStagingPath(path), text);
435
+ staged.push(path);
436
+ }
437
+ } catch (error) {
438
+ for (const path of staged) {rmSync(sealStagingPath(path), { force: true });}
439
+ throw error;
440
+ }
441
+ for (const { path } of documents) {renameSync(sealStagingPath(path), path);}
442
+ }
443
+
444
+ /**
445
+ * The seal of one edition's two locks, as ONE write.
446
+ *
447
+ * `digests` (`kind:id` -> registry digest) are the image digests this run seals
448
+ * into the images lock. Each biome's `biomeContentHash` is then RECOMPUTED from
449
+ * its sources over that sealed images lock, and the unsealed distribution lock
450
+ * the resolver wrote is turned into the v5 lock by the kernel's
451
+ * `sealDistributionLock`. A distribution lock that is already sealed is VERIFIED
452
+ * instead: every hash is recomputed and a mismatch is refused, naming the biome.
453
+ *
454
+ * Nothing is written until both documents exist in memory, and the distribution
455
+ * lock is renamed LAST: a seal that fails for any reason before that leaves the
456
+ * committed pair as it was, and the one state a crash between the two renames
457
+ * can leave (images sealed, distribution unsealed) is refused by every reader of
458
+ * a sealed edition.
459
+ */
460
+ export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest }) {
461
+ const schemas = await loadKernelDistributionSchemas();
462
+ const lockPath = join(root, distributionId, 'distribution.lock.json');
463
+ const imagesPath = join(root, distributionId, 'images.lock.json');
464
+ // The file is re-read rather than the PARSED lock re-serialised: the parse is a
465
+ // validation, not a round-trip, and writing back a schema-normalised document
466
+ // would silently reformat or drop anything the parser does not model.
467
+ const rawImages = JSON.parse(readFileSync(imagesPath, 'utf8'));
468
+ let applied = 0;
469
+ for (const subject of rawImages.subjects) {
470
+ const digest = digests.get(`${subject.kind}:${subject.id}`);
471
+ if (digest) {
472
+ subject.imageDigest = digest;
473
+ applied += 1;
474
+ }
475
+ }
476
+ if (applied !== digests.size) {
477
+ throw new Error(
478
+ `seal-image-digests: planned ${digests.size} writes but matched ${applied} subjects ` +
479
+ 'in the file on disk; nothing written',
480
+ );
481
+ }
482
+ const raw = JSON.parse(readFileSync(lockPath, 'utf8'));
483
+ const imageLock = schemas.parseImageLock(rawImages);
484
+ const pinned = vendoredCommit ?? ((repo) => createPinnedSourceAuthority({ pins: committedRepositoryPins() }).vendoredSourceCommitForRepository(repo));
485
+ const lockDocument = raw !== null && typeof raw === 'object' && 'unsealedSchema' in raw ? schemas.parseUnsealedDistributionLock(raw) : schemas.parseDistributionLock(raw);
486
+ const result = await sealOrVerifyLock({
487
+ schemas,
488
+ lock: lockDocument,
489
+ imageLock,
490
+ locate: sourceTreeLocator(externalSourceTrees(lockDocument)),
491
+ vendoredCommit: pinned,
492
+ catalogue,
493
+ catalogueDigest,
494
+ check,
495
+ lockPath,
496
+ });
497
+ replaceLocksTogether([
498
+ ...(applied > 0 ? [{ path: imagesPath, text: `${JSON.stringify(rawImages, null, 2)}\n` }] : []),
499
+ ...(result.sealed ? [{ path: lockPath, text: serializeJson(result.lock) }] : []),
500
+ ]);
501
+ if (applied > 0) {console.log(`[${label}] sealed ${applied} subject(s) into ${imagesPath}.`);}
502
+ if (result.sealed) {
503
+ console.log(`[${label}] sealed ${result.lock.biomes.length} biomeContentHash(es) into ${lockPath}.`);
504
+ } else {
505
+ console.log(`[${label}] ${result.lock.biomes.length} biomeContentHash(es) recomputed from their inputs and agree with ${lockPath}.`);
506
+ }
507
+ return result.lock;
508
+ }
509
+
510
+ /**
511
+ * The decision, with no files: an UNSEALED lock is sealed (refused under `check`); a SEALED lock is VERIFIED -
512
+ * every hash recomputed, a mismatch refused naming the biome.
513
+ */
514
+ export async function sealOrVerifyLock({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, check = false, lockPath = 'distribution.lock.json' }) {
515
+ if ('unsealedSchema' in lock) {
516
+ if (check) {
517
+ throw new Error(`${lockPath} is unsealed (no biomeContentHash); run \`xema edition seal ${lock.distributionId}\``);
518
+ }
519
+ return { sealed: true, lock: await sealLock({ schemas, unsealed: lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest }) };
520
+ }
521
+ await verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest });
522
+ return { sealed: false, lock };
523
+ }
524
+
525
+ async function main() {
526
+ const args = process.argv.slice(2);
527
+ const { distributionId, rest, check, ifPublished, publicationVerdict, catalogueFlags } =
528
+ parseSealArgs(args);
529
+ if (!distributionId || rest.length > 0) {
530
+ throw new Error(
531
+ 'usage: xema edition seal <distributionId> ' +
532
+ `[--check | --if-published | --publication-verdict] ` +
533
+ `[--catalogue oci://<repository>@sha256:<digest> [--catalogue-username <name> --catalogue-token-env <VAR>]]`,
534
+ );
535
+ }
536
+ // The modes answer different questions and combining them would make the
537
+ // verdict depend on precedence nobody declared.
538
+ const selected = [check, ifPublished, publicationVerdict].filter(Boolean).length;
539
+ if (selected > 1) {
540
+ throw new Error(
541
+ 'seal-image-digests: --check, --if-published and --publication-verdict are mutually exclusive',
542
+ );
543
+ }
544
+
545
+ // PUBLICATION GATE — offline and structural. It returns BEFORE any call that
546
+ // would load the built kernel contracts, so it needs no sibling repository, no
547
+ // credential and no `node_modules`.
548
+ //
549
+ // The distinction is the IMPORT versus the CALL, and getting it wrong once
550
+ // cost a needless refactor: importing `verify-image-platforms.mjs`,
551
+ // `oras.mjs` and `lib.mjs` is all FINE without a kernel —
552
+ // measured, all three load in a flat checkout — because
553
+ // `loadKernelDistributionSchemas()` is invoked at USE time, not at module
554
+ // load. What fails in the publication checkout is CALLING
555
+ // `loadImageLockForVerification`. So the fix is to not call it, which is what
556
+ // this early return does; making the imports dynamic bought nothing and was
557
+ // reverted.
558
+ // EVERY mode, the offline publication verdict included: a release edition
559
+ // never carries develop content, whatever the sealer is asked to do with it.
560
+ assertReleaseEditionTree(join(editionsRoot(), distributionId));
561
+
562
+ if (publicationVerdict) {
563
+ const lockPath = join(editionsRoot(), distributionId, 'images.lock.json');
564
+ const raw = JSON.parse(readFileSync(lockPath, 'utf8'));
565
+ const state = classifyPublicationState({
566
+ distributionId,
567
+ subjects: raw.subjects,
568
+ });
569
+ // STDOUT CARRIES EXACTLY ONE MACHINE LINE, and nothing else. The publish
570
+ // loop branches on it, and accepts only these two shapes: anything else —
571
+ // including an empty stdout from a crash — is a failure there, never a
572
+ // withhold. The human explanation goes to stderr.
573
+ if (state.verdict === PublicationVerdict.Publishable) {
574
+ console.error(
575
+ `[dist:seal-digests --publication-verdict] "${distributionId}": all ` +
576
+ `${state.subjectCount} subject(s) carry a canonical imageDigest; publishable. ` +
577
+ `(${lockPath})`,
578
+ );
579
+ console.log(`${state.verdict} ${distributionId} ${state.subjectCount}`);
580
+ return;
581
+ }
582
+ console.error(
583
+ `[dist:seal-digests --publication-verdict] "${distributionId}": WITHHELD — all ` +
584
+ `${state.unsealedCount} subject(s) are unsealed. This is a resting state (owner ` +
585
+ 'ruling 2026-09-29: an unsealed lock blocks PUBLICATION and nothing else), so ' +
586
+ 'nothing is uploaded for it. To publish it, seal it once its producers have ' +
587
+ `built:\n ${state.remedy}`,
588
+ );
589
+ console.log(
590
+ `${state.verdict} ${distributionId} ${state.unsealedCount} ${state.subjectCount}`,
591
+ );
592
+ return;
593
+ }
594
+
595
+ // THE ROOT MUST REACH THE READ, NOT ONLY THE WRITE.
596
+ //
597
+ // `loadImageLockForVerification` takes `(distributionId, distributionsRoot =
598
+ // dataRoot())`. Called with one argument it silently read the DEFAULT
599
+ // root while the write below used `editionsRoot()` — so with an external
600
+ // root configured, this script verified ONE lock and would have sealed the
601
+ // digests it proved into a DIFFERENT one. It failed safe only when the id was
602
+ // absent from the default root (ENOENT); an edition present in BOTH roots
603
+ // would have been sealed from the wrong verification, silently.
604
+ //
605
+ // That fix reached the LOCK read and NOT the sibling seven lines below it,
606
+ // and the sentence that used to stand here — "one root, resolved once, used
607
+ // by every path in this function" — was the reason: a reader confirms it
608
+ // against the call it is standing next to and stops. `verifyDistributionPlatforms`
609
+ // defaults `distributionsRoot` to `dataRoot()`, so the REGISTRY
610
+ // CREDENTIALS were still read out of this repository while the lock came from
611
+ // the configured root.
612
+ //
613
+ // The two halves fail with OPPOSITE loudness, which is why only the quiet one
614
+ // survived: a lock read against the wrong root is an ENOENT, and a credential
615
+ // read against the wrong root simply authenticates against a different
616
+ // declaration file and proceeds. A customer root declaring its own registry
617
+ // authority would have been verified with the vendor's declarations.
618
+ //
619
+ // Both reads now take `root`. Asserted BEHAVIOURALLY in
620
+ // `seal-image-digests.test.mjs` — the external root declares a sentinel
621
+ // `authTokenEnv` and the test reads which variable the refusal names, because
622
+ // no structural check can tell a threaded argument from an omitted one.
623
+ const root = editionsRoot();
624
+ // CUSTOMER MODE: the digest-pinned catalogue the edition was resolved against.
625
+ // Its vendor biomes' tree ids are inputs on ITS authority (biome-content.mjs).
626
+ const fetched =
627
+ catalogueFlags.has(CATALOGUE_FLAG)
628
+ ? await fetchCatalogue({
629
+ ref: catalogueFlags.get(CATALOGUE_FLAG),
630
+ username: catalogueFlags.get(CATALOGUE_USERNAME_FLAG),
631
+ tokenEnv: catalogueFlags.get(CATALOGUE_TOKEN_ENV_FLAG),
632
+ })
633
+ : undefined;
634
+ const catalogue = fetched?.catalogue;
635
+ const catalogueDigest = fetched?.digest;
636
+ const imageLock = await loadImageLockForVerification(distributionId, root);
637
+
638
+ // Throws on ANY unresolvable subject or missing required platform, before a
639
+ // single byte of the lock is touched.
640
+ let report;
641
+ try {
642
+ report = await verifyDistributionPlatforms({ imageLock, distributionsRoot: root });
643
+ } catch (error) {
644
+ // RECONCILE: a coordinate this pass just re-pinned has not been built yet.
645
+ // Skip the EDITION — never some of its subjects — and leave the lock
646
+ // entirely unsealed for the publication gate to refuse.
647
+ if (ifPublished && isNotPublishedYet(error)) {
648
+ console.log(
649
+ `[dist:seal-digests --if-published] "${distributionId}": not published yet — ` +
650
+ `${error.message.split('\n')[0]}`,
651
+ );
652
+ console.log(
653
+ `[dist:seal-digests --if-published] skipping this edition ENTIRELY and writing ` +
654
+ 'nothing. It stays unsealed, and publication refuses an unsealed lock, so ' +
655
+ 'nothing installable is produced from it until its producers have built.',
656
+ );
657
+ return;
658
+ }
659
+ throw error;
660
+ }
661
+
662
+ // ── BEFORE ANY DIGEST IS WRITTEN ───────────────────────────────────────────
663
+ //
664
+ // Can the images of this edition READ the lock it is about to be sealed
665
+ // against? The plane resolves with the contracts build in
666
+ // `repos/xema-kernel-contracts`, which runs ahead of what ships, and
667
+ // `DistributionLockSchema` is `.strict()` — so a key the plane knows and a
668
+ // pinned repository does not makes the whole artifact unreadable AT BOOT.
669
+ // That is not hypothetical: on 2026-09-17 `imageNamePrefix` did exactly this
670
+ // to three presenca-dev services at once, and the edition had sealed,
671
+ // published and installed perfectly first.
672
+ //
673
+ // It is asked of every pinned repository that SHIPS AN IMAGE INTO THIS
674
+ // EDITION and PARSES A LOCK — never of `repos/xema-base` alone, which
675
+ // declares the NEWEST range in the fleet and is therefore the weakest
676
+ // possible subject, and never of every pin, which grades repositories the
677
+ // edition's lock cannot reach. Both narrowings are DERIVED (from this
678
+ // edition's `images.lock.json` and from the pinned commit's own source);
679
+ // see `platform-lock-readability.mjs` for the measurement behind each.
680
+ //
681
+ // WHY HERE AND NOT BEFORE `verifyDistributionPlatforms`, which is where the
682
+ // original of this call sat. Three placements were available and only this
683
+ // one is correct today:
684
+ //
685
+ // • before the lock read — `--publication-verdict` returns above it, and that
686
+ // gate must stay offline and structural. It would also need a sibling
687
+ // repository and the registry to answer a question about publication.
688
+ // • before the verify — an edition whose coordinates are NOT PUBLISHED YET
689
+ // is the ORDINARY daily outcome, and `--if-published` skips it in the
690
+ // catch below. Asking first would make an edition nothing is about to
691
+ // seal fail HARD on a readability question, and would pay the npm cost
692
+ // for every skipped edition.
693
+ // • HERE — after the skip, before `planDigestSeal` and therefore before any
694
+ // write, so it runs exactly for the editions a digest is about to be
695
+ // written into, and for `--check`, which is a verification mode and
696
+ // should answer it.
697
+ //
698
+ // `runtimeImageSourceIndexPath()` is THIS repository's file rather than the
699
+ // editions root's, and deliberately: the pins are a fact about the vendor
700
+ // plane that BUILT the images, not about whose editions are being sealed.
701
+ // Every other consumer of the index (`dist-check.mjs`, `resolve-images.mjs`)
702
+ // reads it the same way.
703
+ //
704
+ // `imageLock` is THE EDITION'S OWN, already loaded above from `root`, and it
705
+ // is what narrows the question from the fleet to this edition: the pin index
706
+ // is fleet-wide, so grading every pin refuses a seal over a repository that
707
+ // ships nothing here. Measured 2026-09-27, that is not hypothetical — it
708
+ // blocked a customer-edition seal on `repos/xema-operator`, which has
709
+ // ZERO subjects in that edition.
710
+ // ── THE TWO LOCKS READ HERE ARE DIFFERENT CLASSES ────────────────────────
711
+ //
712
+ // `distribution.lock.json` is REBUILDABLE — a fresh resolve reproduces it,
713
+ // and zero of the 9 committed ones carries a digest. `imageLock` is SEALED:
714
+ // 523 of 523 subjects across those 9 editions carry an `imageDigest` that
715
+ // THIS script wrote, and `resolve-images.mjs` emits none, so re-resolving it
716
+ // DISCARDS them rather than recomputing them. They appear as two arguments
717
+ // to one call, which is exactly the shape that invites treating them alike;
718
+ // `dist-check.mjs`'s `compareCommittedLock` carries the full statement of
719
+ // the split, at its own read.
720
+ //
721
+ // The practical rule for anything added below: the distribution lock may be
722
+ // regenerated to answer a question; the image lock may not.
723
+ await assertPlatformCanParseLock({
724
+ lock: JSON.parse(
725
+ readFileSync(join(root, distributionId, 'distribution.lock.json'), 'utf8'),
726
+ ),
727
+ imageLock,
728
+ distributionId,
729
+ runtimeSourceIndex: JSON.parse(
730
+ readFileSync(runtimeImageSourceIndexPath(), 'utf8'),
731
+ ),
732
+ });
733
+
734
+ const { writes, unchanged } = planDigestSeal({
735
+ subjects: imageLock.subjects,
736
+ verified: report.subjects,
737
+ });
738
+
739
+ const label = check ? 'dist:seal-digests --check' : 'dist:seal-digests';
740
+ console.log(
741
+ `[${label}] "${distributionId}": ${report.subjectCount} subjects verified against ` +
742
+ `${report.registries.join(', ')}; ${unchanged.length} already sealed, ${writes.length} to seal.`,
743
+ );
744
+ for (const w of writes) {console.log(` seal ${w.key} ${w.coordinate} -> ${w.digest}`);}
745
+
746
+ if (writes.length === 0) {
747
+ console.log(`[${label}] lock is fully sealed and agrees with the registry.`);
748
+ await sealEditionLocks({ distributionId, root, check, label, catalogue, catalogueDigest });
749
+ return;
750
+ }
751
+
752
+ // ── FLEET-SCOPED FINALITY ──────────────────────────────────────────────────
753
+ //
754
+ // Everything above this point is PER-EDITION and passed. That is exactly the
755
+ // state `xema-internal` was in at 09:25 on 2026-09-18, when it was sealed
756
+ // against 40 coordinates that were still amd64-only and whose arm64
757
+ // supplement then moved 63 of its 66 digests.
758
+ //
759
+ // A seal pins what a MUTABLE tag resolves to, so it is stable only once the
760
+ // coordinate is FINAL — and finality is a property of the FLEET, because the
761
+ // edition that can still supplement the coordinate is a DIFFERENT one. Asked
762
+ // here, over `writes`, because this is the moment the regression is
763
+ // introduced and the repository holding every edition's lock is where the
764
+ // question is answerable at all.
765
+ const { editions, withoutLock } = readFleetImageLocks(root);
766
+ const fleetRequirements = collectFleetPlatformRequirements({ editions });
767
+ try {
768
+ const finality = assertCoordinatesFinal({
769
+ distributionId,
770
+ writes,
771
+ verified: report.subjects,
772
+ fleetRequirements,
773
+ });
774
+ // The CORPUS, not a confirmation: how many coordinates were graded against
775
+ // how many editions, and which editions made no claim. Without the
776
+ // denominators a pass cannot be told from a pass over nothing, and the
777
+ // editions root is printed because it BOUNDS the fleet this answer is about.
778
+ console.log(
779
+ `[${label}] finality: ${finality.coordinateCount} coordinate(s) graded against ` +
780
+ `${editions.length} edition(s) under ${root}` +
781
+ (withoutLock.length > 0
782
+ ? ` (${withoutLock.length} declared no image lock: ${withoutLock.join(', ')})`
783
+ : '') +
784
+ `; ${finality.coordinateCount - finality.pendingElsewhere.length} final for every ` +
785
+ `edition, ${finality.pendingElsewhere.length} final for this edition's own platforms only.`,
786
+ );
787
+ // THE ACCEPTED RESIDUAL, recorded one line per coordinate (owner ruling
788
+ // 2026-09-29): another edition still awaits a platform THIS edition does not
789
+ // declare, so a later supplement may move the tag. This seal keeps pinning
790
+ // the manifest it verified, which serves every platform this edition needs.
791
+ for (const pending of finality.pendingElsewhere) {
792
+ console.log(
793
+ `[${label}] RECORDED: ${pending.key} ${pending.coordinate} — ` +
794
+ pending.unmet
795
+ .map((u) => `${u.platform} pending for ${u.requiredBy.join(', ')}`)
796
+ .join('; ') +
797
+ `; not required by "${distributionId}" (serves: ${pending.served.join(', ') || 'nothing'}).`,
798
+ );
799
+ }
800
+ } catch (error) {
801
+ // RECONCILE: another edition is still awaiting a platform here. That is the
802
+ // ORDINARY transient state while a supplement builds — the same shape as
803
+ // "not published yet", and treated the same way for the same reason: a
804
+ // daily job that reddens for a state nobody can act on today is one people
805
+ // learn to bypass. The edition is skipped WHOLE and stays unsealed, so the
806
+ // publication gate still refuses to ship it.
807
+ if (ifPublished && isNotFinalYet(error)) {
808
+ console.log(`[dist:seal-digests --if-published] ${error.message}`);
809
+ console.log(
810
+ `[dist:seal-digests --if-published] skipping this edition ENTIRELY and writing ` +
811
+ 'nothing. It stays unsealed, and publication refuses an unsealed lock.',
812
+ );
813
+ return;
814
+ }
815
+ throw error;
816
+ }
817
+
818
+ if (check) {
819
+ throw new Error(
820
+ `${writes.length} subject(s) carry no imageDigest. Run without --check to seal them. ` +
821
+ 'An unsealed lock cannot be installed by digest, and a tag-based install is ' +
822
+ 'mutable lookup in the production path.',
823
+ );
824
+ }
825
+
826
+ await sealEditionLocks({
827
+ distributionId,
828
+ root,
829
+ digests: new Map(writes.map((w) => [w.key, w.digest])),
830
+ check,
831
+ label,
832
+ catalogue,
833
+ catalogueDigest,
834
+ });
835
+ }
836
+
837
+ if (isEntryModule(import.meta.url)) {
838
+ main().catch((error) => {
839
+ console.error(`[dist:seal-digests] ${error.message}`);
840
+ // The footer is MODE-AWARE. Pointing a reader at a registry credential when
841
+ // the gate that just failed never opens a socket is a remedy for the wrong
842
+ // problem, and a wrong remedy costs more than no remedy.
843
+ const offline = process.argv.includes('--publication-verdict');
844
+ console.error(
845
+ offline
846
+ ? '[dist:seal-digests] fail-closed: this gate is OFFLINE — it read the lock and ' +
847
+ 'nothing else, so no credential, network or registry state is involved in ' +
848
+ 'the verdict above.'
849
+ : '[dist:seal-digests] fail-closed: no digest above was assumed, and a partial ' +
850
+ 'seal is never written. Registry credentials are declared in ' +
851
+ 'image-registry-credentials.json.',
852
+ );
853
+ process.exit(1);
854
+ });
855
+ }