@xemahq/xema 0.26.0 → 0.28.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 (230) 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 -2
  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 +54283 -0
  28. package/dist/edition/verify-image-evidence.bundle.provenance.json +2877 -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 +313 -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 +284 -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 +205 -0
  152. package/edition/edition-constants.mjs +57 -0
  153. package/edition/edition-contract.json +30 -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 +187 -0
  158. package/edition/evidence-checkout.mjs +439 -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 +294 -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-delivery-entitlement-descriptor.mjs +106 -0
  167. package/edition/image-evidence-replay-state.mjs +242 -0
  168. package/edition/image-evidence.mjs +1233 -0
  169. package/edition/kernel-contract-authority.mjs +443 -0
  170. package/edition/lib.mjs +3021 -0
  171. package/edition/no-follow-file.mjs +94 -0
  172. package/edition/oci-evidence-verbs.mjs +392 -0
  173. package/edition/oci-image-source-evidence.mjs +239 -0
  174. package/edition/oras.mjs +648 -0
  175. package/edition/pin-source.mjs +227 -0
  176. package/edition/plan-image-evidence-refresh.mjs +408 -0
  177. package/edition/platform-lock-readability.mjs +892 -0
  178. package/edition/platform-service-class.mjs +18 -0
  179. package/edition/platform-service-source-authority.mjs +105 -0
  180. package/edition/positive-closure.mjs +162 -0
  181. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  182. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  183. package/edition/produce-image-source-evidence.mjs +765 -0
  184. package/edition/producer-evidence-bundle.mjs +157 -0
  185. package/edition/registry-credentials.mjs +193 -0
  186. package/edition/release-catalogue.mjs +39 -0
  187. package/edition/release-channel-guard.mjs +109 -0
  188. package/edition/release-edition-artifact.mjs +157 -0
  189. package/edition/remedy-command.mjs +89 -0
  190. package/edition/repin-evidence-trust.mjs +101 -0
  191. package/edition/required-resource-provider.mjs +58 -0
  192. package/edition/resolve-chart-set.mjs +415 -0
  193. package/edition/resolve-customer-edition.mjs +288 -0
  194. package/edition/resolve-distribution.mjs +2318 -0
  195. package/edition/resolve-edition.mjs +186 -0
  196. package/edition/resolve-images.mjs +1606 -0
  197. package/edition/seal-image-digests.mjs +855 -0
  198. package/edition/sealed-evidence-accept.mjs +82 -0
  199. package/edition/source-authority.mjs +725 -0
  200. package/edition/source-closure.mjs +116 -0
  201. package/edition/strict-json.mjs +121 -0
  202. package/edition/string-order.mjs +5 -0
  203. package/edition/trust-policy-arming.mjs +120 -0
  204. package/edition/trusted-executable.mjs +166 -0
  205. package/edition/verifier-binding.mjs +45 -0
  206. package/edition/verifier-files.mjs +8 -0
  207. package/edition/verifier-kernel-schemas.mjs +22 -0
  208. package/edition/verifier-provenance.mjs +64 -0
  209. package/edition/verify-image-evidence.mjs +458 -0
  210. package/edition/verify-image-platforms.mjs +495 -0
  211. package/edition/why.mjs +56 -0
  212. package/edition/workspace-sections.mjs +195 -0
  213. package/examples/06-biome-sources.yaml +1 -0
  214. package/package.json +14 -6
  215. package/dist/commands/dist.d.ts +0 -16
  216. package/dist/commands/dist.d.ts.map +0 -1
  217. package/dist/commands/dist.js +0 -182
  218. package/dist/commands/dist.js.map +0 -1
  219. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  220. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  221. package/dist/runtime/distribution/available-biome.js +0 -156
  222. package/dist/runtime/distribution/available-biome.js.map +0 -1
  223. package/dist/runtime/distribution/compose.d.ts +0 -23
  224. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  225. package/dist/runtime/distribution/compose.js +0 -48
  226. package/dist/runtime/distribution/compose.js.map +0 -1
  227. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  228. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  229. package/dist/runtime/runner/chart-trust.js +0 -270
  230. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,1251 @@
