@mannyc1/ts-release 0.0.6 → 0.2.2

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 (639) hide show
  1. package/ARCHITECTURE.md +124 -87
  2. package/CHANGELOG.md +115 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -540
  5. package/SPEC.md +206 -305
  6. package/apps/release-ts/README.md +32 -0
  7. package/apps/release-ts/src/cli/command.ts +164 -0
  8. package/apps/release-ts/src/cli/commands.ts +351 -0
  9. package/apps/release-ts/src/cli/main.ts +43 -0
  10. package/apps/release-ts/src/cli/node-main.ts +49 -0
  11. package/apps/ts-release-agents/LICENSE +21 -0
  12. package/apps/ts-release-agents/README.md +21 -0
  13. package/apps/ts-release-agents/evals/cases.json +11 -0
  14. package/apps/ts-release-agents/manifests/claude.json +5 -0
  15. package/apps/ts-release-agents/manifests/codex.json +5 -0
  16. package/apps/ts-release-agents/package.json +11 -0
  17. package/apps/ts-release-agents/skills/release/SKILL.md +48 -0
  18. package/apps/ts-release-agents/skills/release/references/configuration.md +18 -0
  19. package/apps/ts-release-agents/skills/release/references/recovery.md +10 -0
  20. package/apps/ts-release-agents/skills/release/references/staged-workflow.md +12 -0
  21. package/apps/ts-release-agents/skills/release/references/target-selection.md +13 -0
  22. package/apps/ts-release-agents/skills/release/references/verification.md +14 -0
  23. package/apps/ts-release-agents/src/build.ts +105 -0
  24. package/apps/ts-release-agents/src/check.ts +117 -0
  25. package/apps/ts-release-agents/src/install.ts +121 -0
  26. package/apps/ts-release-agents/test/contract.test.ts +56 -0
  27. package/apps/ts-release-agents/tsconfig.json +5 -0
  28. package/dist/api/api.d.ts +15 -0
  29. package/dist/api/api.d.ts.map +1 -0
  30. package/dist/api/api.js +244 -0
  31. package/dist/api/api.js.map +1 -0
  32. package/dist/api/errors.d.ts +39 -0
  33. package/dist/api/errors.d.ts.map +1 -0
  34. package/dist/api/errors.js +32 -0
  35. package/dist/api/errors.js.map +1 -0
  36. package/dist/api/input.d.ts +10 -0
  37. package/dist/api/input.d.ts.map +1 -0
  38. package/dist/api/input.js +73 -0
  39. package/dist/api/input.js.map +1 -0
  40. package/dist/api/runtime.d.ts +18 -0
  41. package/dist/api/runtime.d.ts.map +1 -0
  42. package/dist/api/runtime.js +7 -0
  43. package/dist/api/runtime.js.map +1 -0
  44. package/dist/api/types.d.ts +76 -0
  45. package/dist/api/types.d.ts.map +1 -0
  46. package/dist/api/types.js +17 -0
  47. package/dist/api/types.js.map +1 -0
  48. package/dist/bin/ts-release.js +39239 -0
  49. package/dist/capabilities/bun-targets.d.ts +3 -0
  50. package/dist/capabilities/bun-targets.d.ts.map +1 -0
  51. package/dist/capabilities/bun-targets.js +3 -0
  52. package/dist/capabilities/bun-targets.js.map +1 -0
  53. package/dist/capabilities/field-ownership.d.ts +17 -0
  54. package/dist/capabilities/field-ownership.d.ts.map +1 -0
  55. package/dist/capabilities/field-ownership.js +40 -0
  56. package/dist/capabilities/field-ownership.js.map +1 -0
  57. package/dist/capabilities/module.d.ts +91 -0
  58. package/dist/capabilities/module.d.ts.map +1 -0
  59. package/dist/capabilities/module.js +2 -0
  60. package/dist/capabilities/module.js.map +1 -0
  61. package/dist/capabilities/registry.d.ts +829 -0
  62. package/dist/capabilities/registry.d.ts.map +1 -0
  63. package/dist/capabilities/registry.js +291 -0
  64. package/dist/capabilities/registry.js.map +1 -0
  65. package/dist/config/config.d.ts +6 -0
  66. package/dist/config/config.d.ts.map +1 -0
  67. package/dist/config/config.js +42 -0
  68. package/dist/config/config.js.map +1 -0
  69. package/dist/correction/coordinator.d.ts +248 -0
  70. package/dist/correction/coordinator.d.ts.map +1 -0
  71. package/dist/correction/coordinator.js +274 -0
  72. package/dist/correction/coordinator.js.map +1 -0
  73. package/dist/correction/index.d.ts +3 -0
  74. package/dist/correction/index.d.ts.map +1 -0
  75. package/dist/correction/index.js +3 -0
  76. package/dist/correction/index.js.map +1 -0
  77. package/dist/correction/intent.d.ts +111 -0
  78. package/dist/correction/intent.d.ts.map +1 -0
  79. package/dist/correction/intent.js +146 -0
  80. package/dist/correction/intent.js.map +1 -0
  81. package/dist/drivers/archive.d.ts +8 -0
  82. package/dist/drivers/archive.d.ts.map +1 -0
  83. package/dist/drivers/archive.js +101 -0
  84. package/dist/drivers/archive.js.map +1 -0
  85. package/dist/drivers/contain.d.ts +6 -0
  86. package/dist/drivers/contain.d.ts.map +1 -0
  87. package/dist/drivers/contain.js +17 -0
  88. package/dist/drivers/contain.js.map +1 -0
  89. package/dist/drivers/environment.d.ts +4 -0
  90. package/dist/drivers/environment.d.ts.map +1 -0
  91. package/dist/drivers/environment.js +18 -0
  92. package/dist/drivers/environment.js.map +1 -0
  93. package/dist/drivers/errors.d.ts +9 -0
  94. package/dist/drivers/errors.d.ts.map +1 -0
  95. package/dist/drivers/errors.js +6 -0
  96. package/dist/drivers/errors.js.map +1 -0
  97. package/dist/drivers/glob.d.ts +2 -0
  98. package/dist/drivers/glob.d.ts.map +1 -0
  99. package/dist/drivers/glob.js +162 -0
  100. package/dist/drivers/glob.js.map +1 -0
  101. package/dist/drivers/process.d.ts +48 -0
  102. package/dist/drivers/process.d.ts.map +1 -0
  103. package/dist/drivers/process.js +257 -0
  104. package/dist/drivers/process.js.map +1 -0
  105. package/dist/drivers/redact.d.ts +4 -0
  106. package/dist/drivers/redact.d.ts.map +1 -0
  107. package/dist/drivers/redact.js +24 -0
  108. package/dist/drivers/redact.js.map +1 -0
  109. package/dist/drivers/seccomp-helper-source.d.ts +7 -0
  110. package/dist/drivers/seccomp-helper-source.d.ts.map +1 -0
  111. package/dist/drivers/seccomp-helper-source.js +121 -0
  112. package/dist/drivers/seccomp-helper-source.js.map +1 -0
  113. package/dist/drivers/utils.d.ts +4 -0
  114. package/dist/drivers/utils.d.ts.map +1 -0
  115. package/dist/drivers/utils.js +5 -0
  116. package/dist/drivers/utils.js.map +1 -0
  117. package/dist/drivers/workspace.d.ts +6 -0
  118. package/dist/drivers/workspace.d.ts.map +1 -0
  119. package/dist/drivers/workspace.js +49 -0
  120. package/dist/drivers/workspace.js.map +1 -0
  121. package/dist/extensions/provider-adapter.d.ts +48 -0
  122. package/dist/extensions/provider-adapter.d.ts.map +1 -0
  123. package/dist/extensions/provider-adapter.js +64 -0
  124. package/dist/extensions/provider-adapter.js.map +1 -0
  125. package/dist/host.d.ts +33 -0
  126. package/dist/host.d.ts.map +1 -0
  127. package/dist/host.js +20 -0
  128. package/dist/host.js.map +1 -0
  129. package/dist/index.d.ts +19 -1
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +12 -1
  132. package/dist/index.js.map +1 -1
  133. package/dist/model/artifact-collection.d.ts +84 -0
  134. package/dist/model/artifact-collection.d.ts.map +1 -0
  135. package/dist/model/artifact-collection.js +185 -0
  136. package/dist/model/artifact-collection.js.map +1 -0
  137. package/dist/model/authority.d.ts +89 -0
  138. package/dist/model/authority.d.ts.map +1 -0
  139. package/dist/model/authority.js +80 -0
  140. package/dist/model/authority.js.map +1 -0
  141. package/dist/model/bun-targets.d.ts +34 -0
  142. package/dist/model/bun-targets.d.ts.map +1 -0
  143. package/dist/model/bun-targets.js +17 -0
  144. package/dist/model/bun-targets.js.map +1 -0
  145. package/dist/model/canonical.d.ts +8 -0
  146. package/dist/model/canonical.d.ts.map +1 -0
  147. package/dist/model/canonical.js +205 -0
  148. package/dist/model/canonical.js.map +1 -0
  149. package/dist/model/catalog.d.ts +76 -0
  150. package/dist/model/catalog.d.ts.map +1 -0
  151. package/dist/model/catalog.js +174 -0
  152. package/dist/model/catalog.js.map +1 -0
  153. package/dist/model/digest.d.ts +53 -0
  154. package/dist/model/digest.d.ts.map +1 -0
  155. package/dist/model/digest.js +127 -0
  156. package/dist/model/digest.js.map +1 -0
  157. package/dist/model/errors.d.ts +14 -0
  158. package/dist/model/errors.d.ts.map +1 -0
  159. package/dist/model/errors.js +8 -0
  160. package/dist/model/errors.js.map +1 -0
  161. package/dist/model/primitives.d.ts +19 -0
  162. package/dist/model/primitives.d.ts.map +1 -0
  163. package/dist/model/primitives.js +18 -0
  164. package/dist/model/primitives.js.map +1 -0
  165. package/dist/model/pypi.d.ts +12 -0
  166. package/dist/model/pypi.d.ts.map +1 -0
  167. package/dist/model/pypi.js +20 -0
  168. package/dist/model/pypi.js.map +1 -0
  169. package/dist/model/python-distribution.d.ts +33 -0
  170. package/dist/model/python-distribution.d.ts.map +1 -0
  171. package/dist/model/python-distribution.js +367 -0
  172. package/dist/model/python-distribution.js.map +1 -0
  173. package/dist/model/secret-patterns.d.ts +2 -0
  174. package/dist/model/secret-patterns.d.ts.map +1 -0
  175. package/dist/model/secret-patterns.js +12 -0
  176. package/dist/model/secret-patterns.js.map +1 -0
  177. package/dist/platform/bun.d.ts +7 -0
  178. package/dist/platform/bun.d.ts.map +1 -0
  179. package/dist/platform/bun.js +15 -0
  180. package/dist/platform/bun.js.map +1 -0
  181. package/dist/platform/credentials.d.ts +32 -0
  182. package/dist/platform/credentials.d.ts.map +1 -0
  183. package/dist/platform/credentials.js +772 -0
  184. package/dist/platform/credentials.js.map +1 -0
  185. package/dist/platform/host-support.d.ts +2 -0
  186. package/dist/platform/host-support.d.ts.map +1 -0
  187. package/dist/platform/host-support.js +4 -0
  188. package/dist/platform/host-support.js.map +1 -0
  189. package/dist/platform/node.d.ts +7 -0
  190. package/dist/platform/node.d.ts.map +1 -0
  191. package/dist/platform/node.js +15 -0
  192. package/dist/platform/node.js.map +1 -0
  193. package/dist/platform/release-runtime.d.ts +11 -0
  194. package/dist/platform/release-runtime.d.ts.map +1 -0
  195. package/dist/platform/release-runtime.js +34 -0
  196. package/dist/platform/release-runtime.js.map +1 -0
  197. package/dist/platform/services.d.ts +15 -0
  198. package/dist/platform/services.d.ts.map +1 -0
  199. package/dist/platform/services.js +30 -0
  200. package/dist/platform/services.js.map +1 -0
  201. package/dist/platform/source-observer.d.ts +5 -0
  202. package/dist/platform/source-observer.d.ts.map +1 -0
  203. package/dist/platform/source-observer.js +159 -0
  204. package/dist/platform/source-observer.js.map +1 -0
  205. package/dist/provider-sdk.d.ts +14 -0
  206. package/dist/provider-sdk.d.ts.map +1 -0
  207. package/dist/provider-sdk.js +10 -0
  208. package/dist/provider-sdk.js.map +1 -0
  209. package/dist/publication/adapter.d.ts +212 -0
  210. package/dist/publication/adapter.d.ts.map +1 -0
  211. package/dist/publication/adapter.js +83 -0
  212. package/dist/publication/adapter.js.map +1 -0
  213. package/dist/publication/authority.d.ts +137 -0
  214. package/dist/publication/authority.d.ts.map +1 -0
  215. package/dist/publication/authority.js +233 -0
  216. package/dist/publication/authority.js.map +1 -0
  217. package/dist/publication/catalog-git.d.ts +37 -0
  218. package/dist/publication/catalog-git.d.ts.map +1 -0
  219. package/dist/publication/catalog-git.js +451 -0
  220. package/dist/publication/catalog-git.js.map +1 -0
  221. package/dist/publication/claim.d.ts +39 -0
  222. package/dist/publication/claim.d.ts.map +1 -0
  223. package/dist/publication/claim.js +31 -0
  224. package/dist/publication/claim.js.map +1 -0
  225. package/dist/publication/coordinator.d.ts +246 -0
  226. package/dist/publication/coordinator.d.ts.map +1 -0
  227. package/dist/publication/coordinator.js +370 -0
  228. package/dist/publication/coordinator.js.map +1 -0
  229. package/dist/publication/github.d.ts +36 -0
  230. package/dist/publication/github.d.ts.map +1 -0
  231. package/dist/publication/github.js +644 -0
  232. package/dist/publication/github.js.map +1 -0
  233. package/dist/publication/http.d.ts +69 -0
  234. package/dist/publication/http.d.ts.map +1 -0
  235. package/dist/publication/http.js +29 -0
  236. package/dist/publication/http.js.map +1 -0
  237. package/dist/publication/index.d.ts +5 -0
  238. package/dist/publication/index.d.ts.map +1 -0
  239. package/dist/publication/index.js +5 -0
  240. package/dist/publication/index.js.map +1 -0
  241. package/dist/publication/npm.d.ts +40 -0
  242. package/dist/publication/npm.d.ts.map +1 -0
  243. package/dist/publication/npm.js +359 -0
  244. package/dist/publication/npm.js.map +1 -0
  245. package/dist/publication/profiles.d.ts +136 -0
  246. package/dist/publication/profiles.d.ts.map +1 -0
  247. package/dist/publication/profiles.js +90 -0
  248. package/dist/publication/profiles.js.map +1 -0
  249. package/dist/publication/publisher.d.ts +82 -0
  250. package/dist/publication/publisher.d.ts.map +1 -0
  251. package/dist/publication/publisher.js +53 -0
  252. package/dist/publication/publisher.js.map +1 -0
  253. package/dist/publication/pypi.d.ts +45 -0
  254. package/dist/publication/pypi.d.ts.map +1 -0
  255. package/dist/publication/pypi.js +369 -0
  256. package/dist/publication/pypi.js.map +1 -0
  257. package/dist/publication/recovery.d.ts +663 -0
  258. package/dist/publication/recovery.d.ts.map +1 -0
  259. package/dist/publication/recovery.js +252 -0
  260. package/dist/publication/recovery.js.map +1 -0
  261. package/dist/publication/report.d.ts +1061 -0
  262. package/dist/publication/report.d.ts.map +1 -0
  263. package/dist/publication/report.js +441 -0
  264. package/dist/publication/report.js.map +1 -0
  265. package/dist/recipes/config.d.ts +296 -0
  266. package/dist/recipes/config.d.ts.map +1 -0
  267. package/dist/recipes/config.js +325 -0
  268. package/dist/recipes/config.js.map +1 -0
  269. package/dist/release/capabilities.d.ts +12 -0
  270. package/dist/release/capabilities.d.ts.map +1 -0
  271. package/dist/release/capabilities.js +395 -0
  272. package/dist/release/capabilities.js.map +1 -0
  273. package/dist/release/compiler.d.ts +5 -0
  274. package/dist/release/compiler.d.ts.map +1 -0
  275. package/dist/release/compiler.js +19 -0
  276. package/dist/release/compiler.js.map +1 -0
  277. package/dist/release/config.d.ts +5 -0
  278. package/dist/release/config.d.ts.map +1 -0
  279. package/dist/release/config.js +10 -0
  280. package/dist/release/config.js.map +1 -0
  281. package/dist/release/context.d.ts +96 -0
  282. package/dist/release/context.d.ts.map +1 -0
  283. package/dist/release/context.js +168 -0
  284. package/dist/release/context.js.map +1 -0
  285. package/dist/release/graph.d.ts +271 -0
  286. package/dist/release/graph.d.ts.map +1 -0
  287. package/dist/release/graph.js +766 -0
  288. package/dist/release/graph.js.map +1 -0
  289. package/dist/release/inspect.d.ts +88 -0
  290. package/dist/release/inspect.d.ts.map +1 -0
  291. package/dist/release/inspect.js +75 -0
  292. package/dist/release/inspect.js.map +1 -0
  293. package/dist/release/prepare.d.ts +24 -0
  294. package/dist/release/prepare.d.ts.map +1 -0
  295. package/dist/release/prepare.js +1116 -0
  296. package/dist/release/prepare.js.map +1 -0
  297. package/dist/release/prepared-ref.d.ts +60 -0
  298. package/dist/release/prepared-ref.d.ts.map +1 -0
  299. package/dist/release/prepared-ref.js +131 -0
  300. package/dist/release/prepared-ref.js.map +1 -0
  301. package/dist/release/prepared-store.d.ts +90 -0
  302. package/dist/release/prepared-store.d.ts.map +1 -0
  303. package/dist/release/prepared-store.js +337 -0
  304. package/dist/release/prepared-store.js.map +1 -0
  305. package/dist/release/prepared.d.ts +172 -0
  306. package/dist/release/prepared.d.ts.map +1 -0
  307. package/dist/release/prepared.js +268 -0
  308. package/dist/release/prepared.js.map +1 -0
  309. package/dist/release/staging.d.ts +31 -0
  310. package/dist/release/staging.d.ts.map +1 -0
  311. package/dist/release/staging.js +218 -0
  312. package/dist/release/staging.js.map +1 -0
  313. package/dist/resolve/authored.d.ts +53 -0
  314. package/dist/resolve/authored.d.ts.map +1 -0
  315. package/dist/resolve/authored.js +55 -0
  316. package/dist/resolve/authored.js.map +1 -0
  317. package/dist/resolve/encode.d.ts +3 -0
  318. package/dist/resolve/encode.d.ts.map +1 -0
  319. package/dist/resolve/encode.js +21 -0
  320. package/dist/resolve/encode.js.map +1 -0
  321. package/dist/resolve/errors.d.ts +7 -0
  322. package/dist/resolve/errors.d.ts.map +1 -0
  323. package/dist/resolve/errors.js +17 -0
  324. package/dist/resolve/errors.js.map +1 -0
  325. package/dist/resolve/facts.d.ts +12 -0
  326. package/dist/resolve/facts.d.ts.map +1 -0
  327. package/dist/resolve/facts.js +18 -0
  328. package/dist/resolve/facts.js.map +1 -0
  329. package/dist/resolve/resolve.d.ts +10 -0
  330. package/dist/resolve/resolve.d.ts.map +1 -0
  331. package/dist/resolve/resolve.js +242 -0
  332. package/dist/resolve/resolve.js.map +1 -0
  333. package/dist/store.d.ts +11 -0
  334. package/dist/store.d.ts.map +1 -0
  335. package/dist/store.js +9 -0
  336. package/dist/store.js.map +1 -0
  337. package/examples/README.md +20 -30
  338. package/examples/github-release/release.config.json +11 -18
  339. package/examples/homebrew-tap/README.md +11 -0
  340. package/examples/homebrew-tap/release.config.json +40 -28
  341. package/examples/multi-target/release.config.json +28 -53
  342. package/examples/npm-first-publish/release.config.json +23 -25
  343. package/examples/npm-only/release.config.json +22 -30
  344. package/examples/portable-cli/README.md +17 -0
  345. package/examples/portable-cli/artifacts/portable-example-0.1.0-darwin-arm64 +1 -0
  346. package/examples/portable-cli/artifacts/portable-example-0.1.0-darwin-x64 +1 -0
  347. package/examples/portable-cli/artifacts/portable-example-0.1.0-linux-arm64 +1 -0
  348. package/examples/portable-cli/artifacts/portable-example-0.1.0-linux-x64 +1 -0
  349. package/examples/portable-cli/artifacts/portable-example-0.1.0-windows-x64.exe +1 -0
  350. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-macosx_10_15_x86_64.whl +1 -0
  351. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-macosx_11_0_arm64.whl +1 -0
  352. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-manylinux2014_aarch64.whl +1 -0
  353. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-manylinux2014_x86_64.whl +1 -0
  354. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-win_amd64.whl +1 -0
  355. package/examples/portable-cli/package.json +9 -0
  356. package/examples/portable-cli/release.config.json +53 -0
  357. package/examples/portable-cli/src/cli.ts +3 -0
  358. package/examples/pypi-registry/README.md +8 -0
  359. package/examples/scoop-bucket/README.md +10 -0
  360. package/examples/scoop-bucket/release.config.json +40 -30
  361. package/package.json +61 -154
  362. package/src/api/api.ts +365 -0
  363. package/src/api/errors.ts +36 -0
  364. package/src/api/input.ts +92 -0
  365. package/src/api/runtime.ts +14 -0
  366. package/src/api/types.ts +103 -0
  367. package/src/capabilities/bun-targets.ts +2 -0
  368. package/src/capabilities/field-ownership.ts +60 -0
  369. package/src/capabilities/module.ts +158 -0
  370. package/src/capabilities/registry.ts +333 -0
  371. package/src/config/config.ts +48 -0
  372. package/src/correction/coordinator.ts +336 -0
  373. package/src/correction/index.ts +2 -0
  374. package/src/correction/intent.ts +172 -0
  375. package/src/drivers/archive.ts +102 -0
  376. package/src/drivers/contain.ts +20 -0
  377. package/src/drivers/environment.ts +28 -0
  378. package/src/drivers/errors.ts +6 -0
  379. package/src/drivers/glob.ts +137 -0
  380. package/src/drivers/process.ts +340 -0
  381. package/src/drivers/redact.ts +31 -0
  382. package/src/drivers/seccomp-helper-source.ts +121 -0
  383. package/src/drivers/utils.ts +10 -0
  384. package/src/drivers/workspace.ts +40 -0
  385. package/src/extensions/provider-adapter.ts +99 -0
  386. package/src/host.ts +140 -0
  387. package/src/index.ts +64 -0
  388. package/src/model/artifact-collection.ts +237 -0
  389. package/src/model/authority.ts +116 -0
  390. package/src/model/bun-targets.ts +34 -0
  391. package/src/model/canonical.ts +205 -0
  392. package/src/model/catalog.ts +220 -0
  393. package/src/model/digest.ts +172 -0
  394. package/src/model/errors.ts +6 -0
  395. package/src/model/primitives.ts +37 -0
  396. package/src/model/pypi.ts +30 -0
  397. package/src/model/python-distribution.ts +360 -0
  398. package/src/model/secret-patterns.ts +11 -0
  399. package/src/platform/bun.ts +38 -0
  400. package/src/platform/credentials.ts +1083 -0
  401. package/src/platform/host-support.ts +4 -0
  402. package/src/platform/node.ts +38 -0
  403. package/src/platform/release-runtime.ts +47 -0
  404. package/src/platform/services.ts +57 -0
  405. package/src/platform/source-observer.ts +193 -0
  406. package/src/provider-sdk.ts +48 -0
  407. package/src/publication/adapter.ts +132 -0
  408. package/src/publication/authority.ts +397 -0
  409. package/src/publication/catalog-git.ts +603 -0
  410. package/src/publication/claim.ts +51 -0
  411. package/src/publication/coordinator.ts +578 -0
  412. package/src/publication/github.ts +954 -0
  413. package/src/publication/http.ts +101 -0
  414. package/src/publication/index.ts +4 -0
  415. package/src/publication/npm.ts +525 -0
  416. package/src/publication/profiles.ts +94 -0
  417. package/src/publication/publisher.ts +130 -0
  418. package/src/publication/pypi.ts +463 -0
  419. package/src/publication/recovery.ts +349 -0
  420. package/src/publication/report.ts +628 -0
  421. package/src/recipes/config.ts +395 -0
  422. package/src/release/capabilities.ts +459 -0
  423. package/src/release/compiler.ts +28 -0
  424. package/src/release/config.ts +12 -0
  425. package/src/release/context.ts +223 -0
  426. package/src/release/graph.ts +866 -0
  427. package/src/release/inspect.ts +83 -0
  428. package/src/release/prepare.ts +1331 -0
  429. package/src/release/prepared-ref.ts +193 -0
  430. package/src/release/prepared-store.ts +377 -0
  431. package/src/release/prepared.ts +299 -0
  432. package/src/release/staging.ts +257 -0
  433. package/src/resolve/authored.ts +64 -0
  434. package/src/resolve/encode.ts +22 -0
  435. package/src/resolve/errors.ts +15 -0
  436. package/src/resolve/facts.ts +18 -0
  437. package/src/resolve/resolve.ts +328 -0
  438. package/src/store.ts +28 -0
  439. package/templates/README.md +55 -59
  440. package/templates/bun-cli-github/README.md +12 -0
  441. package/templates/bun-cli-github/release.config.json +51 -0
  442. package/templates/github-actions/release.yml +57 -0
  443. package/templates/github-actions/reviewed-release.yml +80 -0
  444. package/templates/multi-target-homebrew/README.md +10 -0
  445. package/templates/multi-target-homebrew/release.config.json +43 -60
  446. package/templates/multi-target-scoop/README.md +10 -0
  447. package/templates/multi-target-scoop/release.config.json +43 -62
  448. package/templates/npm-github/release.config.json +25 -38
  449. package/templates/npm-github/reviewed-release.config.json +37 -0
  450. package/templates/npm-only/README.md +10 -0
  451. package/templates/npm-only/release.config.json +15 -30
  452. package/templates/portable-cli/README.md +13 -0
  453. package/templates/portable-cli/release.config.json +53 -0
  454. package/dist/config/errors.d.ts +0 -24
  455. package/dist/config/errors.d.ts.map +0 -1
  456. package/dist/config/errors.js +0 -19
  457. package/dist/config/errors.js.map +0 -1
  458. package/dist/config/load.d.ts +0 -9
  459. package/dist/config/load.d.ts.map +0 -1
  460. package/dist/config/load.js +0 -29
  461. package/dist/config/load.js.map +0 -1
  462. package/dist/config/schema.d.ts +0 -12
  463. package/dist/config/schema.d.ts.map +0 -1
  464. package/dist/config/schema.js +0 -19
  465. package/dist/config/schema.js.map +0 -1
  466. package/dist/domain/artifact.d.ts +0 -36
  467. package/dist/domain/artifact.d.ts.map +0 -1
  468. package/dist/domain/artifact.js +0 -29
  469. package/dist/domain/artifact.js.map +0 -1
  470. package/dist/domain/evidence.d.ts +0 -108
  471. package/dist/domain/evidence.d.ts.map +0 -1
  472. package/dist/domain/evidence.js +0 -96
  473. package/dist/domain/evidence.js.map +0 -1
  474. package/dist/domain/operation.d.ts +0 -153
  475. package/dist/domain/operation.d.ts.map +0 -1
  476. package/dist/domain/operation.js +0 -320
  477. package/dist/domain/operation.js.map +0 -1
  478. package/dist/domain/release.d.ts +0 -141
  479. package/dist/domain/release.d.ts.map +0 -1
  480. package/dist/domain/release.js +0 -127
  481. package/dist/domain/release.js.map +0 -1
  482. package/dist/domain/remote-state.d.ts +0 -85
  483. package/dist/domain/remote-state.d.ts.map +0 -1
  484. package/dist/domain/remote-state.js +0 -77
  485. package/dist/domain/remote-state.js.map +0 -1
  486. package/dist/domain/status.d.ts +0 -43
  487. package/dist/domain/status.d.ts.map +0 -1
  488. package/dist/domain/status.js +0 -51
  489. package/dist/domain/status.js.map +0 -1
  490. package/dist/domain/target.d.ts +0 -135
  491. package/dist/domain/target.d.ts.map +0 -1
  492. package/dist/domain/target.js +0 -137
  493. package/dist/domain/target.js.map +0 -1
  494. package/dist/host/host.d.ts +0 -32
  495. package/dist/host/host.d.ts.map +0 -1
  496. package/dist/host/host.js +0 -24
  497. package/dist/host/host.js.map +0 -1
  498. package/dist/host/http-live.d.ts +0 -6
  499. package/dist/host/http-live.d.ts.map +0 -1
  500. package/dist/host/http-live.js +0 -86
  501. package/dist/host/http-live.js.map +0 -1
  502. package/dist/host/http.d.ts +0 -43
  503. package/dist/host/http.d.ts.map +0 -1
  504. package/dist/host/http.js +0 -59
  505. package/dist/host/http.js.map +0 -1
  506. package/dist/host/platform.d.ts +0 -10
  507. package/dist/host/platform.d.ts.map +0 -1
  508. package/dist/host/platform.js +0 -114
  509. package/dist/host/platform.js.map +0 -1
  510. package/dist/host/test.d.ts +0 -23
  511. package/dist/host/test.d.ts.map +0 -1
  512. package/dist/host/test.js +0 -222
  513. package/dist/host/test.js.map +0 -1
  514. package/dist/internal/workflow-phases.d.ts +0 -3
  515. package/dist/internal/workflow-phases.d.ts.map +0 -1
  516. package/dist/internal/workflow-phases.js +0 -2
  517. package/dist/internal/workflow-phases.js.map +0 -1
  518. package/dist/internal/workspace-path.d.ts +0 -15
  519. package/dist/internal/workspace-path.d.ts.map +0 -1
  520. package/dist/internal/workspace-path.js +0 -36
  521. package/dist/internal/workspace-path.js.map +0 -1
  522. package/dist/planner/artifact-inventory.d.ts +0 -9
  523. package/dist/planner/artifact-inventory.d.ts.map +0 -1
  524. package/dist/planner/artifact-inventory.js +0 -84
  525. package/dist/planner/artifact-inventory.js.map +0 -1
  526. package/dist/planner/create-release-plan.d.ts +0 -5
  527. package/dist/planner/create-release-plan.d.ts.map +0 -1
  528. package/dist/planner/create-release-plan.js +0 -25
  529. package/dist/planner/create-release-plan.js.map +0 -1
  530. package/dist/planner/errors.d.ts +0 -81
  531. package/dist/planner/errors.d.ts.map +0 -1
  532. package/dist/planner/errors.js +0 -70
  533. package/dist/planner/errors.js.map +0 -1
  534. package/dist/planner/evidence-recorder.d.ts +0 -26
  535. package/dist/planner/evidence-recorder.d.ts.map +0 -1
  536. package/dist/planner/evidence-recorder.js +0 -330
  537. package/dist/planner/evidence-recorder.js.map +0 -1
  538. package/dist/planner/executor.d.ts +0 -58
  539. package/dist/planner/executor.d.ts.map +0 -1
  540. package/dist/planner/executor.js +0 -159
  541. package/dist/planner/executor.js.map +0 -1
  542. package/dist/planner/normalize-release.d.ts +0 -16
  543. package/dist/planner/normalize-release.d.ts.map +0 -1
  544. package/dist/planner/normalize-release.js +0 -218
  545. package/dist/planner/normalize-release.js.map +0 -1
  546. package/dist/planner/reconcile.d.ts +0 -17
  547. package/dist/planner/reconcile.d.ts.map +0 -1
  548. package/dist/planner/reconcile.js +0 -261
  549. package/dist/planner/reconcile.js.map +0 -1
  550. package/dist/planner/release-eligibility.d.ts +0 -39
  551. package/dist/planner/release-eligibility.d.ts.map +0 -1
  552. package/dist/planner/release-eligibility.js +0 -578
  553. package/dist/planner/release-eligibility.js.map +0 -1
  554. package/dist/planner/render-plan.d.ts +0 -15
  555. package/dist/planner/render-plan.d.ts.map +0 -1
  556. package/dist/planner/render-plan.js +0 -238
  557. package/dist/planner/render-plan.js.map +0 -1
  558. package/dist/planner/status.d.ts +0 -40
  559. package/dist/planner/status.d.ts.map +0 -1
  560. package/dist/planner/status.js +0 -313
  561. package/dist/planner/status.js.map +0 -1
  562. package/dist/targets/adapter-helpers.d.ts +0 -64
  563. package/dist/targets/adapter-helpers.d.ts.map +0 -1
  564. package/dist/targets/adapter-helpers.js +0 -124
  565. package/dist/targets/adapter-helpers.js.map +0 -1
  566. package/dist/targets/adapter.d.ts +0 -17
  567. package/dist/targets/adapter.d.ts.map +0 -1
  568. package/dist/targets/adapter.js +0 -2
  569. package/dist/targets/adapter.js.map +0 -1
  570. package/dist/targets/github-release.d.ts +0 -25
  571. package/dist/targets/github-release.d.ts.map +0 -1
  572. package/dist/targets/github-release.js +0 -82
  573. package/dist/targets/github-release.js.map +0 -1
  574. package/dist/targets/github.d.ts +0 -11
  575. package/dist/targets/github.d.ts.map +0 -1
  576. package/dist/targets/github.js +0 -125
  577. package/dist/targets/github.js.map +0 -1
  578. package/dist/targets/homebrew.d.ts +0 -11
  579. package/dist/targets/homebrew.d.ts.map +0 -1
  580. package/dist/targets/homebrew.js +0 -97
  581. package/dist/targets/homebrew.js.map +0 -1
  582. package/dist/targets/live.d.ts +0 -5
  583. package/dist/targets/live.d.ts.map +0 -1
  584. package/dist/targets/live.js +0 -50
  585. package/dist/targets/live.js.map +0 -1
  586. package/dist/targets/npm.d.ts +0 -10
  587. package/dist/targets/npm.d.ts.map +0 -1
  588. package/dist/targets/npm.js +0 -118
  589. package/dist/targets/npm.js.map +0 -1
  590. package/dist/targets/pypi.d.ts +0 -11
  591. package/dist/targets/pypi.d.ts.map +0 -1
  592. package/dist/targets/pypi.js +0 -103
  593. package/dist/targets/pypi.js.map +0 -1
  594. package/dist/targets/registry.d.ts +0 -25
  595. package/dist/targets/registry.d.ts.map +0 -1
  596. package/dist/targets/registry.js +0 -34
  597. package/dist/targets/registry.js.map +0 -1
  598. package/dist/targets/scoop.d.ts +0 -11
  599. package/dist/targets/scoop.d.ts.map +0 -1
  600. package/dist/targets/scoop.js +0 -78
  601. package/dist/targets/scoop.js.map +0 -1
  602. package/dist/types/effect-internal.d.ts +0 -5
  603. package/dist/types/effect-internal.d.ts.map +0 -1
  604. package/dist/types/effect-internal.js +0 -2
  605. package/dist/types/effect-internal.js.map +0 -1
  606. package/dist/workflows/config.d.ts +0 -252
  607. package/dist/workflows/config.d.ts.map +0 -1
  608. package/dist/workflows/config.js +0 -465
  609. package/dist/workflows/config.js.map +0 -1
  610. package/dist/workflows/diagnostics.d.ts +0 -62
  611. package/dist/workflows/diagnostics.d.ts.map +0 -1
  612. package/dist/workflows/diagnostics.js +0 -527
  613. package/dist/workflows/diagnostics.js.map +0 -1
  614. package/dist/workflows/evidence.d.ts +0 -26
  615. package/dist/workflows/evidence.d.ts.map +0 -1
  616. package/dist/workflows/evidence.js +0 -44
  617. package/dist/workflows/evidence.js.map +0 -1
  618. package/dist/workflows/index.d.ts +0 -7
  619. package/dist/workflows/index.d.ts.map +0 -1
  620. package/dist/workflows/index.js +0 -6
  621. package/dist/workflows/index.js.map +0 -1
  622. package/dist/workflows/init.d.ts +0 -84
  623. package/dist/workflows/init.d.ts.map +0 -1
  624. package/dist/workflows/init.js +0 -441
  625. package/dist/workflows/init.js.map +0 -1
  626. package/dist/workflows/live.d.ts +0 -9
  627. package/dist/workflows/live.d.ts.map +0 -1
  628. package/dist/workflows/live.js +0 -7
  629. package/dist/workflows/live.js.map +0 -1
  630. package/dist/workflows/options.d.ts +0 -30
  631. package/dist/workflows/options.d.ts.map +0 -1
  632. package/dist/workflows/options.js +0 -16
  633. package/dist/workflows/options.js.map +0 -1
  634. package/examples/non-strict-skips/artifacts/release-example-skips-0.1.0.tgz +0 -1
  635. package/examples/non-strict-skips/release.config.json +0 -31
  636. package/examples/pypi-registry/release.config.json +0 -31
  637. package/templates/github-actions/plan-and-approved-execute.yml +0 -58
  638. package/templates/github-actions/plan-only.yml +0 -33
  639. package/templates/github-actions/trusted-publishing.yml +0 -61
