@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,227 @@
1
+ // pin-source — where the resolver reads each first-party repository's commit.
2
+ //
3
+ // PUBLISHED (the default, and the only behaviour before this module): the
4
+ // committed `runtime-image-source-index.json#repositoryPins`, untouched.
5
+ //
6
+ // SNAPSHOT: one frozen map of pins, replacing the published ones for ONE
7
+ // in-memory resolution (the develop pipeline resolves a whole edition from it).
8
+ // Nothing is written back to the index, and there is NO fallback: a repo in the
9
+ // graph that the snapshot does not name is refused, never read from the
10
+ // published pin. This module owns the SHAPE of that map: a flat object whose
11
+ // keys live in six closed namespaces and whose values are exact 40-hex SHAs.
12
+ //
13
+ // repos/<x> | submodules/<x> every `repositoryPins[].repo` of the runtime
14
+ // image source index (the graph), no more, no fewer
15
+ // <pinRoles.data> the commit of the data root checkout (editions and indices)
16
+ // <pinRoles.kernel> the commit of the kernel the authorization lock is sealed
17
+ // with (may match repos/<x>, but is NOT in the graph)
18
+ // <pinRoles.deploy> the commit of the chart tree the chart-set lock digests
19
+ // editions-root the commit of the editions root checkout; present
20
+ // only when that root is NOT this repository
21
+ // sources/<name> one per `sources[]` entry declared in the chain
22
+
23
+ import { workspaceSection } from './edition-config.mjs';
24
+ import { compareStrings, serializeJson, sha256Hex } from './lib.mjs';
25
+ import { PinRole } from './workspace-sections.mjs';
26
+
27
+ export const PinSourceKind = Object.freeze({
28
+ Published: 'published',
29
+ Snapshot: 'snapshot',
30
+ });
31
+
32
+ const SHA_PATTERN = /^[0-9a-f]{40}$/;
33
+
34
+ export const SnapshotPinNamespace = Object.freeze({
35
+ Graph: 'graph',
36
+ Distributions: 'distributions',
37
+ Kernel: 'kernel',
38
+ ChartSource: 'chart-source',
39
+ EditionsRoot: 'editions-root',
40
+ Source: 'source',
41
+ });
42
+
43
+ // The three checked-out roles a snapshot pins (data, kernel, deploy) are a closed
44
+ // set the library defines; the repository key each is pinned under is the data
45
+ // root's (`pinRoles` in edition-workspace.json), so a repository rename never
46
+ // needs a library release. `editions-root` is not a repository key but a
47
+ // protocol key of its own.
48
+ export const EDITIONS_ROOT_PIN_KEY = 'editions-root';
49
+
50
+ const pinKeyOf = (role) => workspaceSection('pinRoles', 'a snapshot pin')[role];
51
+
52
+ export const dataPinKey = () => pinKeyOf(PinRole.Data);
53
+ export const kernelPinKey = () => pinKeyOf(PinRole.Kernel);
54
+ export const deployPinKey = () => pinKeyOf(PinRole.Deploy);
55
+ const SOURCE_PIN_PREFIX = 'sources/';
56
+ const GRAPH_PIN_PATTERN = /^(?:repos|submodules)\/[a-z0-9][a-z0-9-]*$/;
57
+
58
+ /** The snapshot key of one declared `sources[]` entry. */
59
+ export function sourcePinKey(name) {
60
+ return `${SOURCE_PIN_PREFIX}${name}`;
61
+ }
62
+
63
+ /** The `sources[].name` a Source-namespace key pins. */
64
+ export function sourceNameOfPinKey(key) {
65
+ return key.slice(SOURCE_PIN_PREFIX.length);
66
+ }
67
+
68
+ /** Which closed namespace a snapshot key belongs to; refuses a key in none. */
69
+ export function snapshotPinNamespace(key) {
70
+ if (key === dataPinKey()) {return SnapshotPinNamespace.Distributions;}
71
+ if (key === kernelPinKey()) {return SnapshotPinNamespace.Kernel;}
72
+ if (key === deployPinKey()) {return SnapshotPinNamespace.ChartSource;}
73
+ if (key === EDITIONS_ROOT_PIN_KEY) {return SnapshotPinNamespace.EditionsRoot;}
74
+ if (key.startsWith(SOURCE_PIN_PREFIX) && key.length > SOURCE_PIN_PREFIX.length) {
75
+ return SnapshotPinNamespace.Source;
76
+ }
77
+ if (GRAPH_PIN_PATTERN.test(key)) {return SnapshotPinNamespace.Graph;}
78
+ throw new Error(
79
+ `snapshot pin key "${key}" is in no namespace; expected a graph repository (repos/<x> or ` +
80
+ `submodules/<x>), "${dataPinKey()}", "${kernelPinKey()}", "${deployPinKey()}", "${EDITIONS_ROOT_PIN_KEY}" or "${SOURCE_PIN_PREFIX}<name>"`,
81
+ );
82
+ }
83
+
84
+ function assertPinsObject(pins) {
85
+ if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
86
+ throw new Error('snapshot pins must be an object mapping key -> 40-hex commit');
87
+ }
88
+ }
89
+
90
+ function assertExactShas(pins) {
91
+ for (const [key, sha] of Object.entries(pins)) {
92
+ if (typeof sha !== 'string' || !SHA_PATTERN.test(sha)) {
93
+ throw new Error(
94
+ `snapshot pin for "${key}" is not a 40-hex commit SHA: ${JSON.stringify(sha)}`,
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ function graphPinsOf(pins) {
101
+ return Object.fromEntries(
102
+ Object.entries(pins).filter(([key]) => snapshotPinNamespace(key) === SnapshotPinNamespace.Graph),
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Validate a COMPLETE snapshot against the graph and the composed chain.
108
+ * `declaredSourceNames` are the `sources[].name` entries the chain declares;
109
+ * `editionsRootExternal` is whether the editions root is another repository.
110
+ * Every refusal names the offending key; nothing falls back to a published pin.
111
+ */
112
+ export function validateSnapshotPins(
113
+ pins,
114
+ { runtimeSourceIndex, declaredSourceNames, editionsRootExternal },
115
+ ) {
116
+ assertPinsObject(pins);
117
+ assertExactShas(pins);
118
+ for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
119
+ checkGraphPins(runtimeSourceIndex.repositoryPins, pins);
120
+ for (const [key, what] of [
121
+ [dataPinKey(), 'the commit of the data root checkout'],
122
+ [kernelPinKey(), 'the commit of the kernel the authorization lock is sealed with'],
123
+ [deployPinKey(), 'the commit of the chart tree the chart-set lock digests'],
124
+ ]) {
125
+ if (!(key in pins)) {throw new Error(`snapshot is missing "${key}", ${what}`);}
126
+ }
127
+ if (editionsRootExternal && !(EDITIONS_ROOT_PIN_KEY in pins)) {
128
+ throw new Error(`snapshot is missing "${EDITIONS_ROOT_PIN_KEY}": the editions root is not this repository`);
129
+ }
130
+ if (!editionsRootExternal && EDITIONS_ROOT_PIN_KEY in pins) {
131
+ throw new Error(`snapshot names "${EDITIONS_ROOT_PIN_KEY}", but the editions root is this repository`);
132
+ }
133
+ const declared = new Set(declaredSourceNames);
134
+ for (const name of declared) {
135
+ if (!(sourcePinKey(name) in pins)) {
136
+ throw new Error(`snapshot is missing "${sourcePinKey(name)}": the chain declares source "${name}"`);
137
+ }
138
+ }
139
+ for (const key of Object.keys(pins)) {
140
+ if (snapshotPinNamespace(key) === SnapshotPinNamespace.Source && !declared.has(sourceNameOfPinKey(key))) {
141
+ throw new Error(`snapshot names "${key}", but the chain declares no source "${sourceNameOfPinKey(key)}"`);
142
+ }
143
+ }
144
+ }
145
+
146
+ /** The pins sorted by key: the one canonical order, shared by the hash and the artifact. */
147
+ export function sortedSnapshotPins(pins) {
148
+ assertPinsObject(pins);
149
+ return Object.fromEntries(Object.entries(pins).sort(([a], [b]) => compareStrings(a, b)));
150
+ }
151
+
152
+ /** sha256 hex of the canonical JSON of the pins sorted by key: the snapshot's identity. */
153
+ export function snapshotHash(pins) {
154
+ return sha256Hex(serializeJson(sortedSnapshotPins(pins)));
155
+ }
156
+
157
+ /** A declared source with its ref replaced by the snapshot pin; identity under a published pin source. */
158
+ export function pinnedSource(source, pinSource = PUBLISHED_PIN_SOURCE) {
159
+ if (pinSource?.kind !== PinSourceKind.Snapshot) {return source;}
160
+ const key = sourcePinKey(source.name);
161
+ const ref = pinSource.pins[key];
162
+ if (ref === undefined) {
163
+ throw new Error(`snapshot has no pin "${key}" for the declared source "${source.name}"; there is no fallback to its declared ref`);
164
+ }
165
+ return { ...source, ref };
166
+ }
167
+
168
+ export const PUBLISHED_PIN_SOURCE = Object.freeze({ kind: PinSourceKind.Published });
169
+
170
+ /** @param {Record<string,string>} pins repo -> 40-hex commit */
171
+ export function snapshotPinSource(pins) {
172
+ return Object.freeze({ kind: PinSourceKind.Snapshot, pins });
173
+ }
174
+
175
+ /**
176
+ * Apply a pin source to a parsed runtime image source index. Published returns
177
+ * the SAME object (inert, byte for byte). Snapshot returns a copy whose
178
+ * `repositoryPins` carry the snapshot SHAs.
179
+ */
180
+ export function applyPinSource(runtimeSourceIndex, pinSource = PUBLISHED_PIN_SOURCE) {
181
+ switch (pinSource?.kind) {
182
+ case PinSourceKind.Published:
183
+ return runtimeSourceIndex;
184
+ case PinSourceKind.Snapshot:
185
+ return applySnapshot(runtimeSourceIndex, pinSource.pins);
186
+ default:
187
+ throw new Error(
188
+ `unknown pin source kind ${JSON.stringify(pinSource?.kind)}; expected ` +
189
+ `${Object.values(PinSourceKind).join(' | ')}`,
190
+ );
191
+ }
192
+ }
193
+
194
+ function checkGraphPins(graph, pins) {
195
+ const graphRepos = new Set(graph.map((pin) => pin.repo));
196
+ for (const repo of Object.keys(graphPinsOf(pins))) {
197
+ if (!graphRepos.has(repo)) {
198
+ throw new Error(`snapshot names "${repo}", which is not a repository in the graph`);
199
+ }
200
+ }
201
+ const missing = [...graphRepos].filter((repo) => !(repo in pins));
202
+ if (missing.length > 0) {
203
+ throw new Error(
204
+ `snapshot is missing ${missing.length} repositor${missing.length === 1 ? 'y' : 'ies'} ` +
205
+ `in the graph: ${missing.join(', ')}. There is no fallback to the published pin.`,
206
+ );
207
+ }
208
+ }
209
+
210
+ function applySnapshot(runtimeSourceIndex, pins) {
211
+ assertPinsObject(pins);
212
+ assertExactShas(pins);
213
+ for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
214
+ const graph = runtimeSourceIndex.repositoryPins;
215
+ checkGraphPins(graph, pins);
216
+ return {
217
+ ...runtimeSourceIndex,
218
+ repositoryPins: graph.map((pin) => ({
219
+ ...pin,
220
+ // The IMAGE axis only. `vendoredSourceCommit` is the SOURCE axis (the
221
+ // published tip vendored bytes come from) and is deliberately NOT fused
222
+ // with it: see `build-runtime-image-source-index.mjs#repositoryIdentity`
223
+ // for the measured production failure that fusing the two caused.
224
+ sourceCommit: pins[pin.repo],
225
+ })),
226
+ };
227
+ }
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ // ═══════════════════════════════════════════════════════════════════════════
3
+ // WHICH PRODUCING REPOSITORIES MUST RE-SIGN BEFORE THE NEXT RELEASE?
4
+ //
5
+ // xema edition evidence plan-refresh \
6
+ // --data-root <dir> --editions-root <dir> --kernel <dir> \
7
+ // [--kernel-checkout exact|preview] \
8
+ // --distribution <id> [repeatable] \
9
+ // [--horizon-seconds <n>] [--now <epoch-millis>] [--github-output <path>]
10
+ //
11
+ // Producer evidence is read from ONE place: the OCI referrers of each locked
12
+ // image, in the registry the lock names.
13
+ //
14
+ // `--kernel-checkout exact` (the default) loads the kernel schemas from a clean
15
+ // kernel checkout at exactly the commit the data root's evidence-trust pin
16
+ // records. `preview` loads whatever kernel is built at `--kernel`: that build
17
+ // is held to no commit, so its plan is a LOCAL PREVIEW. It says so on every
18
+ // run and cannot write `--github-output`, so no workflow can act on it.
19
+ //
20
+ // WHY THIS EXISTS
21
+ //
22
+ // Producer evidence carries a hard `expiresAt` that the offline verifier
23
+ // enforces. Nothing was watching it. Evidence was produced only as a
24
+ // side-effect of an image BUILD (or a manual dispatch), so a subject whose
25
+ // source has not changed in a week has evidence that quietly ages out — and
26
+ // the failure surfaces at the worst possible moment: at a customer install, or
27
+ // as a release that cannot be delivered, with a green CI behind it.
28
+ //
29
+ // This is the standing question asked on a schedule instead: for every subject
30
+ // of every named edition, is there a published producer bundle, is it usable,
31
+ // and does it still have enough life to survive the release that will consume
32
+ // it? Anything else is a refresh that is DUE, reported per producing
33
+ // repository so the owner of that repository can act on one line.
34
+ //
35
+ // EXIT CODES — the distinction is deliberate
36
+ //
37
+ // 0 every subject is CURRENT.
38
+ // 2 a verdict was reached and a refresh IS due. This is the loud failure the
39
+ // scheduled workflow exists to produce.
40
+ // 1 NO verdict could be reached (bad arguments, unreadable locks, a lock and
41
+ // the runtime index disagreeing about a repository pin). A planner that
42
+ // cannot decide must never report "nothing to do".
43
+ //
44
+ // THE HORIZON IS THE REUSE FLOOR
45
+ //
46
+ // The default horizon is `MIN_REMAINING_TTL_SECONDS`, the same floor
47
+ // `check-committed-image-evidence.mjs` uses to decide a committed bundle is
48
+ // too close to expiry to hand forward. One number, one meaning: below it, the
49
+ // bundle is not something a release should be built on.
50
+ // ═══════════════════════════════════════════════════════════════════════════
51
+
52
+ import { appendFileSync } from 'node:fs';
53
+ import { isAbsolute } from 'node:path';
54
+
55
+ import { MIN_REMAINING_TTL_SECONDS } from './check-committed-image-evidence.mjs';
56
+ import {
57
+ expectedProducerSource,
58
+ parseEvidenceCliFlags,
59
+ } from './distribution-evidence-set.mjs';
60
+ import {
61
+ WorkspaceRoot,
62
+ configureEditionWorkspaceFromArgv,
63
+ dataRoot,
64
+ editionsRoot,
65
+ isEntryModule,
66
+ } from './edition-workspace.mjs';
67
+ import {
68
+ canonicalJson,
69
+ loadValidatedLocks,
70
+ } from './image-evidence.mjs';
71
+ import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
72
+ import { compareStrings, loadKernelDistributionSchemas } from './lib.mjs';
73
+ import {
74
+ createRegistryClientResolver,
75
+ resolveImageSourceEvidenceReferrer,
76
+ } from './oci-image-source-evidence.mjs';
77
+ import {
78
+ loadProducerKernelSchemas,
79
+ readPinnedContractAuthority,
80
+ } from './produce-image-source-evidence.mjs';
81
+ import { splitImageRef } from './registry-credentials.mjs';
82
+
83
+ /** Closed set of per-subject refresh verdicts. */
84
+ export const EvidenceRefreshStatus = Object.freeze({
85
+ Current: 'CURRENT',
86
+ Absent: 'ABSENT',
87
+ Unusable: 'UNUSABLE',
88
+ Expired: 'EXPIRED',
89
+ Expiring: 'EXPIRING',
90
+ });
91
+
92
+ /** Where the kernel schemas of a planning run come from. */
93
+ export const KernelCheckout = Object.freeze({
94
+ /** A clean kernel checkout at exactly the pinned authority commit. */
95
+ Exact: 'exact',
96
+ /** The kernel built at `--kernel`, held to no commit: a local preview only. */
97
+ Preview: 'preview',
98
+ });
99
+
100
+ const DUE_STATUSES = Object.freeze(
101
+ new Set([
102
+ EvidenceRefreshStatus.Absent,
103
+ EvidenceRefreshStatus.Unusable,
104
+ EvidenceRefreshStatus.Expired,
105
+ EvidenceRefreshStatus.Expiring,
106
+ ]),
107
+ );
108
+
109
+ /**
110
+ * @returns {Promise<{horizonSeconds: number, refreshDue: boolean,
111
+ * subjects: Array<object>, groups: Array<object>}>}
112
+ */
113
+ export async function planImageEvidenceRefresh({
114
+ distributionIds,
115
+ dataRoot: dataRootDirectory,
116
+ editionsRoot: editionsRootDirectory,
117
+ schemas,
118
+ expectedContractAuthority,
119
+ registryClientFactory,
120
+ horizonSeconds = MIN_REMAINING_TTL_SECONDS,
121
+ now = Date.now(),
122
+ }) {
123
+ if (!Array.isArray(distributionIds) || distributionIds.length === 0) {
124
+ throw new Error('at least one distribution id is required');
125
+ }
126
+ if (!expectedContractAuthority) {
127
+ throw new Error('the expected kernel contract authority is required');
128
+ }
129
+ if (!isAbsolute(dataRootDirectory) || !isAbsolute(editionsRootDirectory)) {
130
+ throw new Error('the data root and the editions root must be absolute paths');
131
+ }
132
+ if (!Number.isSafeInteger(horizonSeconds) || horizonSeconds <= 0) {
133
+ throw new Error('the refresh horizon must be a positive whole number of seconds');
134
+ }
135
+ if (!Number.isSafeInteger(now) || now <= 0) {
136
+ throw new Error('the evaluation instant must be positive epoch milliseconds');
137
+ }
138
+ const clientFor = createRegistryClientResolver({
139
+ dataRoot: dataRootDirectory,
140
+ factory: registryClientFactory,
141
+ });
142
+
143
+ const subjects = [];
144
+ for (const distributionId of [...distributionIds].sort(compareStrings)) {
145
+ const { imageLock, repositoryPins, runtimeSourceIndex } = await loadValidatedLocks(
146
+ distributionId,
147
+ dataRootDirectory,
148
+ schemas,
149
+ { editionsRoot: editionsRootDirectory },
150
+ );
151
+ assertKernelContractAuthorityEqual(
152
+ runtimeSourceIndex.contractAuthority,
153
+ expectedContractAuthority,
154
+ 'runtime-image-source-index.json',
155
+ );
156
+ for (const subject of imageLock.subjects) {
157
+ const repositoryPin = repositoryPins.get(subject.repo);
158
+ const expectedSource = expectedProducerSource({
159
+ subject,
160
+ repositoryPin,
161
+ sourceHashAlgorithm: imageLock.sourceHashAlgorithm,
162
+ });
163
+ let matches = [];
164
+ // Set when the registry ANSWERS that the locked coordinate holds no
165
+ // image. That is a decided verdict for this subject, not an undecidable
166
+ // one, so it is reported rather than thrown — see the exit-code contract
167
+ // in this file's header. Until 2026-09-02 the throw escaped the whole
168
+ // planner: measured that day, ALL 68 locked coordinates were absent from
169
+ // ghcr, and the run died on the first of them and said nothing about the
170
+ // other 67.
171
+ let coordinateAbsence = null;
172
+ {
173
+ const { authority, repository } = splitImageRef(subject.imageRef);
174
+ const client = clientFor(authority);
175
+ let image = null;
176
+ try {
177
+ image = await client.getManifestByTag(repository, subject.contentHash);
178
+ } catch (error) {
179
+ // A registry that says "no such manifest" has decided this subject.
180
+ // Any other failure means the question went UNANSWERED, and a planner
181
+ // that cannot decide must never report "nothing to do" — so it still
182
+ // escapes and the run still exits 1.
183
+ if (error?.status !== 404) {throw error;}
184
+ coordinateAbsence =
185
+ `the registry has no image at the locked coordinate ` +
186
+ `${subject.imageRef}:${subject.contentHash} — nothing was ever ` +
187
+ `published there, so no producer evidence can attest it and the ` +
188
+ `lock must be re-resolved against a commit that was actually built`;
189
+ }
190
+ if (image !== null) {
191
+ const artifact = await resolveImageSourceEvidenceReferrer({
192
+ client,
193
+ imageRepository: subject.imageRef,
194
+ subjectDigest: image.digest,
195
+ schemas,
196
+ });
197
+ if (artifact !== null) {
198
+ matches = artifact.records.filter(
199
+ (record) =>
200
+ canonicalJson(record.statement.predicate.source) ===
201
+ canonicalJson(expectedSource),
202
+ );
203
+ }
204
+ }
205
+ }
206
+ const verdict =
207
+ coordinateAbsence !== null
208
+ ? {
209
+ status: EvidenceRefreshStatus.Unusable,
210
+ remainingSeconds: null,
211
+ reason: coordinateAbsence,
212
+ }
213
+ : classifySubject({ matches, horizonSeconds, now });
214
+ subjects.push({
215
+ distributionId,
216
+ subjectKind: subject.kind,
217
+ subjectId: subject.id,
218
+ repo: subject.repo,
219
+ repositoryUrl: repositoryPin.repositoryUrl,
220
+ sourceCommit: subject.sourceCommit,
221
+ ...verdict,
222
+ });
223
+ }
224
+ }
225
+
226
+ subjects.sort(
227
+ (left, right) =>
228
+ compareStrings(left.repo, right.repo) ||
229
+ compareStrings(left.distributionId, right.distributionId) ||
230
+ compareStrings(left.subjectKind, right.subjectKind) ||
231
+ compareStrings(left.subjectId, right.subjectId),
232
+ );
233
+ const groups = [];
234
+ for (const subject of subjects) {
235
+ const incumbent = groups.at(-1);
236
+ if (incumbent?.repo === subject.repo) {
237
+ incumbent.subjects.push(subject);
238
+ incumbent.due ||= DUE_STATUSES.has(subject.status);
239
+ continue;
240
+ }
241
+ groups.push({
242
+ repo: subject.repo,
243
+ repositoryUrl: subject.repositoryUrl,
244
+ subjects: [subject],
245
+ due: DUE_STATUSES.has(subject.status),
246
+ });
247
+ }
248
+ return {
249
+ horizonSeconds,
250
+ refreshDue: subjects.some((subject) => DUE_STATUSES.has(subject.status)),
251
+ subjects,
252
+ groups,
253
+ };
254
+ }
255
+
256
+ function classifySubject({ matches, horizonSeconds, now }) {
257
+ if (matches.length === 0) {
258
+ return {
259
+ status: EvidenceRefreshStatus.Absent,
260
+ remainingSeconds: null,
261
+ reason: 'no published producer evidence attests this source coordinate',
262
+ };
263
+ }
264
+ const [match] = matches;
265
+ const expiresAt = match.statement.predicate.producerRun.expiresAt;
266
+ const remainingSeconds = Math.floor((Date.parse(expiresAt) - now) / 1000);
267
+ if (!Number.isFinite(remainingSeconds)) {
268
+ return {
269
+ status: EvidenceRefreshStatus.Unusable,
270
+ remainingSeconds: null,
271
+ reason: `published statement has an unreadable expiresAt "${String(expiresAt)}"`,
272
+ };
273
+ }
274
+ if (remainingSeconds <= 0) {
275
+ return {
276
+ status: EvidenceRefreshStatus.Expired,
277
+ remainingSeconds,
278
+ reason: `evidence EXPIRED at ${expiresAt}; it can never verify again`,
279
+ };
280
+ }
281
+ if (remainingSeconds < horizonSeconds) {
282
+ return {
283
+ status: EvidenceRefreshStatus.Expiring,
284
+ remainingSeconds,
285
+ reason: `evidence expires in ${remainingSeconds}s, inside the ${horizonSeconds}s refresh horizon`,
286
+ };
287
+ }
288
+ return {
289
+ status: EvidenceRefreshStatus.Current,
290
+ remainingSeconds,
291
+ reason: `evidence is usable for another ${remainingSeconds}s`,
292
+ };
293
+ }
294
+
295
+ /** One line per subject, plus GitHub annotations grouped by producing repo. */
296
+ export function renderRefreshPlan(plan) {
297
+ const lines = [];
298
+ for (const group of plan.groups) {
299
+ const due = group.subjects.filter((subject) =>
300
+ DUE_STATUSES.has(subject.status),
301
+ );
302
+ if (due.length === 0) {
303
+ lines.push(
304
+ `[image-evidence-refresh] ${group.repo}: all ${group.subjects.length} subject(s) CURRENT`,
305
+ );
306
+ continue;
307
+ }
308
+ lines.push(
309
+ `::error::[image-evidence-refresh] ${group.repo} (${group.repositoryUrl}) must re-run its ` +
310
+ `image-source evidence producer for ${due.length} of ${group.subjects.length} subject(s): ` +
311
+ due
312
+ .map(
313
+ (subject) =>
314
+ `${subject.subjectKind}:${subject.subjectId} [${subject.distributionId}] ${subject.status} — ${subject.reason}`,
315
+ )
316
+ .join(' | '),
317
+ );
318
+ }
319
+ lines.push(
320
+ plan.refreshDue
321
+ ? `[image-evidence-refresh] REFRESH DUE for ${
322
+ plan.subjects.filter((subject) => DUE_STATUSES.has(subject.status))
323
+ .length
324
+ } of ${plan.subjects.length} subject(s) within a ${plan.horizonSeconds}s horizon`
325
+ : `[image-evidence-refresh] all ${plan.subjects.length} subject(s) hold evidence beyond the ${plan.horizonSeconds}s horizon`,
326
+ );
327
+ return lines;
328
+ }
329
+
330
+ /** The step outputs a workflow reads: whether a refresh is due, how many subjects, and which producing repositories. */
331
+ export function renderGithubOutputs(plan) {
332
+ const dueCount = plan.subjects.filter((subject) => DUE_STATUSES.has(subject.status)).length;
333
+ return (
334
+ `refresh-due=${plan.refreshDue}\n` +
335
+ `due-subjects=${dueCount}\n` +
336
+ `total-subjects=${plan.subjects.length}\n` +
337
+ `due-repositories=${plan.groups
338
+ .filter((group) => group.due)
339
+ .map((group) => group.repo)
340
+ .join(',')}\n`
341
+ );
342
+ }
343
+
344
+ async function main() {
345
+ const argv = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
346
+ WorkspaceRoot.Data,
347
+ WorkspaceRoot.Editions,
348
+ WorkspaceRoot.Kernel,
349
+ ]);
350
+ const flags = parseEvidenceCliFlags(argv, {
351
+ single: [
352
+ 'horizon-seconds',
353
+ 'now',
354
+ 'github-output',
355
+ 'kernel-checkout',
356
+ ],
357
+ repeated: ['distribution'],
358
+ });
359
+ const horizonRaw = flags.optional('horizon-seconds');
360
+ const nowRaw = flags.optional('now');
361
+ const kernelCheckout = flags.optional('kernel-checkout') ?? KernelCheckout.Exact;
362
+ if (!Object.values(KernelCheckout).includes(kernelCheckout)) {
363
+ throw new Error(`--kernel-checkout must be one of ${Object.values(KernelCheckout).join(', ')}`);
364
+ }
365
+ const githubOutput = flags.optional('github-output');
366
+ const preview = kernelCheckout === KernelCheckout.Preview;
367
+ if (preview && githubOutput !== undefined) {
368
+ throw new Error(
369
+ '--kernel-checkout preview plans with a kernel build that is held to no commit; ' +
370
+ 'its plan is a local preview and cannot be written to --github-output',
371
+ );
372
+ }
373
+ const schemas = preview
374
+ ? await loadKernelDistributionSchemas()
375
+ : (await loadProducerKernelSchemas({ dataRoot: dataRoot() })).schemas;
376
+ const plan = await planImageEvidenceRefresh({
377
+ distributionIds: flags.requireMany('distribution'),
378
+ dataRoot: dataRoot(),
379
+ editionsRoot: editionsRoot(),
380
+ schemas,
381
+ expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
382
+ horizonSeconds:
383
+ horizonRaw === undefined ? MIN_REMAINING_TTL_SECONDS : Number(horizonRaw),
384
+ now: nowRaw === undefined ? Date.now() : Number(nowRaw),
385
+ });
386
+ if (preview) {
387
+ console.log(
388
+ '[image-evidence-refresh] LOCAL PREVIEW: planned with the kernel built at --kernel, which is held to no commit. ' +
389
+ 'Nothing may be sealed or scheduled from this plan; run without --kernel-checkout preview for a verdict.',
390
+ );
391
+ }
392
+ for (const line of renderRefreshPlan(plan)) {
393
+ console.log(line);
394
+ }
395
+ if (githubOutput !== undefined) {
396
+ appendFileSync(githubOutput, renderGithubOutputs(plan), 'utf8');
397
+ }
398
+ if (plan.refreshDue) {
399
+ process.exitCode = 2;
400
+ }
401
+ }
402
+
403
+ if (isEntryModule(import.meta.url)) {
404
+ main().catch((error) => {
405
+ console.error(`[image-evidence-refresh] ${error.message}`);
406
+ process.exitCode = 1;
407
+ });
408
+ }