1
+ #!/usr/bin/env node
2
+ // assemble-producer-input — producer-input assembly for detached image-source
3
+ // evidence (plan phase E1).
4
+ //
5
+ // xema edition evidence assemble-input \
6
+ // --output <absolute-path> \
7
+ // --image-repository <registry>/<namespace>/<service> \
8
+ // --manifest-digest sha256:<64-hex> \
9
+ // --subject-kind service --subject-id <service> \
10
+ // --source-repo repos/<repo> \
11
+ // --source-path repos/<repo>/<biome-or-package-path> \
12
+ // --source-commit <40-hex> \
13
+ // --provenance <absolute-predicate-json> \
14
+ // [--ttl-seconds <1..policy-max>] \
15
+ // [--source-manifest-hash <64-hex>] [--source-tree-hash <64-hex>]
16
+ //
17
+ // Emits the EXACT producer input manifest `produce-image-source-evidence.mjs`
18
+ // accepts. The assembled manifest is validated with the producer's own gates —
19
+ // `assertProducerInput`, the full kernel statement schema, and (from the
20
+ // GITHUB_* environment) `assertGitHubProducerContext` — BEFORE it is written:
21
+ // the assembler never emits a manifest the producer would reject.
22
+ //
23
+ // Field derivation:
24
+ // * contractAuthority — the verifier-embedded kernel contract authority
25
+ // (single source of truth; the offline verifier only
26
+ // ever accepts that exact object).
27
+ // * sourceHashAlgorithm — read from `@xemahq/distribution-source-hash` via
28
+ // lib.mjs `imageInputSourceHashAlgorithm()` — the one
29
+ // algorithm an image lock may name.
30
+ // * producerWorkflowSha — GITHUB_SHA of the running producer workflow.
31
+ // * signer — derived from the running workflow's context:
32
+ // certificateIdentity is exactly
33
+ // https://github.com/<repo>/<workflow-path>@<ref>.
34
+ // * builder — builderId is the building workflow; buildType and
35
+ // the definition/parameters digests are read off the
36
+ // BuildKit SLSA v1 predicate the build exported
37
+ // (`--provenance`) — the same bytes the workflow
38
+ // cosign-attests, so the evidence cannot describe a
39
+ // different build than the one that ran.
40
+ // * sourceManifestHash — the kernel computeBiomeSourceManifestHash of the git-blob-at-commit,
41
+ // the exact runtime-image-source-index algorithm
42
+ // (lib.mjs `readGitJsonAtCommit` + `serializeJson`).
43
+ // * sourceTreeHash — computed here with lib.mjs `hashImageBuildInputs`
44
+ // under `imageInputSourceHashAlgorithm()` — the SAME
45
+ // functions and package the lock resolver and every
46
+ // producer's `compute-image-tag.mjs` use. The request
47
+ // is reconstructed exactly as the producing build saw
48
+ // it: the service directory comes from the subject's
49
+ // own manifest at the build commit
50
+ // (resolve-images `subjectServicePath`), and the
51
+ // vendored cross-repo inputs + build-step directories
52
+ // come from the producing checkout's committed
53
+ // `tooling/release/cross-repo-build-inputs.json` AT
54
+ // THAT COMMIT — never the live distribution index,
55
+ // which may already pin newer source revisions.
56
+ // * manifestDigest cross-check, manifestMediaType, platforms — read from the
57
+ // registry with the fail-closed OCI client at the canonical
58
+ // `<imageRepository>:<sourceTreeHash>` coordinate: the digest served there
59
+ // must be exactly the caller's `--manifest-digest`, and platforms are
60
+ // proven by their hash-verified image configs (verify-image-platforms
61
+ // `resolvePublishedPlatforms`).
62
+ // * sbomDigest, provenanceDigest — the cosign attestation artifacts the
63
+ // producing workflow attached to the delivered manifest digest: the
64
+ // `sha256-<digest>.att` manifest's DSSE envelope layers, classified by
65
+ // their `predicateType` annotation and bound to the delivered digest
66
+ // through their decoded in-toto statement subject. A missing attestation
67
+ // fails fast naming the cosign `--type` flag that emits it.
68
+ // MULTIPLICITY IS EXPECTED: a multi-arch index is the product of one
69
+ // build per platform, and lanes attest accordingly — one SLSA + one SBOM
70
+ // attestation per architecture, all bound to the index digest (plus
71
+ // further copies whenever a lane re-runs, because cosign attest APPENDS).
72
+ // The assembler therefore selects ONE deterministic witness per type:
73
+ // the provenance layer whose statement names the PRIMARY build's own
74
+ // builder id + invocation id (the identity of the caller's `--provenance`
75
+ // predicate, so the statement's artifact section and build section
76
+ // describe the SAME build), and — no caller-side SBOM reference exists —
77
+ // the smallest SBOM layer digest. A subject with no layer describing the
78
+ // primary build fails fast.
79
+ //
80
+ // The runtime image source index, the registry credential declaration and the
81
+ // declared trust policy are read from the data root (`--data-root`), and the
82
+ // subject's source objects from the source checkouts under `--sources-root`
83
+ // at their aggregator-relative paths.
84
+ //
85
+ // Registry credentials come from image-registry-credentials.json (the file
86
+ // names environment variables, never secrets).
87
+
88
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
89
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
90
+
91
+ import { editionWorkspaceConfig } from './edition-config.mjs';
92
+ import {
93
+ WorkspaceRoot,
94
+ configureEditionWorkspaceFromArgv,
95
+ dataRoot,
96
+ isEntryModule,
97
+ sourcePath as sourceCheckoutPath,
98
+ } from './edition-workspace.mjs';
99
+ import { COMMIT_PATTERN, GITHUB_REF_PATTERN, GITHUB_REPOSITORY_PATTERN, SHA256_DIGEST_PATTERN, SHA256_HEX_PATTERN } from './evidence-grammars.mjs';
100
+ import { firstPartySourceId } from './first-party-source-ids.mjs';
101
+ import { canonicalJson } from './image-evidence.mjs';
102
+ import {
103
+ assertGitCommitExists,
104
+ compareStrings,
105
+ hashImageBuildInputs,
106
+ imageInputSourceHashAlgorithm,
107
+ loadKernelDistributionSchemas,
108
+ readGitJsonAtCommit,
109
+ serializeJson,
110
+ sha256Hex,
111
+ } from './lib.mjs';
112
+ import { OrasRegistry } from './oras.mjs';
113
+ import { assertGitHubProducerContext, assertProducerInput, buildImageSourceStatements, readPinnedContractAuthority, sandboxedGit } from './produce-image-source-evidence.mjs';
114
+ import {
115
+ loadRegistryCredentialDeclarations,
116
+ resolveRegistryCredential,
117
+ splitImageRef,
118
+ } from './registry-credentials.mjs';
119
+ import {
120
+ COMPOSED_PROCESS_DECLARATION_FILE,
121
+ COMPOSED_PROCESS_IMAGE_PATH,
122
+ composedProcessSubjectId,
123
+ subjectServicePath,
124
+ } from './resolve-images.mjs';
125
+ import {
126
+ platformIdentity,
127
+ resolvePublishedPlatforms,
128
+ } from './verify-image-platforms.mjs';
129
+
130
+ const SOURCE_REPO_PATTERN = /^(?:repos|submodules)\/[a-zA-Z0-9_.-]+$/;
131
+ // The kernel's `CanonicalIdentifierSchema` shape (runId / subject id).
132
+ const CANONICAL_IDENTIFIER_PATTERN =
133
+ /^[a-zA-Z0-9](?:[a-zA-Z0-9._:/@+-]{0,254}[a-zA-Z0-9])?$/;
134
+ // cosign v2 attaches every attestation of a subject as one layer of the
135
+ // single `sha256-<subject-digest>.att` manifest; each layer is a DSSE
136
+ // envelope whose `predicateType` annotation names the attestation.
137
+ const ATTESTATION_TAG_SUFFIX = '.att';
138
+ const DSSE_ENVELOPE_LAYER_MEDIA_TYPE = 'application/vnd.dsse.envelope.v1+json';
139
+ const IN_TOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json';
140
+ const PREDICATE_TYPE_ANNOTATION = 'predicateType';
141
+ const SBOM_PREDICATE_TYPE = 'https://spdx.dev/Document';
142
+ const PROVENANCE_PREDICATE_TYPE = 'https://slsa.dev/provenance/v1';
143
+ // The third mandatory deploy predicate (see the producing workflows). It
144
+ // legitimately shares the `.att` manifest but occupies no producer-input slot.
145
+ const DEPLOY_SOURCE_PREDICATE_TYPE =
146
+ 'https://xema.dev/attestations/deploy-source/v1';
147
+ const MAX_ATTESTATION_LAYER_BYTES = 64 * 1024 * 1024;
148
+ // The vendoring record the producing workflow consumed — committed in the
149
+ // producing repository, read AT THE BUILD COMMIT (see header).
150
+ const CROSS_REPO_RECORD_PATH = 'tooling/release/cross-repo-build-inputs.json';
151
+
152
+ function canonicalRelativePath(value, label) {
153
+ if (
154
+ typeof value !== 'string' ||
155
+ value.length === 0 ||
156
+ value !== value.normalize('NFC') ||
157
+ value.startsWith('/') ||
158
+ value.endsWith('/') ||
159
+ value
160
+ .split('/')
161
+ .some((segment) => segment === '' || segment === '.' || segment === '..')
162
+ ) {
163
+ throw new Error(
164
+ `${label} must be an NFC-normalized repository-relative path without traversal`,
165
+ );
166
+ }
167
+ return value;
168
+ }
169
+
170
+ export function assertWorkflowContext(env) {
171
+ const repository = env.GITHUB_REPOSITORY;
172
+ const ref = env.GITHUB_REF;
173
+ const sha = env.GITHUB_SHA;
174
+ const workflowRef = env.GITHUB_WORKFLOW_REF;
175
+ if (!GITHUB_REPOSITORY_PATTERN.test(repository ?? '')) {
176
+ throw new Error('GITHUB_REPOSITORY is not an exact owner/repo slug');
177
+ }
178
+ if (!GITHUB_REF_PATTERN.test(ref ?? '')) {
179
+ throw new Error('GITHUB_REF is not an exact branch or tag ref');
180
+ }
181
+ if (!COMMIT_PATTERN.test(sha ?? '')) {
182
+ throw new Error('GITHUB_SHA is not an exact 40-hex commit');
183
+ }
184
+ const prefix = `${repository}/`;
185
+ const suffix = `@${ref}`;
186
+ if (
187
+ typeof workflowRef !== 'string' ||
188
+ !workflowRef.startsWith(prefix) ||
189
+ !workflowRef.endsWith(suffix) ||
190
+ workflowRef.length <= prefix.length + suffix.length
191
+ ) {
192
+ throw new Error(
193
+ `GITHUB_WORKFLOW_REF "${workflowRef}" is not exactly <repository>/<workflow-path>@<ref> for this run`,
194
+ );
195
+ }
196
+ const path = workflowRef.slice(
197
+ prefix.length,
198
+ workflowRef.length - suffix.length,
199
+ );
200
+ if (
201
+ !path.startsWith('.github/workflows/') ||
202
+ path.split('/').some((segment) => !segment || segment === '..')
203
+ ) {
204
+ throw new Error(
205
+ `GITHUB_WORKFLOW_REF names "${path}", which is not an exact workflow file under .github/workflows/`,
206
+ );
207
+ }
208
+ const runId = env.GITHUB_RUN_ID;
209
+ if (!CANONICAL_IDENTIFIER_PATTERN.test(runId ?? '')) {
210
+ throw new Error('GITHUB_RUN_ID is not a usable run identifier');
211
+ }
212
+ const runAttempt = Number(env.GITHUB_RUN_ATTEMPT);
213
+ if (
214
+ !Number.isSafeInteger(runAttempt) ||
215
+ runAttempt < 1 ||
216
+ String(runAttempt) !== env.GITHUB_RUN_ATTEMPT
217
+ ) {
218
+ throw new Error('GITHUB_RUN_ATTEMPT is not a positive integer');
219
+ }
220
+ return { repository, ref, sha, path, workflowRef, runId, runAttempt };
221
+ }
222
+
223
+ /**
224
+ * The build identity section of the producer input. `buildType` and the two
225
+ * digests are read off the BuildKit SLSA v1 predicate the producing workflow
226
+ * exported from the image it just pushed — the same bytes it cosign-attests
227
+ * and the deploy plane re-verifies — so the evidence can never describe a
228
+ * different build than the one that ran.
229
+ */
230
+ export function deriveBuilder({ provenancePath, sourceCommit }) {
231
+ if (!isAbsolute(provenancePath)) {
232
+ throw new Error('--provenance must be an absolute path');
233
+ }
234
+ let predicate;
235
+ try {
236
+ predicate = JSON.parse(readFileSync(provenancePath, 'utf8'));
237
+ } catch (error) {
238
+ throw new Error(
239
+ `cannot read the exported BuildKit provenance predicate at ${provenancePath}: ${error.message}`,
240
+ );
241
+ }
242
+ const label = 'exported BuildKit SLSA v1 provenance predicate';
243
+ if (
244
+ typeof predicate !== 'object' ||
245
+ predicate === null ||
246
+ Array.isArray(predicate)
247
+ ) {
248
+ throw new Error(`${label} is not a JSON object`);
249
+ }
250
+ const buildDefinition = predicate.buildDefinition;
251
+ if (
252
+ typeof buildDefinition !== 'object' ||
253
+ buildDefinition === null ||
254
+ Array.isArray(buildDefinition)
255
+ ) {
256
+ throw new Error(
257
+ `${label} has no buildDefinition object — SLSA v1 (BuildKit >= v0.28) is required; ` +
258
+ 'pin the BuildKit image and pass provenance: mode=max,version=v1 to docker/build-push-action',
259
+ );
260
+ }
261
+ // SLSA v1 names the build type INSIDE buildDefinition; a top-level buildType
262
+ // is the retired v0.2 shape.
263
+ const buildType = buildDefinition.buildType;
264
+ if (typeof buildType !== 'string' || buildType.length === 0) {
265
+ throw new Error(
266
+ `${label} names no buildDefinition.buildType — is this SLSA v1 output?`,
267
+ );
268
+ }
269
+ const revision =
270
+ predicate.runDetails?.metadata?.buildkit_metadata?.vcs?.revision;
271
+ if (revision !== sourceCommit) {
272
+ throw new Error(
273
+ `${label} records VCS revision "${String(revision)}", not source commit ${sourceCommit} — ` +
274
+ 'the exported predicate must describe exactly the build this evidence names',
275
+ );
276
+ }
277
+ const parameters = {};
278
+ if ('externalParameters' in buildDefinition) {
279
+ parameters.externalParameters = buildDefinition.externalParameters;
280
+ }
281
+ if ('internalParameters' in buildDefinition) {
282
+ parameters.internalParameters = buildDefinition.internalParameters;
283
+ }
284
+ if (Object.keys(parameters).length === 0) {
285
+ throw new Error(
286
+ `${label} carries neither externalParameters nor internalParameters — nothing binds the build's inputs`,
287
+ );
288
+ }
289
+ return {
290
+ buildType,
291
+ buildDefinitionDigest: `sha256:${sha256Hex(canonicalJson(buildDefinition))}`,
292
+ parametersDigest: `sha256:${sha256Hex(canonicalJson(parameters))}`,
293
+ // The parsed predicate itself: the registry scan below binds the
294
+ // statement's provenance witness to this exact build's identity.
295
+ predicate,
296
+ };
297
+ }
298
+
299
+ /**
300
+ * The identity the PRIMARY build's SLSA attestation must carry: builder id +
301
+ * invocation id, read off a decoded SLSA v1 predicate. Both spellings of the
302
+ * invocation key occur in the wild — BuildKit's own rendering writes
303
+ * `invocationId`, lanes whose predicates passed through other renderings
304
+ * carry `invocationID` — so both are read, never rewritten.
305
+ */
306
+ function slsaBuildIdentity(predicate, label) {
307
+ const builderId = predicate?.runDetails?.builder?.id;
308
+ if (typeof builderId !== 'string' || builderId.length === 0) {
309
+ throw new Error(`${label} records no runDetails.builder.id — the builder identity is what binds an attestation to one build`);
310
+ }
311
+ const metadata = predicate?.runDetails?.metadata;
312
+ const invocationId =
313
+ typeof metadata?.invocationId === 'string'
314
+ ? metadata.invocationId
315
+ : typeof metadata?.invocationID === 'string'
316
+ ? metadata.invocationID
317
+ : undefined;
318
+ if (invocationId === undefined || invocationId.length === 0) {
319
+ throw new Error(
320
+ `${label} records no runDetails.metadata.invocationId/invocationID — without an invocation id one build's attestation cannot be told from another's`,
321
+ );
322
+ }
323
+ return { builderId, invocationId };
324
+ }
325
+
326
+ /**
327
+ * The vendoring record the producing checkout committed — read at the BUILD
328
+ * commit, because it pins the cross-repo bytes the build actually consumed.
329
+ * Absent record (a repository with no cross-repo inputs) returns null.
330
+ *
331
+ * EVERY field this function returns is validated HERE, before any consumer
332
+ * filters it. An entry that drops out of the source-hash closure does not fail
333
+ * — it produces a DIFFERENT `sourceTreeHash`, and the only symptom is the
334
+ * registry cross-check reporting "the image was not published from this source
335
+ * identity" hundreds of lines later, naming neither the record nor the entry.
336
+ * A malformed vendoring record is unusable, full stop; it must say so here.
337
+ */
338
+ export function readCrossRepoRecord(repoDir, commit, repo) {
339
+ const label = `${repo}@${commit}:${CROSS_REPO_RECORD_PATH}`;
340
+ let present = true;
341
+ try {
342
+ sandboxedGit(repoDir, [
343
+ 'cat-file',
344
+ '-e',
345
+ `${commit}:${CROSS_REPO_RECORD_PATH}`,
346
+ ]);
347
+ } catch {
348
+ present = false;
349
+ }
350
+ if (!present) {return null;}
351
+ let record;
352
+ try {
353
+ record = readGitJsonAtCommit(repoDir, CROSS_REPO_RECORD_PATH, commit);
354
+ } catch (error) {
355
+ throw new Error(`${label} is not readable JSON: ${error.message}`);
356
+ }
357
+ if (record?.schemaVersion !== 1 || !Array.isArray(record.inputs)) {
358
+ throw new Error(`${label} is not a schemaVersion 1 cross-repo record`);
359
+ }
360
+ // Build-step context directories ride straight into `hashImageBuildInputs`.
361
+ // They were consumed with `?? []` and NO validation at all, so a record
362
+ // carrying a non-array (or a traversing path) silently hashed a different
363
+ // closure than the build consumed.
364
+ const buildStepContextDirectories = record.buildStepContextDirectories ?? [];
365
+ if (!Array.isArray(buildStepContextDirectories)) {
366
+ throw new Error(
367
+ `${label} buildStepContextDirectories must be an array of repository-relative directories`,
368
+ );
369
+ }
370
+ for (const directory of buildStepContextDirectories) {
371
+ canonicalRelativePath(
372
+ directory,
373
+ `${label} buildStepContextDirectories entry`,
374
+ );
375
+ }
376
+ // Shape FIRST, selection second. The `consumerService` filter used to run
377
+ // BEFORE this validation, so an entry MISSING `consumerService` compared
378
+ // `undefined !== null && undefined !== id` — true — and was silently skipped
379
+ // by the very check that exists to reject it.
380
+ for (const input of record.inputs) {
381
+ if (
382
+ input === null ||
383
+ typeof input !== 'object' ||
384
+ Array.isArray(input) ||
385
+ (input.consumerService !== null &&
386
+ typeof input.consumerService !== 'string') ||
387
+ (typeof input.consumerService === 'string' &&
388
+ input.consumerService.length === 0) ||
389
+ typeof input.sourceRepo !== 'string' ||
390
+ !SOURCE_REPO_PATTERN.test(input.sourceRepo) ||
391
+ !COMMIT_PATTERN.test(input.sourceCommit ?? '') ||
392
+ typeof input.sourcePath !== 'string' ||
393
+ typeof input.destinationPath !== 'string'
394
+ ) {
395
+ throw new Error(`${label} carries a malformed input ${serializeJson(input ?? null)}`);
396
+ }
397
+ canonicalRelativePath(input.sourcePath, `${label} input sourcePath`);
398
+ canonicalRelativePath(
399
+ input.destinationPath,
400
+ `${label} input destinationPath`,
401
+ );
402
+ }
403
+ return { inputs: record.inputs, buildStepContextDirectories };
404
+ }
405
+
406
+ /**
407
+ * The subject's source identity, computed with the exact lock algorithms:
408
+ * sourceManifestHash (the runtime-image-source-index algorithm),
409
+ * sourceTreeHash (the ImageInputV2 build-input closure), and the
410
+ * repositoryUrl the verifier binds (the runtime-image-source-index pin).
411
+ */
412
+ /**
413
+ * The `platform-service-index.json` entry a `platform`-kind subject is built
414
+ * from.
415
+ *
416
+ * `platform` subjects are the fleet's only repository-ROOT subjects: their lock
417
+ * entry is synthesized in `resolve-images.mjs` from
418
+ * `distributionLock.platformServices[].source`, with `path === repo` and
419
+ * `sourceManifestPath === '<repo>/package.json'` — they are whole repositories
420
+ * (the five generic submodules), not biome directories.
421
+ *
422
+ * `deriveSubjectSourceIdentity` recognized only two shapes: a supplemental
423
+ * subject declared in the runtime index, or a biome subject whose path names a
424
+ * directory inside the repository. A `platform` subject is neither, so EVERY one
425
+ * of them failed deterministically — before any registry hop — on "is not a
426
+ * supplemental subject, so its source path must name a biome directory". All
427
+ * five generic submodules were therefore unable to produce image-source evidence
428
+ * at all.
429
+ *
430
+ * The index is the SAME authority `resolve-images.mjs` resolved the lock's
431
+ * platform subjects from, so resolving through it here is what makes the
432
+ * assembler's `sourceTreeHash` equal the lock's `contentHash` — the equality the
433
+ * whole producer contract rests on. `imageKey` (not `name`) is the subject id,
434
+ * because that is the field `resolve-images.mjs` uses for `subjectId`.
435
+ */
436
+ function findPlatformServiceSource({ readPlatformServiceIndex, kind, id, repo }) {
437
+ if (typeof readPlatformServiceIndex !== 'function') {
438
+ throw new Error(
439
+ `subject ${kind}:${id} is a platform subject, which is resolved from ` +
440
+ 'platform-service-index.json, but no reader for that index was provided',
441
+ );
442
+ }
443
+ const matches = readPlatformServiceIndex().services.filter(
444
+ (candidate) => candidate.imageKey === id && candidate.repo === repo,
445
+ );
446
+ if (matches.length !== 1) {
447
+ throw new Error(
448
+ `subject ${kind}:${id} matches ${matches.length} entries of ` +
449
+ `platform-service-index.json for repository "${repo}"; exactly one is ` +
450
+ 'required — a platform subject is resolved from that index, which is the ' +
451
+ 'same authority resolve-images.mjs built the lock from',
452
+ );
453
+ }
454
+ return matches[0];
455
+ }
456
+
457
+ export async function deriveSubjectSourceIdentity({
458
+ runtimeIndex,
459
+ // Lazy on purpose: platform-service-index.json is needed ONLY to resolve a
460
+ // `platform` subject, and requiring it for every subject made the file an
461
+ // unconditional input of a derivation that mostly does not read it.
462
+ readPlatformServiceIndex,
463
+ // The kernel enum member, passed in rather than written as a literal: the
464
+ // subject kind is a closed set the kernel owns, and a local copy is a second
465
+ // source of truth waiting to drift.
466
+ platformSubjectKind,
467
+ // The kernel's `RuntimeImageSubjectKind.ComposedProcess`, passed in for the
468
+ // reason `platformSubjectKind` is. Undefined: no subject is a composed one.
469
+ composedProcessSubjectKind = undefined,
470
+ declaration,
471
+ }) {
472
+ const { kind, id, repo, path, sourceCommit } = declaration;
473
+ if (typeof platformSubjectKind !== 'string' || !platformSubjectKind) {
474
+ throw new Error(
475
+ 'the kernel platform subject kind is required to resolve a source identity',
476
+ );
477
+ }
478
+ const pin = runtimeIndex.repositoryPins.find(
479
+ (candidate) => candidate.repo === repo,
480
+ );
481
+ if (!pin) {
482
+ throw new Error(
483
+ `subject ${kind}:${id} names source repository "${repo}", which has no ` +
484
+ 'repository pin in runtime-image-source-index.json — it is not an image source repository',
485
+ );
486
+ }
487
+ const repoDir = sourceCheckoutPath(repo);
488
+ assertGitCommitExists(repoDir, sourceCommit);
489
+ // Supplemental subjects (host shell, independently sourced platform images)
490
+ // declare their source manifest in the runtime index; biome subjects carry
491
+ // `<biome>/xema-biome.json`.
492
+ const supplemental = runtimeIndex.supplementalSubjects.find(
493
+ (candidate) => candidate.kind === kind && candidate.id === id,
494
+ );
495
+ let manifestRepoRelativePath;
496
+ let subjectForServicePath = { kind, id, repo, path };
497
+ if (supplemental) {
498
+ if (supplemental.repo !== repo || supplemental.path !== path) {
499
+ throw new Error(
500
+ `subject ${kind}:${id} is a supplemental subject of ${supplemental.repo}:${supplemental.path} ` +
501
+ `in runtime-image-source-index.json, not of ${repo}:${path}`,
502
+ );
503
+ }
504
+ if (!supplemental.sourceManifestPath.startsWith(`${repo}/`)) {
505
+ throw new Error(
506
+ `subject ${kind}:${id} sourceManifestPath "${supplemental.sourceManifestPath}" ` +
507
+ `escapes its repository "${repo}"`,
508
+ );
509
+ }
510
+ manifestRepoRelativePath = supplemental.sourceManifestPath.slice(
511
+ repo.length + 1,
512
+ );
513
+ subjectForServicePath = {
514
+ ...subjectForServicePath,
515
+ sourceManifestPath: manifestRepoRelativePath,
516
+ };
517
+ } else if (composedProcessSubjectKind !== undefined && kind === composedProcessSubjectKind) {
518
+ // The composed-process image of one build: a Dockerfile-only image at the
519
+ // repository's `infra/composed-process/`, whose declaration is its
520
+ // `image-build-inputs.json` — exactly the shape resolve-images.mjs names
521
+ // in the lock (`readComposedProcessDeclarations`).
522
+ if (path !== `${repo}/${COMPOSED_PROCESS_IMAGE_PATH}`) {
523
+ throw new Error(
524
+ `subject ${kind}:${id} declares source path "${path}", but a composed-process ` +
525
+ `image is built from "${repo}/${COMPOSED_PROCESS_IMAGE_PATH}"`,
526
+ );
527
+ }
528
+ const expectedId = composedProcessSubjectId(firstPartySourceId(repo));
529
+ if (id !== expectedId) {
530
+ throw new Error(
531
+ `subject ${kind}:${id} is not the composed-process subject of "${repo}" ("${expectedId}")`,
532
+ );
533
+ }
534
+ manifestRepoRelativePath = `${COMPOSED_PROCESS_IMAGE_PATH}/${COMPOSED_PROCESS_DECLARATION_FILE}`;
535
+ subjectForServicePath = {
536
+ ...subjectForServicePath,
537
+ sourceManifestPath: manifestRepoRelativePath,
538
+ };
539
+ } else if (kind === platformSubjectKind) {
540
+ // The third shape: a whole-repository platform service. See
541
+ // `findPlatformServiceSource`.
542
+ const service = findPlatformServiceSource({
543
+ readPlatformServiceIndex,
544
+ kind,
545
+ id,
546
+ repo,
547
+ });
548
+ if (path !== repo) {
549
+ throw new Error(
550
+ `subject ${kind}:${id} declares source path "${path}", but a platform ` +
551
+ `subject is the whole repository "${repo}" — resolve-images.mjs sets ` +
552
+ 'path === repo for it, and any other path hashes a closure the lock never named',
553
+ );
554
+ }
555
+ if (!service.path.startsWith(`${repo}/`)) {
556
+ throw new Error(
557
+ `subject ${kind}:${id} platform-service-index path "${service.path}" ` +
558
+ `escapes its repository "${repo}"`,
559
+ );
560
+ }
561
+ manifestRepoRelativePath = service.path.slice(repo.length + 1);
562
+ // Setting `sourceManifestPath` is what makes `subjectServicePath` return the
563
+ // repository ROOT instead of reading a `xema-biome.json` that does not
564
+ // exist — exactly the path whose build-input closure produced the lock's
565
+ // contentHash.
566
+ subjectForServicePath = {
567
+ ...subjectForServicePath,
568
+ sourceManifestPath: manifestRepoRelativePath,
569
+ };
570
+ } else {
571
+ if (path === repo) {
572
+ throw new Error(
573
+ `subject ${kind}:${id} is not a supplemental subject, so its source path ` +
574
+ 'must name a biome directory inside the repository, not the repository root',
575
+ );
576
+ }
577
+ manifestRepoRelativePath = `${path.slice(repo.length + 1)}/xema-biome.json`;
578
+ }
579
+ const { computeBiomeSourceManifestHash } = await loadKernelDistributionSchemas();
580
+ const sourceManifestHash = await computeBiomeSourceManifestHash(
581
+ readGitJsonAtCommit(repoDir, manifestRepoRelativePath, sourceCommit),
582
+ );
583
+ const servicePath = subjectServicePath(subjectForServicePath, sourceCommit);
584
+ // Vendored cross-repo inputs EXACTLY as the build consumed them: the record
585
+ // committed in the producing checkout at the build commit. A `null`
586
+ // consumerService vendors for EVERY service of the consumer repository.
587
+ // `readCrossRepoRecord` has already proven EVERY entry well-formed, so the
588
+ // filter below can only ever narrow a valid set — it can no longer be the
589
+ // thing that hides an invalid one.
590
+ const record = readCrossRepoRecord(repoDir, sourceCommit, repo);
591
+ const crossRepoInputs = [];
592
+ for (const input of record?.inputs ?? []) {
593
+ // A composed image carries EVERY service of its build, so it vendors every
594
+ // input the record declares — the same set resolve-images.mjs hashes.
595
+ const composed =
596
+ composedProcessSubjectKind !== undefined && kind === composedProcessSubjectKind;
597
+ if (!composed && input.consumerService !== null && input.consumerService !== id) {
598
+ continue;
599
+ }
600
+ const sourceRepoDir = sourceCheckoutPath(input.sourceRepo);
601
+ assertGitCommitExists(sourceRepoDir, input.sourceCommit);
602
+ crossRepoInputs.push({
603
+ destinationPath: input.destinationPath,
604
+ sourceRepoDir,
605
+ sourcePath: input.sourcePath,
606
+ sourceCommit: input.sourceCommit,
607
+ });
608
+ }
609
+ const sourceTreeHash = hashImageBuildInputs(
610
+ {
611
+ repoDir,
612
+ service: id,
613
+ servicePath,
614
+ commit: sourceCommit,
615
+ crossRepoInputs,
616
+ buildStepDirectories: [...(record?.buildStepContextDirectories ?? [])],
617
+ },
618
+ imageInputSourceHashAlgorithm(),
619
+ );
620
+ return {
621
+ repositoryUrl: pin.repositoryUrl,
622
+ sourceManifestHash,
623
+ sourceTreeHash,
624
+ };
625
+ }
626
+
627
+ /**
628
+ * The SBOM + provenance digests carried by the subject's cosign attestation
629
+ * manifest (`sha256-<digest>.att`): the DSSE envelope layer digests, bound to
630
+ * the delivered digest through their decoded statement subject. Fail-closed on
631
+ * every gap, naming the pipeline step that must emit the missing artifact.
632
+ *
633
+ * A merged multi-arch index legitimately carries SEVERAL attestations of each
634
+ * type — one per platform build, plus one more per lane re-run, because
635
+ * `cosign attest` appends. Every candidate is validated (envelope shape,
636
+ * predicate type, subject binding) and then ONE witness per type is selected
637
+ * deterministically:
638
+ *
639
+ * * SLSA provenance — the candidate whose statement names the SAME build as
640
+ * the caller's exported `--provenance` predicate (equal builder id AND
641
+ * invocation id), so the statement's artifact and build sections describe
642
+ * one and the same build. Ties (identical re-attestations of that build)
643
+ * resolve to the smallest layer digest; no match fails loudly.
644
+ * * SBOM — no caller-side reference exists, so the smallest layer digest
645
+ * among the validated candidates. Content-addressed, stable, and every
646
+ * candidate already proved it is an SBOM attestation bound to the
647
+ * delivered digest.
648
+ */
649
+ async function resolveCosignAttestationDigests({
650
+ client,
651
+ repository,
652
+ imageRepository,
653
+ manifestDigest,
654
+ label,
655
+ referenceProvenance,
656
+ }) {
657
+ const attestationTag = `sha256-${manifestDigest.slice('sha256:'.length)}${ATTESTATION_TAG_SUFFIX}`;
658
+ let attestation;
659
+ try {
660
+ attestation = await client.getManifestByTag(repository, attestationTag);
661
+ } catch (error) {
662
+ throw new Error(
663
+ `${label} has no cosign attestation manifest at tag ${attestationTag}: ${error.message}. ` +
664
+ 'The producing workflow must `cosign attest` the SBOM (--type spdxjson) and the SLSA ' +
665
+ `provenance (--type slsaprovenance1) against the exact delivered digest ${manifestDigest}. ` +
666
+ 'If this digest is a multi-arch index merged AFTER the build attested the pre-merge image ' +
667
+ '(a separate multi-arch merge workflow), the merge step must re-run those attest calls on the merged ' +
668
+ 'index.',
669
+ );
670
+ }
671
+ if (attestation.isIndex) {
672
+ throw new Error(
673
+ `${label} attestation tag ${attestationTag} resolved to an index; the assembler expects the ` +
674
+ 'single-manifest / multi-layer shape cosign v2.6.3 writes',
675
+ );
676
+ }
677
+ const layers = Array.isArray(attestation.document.layers)
678
+ ? attestation.document.layers
679
+ : [];
680
+ if (layers.length === 0) {
681
+ throw new Error(
682
+ `${label} attestation manifest ${attestation.digest} carries no layers`,
683
+ );
684
+ }
685
+ const deliveredHex = manifestDigest.slice('sha256:'.length);
686
+ const sbomCandidates = [];
687
+ const provenanceCandidates = [];
688
+ for (const layer of layers) {
689
+ if (layer?.mediaType !== DSSE_ENVELOPE_LAYER_MEDIA_TYPE) {
690
+ throw new Error(
691
+ `${label} attestation manifest ${attestation.digest} carries a ` +
692
+ `"${String(layer?.mediaType)}" layer; only ${DSSE_ENVELOPE_LAYER_MEDIA_TYPE} ` +
693
+ 'layers are admissible',
694
+ );
695
+ }
696
+ const predicateType = layer.annotations?.[PREDICATE_TYPE_ANNOTATION];
697
+ if (typeof predicateType !== 'string' || predicateType.length === 0) {
698
+ throw new Error(
699
+ `${label} attestation layer ${layer.digest} has no "${PREDICATE_TYPE_ANNOTATION}" annotation`,
700
+ );
701
+ }
702
+ if (predicateType === DEPLOY_SOURCE_PREDICATE_TYPE) {continue;}
703
+ if (
704
+ predicateType !== SBOM_PREDICATE_TYPE &&
705
+ predicateType !== PROVENANCE_PREDICATE_TYPE
706
+ ) {
707
+ throw new Error(
708
+ `${label} attestation layer ${layer.digest} has unrecognized predicate type ` +
709
+ `"${predicateType}" — only ${SBOM_PREDICATE_TYPE}, ${PROVENANCE_PREDICATE_TYPE} and ` +
710
+ `${DEPLOY_SOURCE_PREDICATE_TYPE} may hang off a producer subject`,
711
+ );
712
+ }
713
+ // Fetching the envelope proves it is stored and content-addressed, and the
714
+ // decoded statement must bind EXACTLY the delivered digest: an attestation
715
+ // that names any other subject describes a different image.
716
+ const bytes = await client.getBlob(
717
+ repository,
718
+ layer.digest,
719
+ MAX_ATTESTATION_LAYER_BYTES,
720
+ );
721
+ let envelope;
722
+ try {
723
+ envelope = JSON.parse(bytes.toString('utf8'));
724
+ } catch {
725
+ throw new Error(
726
+ `${label} attestation layer ${layer.digest} is not valid JSON`,
727
+ );
728
+ }
729
+ if (envelope?.payloadType !== IN_TOTO_PAYLOAD_TYPE) {
730
+ throw new Error(
731
+ `${label} attestation layer ${layer.digest} is not an ${IN_TOTO_PAYLOAD_TYPE} DSSE envelope`,
732
+ );
733
+ }
734
+ let statement;
735
+ try {
736
+ statement = JSON.parse(
737
+ Buffer.from(String(envelope.payload), 'base64').toString('utf8'),
738
+ );
739
+ } catch {
740
+ throw new Error(
741
+ `${label} attestation layer ${layer.digest} carries no decodable in-toto statement`,
742
+ );
743
+ }
744
+ if (statement?.predicateType !== predicateType) {
745
+ throw new Error(
746
+ `${label} attestation layer ${layer.digest} annotates "${predicateType}" ` +
747
+ `but its statement declares "${String(statement?.predicateType)}"`,
748
+ );
749
+ }
750
+ const expectedSubject = {
751
+ name: imageRepository,
752
+ digest: { sha256: deliveredHex },
753
+ };
754
+ if (
755
+ !Array.isArray(statement.subject) ||
756
+ statement.subject.length !== 1 ||
757
+ JSON.stringify(statement.subject[0]) !== JSON.stringify(expectedSubject)
758
+ ) {
759
+ throw new Error(
760
+ `${label} attestation layer ${layer.digest} binds ` +
761
+ `${serializeJson(statement.subject ?? null)}, not the delivered subject ` +
762
+ `${serializeJson(expectedSubject)} — the producing workflow must attest the exact ` +
763
+ 'digest the canonical coordinate serves',
764
+ );
765
+ }
766
+ if (predicateType === SBOM_PREDICATE_TYPE) {
767
+ sbomCandidates.push({ digest: layer.digest, statement });
768
+ } else {
769
+ provenanceCandidates.push({ digest: layer.digest, statement });
770
+ }
771
+ }
772
+ if (sbomCandidates.length === 0) {
773
+ throw new Error(
774
+ `${label} carries no SBOM attestation (${SBOM_PREDICATE_TYPE}) — the producing workflow must run ` +
775
+ `cosign attest --yes --type spdxjson --predicate <spdx.json> ${imageRepository}@${manifestDigest}`,
776
+ );
777
+ }
778
+ if (provenanceCandidates.length === 0) {
779
+ throw new Error(
780
+ `${label} carries no SLSA provenance attestation (${PROVENANCE_PREDICATE_TYPE}) — the producing ` +
781
+ `workflow must run cosign attest --yes --type slsaprovenance1 --predicate <slsa-provenance.json> ` +
782
+ `${imageRepository}@${manifestDigest}`,
783
+ );
784
+ }
785
+ // The provenance witness must describe the SAME build the statement's build
786
+ // section describes — the caller's exported predicate. A multi-platform
787
+ // index carries one attestation per platform build; picking any other
788
+ // build's would attest the index with a different source's provenance.
789
+ const referenceIdentity = slsaBuildIdentity(
790
+ referenceProvenance,
791
+ 'the exported --provenance predicate',
792
+ );
793
+ const matches = provenanceCandidates.filter((candidate) => {
794
+ try {
795
+ const identity = slsaBuildIdentity(
796
+ candidate.statement.predicate,
797
+ `attestation layer ${candidate.digest}`,
798
+ );
799
+ return (
800
+ identity.builderId === referenceIdentity.builderId &&
801
+ identity.invocationId === referenceIdentity.invocationId
802
+ );
803
+ } catch {
804
+ // A candidate whose predicate names no build identity can never be the
805
+ // primary build's witness; it stays admissible for nothing.
806
+ return false;
807
+ }
808
+ });
809
+ if (matches.length === 0) {
810
+ throw new Error(
811
+ `${label} carries ${provenanceCandidates.length} SLSA provenance attestation(s) but none ` +
812
+ `describes the primary build (builder "${referenceIdentity.builderId}", invocation ` +
813
+ `"${referenceIdentity.invocationId}") — the lane must re-attest the merged index with ` +
814
+ 'that build\'s own predicate (mirror release-arm64-host-web.yaml\'s attest step)',
815
+ );
816
+ }
817
+ matches.sort((left, right) => compareStrings(left.digest, right.digest));
818
+ const provenanceDigest = matches[0].digest;
819
+ const sbomsByDigest = [...sbomCandidates].sort((left, right) =>
820
+ compareStrings(left.digest, right.digest),
821
+ );
822
+ const sbomDigest = sbomsByDigest[0].digest;
823
+ return { sbomDigest, provenanceDigest };
824
+ }
825
+
826
+ /**
827
+ * Registry side of one subject, resolved at the canonical
828
+ * `<imageRepository>:<sourceTreeHash>` coordinate: the served manifest digest
829
+ * (which must be exactly the caller's), its media type, the hash-proven
830
+ * platform set, and the SBOM/provenance attestation digests.
831
+ * `referenceProvenance` is the caller's parsed exported BuildKit predicate —
832
+ * the identity anchor the provenance-witness selection binds to.
833
+ */
834
+ export async function resolveImageArtifactEvidence({
835
+ imageRepository,
836
+ manifestDigest,
837
+ sourceTreeHash,
838
+ referenceProvenance,
839
+ distributionsRoot = dataRoot(),
840
+ env = process.env,
841
+ fetchImpl = globalThis.fetch,
842
+ registryFor = null,
843
+ }) {
844
+ if (!SHA256_DIGEST_PATTERN.test(manifestDigest ?? '')) {
845
+ throw new Error('--manifest-digest must be an exact sha256:<64-hex> digest');
846
+ }
847
+ if (!SHA256_HEX_PATTERN.test(sourceTreeHash ?? '')) {
848
+ throw new Error('sourceTreeHash must be 64 lowercase sha256 hex characters');
849
+ }
850
+ if (
851
+ typeof referenceProvenance !== 'object' ||
852
+ referenceProvenance === null ||
853
+ Array.isArray(referenceProvenance)
854
+ ) {
855
+ throw new Error(
856
+ 'referenceProvenance (the parsed exported BuildKit SLSA v1 predicate) is required — ' +
857
+ 'it is what binds the selected provenance attestation to the primary build',
858
+ );
859
+ }
860
+ const { authority, repository } = splitImageRef(imageRepository);
861
+ const declarations = loadRegistryCredentialDeclarations(distributionsRoot);
862
+ const credential = resolveRegistryCredential({ authority, declarations, env });
863
+ const client = registryFor
864
+ ? registryFor({ authority, credential })
865
+ : new OrasRegistry({ credential, fetchImpl });
866
+ const coordinate = `${imageRepository}:${sourceTreeHash}`;
867
+ const top = await client.getManifestByTag(repository, sourceTreeHash);
868
+ if (top.digest !== manifestDigest) {
869
+ throw new Error(
870
+ `${coordinate} serves ${top.digest}, not the caller-declared ${manifestDigest} — ` +
871
+ 'the image was not published from this source identity (wrong build commit, ' +
872
+ 'or the cross-repo record at the build commit differs from the one this source carries)',
873
+ );
874
+ }
875
+ // Proves EVERY platform by its own hash-verified image config and refuses
876
+ // attestation children as platforms — exactly the evidence the producer
877
+ // input carries.
878
+ const published = await resolvePublishedPlatforms({
879
+ client,
880
+ repository,
881
+ tag: sourceTreeHash,
882
+ coordinate,
883
+ });
884
+ if (published.indexDigest !== top.digest) {
885
+ throw new Error(
886
+ `${coordinate} did not resolve to the exact published manifest this evidence names`,
887
+ );
888
+ }
889
+ const label = `${imageRepository}@${top.digest}`;
890
+ const { sbomDigest, provenanceDigest } =
891
+ await resolveCosignAttestationDigests({
892
+ client,
893
+ repository,
894
+ imageRepository,
895
+ manifestDigest: top.digest,
896
+ label,
897
+ referenceProvenance,
898
+ });
899
+ const platforms = published.platforms
900
+ .map((entry) => ({
901
+ ...entry.platform,
902
+ manifestDigest: entry.manifestDigest,
903
+ }))
904
+ // The kernel's canonical platform order: os/architecture/variant,
905
+ // code-point comparison (assertPlatforms re-checks this on verification).
906
+ .sort((left, right) =>
907
+ compareStrings(platformIdentity(left), platformIdentity(right)),
908
+ );
909
+ return {
910
+ manifestDigest: top.digest,
911
+ manifestMediaType: top.mediaType,
912
+ platforms,
913
+ sbomDigest,
914
+ provenanceDigest,
915
+ };
916
+ }
917
+
918
+ /**
919
+ * The producer's DECLARED evidence lifetime, in seconds.
920
+ *
921
+ * This used to default to the trust policy's `maxProducerTtlSeconds` (the
922
+ * CEILING), which is wrong in two independent ways:
923
+ *
924
+ * 1. It collapses two numbers that must move independently. The ceiling is the
925
+ * VERIFIER's tolerance; this is the PRODUCER's intent. With them equal,
926
+ * `assertTimeWindow`'s `expires - issued > maxTtlSeconds` sits exactly on
927
+ * the boundary, so lowering the policy ceiling by one second retroactively
928
+ * invalidates every bundle ever signed — the ceiling could never be
929
+ * tightened without re-producing the entire fleet's evidence.
930
+ * 2. "As long as permitted" is not a decision. The offline verifier has NO
931
+ * revocation channel, so this TTL *is* the revocation window for a
932
+ * mis-issued or leaked bundle. Taking the maximum by default makes that
933
+ * window as wide as the policy allows, by omission rather than by choice.
934
+ *
935
+ * 72h is the shortest value that still spans a weekend: a bundle produced late
936
+ * Friday is still valid Monday morning, so a release or delivery that slips
937
+ * over a weekend never needs an out-of-band refresh. Everything shorter buys
938
+ * marginal revocation latency and pays for it in operational surprise.
939
+ *
940
+ * A producer that genuinely needs a different lifetime passes `--ttl-seconds`;
941
+ * the ceiling still bounds it, and the value is asserted against the ceiling
942
+ * below so this constant can never quietly exceed the policy.
943
+ */
944
+ export const DEFAULT_PRODUCER_TTL_SECONDS = 3 * 24 * 60 * 60;
945
+
946
+ function readProducerTtlSeconds(distributionsRoot) {
947
+ const declared = editionWorkspaceConfig().evidenceTrustPolicy.policy;
948
+ if (declared === null) {
949
+ throw new Error(
950
+ `${distributionsRoot} declares no evidenceTrustPolicy in edition-workspace.json; the producer TTL ceiling comes from it`,
951
+ );
952
+ }
953
+ const policyPath = join(distributionsRoot, declared);
954
+ let policy;
955
+ try {
956
+ policy = JSON.parse(readFileSync(policyPath, 'utf8'));
957
+ } catch (error) {
958
+ throw new Error(
959
+ `cannot read the committed trust policy at ${policyPath}: ${error.message}`,
960
+ );
961
+ }
962
+ if (
963
+ policy?.schemaVersion !== 1 ||
964
+ !Number.isSafeInteger(policy.maxProducerTtlSeconds) ||
965
+ policy.maxProducerTtlSeconds < 1
966
+ ) {
967
+ throw new Error(
968
+ `${policyPath} does not declare a positive integer maxProducerTtlSeconds`,
969
+ );
970
+ }
971
+ return policy.maxProducerTtlSeconds;
972
+ }
973
+
974
+ function rfc3339(timestamp) {
975
+ return new Date(timestamp).toISOString().replace(/\.\d{3}Z$/, 'Z');
976
+ }
977
+
978
+ /**
979
+ * Assemble the complete producer input manifest for ONE image subject and
980
+ * prove — before anything is written — that it satisfies the producer's exact
981
+ * field contract, the kernel statement schema, and the running GitHub context
982
+ * the producer action will compare it against.
983
+ */
984
+ export async function assembleProducerInput({
985
+ imageRepository,
986
+ manifestDigest,
987
+ subjectKind,
988
+ subjectId,
989
+ sourceRepo,
990
+ sourcePath,
991
+ sourceCommit,
992
+ provenancePath,
993
+ sourceManifestHash: declaredSourceManifestHash,
994
+ sourceTreeHash: declaredSourceTreeHash,
995
+ ttlSeconds,
996
+ distributionsRoot = dataRoot(),
997
+ env = process.env,
998
+ fetchImpl = globalThis.fetch,
999
+ registryFor = null,
1000
+ now = Date.now(),
1001
+ }) {
1002
+ const schemas = await loadKernelDistributionSchemas();
1003
+ const contractAuthority = readPinnedContractAuthority({
1004
+ dataRoot: distributionsRoot,
1005
+ schemas,
1006
+ });
1007
+ const subjectKinds = Object.values(schemas.RuntimeImageSubjectKind);
1008
+ if (!subjectKinds.includes(subjectKind)) {
1009
+ throw new Error(
1010
+ `--subject-kind "${String(subjectKind)}" must be one of ${subjectKinds.join(', ')}`,
1011
+ );
1012
+ }
1013
+ if (!CANONICAL_IDENTIFIER_PATTERN.test(subjectId ?? '')) {
1014
+ throw new Error('--subject-id is not a canonical identifier');
1015
+ }
1016
+ if (!SOURCE_REPO_PATTERN.test(sourceRepo ?? '')) {
1017
+ throw new Error(
1018
+ `--source-repo "${String(sourceRepo)}" must be an aggregator-plane repository (repos/<name> or submodules/<name>)`,
1019
+ );
1020
+ }
1021
+ canonicalRelativePath(sourcePath, '--source-path');
1022
+ if (sourcePath !== sourceRepo && !sourcePath.startsWith(`${sourceRepo}/`)) {
1023
+ throw new Error(`--source-path must stay inside ${sourceRepo}`);
1024
+ }
1025
+ if (!COMMIT_PATTERN.test(sourceCommit ?? '')) {
1026
+ throw new Error('--source-commit must be an exact 40-hex commit');
1027
+ }
1028
+ const context = assertWorkflowContext(env);
1029
+ const runtimeIndex = schemas.parseRuntimeImageSourceIndex(
1030
+ JSON.parse(
1031
+ readFileSync(
1032
+ join(distributionsRoot, 'runtime-image-source-index.json'),
1033
+ 'utf8',
1034
+ ),
1035
+ ),
1036
+ );
1037
+ const source = await deriveSubjectSourceIdentity({
1038
+ runtimeIndex,
1039
+ // Parsed through the kernel schema, not read raw: a `platform` subject's
1040
+ // whole source identity is resolved from this index, so a malformed index
1041
+ // must fail rather than produce a subtly wrong closure.
1042
+ readPlatformServiceIndex: () =>
1043
+ schemas.parsePlatformServiceSourceIndex(
1044
+ JSON.parse(
1045
+ readFileSync(
1046
+ join(distributionsRoot, 'platform-service-index.json'),
1047
+ 'utf8',
1048
+ ),
1049
+ ),
1050
+ ),
1051
+ platformSubjectKind: schemas.RuntimeImageSubjectKind.Platform,
1052
+ composedProcessSubjectKind: schemas.RuntimeImageSubjectKind.ComposedProcess,
1053
+ declaration: {
1054
+ kind: subjectKind,
1055
+ id: subjectId,
1056
+ repo: sourceRepo,
1057
+ path: sourcePath,
1058
+ sourceCommit,
1059
+ },
1060
+ });
1061
+ if (
1062
+ declaredSourceManifestHash !== undefined &&
1063
+ declaredSourceManifestHash !== source.sourceManifestHash
1064
+ ) {
1065
+ throw new Error(
1066
+ `--source-manifest-hash "${String(declaredSourceManifestHash)}" does not match the ` +
1067
+ `${source.sourceManifestHash} computed from ${sourceRepo}@${sourceCommit} — ` +
1068
+ 'the declared source identity is not the one this manifest hashes',
1069
+ );
1070
+ }
1071
+ if (
1072
+ declaredSourceTreeHash !== undefined &&
1073
+ declaredSourceTreeHash !== source.sourceTreeHash
1074
+ ) {
1075
+ throw new Error(
1076
+ `--source-tree-hash "${String(declaredSourceTreeHash)}" does not match the ` +
1077
+ `${source.sourceTreeHash} computed from the build-input closure of ` +
1078
+ `${sourceRepo}@${sourceCommit} — the declared coordinate is not the one this source builds`,
1079
+ );
1080
+ }
1081
+ const builder = deriveBuilder({ provenancePath, sourceCommit });
1082
+ const artifact = await resolveImageArtifactEvidence({
1083
+ imageRepository,
1084
+ manifestDigest,
1085
+ sourceTreeHash: source.sourceTreeHash,
1086
+ referenceProvenance: builder.predicate,
1087
+ distributionsRoot,
1088
+ env,
1089
+ fetchImpl,
1090
+ registryFor,
1091
+ });
1092
+ const maxTtlSeconds = readProducerTtlSeconds(distributionsRoot);
1093
+ // The DEFAULT bows to the policy ceiling; an EXPLICIT `--ttl-seconds` above
1094
+ // it still fails below. Tightening `maxProducerTtlSeconds` is a legitimate
1095
+ // operator decision, and it must not break every producer until someone
1096
+ // edits a constant in this file — but a caller who NAMES a lifetime the
1097
+ // verifier will reject has made a mistake, and gets told so.
1098
+ const effectiveTtlSeconds =
1099
+ ttlSeconds ?? Math.min(DEFAULT_PRODUCER_TTL_SECONDS, maxTtlSeconds);
1100
+ if (
1101
+ !Number.isSafeInteger(effectiveTtlSeconds) ||
1102
+ effectiveTtlSeconds < 1 ||
1103
+ effectiveTtlSeconds > maxTtlSeconds
1104
+ ) {
1105
+ throw new Error(
1106
+ `--ttl-seconds must be an integer between 1 and the trust policy's maxProducerTtlSeconds (${maxTtlSeconds})`,
1107
+ );
1108
+ }
1109
+ const input = {
1110
+ schemaVersion: 1,
1111
+ contractAuthority: { ...contractAuthority },
1112
+ sourceHashAlgorithm: imageInputSourceHashAlgorithm(),
1113
+ producerWorkflowSha: context.sha,
1114
+ signer: {
1115
+ certificateIdentity: `https://github.com/${context.repository}/${context.path}@${context.ref}`,
1116
+ oidcIssuer: 'https://token.actions.githubusercontent.com',
1117
+ githubWorkflow: {
1118
+ repository: context.repository,
1119
+ ref: context.ref,
1120
+ path: context.path,
1121
+ },
1122
+ },
1123
+ builder: {
1124
+ builderId: `https://github.com/${context.repository}/${context.path}`,
1125
+ buildType: builder.buildType,
1126
+ buildDefinitionDigest: builder.buildDefinitionDigest,
1127
+ parametersDigest: builder.parametersDigest,
1128
+ },
1129
+ runId: context.runId,
1130
+ runAttempt: context.runAttempt,
1131
+ issuedAt: rfc3339(now),
1132
+ expiresAt: rfc3339(now + effectiveTtlSeconds * 1000),
1133
+ expectedSubjects: [`${subjectKind}:${subjectId}`],
1134
+ subjects: [
1135
+ {
1136
+ kind: subjectKind,
1137
+ id: subjectId,
1138
+ repositoryUrl: source.repositoryUrl,
1139
+ repo: sourceRepo,
1140
+ path: sourcePath,
1141
+ sourceCommit,
1142
+ sourceManifestHash: source.sourceManifestHash,
1143
+ sourceTreeHash: source.sourceTreeHash,
1144
+ imageRepository,
1145
+ manifestDigest: artifact.manifestDigest,
1146
+ manifestMediaType: artifact.manifestMediaType,
1147
+ platforms: artifact.platforms,
1148
+ sbomDigest: artifact.sbomDigest,
1149
+ provenanceDigest: artifact.provenanceDigest,
1150
+ },
1151
+ ],
1152
+ };
1153
+ // Prove the output satisfies the producer BEFORE anything reads it: the
1154
+ // exact field contract, then the kernel statement schema the producer builds
1155
+ // (the same parse produce-image-source-evidence.mjs runs before signing),
1156
+ // then the GitHub context the producer action compares against.
1157
+ assertProducerInput(input, contractAuthority);
1158
+ buildImageSourceStatements(input, schemas, contractAuthority);
1159
+ assertGitHubProducerContext(input, {
1160
+ repository: context.repository,
1161
+ ref: context.ref,
1162
+ sha: context.sha,
1163
+ workflowRef: context.workflowRef,
1164
+ });
1165
+ return input;
1166
+ }
1167
+
1168
+ function requireValue(flags, name) {
1169
+ const value = flags.get(name);
1170
+ if (value === undefined) {
1171
+ throw new Error(`--${name} is required`);
1172
+ }
1173
+ return value;
1174
+ }
1175
+
1176
+ async function main() {
1177
+ const args = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
1178
+ WorkspaceRoot.Data,
1179
+ WorkspaceRoot.Sources,
1180
+ WorkspaceRoot.Kernel,
1181
+ ]);
1182
+ const flags = new Map();
1183
+ for (let index = 0; index < args.length; index += 2) {
1184
+ const flag = args[index];
1185
+ if (!flag.startsWith('--')) {
1186
+ throw new Error(`unexpected argument "${flag}"`);
1187
+ }
1188
+ const name = flag.slice(2);
1189
+ const value = args[index + 1];
1190
+ if (value === undefined || value.startsWith('--')) {
1191
+ throw new Error(`--${name} requires a value`);
1192
+ }
1193
+ if (flags.has(name)) {
1194
+ throw new Error(`--${name} was given twice`);
1195
+ }
1196
+ flags.set(name, value);
1197
+ }
1198
+ const known = new Set([
1199
+ 'output',
1200
+ 'image-repository',
1201
+ 'manifest-digest',
1202
+ 'subject-kind',
1203
+ 'subject-id',
1204
+ 'source-repo',
1205
+ 'source-path',
1206
+ 'source-commit',
1207
+ 'source-manifest-hash',
1208
+ 'source-tree-hash',
1209
+ 'provenance',
1210
+ 'ttl-seconds',
1211
+ ]);
1212
+ for (const name of flags.keys()) {
1213
+ if (!known.has(name)) {
1214
+ throw new Error(`unknown option "--${name}"`);
1215
+ }
1216
+ }
1217
+ const output = resolve(requireValue(flags, 'output'));
1218
+ const input = await assembleProducerInput({
1219
+ imageRepository: requireValue(flags, 'image-repository'),
1220
+ manifestDigest: requireValue(flags, 'manifest-digest'),
1221
+ subjectKind: requireValue(flags, 'subject-kind'),
1222
+ subjectId: requireValue(flags, 'subject-id'),
1223
+ sourceRepo: requireValue(flags, 'source-repo'),
1224
+ sourcePath: requireValue(flags, 'source-path'),
1225
+ sourceCommit: requireValue(flags, 'source-commit'),
1226
+ provenancePath: resolve(requireValue(flags, 'provenance')),
1227
+ sourceManifestHash: flags.get('source-manifest-hash'),
1228
+ sourceTreeHash: flags.get('source-tree-hash'),
1229
+ ttlSeconds: flags.has('ttl-seconds')
1230
+ ? Number(flags.get('ttl-seconds'))
1231
+ : undefined,
1232
+ });
1233
+ // The output directory is caller scratch space — create it deterministically
1234
+ // (the producer action's input-manifest must be repository-relative, so the
1235
+ // caller cannot sidestep this by pointing at RUNNER_TEMP). `wx` below still
1236
+ // refuses to overwrite an existing manifest.
1237
+ mkdirSync(dirname(output), { recursive: true });
1238
+ writeFileSync(output, serializeJson(input), { flag: 'wx', mode: 0o600 });
1239
+ console.log(
1240
+ `[assemble-producer-input] wrote ${output} for subject ` +
1241
+ `${input.subjects[0].kind}:${input.subjects[0].id} ` +
1242
+ `(${input.subjects[0].imageRepository}@${input.subjects[0].manifestDigest})`,
1243
+ );
1244
+ }
1245
+
1246
+ if (isEntryModule(import.meta.url)) {
1247
+ main().catch((error) => {
1248
+ console.error(`[assemble-producer-input] ${error.message}`);
1249
+ process.exitCode = 1;
1250
+ });
1251
+ }