@@ -0,0 +1,1083 @@
1
+ import { Buffer } from "node:buffer"
2
+ import { chmod, copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import { basename, isAbsolute, join, resolve } from "node:path"
5
+ import * as Config from "effect/Config"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Exit from "effect/Exit"
8
+ import * as Layer from "effect/Layer"
9
+ import * as Option from "effect/Option"
10
+ import * as Redacted from "effect/Redacted"
11
+ import * as Schema from "effect/Schema"
12
+ import * as Stream from "effect/Stream"
13
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
14
+ import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
15
+ import * as Semver from "semver"
16
+ import {
17
+ EnvironmentName,
18
+ type CanonicalAudience,
19
+ type CredentialRef,
20
+ type CredentialRequest,
21
+ type SubjectId,
22
+ type ResolvedTrustedPublishingAuthStrategy
23
+ } from "../model/authority.js"
24
+ import { redactOutput } from "../drivers/redact.js"
25
+ import {
26
+ CredentialAudienceMismatch,
27
+ CredentialProvider,
28
+ CredentialPurposeMismatch,
29
+ CredentialStrategyUnsupported,
30
+ CredentialSubjectMismatch,
31
+ CredentialUnavailable,
32
+ type AnonymousAccess,
33
+ type CredentialGrantAcquirer,
34
+ type CredentialProviderShape,
35
+ type MutationCredentialGrant,
36
+ type ScopedSecret,
37
+ type WorkloadIdentity,
38
+ makeCredentialProvider,
39
+ validateGrantForOperation
40
+ } from "../publication/authority.js"
41
+ import {
42
+ AuthorizedMutationHttp,
43
+ HttpAuthorizer,
44
+ type AuthorizedMutationHttpShape,
45
+ type HttpAuthorizerShape,
46
+ type HttpObservationRequest,
47
+ type HttpResponse,
48
+ type MutationHttpRequest,
49
+ type PublicationHttp
50
+ } from "../publication/http.js"
51
+ import {
52
+ CertifiedPublisherSpawn,
53
+ NpmUserConfigResource,
54
+ PublisherExited,
55
+ PublisherOutcomeUnknown,
56
+ RejectedBeforeStart,
57
+ makeNpmUserConfigHandle,
58
+ npmPublishArgv,
59
+ type CertifiedPublisherResult,
60
+ type CertifiedPublisherSpawnShape,
61
+ type CertifiedPublisherSpec,
62
+ type NpmPublishOperation,
63
+ type NpmPublisherSpec,
64
+ type NpmUserConfig,
65
+ type NpmUserConfigInput,
66
+ type NpmUserConfigResourceShape,
67
+ type WorkloadPublisherSpec
68
+ } from "../publication/publisher.js"
69
+ import { canonicalizeRegistryUrl } from "../release/graph.js"
70
+
71
+ export {
72
+ AuthorizedMutationHttp,
73
+ CertifiedPublisherSpawn,
74
+ HttpAuthorizer,
75
+ NpmUserConfigResource,
76
+ PublisherExited,
77
+ PublisherOutcomeUnknown,
78
+ RejectedBeforeStart
79
+ }
80
+ export type {
81
+ AuthorizedMutationHttpShape,
82
+ CertifiedPublisherResult,
83
+ CertifiedPublisherSpawnShape,
84
+ CertifiedPublisherSpec,
85
+ HttpAuthorizerShape,
86
+ HttpObservationRequest,
87
+ MutationHttpRequest,
88
+ NpmPublishOperation,
89
+ NpmPublisherSpec,
90
+ NpmUserConfig,
91
+ NpmUserConfigInput,
92
+ NpmUserConfigResourceShape,
93
+ WorkloadPublisherSpec
94
+ }
95
+
96
+ export class CredentialPlatformError
97
+ extends Schema.TaggedErrorClass<CredentialPlatformError>()("CredentialPlatformError", {
98
+ phase: Schema.Literals(["observe", "mutate", "resource", "spawn"]),
99
+ commitment: Schema.Literals(["before-dispatch", "unknown"]),
100
+ reason: Schema.NonEmptyString
101
+ }) {}
102
+
103
+ export type CredentialPlatformServices =
104
+ | CredentialProvider
105
+ | HttpAuthorizer
106
+ | AuthorizedMutationHttp
107
+ | NpmUserConfigResource
108
+ | CertifiedPublisherSpawn
109
+
110
+ // These are deliberately the only literal spellings in production code. The
111
+ // import-rules AST gate keeps OIDC request material in this host module.
112
+ const oidcRequestUrlName = EnvironmentName.make("ACTIONS_ID_TOKEN_REQUEST_URL")
113
+ const oidcRequestTokenName = EnvironmentName.make("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
114
+ const oidcNames = [oidcRequestUrlName, oidcRequestTokenName] as const
115
+ const githubActionsName = "GITHUB_ACTIONS"
116
+ const githubRepositoryName = "GITHUB_REPOSITORY"
117
+ const githubWorkflowRefName = "GITHUB_WORKFLOW_REF"
118
+ const githubServerUrlName = "GITHUB_SERVER_URL"
119
+ const githubEventName = "GITHUB_EVENT_NAME"
120
+ const githubRepositoryIdName = "GITHUB_REPOSITORY_ID"
121
+ const githubRepositoryOwnerIdName = "GITHUB_REPOSITORY_OWNER_ID"
122
+ const githubRefName = "GITHUB_REF"
123
+ const githubShaName = "GITHUB_SHA"
124
+ const runnerEnvironmentName = "RUNNER_ENVIRONMENT"
125
+ const githubRunIdName = "GITHUB_RUN_ID"
126
+ const githubRunAttemptName = "GITHUB_RUN_ATTEMPT"
127
+
128
+ const portableEnvironmentName = /^[A-Za-z_][A-Za-z0-9_]*$/u
129
+ const certifiedWorkflow = /^\.github\/workflows\/[A-Za-z0-9_.-]+\.ya?ml$/u
130
+ const certifiedRepository = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u
131
+ const certifiedWorkflowRef = /^(?:refs\/(?:heads|tags)\/[A-Za-z0-9][A-Za-z0-9._/-]*|[a-f0-9]{40}(?:[a-f0-9]{24})?)$/u
132
+ const certifiedSourceCommit = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u
133
+ const certifiedEvent = /^[a-z][a-z0-9_]*$/u
134
+ const certifiedDecimalId = /^[1-9][0-9]*$/u
135
+ const certifiedSourceRef = /^refs\/(?:heads|tags)\/[A-Za-z0-9][A-Za-z0-9._/-]*$/u
136
+
137
+ type SecretVault = Map<string, Redacted.Redacted<string>>
138
+
139
+ interface TrustedHostSnapshot {
140
+ readonly GITHUB_ACTIONS: "true"
141
+ readonly GITHUB_REPOSITORY: string
142
+ readonly GITHUB_WORKFLOW_REF: string
143
+ readonly GITHUB_SERVER_URL: "https://github.com"
144
+ readonly GITHUB_EVENT_NAME: string
145
+ readonly GITHUB_REPOSITORY_ID: string
146
+ readonly GITHUB_REPOSITORY_OWNER_ID: string
147
+ readonly GITHUB_REF: string
148
+ readonly GITHUB_SHA: string
149
+ readonly RUNNER_ENVIRONMENT: "github-hosted"
150
+ readonly GITHUB_RUN_ID: string
151
+ readonly GITHUB_RUN_ATTEMPT: string
152
+ }
153
+
154
+ interface TrustedWorkloadMaterial {
155
+ readonly snapshot: TrustedHostSnapshot
156
+ readonly oidc: ReadonlyMap<EnvironmentName, Redacted.Redacted<string>>
157
+ }
158
+
159
+ type TrustedWorkloadVault = WeakMap<WorkloadIdentity, TrustedWorkloadMaterial>
160
+
161
+ type ObservationGrant = AnonymousAccess | ScopedSecret
162
+
163
+ type ObservationGrantMetadata =
164
+ | {
165
+ readonly _tag: "AnonymousAccess"
166
+ readonly subject: SubjectId
167
+ readonly audience: CanonicalAudience
168
+ readonly purposes: ReadonlySet<"observe" | "publish" | "correct">
169
+ }
170
+ | {
171
+ readonly _tag: "ScopedSecret"
172
+ readonly subject: SubjectId
173
+ readonly audience: CanonicalAudience
174
+ readonly purposes: ReadonlySet<"observe" | "publish" | "correct">
175
+ readonly ref: CredentialRef
176
+ }
177
+
178
+ type ObservationGrantRegistry = WeakMap<ObservationGrant, ObservationGrantMetadata>
179
+
180
+ const rememberObservationGrant = (
181
+ observationGrants: ObservationGrantRegistry,
182
+ grant: ObservationGrant
183
+ ): void => {
184
+ observationGrants.set(grant, grant._tag === "AnonymousAccess"
185
+ ? {
186
+ _tag: grant._tag,
187
+ subject: grant.subject,
188
+ audience: grant.audience,
189
+ purposes: new Set(grant.purposes)
190
+ }
191
+ : {
192
+ _tag: grant._tag,
193
+ subject: grant.subject,
194
+ audience: grant.audience,
195
+ purposes: new Set(grant.purposes),
196
+ ref: grant.ref
197
+ })
198
+ }
199
+
200
+ const vaultKey = (subject: SubjectId, name: string): string => `${subject}\u0000${name}`
201
+
202
+ const platformError = (
203
+ phase: CredentialPlatformError["phase"],
204
+ commitment: CredentialPlatformError["commitment"],
205
+ reason: string
206
+ ): CredentialPlatformError => new CredentialPlatformError({ phase, commitment, reason })
207
+
208
+ const unavailable = (request: CredentialRequest, reason: string): CredentialUnavailable =>
209
+ new CredentialUnavailable({
210
+ subject: request.subject,
211
+ provider: request.provider,
212
+ purpose: request.purpose,
213
+ reason
214
+ })
215
+
216
+ const unsupported = (request: CredentialRequest, reason: string): CredentialStrategyUnsupported =>
217
+ new CredentialStrategyUnsupported({
218
+ subject: request.subject,
219
+ provider: request.provider,
220
+ strategy: request.strategy.kind,
221
+ reason
222
+ })
223
+
224
+ const readSecret = (
225
+ request: CredentialRequest,
226
+ name: string
227
+ ): Effect.Effect<Redacted.Redacted<string>, CredentialUnavailable> =>
228
+ Config.redacted(name).pipe(
229
+ Effect.mapError(() => unavailable(request, `Credential reference ${name} is unavailable.`))
230
+ )
231
+
232
+ const trustedStrategyIssue = (request: CredentialRequest): string | undefined => {
233
+ const strategy = request.strategy
234
+ if (strategy.kind !== "trusted-publishing") return "The request does not carry trusted-publishing intent."
235
+ if (request.provider !== "npm" || request.audience !== "https://registry.npmjs.org/" ||
236
+ request.purpose !== "publish") {
237
+ return "Trusted publishing is certified only for npm publish at the canonical npm registry."
238
+ }
239
+ if (strategy.identityProvider !== "github-actions" || strategy.runnerClass !== "github-hosted" ||
240
+ !certifiedRepository.test(strategy.repository) || !certifiedWorkflow.test(strategy.workflow) ||
241
+ !certifiedWorkflowRef.test(strategy.workflowRef) ||
242
+ !certifiedSourceCommit.test(strategy.sourceCommit) ||
243
+ strategy.provenanceEnvironmentContract !== "github-actions-npm-provenance-v1" ||
244
+ strategy.allowedAction !== "npm-publish-direct" || strategy.publisherSink !== "certified-npm-cli") {
245
+ return "Trusted publishing requires one exact GitHub-hosted workflow/ref, source commit, provenance environment contract, and certified direct npm sink."
246
+ }
247
+ return undefined
248
+ }
249
+
250
+ /** Validate non-secret GitHub host facts before either OIDC request value is read. */
251
+ const verifyTrustedHostIdentity = Effect.fn("EnvironmentCredentialProvider.verifyTrustedHostIdentity")(
252
+ function*(request: CredentialRequest) {
253
+ const issue = trustedStrategyIssue(request)
254
+ if (issue !== undefined) return yield* unsupported(request, issue)
255
+ if (request.strategy.kind !== "trusted-publishing") return yield* unsupported(request, issue ?? "Invalid strategy.")
256
+ const expected: ReadonlyArray<readonly [string, string]> = [
257
+ [githubActionsName, "true"],
258
+ [githubRepositoryName, request.strategy.repository],
259
+ [githubWorkflowRefName,
260
+ `${request.strategy.repository}/${request.strategy.workflow}@${request.strategy.workflowRef}`],
261
+ [githubServerUrlName, "https://github.com"],
262
+ [githubShaName, request.strategy.sourceCommit],
263
+ [runnerEnvironmentName, "github-hosted"]
264
+ ]
265
+ const values: Record<string, string> = {}
266
+ for (const [name, value] of expected) {
267
+ const observed = yield* Config.string(name).pipe(
268
+ Effect.mapError(() => unsupported(request, `Trusted publishing host identity ${name} is unavailable.`))
269
+ )
270
+ if (observed !== value) {
271
+ return yield* unsupported(request, `Trusted publishing host identity ${name} does not match prepared intent.`)
272
+ }
273
+ values[name] = observed
274
+ }
275
+ const shaped: ReadonlyArray<readonly [string, RegExp, string]> = [
276
+ [githubEventName, certifiedEvent, "a canonical GitHub event name"],
277
+ [githubRepositoryIdName, certifiedDecimalId, "a canonical positive decimal repository ID"],
278
+ [githubRepositoryOwnerIdName, certifiedDecimalId, "a canonical positive decimal repository-owner ID"],
279
+ [githubRefName, certifiedSourceRef, "a canonical heads/tags source ref"],
280
+ [githubRunIdName, certifiedDecimalId, "a canonical positive decimal run ID"],
281
+ [githubRunAttemptName, certifiedDecimalId, "a canonical positive decimal run attempt"]
282
+ ]
283
+ for (const [name, pattern, description] of shaped) {
284
+ const observed = yield* Config.string(name).pipe(
285
+ Effect.mapError(() => unsupported(request, `Trusted publishing provenance fact ${name} is unavailable.`))
286
+ )
287
+ if (!pattern.test(observed)) {
288
+ return yield* unsupported(request, `Trusted publishing provenance fact ${name} is not ${description}.`)
289
+ }
290
+ values[name] = observed
291
+ }
292
+ return Object.freeze({
293
+ GITHUB_ACTIONS: values[githubActionsName] as "true",
294
+ GITHUB_REPOSITORY: values[githubRepositoryName]!,
295
+ GITHUB_WORKFLOW_REF: values[githubWorkflowRefName]!,
296
+ GITHUB_SERVER_URL: values[githubServerUrlName] as "https://github.com",
297
+ GITHUB_EVENT_NAME: values[githubEventName]!,
298
+ GITHUB_REPOSITORY_ID: values[githubRepositoryIdName]!,
299
+ GITHUB_REPOSITORY_OWNER_ID: values[githubRepositoryOwnerIdName]!,
300
+ GITHUB_REF: values[githubRefName]!,
301
+ GITHUB_SHA: values[githubShaName]!,
302
+ RUNNER_ENVIRONMENT: values[runnerEnvironmentName] as "github-hosted",
303
+ GITHUB_RUN_ID: values[githubRunIdName]!,
304
+ GITHUB_RUN_ATTEMPT: values[githubRunAttemptName]!
305
+ }) satisfies TrustedHostSnapshot
306
+ }
307
+ )
308
+
309
+ /**
310
+ * A raw environment token has no host-verifiable downscope metadata. Treat it
311
+ * conservatively as bundled provider authority instead of relabeling the same
312
+ * bearer as a purpose-specific secret merely because one method requested it.
313
+ */
314
+ const environmentTokenPurposes = (
315
+ purpose: CredentialRequest["purpose"]
316
+ ): readonly ["observe", "publish", ...Array<"correct">] => purpose === "correct"
317
+ ? ["observe", "publish", "correct"]
318
+ : ["observe", "publish"]
319
+
320
+ const makeEnvironmentAcquirer = (
321
+ vault: SecretVault,
322
+ onTrustedWorkload?: (material: TrustedWorkloadMaterial) => void
323
+ ): CredentialGrantAcquirer => ({
324
+ acquire: Effect.fn("EnvironmentCredentialProvider.acquire")(function*(request) {
325
+ switch (request.strategy.kind) {
326
+ case "anonymous":
327
+ return { _tag: "AnonymousAccess", purposes: ["observe"] } as const
328
+ case "token": {
329
+ if (request.provider === "npm" && request.purpose === "observe") {
330
+ return yield* unsupported(
331
+ request,
332
+ "Authenticated npm observation is unsupported without a distinct typed read credential."
333
+ )
334
+ }
335
+ const name = request.strategy.credential.toString()
336
+ if (!portableEnvironmentName.test(name)) {
337
+ return yield* unsupported(request, "Token credential references must be portable environment names.")
338
+ }
339
+ const value = yield* readSecret(request, name)
340
+ vault.set(vaultKey(request.subject, name), value)
341
+ return {
342
+ _tag: "ScopedSecret",
343
+ purposes: environmentTokenPurposes(request.purpose),
344
+ ref: request.strategy.credential
345
+ } as const
346
+ }
347
+ case "trusted-publishing": {
348
+ if (onTrustedWorkload === undefined) {
349
+ return yield* unsupported(request, "Trusted workload acquisition requires a private host-material binding.")
350
+ }
351
+ const snapshot = yield* verifyTrustedHostIdentity(request)
352
+ const oidcValues = new Map<EnvironmentName, Redacted.Redacted<string>>()
353
+ for (const name of oidcNames) {
354
+ const value = yield* readSecret(request, name)
355
+ oidcValues.set(name, value)
356
+ }
357
+ onTrustedWorkload(Object.freeze({ snapshot, oidc: oidcValues }))
358
+ return {
359
+ _tag: "WorkloadIdentity",
360
+ purposes: [request.purpose],
361
+ names: oidcNames
362
+ } as const
363
+ }
364
+ }
365
+ })
366
+ })
367
+
368
+ const makeProvider = (
369
+ vault: SecretVault,
370
+ trustedWorkloads: TrustedWorkloadVault,
371
+ observationGrants: ObservationGrantRegistry
372
+ ): CredentialProviderShape => {
373
+ const provider = makeCredentialProvider(makeEnvironmentAcquirer(vault))
374
+ return {
375
+ acquireForObservation: Effect.fn("EnvironmentCredentialProvider.acquireForObservation")(function*(request) {
376
+ const grant = yield* provider.acquireForObservation(request)
377
+ if (grant._tag !== "WorkloadIdentity") {
378
+ rememberObservationGrant(observationGrants, grant)
379
+ }
380
+ return grant
381
+ }),
382
+ acquireForMutation: Effect.fn("EnvironmentCredentialProvider.acquireForMutation")(function*(request, decision) {
383
+ let grant: MutationCredentialGrant
384
+ if (request.strategy.kind === "trusted-publishing") {
385
+ let material: TrustedWorkloadMaterial | undefined
386
+ const trustedProvider = makeCredentialProvider(makeEnvironmentAcquirer(
387
+ vault,
388
+ (acquired) => { material = acquired }
389
+ ))
390
+ grant = yield* trustedProvider.acquireForMutation(request, decision)
391
+ if (grant._tag !== "WorkloadIdentity" || material === undefined) {
392
+ return yield* unavailable(request, "Trusted publishing host evidence was not bound to its workload grant.")
393
+ }
394
+ trustedWorkloads.set(grant, material)
395
+ } else {
396
+ grant = yield* provider.acquireForMutation(request, decision)
397
+ }
398
+ // npm publication tokens never become observation capabilities. GitHub
399
+ // retains its truthfully bundled installation-token read/write model.
400
+ if (grant._tag === "ScopedSecret" && request.provider !== "npm") {
401
+ rememberObservationGrant(observationGrants, grant)
402
+ }
403
+ return grant
404
+ })
405
+ }
406
+ }
407
+
408
+ /** Environment-backed provider projection for hosts that need no sinks. */
409
+ export const makeEnvironmentCredentialProvider = (): CredentialProviderShape =>
410
+ makeProvider(new Map(), new WeakMap(), new WeakMap())
411
+
412
+ const hasAuthorizationHeader = (headers: Readonly<Record<string, string>> | undefined): boolean =>
413
+ headers !== undefined && Object.keys(headers).some((name) => name.toLowerCase() === "authorization")
414
+
415
+ const audienceAllows = (audience: CanonicalAudience, requested: string): boolean => {
416
+ try {
417
+ const expected = new URL(audience)
418
+ const observed = new URL(requested)
419
+ if (expected.protocol !== "https:" || observed.protocol !== "https:") return false
420
+ if (expected.username !== "" || expected.password !== "" || expected.search !== "" || expected.hash !== "") return false
421
+ if (expected.origin !== observed.origin) return false
422
+ const base = expected.pathname.endsWith("/") ? expected.pathname : `${expected.pathname}/`
423
+ return observed.pathname === expected.pathname || observed.pathname.startsWith(base)
424
+ } catch {
425
+ return false
426
+ }
427
+ }
428
+
429
+ const githubUploadAudienceAllows = (audience: CanonicalAudience, requested: string): boolean => {
430
+ try {
431
+ const expected = new URL(audience)
432
+ const observed = new URL(requested)
433
+ if (expected.protocol !== "https:" || expected.hostname !== "api.github.com" ||
434
+ expected.port !== "" || expected.username !== "" || expected.password !== "" ||
435
+ expected.search !== "" || expected.hash !== "") return false
436
+ if (observed.protocol !== "https:" || observed.hostname !== "uploads.github.com" ||
437
+ observed.port !== "" || observed.username !== "" || observed.password !== "" ||
438
+ observed.hash !== "") return false
439
+ const repository = /^\/repos\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u.exec(expected.pathname)?.[0]
440
+ if (repository === undefined ||
441
+ !new RegExp(`^${repository.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\/releases\/[1-9][0-9]*\/assets$`, "u")
442
+ .test(observed.pathname)) return false
443
+ const keys = [...observed.searchParams.keys()]
444
+ return /^\?name=[^&]+(?:&label=[^&]*)?$/u.test(observed.search) &&
445
+ new Set(keys).size === keys.length && observed.searchParams.get("name")!.length > 0
446
+ } catch {
447
+ return false
448
+ }
449
+ }
450
+
451
+ const checkAudience = (
452
+ subject: SubjectId,
453
+ audience: CanonicalAudience,
454
+ requested: string,
455
+ allowGithubUpload = false
456
+ ): Effect.Effect<void, CredentialAudienceMismatch> => audienceAllows(audience, requested) ||
457
+ (allowGithubUpload && githubUploadAudienceAllows(audience, requested))
458
+ ? Effect.void
459
+ : Effect.fail(new CredentialAudienceMismatch({
460
+ subject,
461
+ expected: audience,
462
+ observed: requested
463
+ }))
464
+
465
+ const lookupToken = (
466
+ vault: SecretVault,
467
+ subject: SubjectId,
468
+ ref: CredentialRef
469
+ ): Effect.Effect<Redacted.Redacted<string>, CredentialUnavailable> => {
470
+ const value = vault.get(vaultKey(subject, ref))
471
+ return value === undefined
472
+ ? Effect.fail(new CredentialUnavailable({
473
+ subject,
474
+ provider: "unknown",
475
+ purpose: "unknown",
476
+ reason: "The credential grant has no host-owned secret in this platform boundary."
477
+ }))
478
+ : Effect.succeed(value)
479
+ }
480
+
481
+ const validateObservationGrant = Effect.fn("HttpAuthorizer.validateGrant")(function*(
482
+ input: HttpObservationRequest,
483
+ grant: ObservationGrant,
484
+ observationGrants: ObservationGrantRegistry
485
+ ) {
486
+ const issued = observationGrants.get(grant)
487
+ if (issued === undefined) {
488
+ return yield* new CredentialUnavailable({
489
+ subject: input.subject,
490
+ provider: "unknown",
491
+ purpose: "observe",
492
+ reason: "The supplied observation grant was not issued by this platform boundary."
493
+ })
494
+ }
495
+ if (issued.subject !== input.subject) {
496
+ return yield* new CredentialSubjectMismatch({
497
+ expected: input.subject,
498
+ observed: issued.subject
499
+ })
500
+ }
501
+ if (!issued.purposes.has("observe")) {
502
+ return yield* new CredentialPurposeMismatch({
503
+ subject: input.subject,
504
+ required: "observe",
505
+ granted: [...issued.purposes]
506
+ })
507
+ }
508
+ yield* checkAudience(input.subject, issued.audience, input.url)
509
+ return issued
510
+ })
511
+
512
+ const sendAnonymous = (
513
+ http: PublicationHttp,
514
+ request: HttpObservationRequest
515
+ ): Effect.Effect<HttpResponse, CredentialPlatformError> => {
516
+ if (hasAuthorizationHeader(request.headers)) {
517
+ return Effect.fail(platformError("observe", "before-dispatch", "Callers may not supply an Authorization header."))
518
+ }
519
+ return http.request({
520
+ method: request.method,
521
+ url: request.url,
522
+ ...(request.headers === undefined ? {} : { headers: request.headers }),
523
+ ...(request.body === undefined ? {} : { body: request.body })
524
+ }).pipe(
525
+ Effect.mapError(() => platformError("observe", "unknown", "Observation HTTP transport failed after dispatch."))
526
+ )
527
+ }
528
+
529
+ const sendAuthorized = (
530
+ http: PublicationHttp,
531
+ request: HttpObservationRequest | MutationHttpRequest,
532
+ token: Redacted.Redacted<string>,
533
+ phase: "observe" | "mutate",
534
+ credentialScheme: "bearer" | "pypi-token-basic" = "bearer"
535
+ ): Effect.Effect<HttpResponse, CredentialPlatformError> => {
536
+ if (hasAuthorizationHeader(request.headers)) {
537
+ return Effect.fail(platformError(phase, "before-dispatch", "Callers may not supply an Authorization header."))
538
+ }
539
+ const headers = {
540
+ ...(request.headers ?? {}),
541
+ authorization: credentialScheme === "pypi-token-basic"
542
+ ? `Basic ${Buffer.from(`__token__:${Redacted.value(token)}`, "utf8").toString("base64")}`
543
+ : `Bearer ${Redacted.value(token)}`
544
+ }
545
+ return http.request({
546
+ method: request.method,
547
+ url: request.url,
548
+ headers,
549
+ ...(request.body === undefined ? {} : { body: request.body })
550
+ }).pipe(
551
+ Effect.mapError(() => platformError(phase, "unknown", "Authorized HTTP transport failed after dispatch."))
552
+ )
553
+ }
554
+
555
+ const makeHttpAuthorizer = (
556
+ http: PublicationHttp,
557
+ vault: SecretVault,
558
+ observationGrants: ObservationGrantRegistry
559
+ ): HttpAuthorizerShape => ({
560
+ execute: Effect.fn("HttpAuthorizer.execute")(function*(input, grant) {
561
+ const issued = yield* validateObservationGrant(input, grant, observationGrants)
562
+ if (issued._tag === "AnonymousAccess") {
563
+ return yield* sendAnonymous(http, input)
564
+ }
565
+ const token = yield* lookupToken(vault, issued.subject, issued.ref)
566
+ return yield* sendAuthorized(http, input, token, "observe")
567
+ })
568
+ })
569
+
570
+ const makeAuthorizedMutationHttp = (
571
+ http: PublicationHttp,
572
+ vault: SecretVault
573
+ ): AuthorizedMutationHttpShape => ({
574
+ execute: Effect.fn("AuthorizedMutationHttp.execute")(function*(operation, request, grant) {
575
+ yield* validateGrantForOperation(operation, grant)
576
+ if ((operation.provider === "pypi") !== (request.credentialScheme === "pypi-token-basic")) {
577
+ return yield* platformError(
578
+ "mutate", "before-dispatch",
579
+ "The PyPI Basic credential projection is accepted only for typed PyPI mutations and is mandatory for them."
580
+ )
581
+ }
582
+ yield* checkAudience(operation.subject, operation.audience, request.url, operation.provider === "github")
583
+ const token = yield* lookupToken(vault, grant.subject, grant.ref)
584
+ return yield* sendAuthorized(http, request, token, "mutate", request.credentialScheme)
585
+ })
586
+ })
587
+
588
+ type NpmUserConfigMetadata = {
589
+ readonly directory: string
590
+ readonly path: string
591
+ readonly subject: SubjectId
592
+ readonly grant: ScopedSecret
593
+ readonly token: Redacted.Redacted<string>
594
+ }
595
+
596
+ const npmUserConfigs = new WeakMap<NpmUserConfig, NpmUserConfigMetadata>()
597
+
598
+ const npmAuthLine = (registryUrl: string, token: Redacted.Redacted<string>): string => {
599
+ const registry = new URL(registryUrl)
600
+ const pathname = registry.pathname.endsWith("/") ? registry.pathname : `${registry.pathname}/`
601
+ return `//${registry.host}${pathname}:_authToken=${Redacted.value(token)}`
602
+ }
603
+
604
+ const makeNpmUserConfigResource = (
605
+ vault: SecretVault,
606
+ temporaryRoot?: string
607
+ ): NpmUserConfigResourceShape => ({
608
+ acquire: Effect.fn("NpmUserConfigResource.acquire")(function*(input, grant) {
609
+ yield* validateGrantForOperation(input.operation, grant)
610
+ if (input.operation.provider !== "npm") {
611
+ return yield* platformError("resource", "before-dispatch", "npm user config accepts only the npm provider.")
612
+ }
613
+ yield* checkAudience(input.operation.subject, input.operation.audience, input.registryUrl)
614
+ const token = yield* lookupToken(vault, grant.subject, grant.ref)
615
+ const root = temporaryRoot ?? tmpdir()
616
+ const handle = makeNpmUserConfigHandle()
617
+ const acquired = Effect.tryPromise({
618
+ try: async () => {
619
+ await mkdir(root, { recursive: true, mode: 0o700 })
620
+ const directory = await mkdtemp(join(root, "ts-release-npm-"))
621
+ try {
622
+ const path = join(directory, "userconfig")
623
+ const contents = `${npmAuthLine(input.registryUrl, token)}\nignore-scripts=true\n`
624
+ await writeFile(path, contents, { mode: 0o600, flag: "wx" })
625
+ npmUserConfigs.set(handle, { directory, path, subject: input.operation.subject, grant, token })
626
+ return handle
627
+ } catch (cause) {
628
+ await rm(directory, { recursive: true, force: true })
629
+ throw cause
630
+ }
631
+ },
632
+ catch: () => platformError("resource", "before-dispatch", "Unable to create the scoped npm user config.")
633
+ })
634
+ return yield* Effect.acquireRelease(acquired, (resource) => {
635
+ const metadata = npmUserConfigs.get(resource)
636
+ npmUserConfigs.delete(resource)
637
+ return metadata === undefined
638
+ ? Effect.void
639
+ : Effect.tryPromise({
640
+ try: () => rm(metadata.directory, { recursive: true, force: true }),
641
+ catch: () => undefined
642
+ }).pipe(Effect.catch(() => Effect.void))
643
+ })
644
+ })
645
+ })
646
+
647
+ const collect = (stream: Stream.Stream<Uint8Array, unknown>): Effect.Effect<string, unknown> =>
648
+ Stream.mkString(Stream.decodeText(stream))
649
+
650
+ const minimumTrustedNode = [22, 14, 0] as const
651
+ const minimumTrustedNpm = [11, 5, 1] as const
652
+
653
+ const parseVersion = (value: string): readonly [number, number, number] | undefined => {
654
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?\s*$/u.exec(value)
655
+ if (match === null) return undefined
656
+ return [Number(match[1]), Number(match[2]), Number(match[3])]
657
+ }
658
+
659
+ const versionAtLeast = (
660
+ value: readonly [number, number, number],
661
+ minimum: readonly [number, number, number]
662
+ ): boolean => value[0] > minimum[0] ||
663
+ (value[0] === minimum[0] && (value[1] > minimum[1] ||
664
+ (value[1] === minimum[1] && value[2] >= minimum[2])))
665
+
666
+ const optionalPath = Config.option(Config.string("PATH")).pipe(
667
+ Effect.map(Option.getOrUndefined),
668
+ Effect.orElseSucceed(() => undefined)
669
+ )
670
+
671
+ const closedBaseEnvironment = (path: string | undefined): Record<string, string> =>
672
+ path === undefined ? {} : { PATH: path }
673
+
674
+ const workloadEnvironment = Effect.fn("CertifiedPublisherSpawn.workloadEnvironment")(function*(
675
+ trustedWorkloads: TrustedWorkloadVault,
676
+ grant: WorkloadIdentity
677
+ ) {
678
+ if (grant.names.size !== oidcNames.length || oidcNames.some((name) => !grant.names.has(name))) {
679
+ return yield* new CredentialStrategyUnsupported({
680
+ subject: grant.subject,
681
+ provider: grant.provider,
682
+ strategy: "trusted-publishing",
683
+ reason: "Workload identity lacks the exact certified GitHub Actions OIDC names."
684
+ })
685
+ }
686
+ const material = trustedWorkloads.get(grant)
687
+ if (material === undefined) {
688
+ return yield* new CredentialUnavailable({
689
+ subject: grant.subject,
690
+ provider: grant.provider,
691
+ purpose: "publish",
692
+ reason: "Certified GitHub Actions provenance facts are unavailable in this platform boundary."
693
+ })
694
+ }
695
+ const path = yield* optionalPath
696
+ const env = closedBaseEnvironment(path)
697
+ for (const name of oidcNames) {
698
+ const value = material.oidc.get(name)
699
+ if (value === undefined) {
700
+ return yield* new CredentialUnavailable({
701
+ subject: grant.subject,
702
+ provider: grant.provider,
703
+ purpose: "publish",
704
+ reason: "Certified workload identity material is unavailable in this platform boundary."
705
+ })
706
+ }
707
+ env[name] = Redacted.value(value)
708
+ }
709
+ // npm's OIDC exchange consumes the two host-held request values. Its
710
+ // libnpmpublish provenance builder separately consumes this exact validated
711
+ // non-secret GitHub run snapshot. No ambient environment is inherited.
712
+ Object.assign(env, material.snapshot)
713
+ env.NPM_CONFIG_IGNORE_SCRIPTS = "true"
714
+ return env
715
+ })
716
+
717
+ const validateTrustedNpmOperation = Effect.fn("CertifiedPublisherSpawn.validateTrustedNpmOperation")(
718
+ function*(operation: NpmPublishOperation, grant: WorkloadIdentity) {
719
+ if (operation.provider !== "npm" || operation.purpose !== "publish" ||
720
+ operation.audience !== "https://registry.npmjs.org/" ||
721
+ grant.strategy.identityProvider !== "github-actions" ||
722
+ grant.strategy.runnerClass !== "github-hosted" ||
723
+ grant.strategy.allowedAction !== "npm-publish-direct" ||
724
+ grant.strategy.publisherSink !== "certified-npm-cli") {
725
+ return yield* new CredentialStrategyUnsupported({
726
+ subject: operation.subject,
727
+ provider: operation.provider,
728
+ strategy: "trusted-publishing",
729
+ reason: "The workload grant is not bound to the certified direct npm publisher sink."
730
+ })
731
+ }
732
+ }
733
+ )
734
+
735
+ const publisherSpecKeys = Object.freeze({
736
+ NpmPublisherSpec: [
737
+ "_tag", "access", "cwd", "distTag", "operation", "packageName", "provenance",
738
+ "registryUrl", "tarballPath", "userConfig", "version"
739
+ ],
740
+ WorkloadPublisherSpec: [
741
+ "_tag", "access", "cwd", "distTag", "operation", "packageName", "provenance",
742
+ "registryUrl", "tarballPath", "version"
743
+ ]
744
+ } satisfies Record<CertifiedPublisherSpec["_tag"], ReadonlyArray<string>>)
745
+
746
+ const sameKeys = (value: object, expected: ReadonlyArray<string>): boolean => {
747
+ const observed = Object.keys(value).sort()
748
+ return observed.length === expected.length && observed.every((key, index) => key === expected[index])
749
+ }
750
+
751
+ const certifiedNpmSpecIssue = (spec: CertifiedPublisherSpec): string | undefined => {
752
+ const tagDescriptor = Object.getOwnPropertyDescriptor(spec, "_tag")
753
+ const tag: unknown = tagDescriptor !== undefined && "value" in tagDescriptor
754
+ ? tagDescriptor.value
755
+ : undefined
756
+ if (tag !== "NpmPublisherSpec" && tag !== "WorkloadPublisherSpec") {
757
+ return "The certified npm publisher accepts only its two closed typed spec variants."
758
+ }
759
+ const expectedKeys = publisherSpecKeys[tag]
760
+ const descriptors = Object.getOwnPropertyDescriptors(spec)
761
+ if (!sameKeys(spec, expectedKeys) || expectedKeys.some((key) => {
762
+ const descriptor = descriptors[key]
763
+ return descriptor === undefined || !("value" in descriptor)
764
+ })) {
765
+ return "The certified npm publisher accepts only its exact typed fields; generic argv and override fields are forbidden."
766
+ }
767
+ const operation = spec.operation
768
+ if (operation._tag !== "PublishOperation" || operation.provider !== "npm" || operation.purpose !== "publish") {
769
+ return "The certified npm publisher accepts only an npm publish operation."
770
+ }
771
+ if (typeof spec.packageName !== "string" || spec.packageName.length === 0 ||
772
+ typeof spec.version !== "string" || Semver.valid(spec.version) !== spec.version) {
773
+ return "The certified npm publisher requires one nonempty package name and canonical semantic version."
774
+ }
775
+ if (operation.subject !== `npm:${spec.packageName}@${spec.version}`) {
776
+ return "The certified npm publisher package and version do not match the authorized operation subject."
777
+ }
778
+ try {
779
+ if (canonicalizeRegistryUrl(spec.registryUrl) !== spec.registryUrl ||
780
+ operation.audience.toString() !== spec.registryUrl.toString()) {
781
+ return "The certified npm publisher registry does not match the authorized canonical audience."
782
+ }
783
+ } catch {
784
+ return "The certified npm publisher requires a canonical registry and npm distribution tag."
785
+ }
786
+ if (Semver.validRange(spec.distTag) !== null || spec.distTag.trim() !== spec.distTag ||
787
+ encodeURIComponent(spec.distTag) !== spec.distTag) {
788
+ return "The certified npm publisher requires a canonical registry and npm distribution tag."
789
+ }
790
+ if ((spec.access !== "public" && spec.access !== "restricted") ||
791
+ (spec.provenance !== "required" && spec.provenance !== "automatic" && spec.provenance !== "disabled") ||
792
+ (spec._tag === "NpmPublisherSpec" && spec.provenance === "automatic") ||
793
+ (spec.access === "restricted" && !spec.packageName.startsWith("@")) ||
794
+ (Semver.prerelease(spec.version) !== null && spec.distTag === "latest")) {
795
+ return "The certified npm publisher access, provenance, or dist-tag policy is not an allowed canonical value."
796
+ }
797
+ try {
798
+ const fileName = basename(spec.tarballPath)
799
+ if (!isAbsolute(spec.cwd) || resolve(spec.cwd) !== spec.cwd ||
800
+ !/^[a-f0-9]{64}$/u.test(fileName) ||
801
+ resolve(spec.cwd, "blobs", fileName) !== spec.tarballPath) {
802
+ return "The certified npm publisher tarball must be the canonical prepared-store blob path."
803
+ }
804
+ } catch {
805
+ return "The certified npm publisher tarball must be the canonical prepared-store blob path."
806
+ }
807
+ return undefined
808
+ }
809
+
810
+ const validateCertifiedNpmSpec = Effect.fn("CertifiedPublisherSpawn.validateCertifiedNpmSpec")(
811
+ function*(spec: CertifiedPublisherSpec) {
812
+ const issue = certifiedNpmSpecIssue(spec)
813
+ if (issue !== undefined) {
814
+ return yield* new CredentialStrategyUnsupported({
815
+ subject: spec.operation.subject,
816
+ provider: spec.operation.provider,
817
+ strategy: spec._tag === "WorkloadPublisherSpec" ? "trusted-publishing" : "token",
818
+ reason: issue
819
+ })
820
+ }
821
+ }
822
+ )
823
+
824
+ /** Snapshot caller data once so accessors or concurrent mutation cannot change the admitted command after validation. */
825
+ const snapshotCertifiedNpmSpec = (spec: CertifiedPublisherSpec): CertifiedPublisherSpec => {
826
+ const operation = Object.freeze({
827
+ ...spec.operation,
828
+ decision: Object.freeze({ ...spec.operation.decision })
829
+ }) as NpmPublishOperation
830
+ return Object.freeze({ ...spec, operation }) as CertifiedPublisherSpec
831
+ }
832
+
833
+ const publisherEnvironment = Effect.fn("CertifiedPublisherSpawn.environment")(function*(
834
+ vault: SecretVault,
835
+ trustedWorkloads: TrustedWorkloadVault,
836
+ spec: CertifiedPublisherSpec,
837
+ grant: ScopedSecret | WorkloadIdentity
838
+ ) {
839
+ yield* validateCertifiedNpmSpec(spec)
840
+ if (spec._tag === "NpmPublisherSpec") {
841
+ if (grant._tag !== "ScopedSecret") {
842
+ return yield* new CredentialStrategyUnsupported({
843
+ subject: spec.operation.subject,
844
+ provider: spec.operation.provider,
845
+ strategy: "trusted-publishing",
846
+ reason: "A scoped npm user config requires a scoped-secret grant."
847
+ })
848
+ }
849
+ const metadata = npmUserConfigs.get(spec.userConfig)
850
+ if (metadata === undefined || metadata.subject !== spec.operation.subject || metadata.grant !== grant) {
851
+ return yield* new CredentialUnavailable({
852
+ subject: spec.operation.subject,
853
+ provider: spec.operation.provider,
854
+ purpose: spec.operation.purpose,
855
+ reason: "The npm user config is not active for this exact operation and grant."
856
+ })
857
+ }
858
+ const path = yield* optionalPath
859
+ return {
860
+ ...closedBaseEnvironment(path),
861
+ NPM_CONFIG_USERCONFIG: metadata.path,
862
+ NPM_CONFIG_IGNORE_SCRIPTS: "true"
863
+ }
864
+ }
865
+ if (grant._tag !== "WorkloadIdentity") {
866
+ return yield* new CredentialStrategyUnsupported({
867
+ subject: spec.operation.subject,
868
+ provider: spec.operation.provider,
869
+ strategy: "token",
870
+ reason: "The workload publisher requires a workload-identity grant."
871
+ })
872
+ }
873
+ yield* validateTrustedNpmOperation(spec.operation, grant)
874
+ return yield* workloadEnvironment(trustedWorkloads, grant)
875
+ })
876
+
877
+ const redactPublisherOutput = (
878
+ value: string,
879
+ env: Readonly<Record<string, string>>,
880
+ spec: CertifiedPublisherSpec
881
+ ): string => {
882
+ const metadata = spec._tag === "NpmPublisherSpec" ? npmUserConfigs.get(spec.userConfig) : undefined
883
+ const known = metadata === undefined
884
+ ? env
885
+ : { ...env, PUBLISH_CREDENTIAL: Redacted.value(metadata.token) }
886
+ return redactOutput(value, known)
887
+ }
888
+
889
+ interface NpmPublishTransport {
890
+ readonly directory: string
891
+ readonly path: string
892
+ }
893
+
894
+ /**
895
+ * npm classifies a suffixless local path as a package directory. Prepared
896
+ * artifacts deliberately use digest-only blob names, so expose the already
897
+ * admitted bytes through a private `.tgz` transport name only while npm runs.
898
+ */
899
+ const makeNpmPublishTransport = (
900
+ sourcePath: string,
901
+ temporaryRoot?: string
902
+ ) => {
903
+ const root = temporaryRoot ?? tmpdir()
904
+ const acquired = Effect.tryPromise({
905
+ try: async () => {
906
+ await mkdir(root, { recursive: true, mode: 0o700 })
907
+ const directory = await mkdtemp(join(root, "ts-release-npm-tarball-"))
908
+ try {
909
+ const path = join(directory, "package.tgz")
910
+ await copyFile(sourcePath, path)
911
+ await chmod(path, 0o600)
912
+ return { directory, path }
913
+ } catch (cause) {
914
+ await rm(directory, { recursive: true, force: true })
915
+ throw cause
916
+ }
917
+ },
918
+ catch: () => platformError(
919
+ "resource",
920
+ "before-dispatch",
921
+ "Unable to materialize the verified npm tarball transport path."
922
+ )
923
+ })
924
+ return Effect.acquireRelease(acquired, ({ directory }) => Effect.tryPromise({
925
+ try: () => rm(directory, { recursive: true, force: true }),
926
+ catch: () => undefined
927
+ }).pipe(Effect.catch(() => Effect.void)))
928
+ }
929
+
930
+ const makeCertifiedPublisherSpawn = (
931
+ spawner: ChildProcessSpawner["Service"],
932
+ vault: SecretVault,
933
+ trustedWorkloads: TrustedWorkloadVault,
934
+ temporaryRoot?: string
935
+ ): CertifiedPublisherSpawnShape => {
936
+ const runClosed = Effect.fn("CertifiedPublisherSpawn.runClosed")(function*(
937
+ argv: readonly [string, ...Array<string>]
938
+ ) {
939
+ const path = yield* optionalPath
940
+ const command = ChildProcess.make(argv[0], [...argv.slice(1)], {
941
+ env: closedBaseEnvironment(path),
942
+ extendEnv: false,
943
+ stdin: "ignore",
944
+ stdout: "pipe",
945
+ stderr: "pipe"
946
+ })
947
+ return yield* Effect.scoped(Effect.gen(function*() {
948
+ const spawned = yield* Effect.exit(spawner.spawn(command))
949
+ if (Exit.isFailure(spawned)) {
950
+ return yield* platformError("spawn", "before-dispatch", "Trusted npm preflight could not start its version probe.")
951
+ }
952
+ const output = yield* Effect.exit(Effect.all({
953
+ stdout: collect(spawned.value.stdout),
954
+ stderr: collect(spawned.value.stderr),
955
+ exitCode: spawned.value.exitCode
956
+ }, { concurrency: "unbounded" }))
957
+ if (Exit.isFailure(output)) {
958
+ return yield* platformError("spawn", "before-dispatch", "Trusted npm preflight could not observe its version probe.")
959
+ }
960
+ if (Number(output.value.exitCode) !== 0) {
961
+ return yield* platformError("spawn", "before-dispatch", "Trusted npm preflight version probe was rejected.")
962
+ }
963
+ return output.value.stdout
964
+ }))
965
+ })
966
+
967
+ return {
968
+ preflightTrustedNpm: Effect.fn("CertifiedPublisherSpawn.preflightTrustedNpm")(function*(operation, grant) {
969
+ yield* validateGrantForOperation(operation, grant)
970
+ if (grant._tag !== "WorkloadIdentity") {
971
+ return yield* new CredentialStrategyUnsupported({
972
+ subject: operation.subject,
973
+ provider: operation.provider,
974
+ strategy: "token",
975
+ reason: "Trusted npm preflight requires a workload-identity grant."
976
+ })
977
+ }
978
+ yield* validateTrustedNpmOperation(operation, grant)
979
+ // Validate the exact host-held OIDC names before any provider authority is dispatched.
980
+ yield* workloadEnvironment(trustedWorkloads, grant)
981
+ const node = parseVersion(yield* runClosed(["node", "--version"]))
982
+ const npm = parseVersion(yield* runClosed(["npm", "--version"]))
983
+ if (node === undefined || !versionAtLeast(node, minimumTrustedNode) ||
984
+ npm === undefined || !versionAtLeast(npm, minimumTrustedNpm)) {
985
+ return yield* platformError(
986
+ "spawn",
987
+ "before-dispatch",
988
+ "Trusted npm publishing requires Node >=22.14.0 and npm >=11.5.1."
989
+ )
990
+ }
991
+ }),
992
+ spawn: Effect.fn("CertifiedPublisherSpawn.spawn")(function*(inputSpec, grant) {
993
+ yield* validateGrantForOperation(inputSpec.operation, grant)
994
+ yield* validateCertifiedNpmSpec(inputSpec)
995
+ const spec = snapshotCertifiedNpmSpec(inputSpec)
996
+ const env = yield* publisherEnvironment(vault, trustedWorkloads, spec, grant)
997
+ return yield* Effect.scoped(Effect.gen(function*() {
998
+ const transport = yield* makeNpmPublishTransport(spec.tarballPath, temporaryRoot)
999
+ const argv = npmPublishArgv({ ...spec, tarballPath: transport.path })
1000
+ const command = ChildProcess.make(argv[0], [...argv.slice(1)], {
1001
+ cwd: spec.cwd,
1002
+ env,
1003
+ extendEnv: false,
1004
+ stdin: "ignore",
1005
+ stdout: "pipe",
1006
+ stderr: "pipe"
1007
+ })
1008
+ const spawned = yield* Effect.exit(spawner.spawn(command))
1009
+ if (Exit.isFailure(spawned)) {
1010
+ return {
1011
+ _tag: "RejectedBeforeStart",
1012
+ commitment: "before-dispatch",
1013
+ reason: "The certified publisher process did not return a started handle."
1014
+ } satisfies CertifiedPublisherResult
1015
+ }
1016
+ const output = yield* Effect.exit(Effect.all({
1017
+ stdout: collect(spawned.value.stdout),
1018
+ stderr: collect(spawned.value.stderr),
1019
+ exitCode: spawned.value.exitCode
1020
+ }, { concurrency: "unbounded" }))
1021
+ if (Exit.isFailure(output)) {
1022
+ return {
1023
+ _tag: "PublisherOutcomeUnknown",
1024
+ commitment: "unknown",
1025
+ reason: "The certified publisher started, but its final outcome could not be observed."
1026
+ } satisfies CertifiedPublisherResult
1027
+ }
1028
+ return {
1029
+ _tag: "PublisherExited",
1030
+ commitment: "started",
1031
+ exitCode: Number(output.value.exitCode),
1032
+ stdout: redactPublisherOutput(output.value.stdout, env, spec),
1033
+ stderr: redactPublisherOutput(output.value.stderr, env, spec)
1034
+ } satisfies CertifiedPublisherResult
1035
+ }))
1036
+ })
1037
+ }
1038
+ }
1039
+
1040
+ export interface EnvironmentCredentialPlatform {
1041
+ readonly credentialProvider: CredentialProviderShape
1042
+ readonly httpAuthorizer: HttpAuthorizerShape
1043
+ readonly authorizedMutationHttp: AuthorizedMutationHttpShape
1044
+ readonly npmUserConfigResource: NpmUserConfigResourceShape
1045
+ readonly certifiedPublisherSpawn: CertifiedPublisherSpawnShape
1046
+ }
1047
+
1048
+ export interface EnvironmentCredentialPlatformOptions {
1049
+ readonly temporaryRoot?: string
1050
+ }
1051
+
1052
+ /** All host sinks share one private vault; no secret is returned from it. */
1053
+ export const makeEnvironmentCredentialPlatform = (
1054
+ http: PublicationHttp,
1055
+ spawner: ChildProcessSpawner["Service"],
1056
+ options: EnvironmentCredentialPlatformOptions = {}
1057
+ ): EnvironmentCredentialPlatform => {
1058
+ const vault: SecretVault = new Map()
1059
+ const trustedWorkloads: TrustedWorkloadVault = new WeakMap()
1060
+ const observationGrants: ObservationGrantRegistry = new WeakMap()
1061
+ return {
1062
+ credentialProvider: makeProvider(vault, trustedWorkloads, observationGrants),
1063
+ httpAuthorizer: makeHttpAuthorizer(http, vault, observationGrants),
1064
+ authorizedMutationHttp: makeAuthorizedMutationHttp(http, vault),
1065
+ npmUserConfigResource: makeNpmUserConfigResource(vault, options.temporaryRoot),
1066
+ certifiedPublisherSpawn: makeCertifiedPublisherSpawn(spawner, vault, trustedWorkloads, options.temporaryRoot)
1067
+ }
1068
+ }
1069
+
1070
+ export const makeEnvironmentCredentialPlatformLayer = (
1071
+ http: PublicationHttp,
1072
+ spawner: ChildProcessSpawner["Service"],
1073
+ options: EnvironmentCredentialPlatformOptions = {}
1074
+ ): Layer.Layer<CredentialPlatformServices> => {
1075
+ const platform = makeEnvironmentCredentialPlatform(http, spawner, options)
1076
+ return Layer.mergeAll(
1077
+ Layer.succeed(CredentialProvider)(platform.credentialProvider),
1078
+ Layer.succeed(HttpAuthorizer)(platform.httpAuthorizer),
1079
+ Layer.succeed(AuthorizedMutationHttp)(platform.authorizedMutationHttp),
1080
+ Layer.succeed(NpmUserConfigResource)(platform.npmUserConfigResource),
1081
+ Layer.succeed(CertifiedPublisherSpawn)(platform.certifiedPublisherSpawn)
1082
+ )
1083
+ }