@intentius/chant-lexicon-terraform 0.124.54 → 0.124.56

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 (351) hide show
  1. package/README.md +7 -4
  2. package/dist/ci-setup.d.ts +11 -0
  3. package/dist/ci-setup.d.ts.map +1 -0
  4. package/dist/ci-setup.js +41 -0
  5. package/dist/components/capability-plugin.d.ts +40 -6
  6. package/dist/components/capability-plugin.d.ts.map +1 -1
  7. package/dist/components/capability-plugin.js +174 -30
  8. package/dist/components/compose-group.d.ts +1 -1
  9. package/dist/components/compose-group.d.ts.map +1 -1
  10. package/dist/components/compose-group.js +1 -1
  11. package/dist/components/release-inputs.d.ts +29 -0
  12. package/dist/components/release-inputs.d.ts.map +1 -0
  13. package/dist/components/release-inputs.js +57 -0
  14. package/dist/components/trace-facts.d.ts +16 -0
  15. package/dist/components/trace-facts.d.ts.map +1 -0
  16. package/dist/components/trace-facts.js +19 -0
  17. package/dist/cone.d.ts +53 -0
  18. package/dist/cone.d.ts.map +1 -0
  19. package/dist/cone.js +119 -0
  20. package/dist/cost-hold.d.ts +27 -0
  21. package/dist/cost-hold.d.ts.map +1 -0
  22. package/dist/cost-hold.js +21 -0
  23. package/dist/drift-rows.d.ts +61 -0
  24. package/dist/drift-rows.d.ts.map +1 -0
  25. package/dist/drift-rows.js +108 -0
  26. package/dist/exit-codes.d.ts +55 -0
  27. package/dist/exit-codes.d.ts.map +1 -0
  28. package/dist/exit-codes.js +48 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/integrity.json +7 -4
  33. package/dist/lint/audit-catalog.d.ts.map +1 -1
  34. package/dist/lint/audit-catalog.js +27 -3
  35. package/dist/lint/audit-lineage.d.ts +14 -2
  36. package/dist/lint/audit-lineage.d.ts.map +1 -1
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.js +4 -0
  39. package/dist/lint/post-synth/record-store.d.ts +54 -0
  40. package/dist/lint/post-synth/record-store.d.ts.map +1 -0
  41. package/dist/lint/post-synth/record-store.js +104 -0
  42. package/dist/lint/post-synth/tf001.d.ts +2 -2
  43. package/dist/lint/post-synth/tf001.js +2 -2
  44. package/dist/lint/post-synth/tf024.d.ts +35 -11
  45. package/dist/lint/post-synth/tf024.d.ts.map +1 -1
  46. package/dist/lint/post-synth/tf024.js +92 -20
  47. package/dist/lint/post-synth/tf046.d.ts +64 -0
  48. package/dist/lint/post-synth/tf046.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf046.js +175 -0
  50. package/dist/lint/post-synth/tf047.d.ts +36 -0
  51. package/dist/lint/post-synth/tf047.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf047.js +94 -0
  53. package/dist/lock-rows.d.ts +36 -0
  54. package/dist/lock-rows.d.ts.map +1 -0
  55. package/dist/lock-rows.js +60 -0
  56. package/dist/lock-timeout.d.ts +22 -0
  57. package/dist/lock-timeout.d.ts.map +1 -0
  58. package/dist/lock-timeout.js +32 -0
  59. package/dist/manifest.json +1 -1
  60. package/dist/module-catalog/index.d.ts +69 -0
  61. package/dist/module-catalog/index.d.ts.map +1 -0
  62. package/dist/module-catalog/index.js +110 -0
  63. package/dist/module-catalog/refresh-cli.d.ts +4 -0
  64. package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
  65. package/dist/module-catalog/refresh-cli.js +64 -0
  66. package/dist/module-catalog/static-tier.d.ts +6 -0
  67. package/dist/module-catalog/static-tier.d.ts.map +1 -0
  68. package/dist/module-catalog/static-tier.js +1650 -0
  69. package/dist/module-catalog/types.d.ts +65 -0
  70. package/dist/module-catalog/types.d.ts.map +1 -0
  71. package/dist/module-catalog/types.js +33 -0
  72. package/dist/okf/index.md +3 -1
  73. package/dist/okf/rules/TF024.md +2 -2
  74. package/dist/okf/rules/TF046.md +11 -0
  75. package/dist/okf/rules/TF047.md +11 -0
  76. package/dist/op/activities/terraform.d.ts +29 -0
  77. package/dist/op/activities/terraform.d.ts.map +1 -1
  78. package/dist/op/activities/terraform.js +19 -4
  79. package/dist/pin/consumers.d.ts +35 -0
  80. package/dist/pin/consumers.d.ts.map +1 -0
  81. package/dist/pin/consumers.js +70 -0
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/plugin.js +5 -0
  84. package/dist/registry/address.d.ts +63 -0
  85. package/dist/registry/address.d.ts.map +1 -0
  86. package/dist/registry/address.js +65 -0
  87. package/dist/registry/catalog-read.d.ts +14 -0
  88. package/dist/registry/catalog-read.d.ts.map +1 -0
  89. package/dist/registry/catalog-read.js +58 -0
  90. package/dist/registry/deprecated.d.ts +37 -0
  91. package/dist/registry/deprecated.d.ts.map +1 -0
  92. package/dist/registry/deprecated.js +43 -0
  93. package/dist/registry/docs.d.ts +96 -0
  94. package/dist/registry/docs.d.ts.map +1 -0
  95. package/dist/registry/docs.js +210 -0
  96. package/dist/registry/edge/edge.d.ts +46 -0
  97. package/dist/registry/edge/edge.d.ts.map +1 -0
  98. package/dist/registry/edge/edge.js +194 -0
  99. package/dist/registry/edge/index.d.ts +11 -0
  100. package/dist/registry/edge/index.d.ts.map +1 -0
  101. package/dist/registry/edge/index.js +10 -0
  102. package/dist/registry/edge/lambda-edge.d.ts +76 -0
  103. package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
  104. package/dist/registry/edge/lambda-edge.js +95 -0
  105. package/dist/registry/edge/sigv4-web.d.ts +33 -0
  106. package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
  107. package/dist/registry/edge/sigv4-web.js +56 -0
  108. package/dist/registry/edge/token-source.d.ts +73 -0
  109. package/dist/registry/edge/token-source.d.ts.map +1 -0
  110. package/dist/registry/edge/token-source.js +115 -0
  111. package/dist/registry/edge/worker.d.ts +45 -0
  112. package/dist/registry/edge/worker.d.ts.map +1 -0
  113. package/dist/registry/edge/worker.js +26 -0
  114. package/dist/registry/handler.d.ts +25 -0
  115. package/dist/registry/handler.d.ts.map +1 -0
  116. package/dist/registry/handler.js +75 -0
  117. package/dist/registry/headers-file.d.ts +21 -0
  118. package/dist/registry/headers-file.d.ts.map +1 -0
  119. package/dist/registry/headers-file.js +57 -0
  120. package/dist/registry/index.d.ts +31 -0
  121. package/dist/registry/index.d.ts.map +1 -0
  122. package/dist/registry/index.js +30 -0
  123. package/dist/registry/mirror-paths.d.ts +9 -0
  124. package/dist/registry/mirror-paths.d.ts.map +1 -0
  125. package/dist/registry/mirror-paths.js +8 -0
  126. package/dist/registry/mirror.d.ts +130 -0
  127. package/dist/registry/mirror.d.ts.map +1 -0
  128. package/dist/registry/mirror.js +319 -0
  129. package/dist/registry/proxies.d.ts +73 -0
  130. package/dist/registry/proxies.d.ts.map +1 -0
  131. package/dist/registry/proxies.js +111 -0
  132. package/dist/registry/reader.d.ts +57 -0
  133. package/dist/registry/reader.d.ts.map +1 -0
  134. package/dist/registry/reader.js +198 -0
  135. package/dist/registry/semver.d.ts +16 -0
  136. package/dist/registry/semver.d.ts.map +1 -0
  137. package/dist/registry/semver.js +24 -0
  138. package/dist/registry/serve.d.ts +24 -0
  139. package/dist/registry/serve.d.ts.map +1 -0
  140. package/dist/registry/serve.js +64 -0
  141. package/dist/registry/source-forms.d.ts +79 -0
  142. package/dist/registry/source-forms.d.ts.map +1 -0
  143. package/dist/registry/source-forms.js +202 -0
  144. package/dist/registry/targets/artifactory.d.ts +28 -0
  145. package/dist/registry/targets/artifactory.d.ts.map +1 -0
  146. package/dist/registry/targets/artifactory.js +206 -0
  147. package/dist/registry/targets/fake-artifactory.d.ts +30 -0
  148. package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
  149. package/dist/registry/targets/fake-artifactory.js +142 -0
  150. package/dist/registry/targets/fake-gitlab.d.ts +39 -0
  151. package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
  152. package/dist/registry/targets/fake-gitlab.js +136 -0
  153. package/dist/registry/targets/fake-hcp.d.ts +50 -0
  154. package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
  155. package/dist/registry/targets/fake-hcp.js +202 -0
  156. package/dist/registry/targets/fake-http.d.ts +31 -0
  157. package/dist/registry/targets/fake-http.d.ts.map +1 -0
  158. package/dist/registry/targets/fake-http.js +87 -0
  159. package/dist/registry/targets/fake-nexus.d.ts +27 -0
  160. package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
  161. package/dist/registry/targets/fake-nexus.js +137 -0
  162. package/dist/registry/targets/gitlab.d.ts +53 -0
  163. package/dist/registry/targets/gitlab.d.ts.map +1 -0
  164. package/dist/registry/targets/gitlab.js +187 -0
  165. package/dist/registry/targets/hcp.d.ts +29 -0
  166. package/dist/registry/targets/hcp.d.ts.map +1 -0
  167. package/dist/registry/targets/hcp.js +310 -0
  168. package/dist/registry/targets/hosted.d.ts +62 -0
  169. package/dist/registry/targets/hosted.d.ts.map +1 -0
  170. package/dist/registry/targets/hosted.js +74 -0
  171. package/dist/registry/targets/index.d.ts +18 -0
  172. package/dist/registry/targets/index.d.ts.map +1 -0
  173. package/dist/registry/targets/index.js +17 -0
  174. package/dist/registry/targets/module-target.d.ts +68 -0
  175. package/dist/registry/targets/module-target.d.ts.map +1 -0
  176. package/dist/registry/targets/module-target.js +63 -0
  177. package/dist/registry/targets/nexus.d.ts +43 -0
  178. package/dist/registry/targets/nexus.d.ts.map +1 -0
  179. package/dist/registry/targets/nexus.js +250 -0
  180. package/dist/registry/targets/zip.d.ts +55 -0
  181. package/dist/registry/targets/zip.d.ts.map +1 -0
  182. package/dist/registry/targets/zip.js +278 -0
  183. package/dist/registry/writer.d.ts +101 -0
  184. package/dist/registry/writer.d.ts.map +1 -0
  185. package/dist/registry/writer.js +109 -0
  186. package/dist/rules/record-store.ts +122 -0
  187. package/dist/rules/tf001.ts +2 -2
  188. package/dist/rules/tf024.ts +105 -22
  189. package/dist/rules/tf046.ts +204 -0
  190. package/dist/rules/tf047.ts +114 -0
  191. package/dist/state/azure-blob.d.ts +8 -67
  192. package/dist/state/azure-blob.d.ts.map +1 -1
  193. package/dist/state/azure-blob.js +6 -199
  194. package/dist/state/gcs.d.ts +7 -61
  195. package/dist/state/gcs.d.ts.map +1 -1
  196. package/dist/state/gcs.js +5 -222
  197. package/dist/state/object-store.d.ts +4 -33
  198. package/dist/state/object-store.d.ts.map +1 -1
  199. package/dist/state/object-store.js +4 -17
  200. package/dist/state/s3.d.ts +11 -58
  201. package/dist/state/s3.d.ts.map +1 -1
  202. package/dist/state/s3.js +10 -161
  203. package/dist/state/store.js +2 -2
  204. package/dist/state-ops.d.ts +255 -0
  205. package/dist/state-ops.d.ts.map +1 -0
  206. package/dist/state-ops.js +305 -0
  207. package/dist/state-reads.d.ts +57 -0
  208. package/dist/state-reads.d.ts.map +1 -0
  209. package/dist/state-reads.js +104 -0
  210. package/dist/used-sources.d.ts +54 -0
  211. package/dist/used-sources.d.ts.map +1 -0
  212. package/dist/used-sources.js +157 -0
  213. package/package.json +19 -3
  214. package/src/__fixtures__/drift-record/README.md +5 -0
  215. package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
  216. package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
  217. package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
  218. package/src/__fixtures__/used-sources/root/main.tf +31 -0
  219. package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
  220. package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
  221. package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
  222. package/src/ci-setup.ts +42 -0
  223. package/src/components/capability-plugin.test.ts +26 -14
  224. package/src/components/capability-plugin.ts +185 -31
  225. package/src/components/compose-group.ts +1 -1
  226. package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
  227. package/src/components/pr-loop.tofu.test.ts +1 -1
  228. package/src/components/release-inputs.ts +69 -0
  229. package/src/components/trace-facts.test.ts +21 -0
  230. package/src/components/trace-facts.ts +27 -0
  231. package/src/cone.ts +138 -0
  232. package/src/cost-hold.ts +29 -0
  233. package/src/describe-resources.engine-facts.test.ts +2 -3
  234. package/src/drift-rows.test.ts +135 -0
  235. package/src/drift-rows.ts +137 -0
  236. package/src/exit-codes.test.ts +23 -0
  237. package/src/exit-codes.ts +59 -0
  238. package/src/index.ts +3 -0
  239. package/src/lint/audit-catalog.ts +29 -3
  240. package/src/lint/audit-lineage.ts +14 -2
  241. package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
  242. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
  243. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
  244. package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
  245. package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
  246. package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
  247. package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
  248. package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
  249. package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
  250. package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
  251. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
  252. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
  253. package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
  254. package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
  255. package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
  256. package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
  257. package/src/lint/post-synth/index.ts +4 -0
  258. package/src/lint/post-synth/record-store.ts +122 -0
  259. package/src/lint/post-synth/tf001.ts +2 -2
  260. package/src/lint/post-synth/tf024.test.ts +26 -5
  261. package/src/lint/post-synth/tf024.ts +105 -22
  262. package/src/lint/post-synth/tf046.test.ts +137 -0
  263. package/src/lint/post-synth/tf046.ts +204 -0
  264. package/src/lint/post-synth/tf047.test.ts +56 -0
  265. package/src/lint/post-synth/tf047.ts +114 -0
  266. package/src/lock-rows.test.ts +70 -0
  267. package/src/lock-rows.ts +82 -0
  268. package/src/lock-timeout.ts +34 -0
  269. package/src/module-catalog/index.test.ts +93 -0
  270. package/src/module-catalog/index.ts +159 -0
  271. package/src/module-catalog/refresh-cli.ts +68 -0
  272. package/src/module-catalog/static-tier.ts +1659 -0
  273. package/src/module-catalog/types.ts +88 -0
  274. package/src/op/activities/terraform.ts +46 -5
  275. package/src/op-wave-step.test.ts +109 -0
  276. package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
  277. package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
  278. package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
  279. package/src/pin/consumers.test.ts +57 -0
  280. package/src/pin/consumers.ts +95 -0
  281. package/src/plugin.ts +6 -0
  282. package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
  283. package/src/registry/__fixtures__/docs/documented/README.md +1 -0
  284. package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
  285. package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
  286. package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
  287. package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
  288. package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
  289. package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
  290. package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
  291. package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
  292. package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
  293. package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
  294. package/src/registry/address.ts +100 -0
  295. package/src/registry/catalog-read.ts +56 -0
  296. package/src/registry/deprecated.test.ts +139 -0
  297. package/src/registry/deprecated.ts +76 -0
  298. package/src/registry/docs.test.ts +140 -0
  299. package/src/registry/docs.ts +299 -0
  300. package/src/registry/edge/bundle.test.ts +50 -0
  301. package/src/registry/edge/edge.test.ts +176 -0
  302. package/src/registry/edge/edge.ts +211 -0
  303. package/src/registry/edge/index.ts +10 -0
  304. package/src/registry/edge/lambda-edge.ts +127 -0
  305. package/src/registry/edge/sigv4-web.ts +79 -0
  306. package/src/registry/edge/token-source.test.ts +258 -0
  307. package/src/registry/edge/token-source.ts +169 -0
  308. package/src/registry/edge/worker.ts +61 -0
  309. package/src/registry/handler.ts +89 -0
  310. package/src/registry/headers-file.ts +61 -0
  311. package/src/registry/index.ts +31 -0
  312. package/src/registry/mirror-paths.ts +10 -0
  313. package/src/registry/mirror.test.ts +198 -0
  314. package/src/registry/mirror.ts +395 -0
  315. package/src/registry/proxies.test.ts +153 -0
  316. package/src/registry/proxies.ts +132 -0
  317. package/src/registry/reader.ts +225 -0
  318. package/src/registry/registry.test.ts +262 -0
  319. package/src/registry/semver.ts +34 -0
  320. package/src/registry/serve.ts +82 -0
  321. package/src/registry/source-forms.test.ts +87 -0
  322. package/src/registry/source-forms.ts +216 -0
  323. package/src/registry/targets/artifactory.test.ts +248 -0
  324. package/src/registry/targets/artifactory.ts +257 -0
  325. package/src/registry/targets/fake-artifactory.ts +158 -0
  326. package/src/registry/targets/fake-gitlab.ts +161 -0
  327. package/src/registry/targets/fake-hcp.ts +233 -0
  328. package/src/registry/targets/fake-http.ts +97 -0
  329. package/src/registry/targets/fake-nexus.ts +152 -0
  330. package/src/registry/targets/gitlab.test.ts +208 -0
  331. package/src/registry/targets/gitlab.ts +223 -0
  332. package/src/registry/targets/hcp.test.ts +230 -0
  333. package/src/registry/targets/hcp.ts +360 -0
  334. package/src/registry/targets/hosted.ts +117 -0
  335. package/src/registry/targets/index.ts +17 -0
  336. package/src/registry/targets/module-target.ts +131 -0
  337. package/src/registry/targets/nexus.test.ts +220 -0
  338. package/src/registry/targets/nexus.ts +314 -0
  339. package/src/registry/targets/zip.test.ts +65 -0
  340. package/src/registry/targets/zip.ts +287 -0
  341. package/src/registry/writer.ts +155 -0
  342. package/src/state/azure-blob.ts +6 -221
  343. package/src/state/gcs.ts +5 -252
  344. package/src/state/object-store.ts +4 -32
  345. package/src/state/s3.ts +10 -182
  346. package/src/state/store.ts +2 -2
  347. package/src/state-ops.test.ts +182 -0
  348. package/src/state-ops.ts +462 -0
  349. package/src/state-reads.ts +116 -0
  350. package/src/used-sources.test.ts +75 -0
  351. package/src/used-sources.ts +204 -0
package/src/state/gcs.ts CHANGED
@@ -1,254 +1,7 @@
1
1
  /**
2
- * Google Cloud Storage for a gcs backend's state, over its JSON API with
3
- * `fetch` and `node:crypto`: an object's text and generation, a write, a
4
- * write only when there is no object (the lock), and a delete.
5
- *
6
- * The identity, first match wins: a bearer token (`access_token`, or
7
- * GOOGLE_OAUTH_ACCESS_TOKEN), else a credentials file or its JSON:
8
- *
9
- * external_account the job's OIDC token, traded at Google's STS, then the
10
- * service account it impersonates.
11
- * service_account a key: requests carry a self-signed JWT.
12
- *
13
- * With `impersonate_service_account`, every request carries that service
14
- * account's token, which the identity asks IAM for.
15
- *
16
- * Lifted from terragucci's `report/gcs.ts` (c493e0e), without signed URLs,
17
- * version listing and the re-plan's traded tokens.
2
+ * The Google Cloud Storage client a gcs backend's state uses: chant core's
3
+ * (#4554, `@intentius/chant/object-store`), over the JSON API with `fetch`
4
+ * and `node:crypto`. Writes are conditional on a generation
5
+ * (`ifGenerationMatch`); a lost write is a 412, a StoreConflict.
18
6
  */
19
- import { createSign } from "node:crypto";
20
- import { readFileSync } from "node:fs";
21
- import { defaultFetch, encodeStrict, StoreConflict, StoreError, type StoreFetch } from "./object-store";
22
-
23
- export const GCS_ENDPOINT = "https://storage.googleapis.com";
24
- /** IAM Credentials, where a token for an impersonated service account comes from. */
25
- export const IAM_CREDENTIALS = "https://iamcredentials.googleapis.com";
26
- const CLOUD_PLATFORM = "https://www.googleapis.com/auth/cloud-platform";
27
- const READ_WRITE = "https://www.googleapis.com/auth/devstorage.read_write";
28
-
29
- export interface GcsTarget {
30
- bucket: string;
31
- /** The JSON API's address, no trailing slash. Default https://storage.googleapis.com. */
32
- endpoint: string;
33
- /** The credentials file, read at the first request. */
34
- credentialsFile?: string;
35
- /** The credentials as JSON: a gcs backend's `credentials` given inline. */
36
- credentialsJson?: string;
37
- /** A bearer token used as it is. */
38
- accessToken?: string;
39
- /** The service account every request acts as, through IAM's generateAccessToken. */
40
- impersonate?: string;
41
- /** IAM Credentials' address, no trailing slash. */
42
- iamEndpoint?: string;
43
- }
44
-
45
- interface ExternalAccount {
46
- type: "external_account";
47
- audience: string;
48
- subject_token_type: string;
49
- token_url: string;
50
- service_account_impersonation_url?: string;
51
- credential_source: { file?: string; url?: string; headers?: Record<string, string>; format?: { type?: string; subject_token_field_name?: string } };
52
- }
53
-
54
- interface ServiceAccountKey {
55
- type: "service_account";
56
- client_email: string;
57
- private_key: string;
58
- private_key_id?: string;
59
- }
60
-
61
- type Credentials = ExternalAccount | ServiceAccountKey;
62
-
63
- const b64url = (b: string | Buffer): string => Buffer.from(b).toString("base64url");
64
-
65
- /** A JWT the key signs for itself, which Google's APIs take as an access token for the scope it names. */
66
- export function selfSignedJwt(key: ServiceAccountKey, now: Date): string {
67
- const iat = Math.floor(now.getTime() / 1000);
68
- const head = b64url(JSON.stringify({ alg: "RS256", typ: "JWT", ...(key.private_key_id ? { kid: key.private_key_id } : {}) }));
69
- const body = b64url(JSON.stringify({ iss: key.client_email, sub: key.client_email, scope: READ_WRITE, iat, exp: iat + 3600 }));
70
- return `${head}.${body}.${createSign("RSA-SHA256").update(`${head}.${body}`).sign(key.private_key).toString("base64url")}`;
71
- }
72
-
73
- /** Google's error message, or the start of the body. */
74
- const why = (text: string): string => {
75
- try {
76
- const j = JSON.parse(text) as { error?: string | { message?: string }; error_description?: string };
77
- if (typeof j.error === "object" && j.error?.message) return j.error.message;
78
- if (typeof j.error === "string") return [j.error, j.error_description].filter(Boolean).join(": ");
79
- } catch {
80
- // Not JSON.
81
- }
82
- return text.slice(0, 300);
83
- };
84
-
85
- type Token = { token: string; expires: number };
86
-
87
- export class GcsClient {
88
- private creds?: Credentials;
89
- private token?: Promise<Token>;
90
-
91
- constructor(readonly target: GcsTarget, private readonly fetchFn: StoreFetch = defaultFetch, private readonly readFile: (file: string) => string = (f) => readFileSync(f, "utf-8")) {}
92
-
93
- private where(key: string): string {
94
- return `gs://${this.target.bucket}/${key}`;
95
- }
96
-
97
- private credentials(): Credentials {
98
- if (this.creds) return this.creds;
99
- const source = this.target.credentialsFile ?? "the backend's credentials";
100
- let c: Partial<Credentials>;
101
- try {
102
- c = JSON.parse(this.target.credentialsJson ?? this.readFile(this.target.credentialsFile ?? "")) as Partial<Credentials>;
103
- } catch (e) {
104
- throw new StoreError(`cannot read the credentials in ${source}: ${(e as Error).message}`);
105
- }
106
- if (c.type === "external_account" && c.token_url && c.audience && c.credential_source && (c.credential_source.file || c.credential_source.url)) return (this.creds = c as ExternalAccount);
107
- if (c.type === "service_account" && c.client_email && c.private_key) return (this.creds = c as ServiceAccountKey);
108
- throw new StoreError(`${source} is not an external_account file with a credential_source file or url, nor a service_account key`);
109
- }
110
-
111
- private async post(url: string, headers: Record<string, string>, body: string, what: string): Promise<Record<string, unknown>> {
112
- const res = await this.fetchFn(url, { method: "POST", headers, body });
113
- const text = await res.text();
114
- if (!res.ok) throw new StoreError(`${what}: ${res.status} ${why(text)}`);
115
- try {
116
- return JSON.parse(text) as Record<string, unknown>;
117
- } catch {
118
- throw new StoreError(`${what}: the answer is not JSON`);
119
- }
120
- }
121
-
122
- /** The job's OIDC token, from the file or URL the credentials name. */
123
- private async subjectToken(c: ExternalAccount): Promise<string> {
124
- const src = c.credential_source;
125
- let text: string;
126
- if (src.file) {
127
- try {
128
- text = this.readFile(src.file);
129
- } catch (e) {
130
- throw new StoreError(`cannot read the OIDC token in ${src.file}: ${(e as Error).message}`);
131
- }
132
- } else {
133
- const res = await this.fetchFn(src.url!, { method: "GET", headers: src.headers ?? {} });
134
- text = await res.text();
135
- if (!res.ok) throw new StoreError(`the OIDC token at ${src.url!.split("?")[0]}: ${res.status} ${why(text)}`);
136
- }
137
- const field = src.format?.type === "json" ? src.format.subject_token_field_name : undefined;
138
- let token = text.trim();
139
- if (field) {
140
- try {
141
- token = String((JSON.parse(text) as Record<string, unknown>)[field] ?? "");
142
- } catch {
143
- throw new StoreError(`the job's OIDC token is not JSON with ${field}`);
144
- }
145
- }
146
- if (!token) throw new StoreError("the job's OIDC token is empty");
147
- return token;
148
- }
149
-
150
- private async impersonateWith(url: string, bearer: string, sa: string): Promise<Token> {
151
- const r = await this.post(url, { "content-type": "application/json", authorization: `Bearer ${bearer}` }, JSON.stringify({ scope: [CLOUD_PLATFORM], lifetime: "3600s" }), `impersonating ${sa}`);
152
- if (typeof r.accessToken !== "string") throw new StoreError(`IAM answered the impersonation of ${sa} without a token`);
153
- const at = typeof r.expireTime === "string" ? Date.parse(r.expireTime) : NaN;
154
- return { token: r.accessToken, expires: Number.isNaN(at) ? Date.now() + 3600_000 : at };
155
- }
156
-
157
- /** STS's federated token, then the impersonated service account's, when the credentials name one. */
158
- private async exchange(c: ExternalAccount): Promise<Token> {
159
- const sts = await this.post(
160
- c.token_url,
161
- { "content-type": "application/x-www-form-urlencoded" },
162
- new URLSearchParams({
163
- grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
164
- audience: c.audience,
165
- scope: CLOUD_PLATFORM,
166
- requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
167
- subject_token: await this.subjectToken(c),
168
- subject_token_type: c.subject_token_type,
169
- }).toString(),
170
- `Google STS refused the OIDC token for ${c.audience}`,
171
- );
172
- if (typeof sts.access_token !== "string") throw new StoreError("Google STS answered without a token");
173
- const fed = { token: sts.access_token, expires: Date.now() + Number(sts.expires_in ?? 3600) * 1000 };
174
- if (!c.service_account_impersonation_url) return fed;
175
- return this.impersonateWith(c.service_account_impersonation_url, fed.token, "the service account");
176
- }
177
-
178
- /** The identity's own token: the one the target gives, a key's self-signed JWT, or STS's for the OIDC token. */
179
- private async own(): Promise<Token> {
180
- if (this.target.accessToken) return { token: this.target.accessToken, expires: Date.now() + 3600_000 };
181
- const c = this.credentials();
182
- return c.type === "service_account" ? { token: selfSignedJwt(c, new Date()), expires: Date.now() + 3600_000 } : this.exchange(c);
183
- }
184
-
185
- /** An access token: once per client, again a minute before it expires. */
186
- private async bearer(): Promise<string> {
187
- const sa = this.target.impersonate;
188
- const fresh = async (): Promise<Token> => {
189
- if (!sa) return this.own();
190
- const base = await this.own();
191
- return this.impersonateWith(`${(this.target.iamEndpoint ?? IAM_CREDENTIALS).replace(/\/+$/, "")}/v1/projects/-/serviceAccounts/${encodeURIComponent(sa)}:generateAccessToken`, base.token, sa);
192
- };
193
- const again = (): Promise<Token> =>
194
- (this.token = fresh().catch((e: unknown) => {
195
- this.token = undefined;
196
- throw e;
197
- }));
198
- let held = await (this.token ?? again());
199
- if (held.expires - Date.now() < 60_000) held = await again();
200
- return held.token;
201
- }
202
-
203
- private objectUrl(key: string): string {
204
- return `${this.target.endpoint}/storage/v1/b/${encodeURIComponent(this.target.bucket)}/o/${encodeStrict(key)}`;
205
- }
206
-
207
- /** Write an object; with `ifGenerationMatch`, only when the object still has that generation (`0`: there is none), else StoreConflict. */
208
- async put(key: string, body: string, contentType: string, ifGenerationMatch?: string): Promise<void> {
209
- const t = this.target;
210
- const url = `${t.endpoint}/upload/storage/v1/b/${encodeURIComponent(t.bucket)}/o?uploadType=media&name=${encodeStrict(key)}${ifGenerationMatch !== undefined ? `&ifGenerationMatch=${encodeStrict(ifGenerationMatch)}` : ""}`;
211
- const res = await this.fetchFn(url, { method: "POST", headers: { "content-type": contentType, authorization: `Bearer ${await this.bearer()}` }, body });
212
- if (res.status === 412) throw new StoreConflict(`PUT ${this.where(key)}: 412, it changed since it was read`);
213
- if (!res.ok) throw new StoreError(`PUT ${this.where(key)}: ${res.status} ${why(await res.text())}`);
214
- }
215
-
216
- async get(key: string): Promise<string | undefined> {
217
- const res = await this.fetchFn(`${this.objectUrl(key)}?alt=media`, { method: "GET", headers: { authorization: `Bearer ${await this.bearer()}` } });
218
- if (res.status === 404) return undefined;
219
- if (!res.ok) throw new StoreError(`GET ${this.where(key)}: ${res.status} ${why(await res.text())}`);
220
- return res.text();
221
- }
222
-
223
- /** An object's metadata and never its body: whether it is there, and its generation. */
224
- async head(key: string): Promise<{ exists: boolean; generation?: string }> {
225
- const res = await this.fetchFn(this.objectUrl(key), { method: "GET", headers: { authorization: `Bearer ${await this.bearer()}` } });
226
- const text = await res.text();
227
- if (res.status === 404) return { exists: false };
228
- if (!res.ok) throw new StoreError(`GET ${this.where(key)} metadata: ${res.status} ${why(text)}`);
229
- let g: unknown;
230
- try {
231
- g = (JSON.parse(text) as { generation?: unknown }).generation;
232
- } catch {
233
- // Metadata that is not JSON: no generation.
234
- }
235
- return { exists: true, ...(g !== undefined && g !== null && g !== "" ? { generation: String(g) } : {}) };
236
- }
237
-
238
- /** Write an object only when there is none (ifGenerationMatch=0): true when this write made it, false when one was there. */
239
- async putIfAbsent(key: string, body: string, contentType: string): Promise<boolean> {
240
- try {
241
- await this.put(key, body, contentType, "0");
242
- return true;
243
- } catch (e) {
244
- if (e instanceof StoreConflict) return false;
245
- throw e;
246
- }
247
- }
248
-
249
- /** Delete an object's live generation. One that is not there is deleted already. */
250
- async remove(key: string): Promise<void> {
251
- const res = await this.fetchFn(this.objectUrl(key), { method: "DELETE", headers: { authorization: `Bearer ${await this.bearer()}` } });
252
- if (!res.ok && res.status !== 404) throw new StoreError(`DELETE ${this.where(key)}: ${res.status} ${why(await res.text())}`);
253
- }
254
- }
7
+ export { GCS_ENDPOINT, GcsClient, IAM_CREDENTIALS, selfSignedJwt, type GcsTarget } from "@intentius/chant/object-store";
@@ -1,35 +1,7 @@
1
1
  /**
2
2
  * What the state stores share: the fetch they call, their errors, and how a
3
- * key goes into a URL. Lifted from terragucci's `report/object-store.ts`
4
- * (c493e0e), cut to what a state's lock and write need.
3
+ * key goes into a URL. Since #4554 these are chant core's object-store
4
+ * definitions (`@intentius/chant/object-store`), so a store error thrown by
5
+ * a core client is the one a state caller catches.
5
6
  */
6
-
7
- export type StoreFetch = (url: string, init: { method: string; headers: Record<string, string>; body?: string | Uint8Array }) => Promise<{
8
- ok: boolean;
9
- status: number;
10
- text(): Promise<string>;
11
- /** A store that sends no headers (or a test's fake) gets no conditional writes. */
12
- headers?: { get(name: string): string | null };
13
- }>;
14
-
15
- export class StoreError extends Error {}
16
-
17
- /** A conditional write that lost: the object changed (412) or another write to it was in flight (409). */
18
- export class StoreConflict extends StoreError {}
19
-
20
- /** A condition on a write: the version the object must still have, or `*` for "there is no object yet". */
21
- export type StoreCondition = { ifMatch: string } | { ifNoneMatch: "*" };
22
-
23
- /** A key as a URL path: each segment percent-encoded the strict way (RFC 3986), the slashes kept. */
24
- export const encodePath = (key: string): string => key.split("/").map(encodeStrict).join("/");
25
-
26
- export const encodeStrict = (v: string): string => encodeURIComponent(v).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
27
-
28
- /** The first value of an XML element, unescaped. Enough for the small answers these stores send. */
29
- export const xmlText = (xml: string, tag: string): string | undefined => {
30
- const m = new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml);
31
- return m?.[1]!.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&").trim();
32
- };
33
-
34
- /** The global fetch, as a StoreFetch. */
35
- export const defaultFetch: StoreFetch = (url, init) => fetch(url, init as RequestInit);
7
+ export { defaultFetch, encodePath, encodeStrict, StoreConflict, StoreError, xmlText, type StoreCondition, type StoreFetch } from "@intentius/chant/object-store";
package/src/state/s3.ts CHANGED
@@ -1,193 +1,21 @@
1
1
  /**
2
- * The little of S3 a state's lock and write need: get, put, put only when
3
- * absent, head and delete one object, signed with AWS Signature Version 4
4
- * from `node:crypto`. Any S3-compatible store answers it (S3, MinIO, floci).
5
- * With no static keys, a role is assumed with the job's OIDC token through
6
- * STS AssumeRoleWithWebIdentity. `fetch` is injectable so the signing is
7
- * tested against recorded requests.
8
- *
9
- * Lifted from terragucci's `report/s3.ts` (c493e0e), without presigning,
10
- * version listing and the re-plan's traded credentials.
2
+ * The S3 client a state's lock and write use: chant core's (#4554,
3
+ * `@intentius/chant/object-store`), which signs with chant's one AWS
4
+ * signer. Get, put, put only when absent, head and delete one object; with
5
+ * no static keys, a role is assumed with the job's OIDC token through STS
6
+ * AssumeRoleWithWebIdentity.
11
7
  */
12
- import { createHash, createHmac } from "node:crypto";
13
- import { readFileSync } from "node:fs";
14
- import { defaultFetch, encodePath, encodeStrict, StoreError, xmlText, type StoreFetch } from "./object-store";
8
+ import { s3FromEnv as coreS3FromEnv, type S3Target } from "@intentius/chant/object-store";
15
9
 
16
- export interface S3Credentials {
17
- accessKeyId: string;
18
- secretAccessKey: string;
19
- sessionToken?: string;
20
- }
21
-
22
- /** A role to assume with the job's OIDC token, the way the AWS SDKs read `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE`. */
23
- export interface S3WebIdentity {
24
- roleArn: string;
25
- tokenFile: string;
26
- sessionName: string;
27
- /** STS's address, https://host[:port], no path. */
28
- endpoint: string;
29
- }
30
-
31
- export interface S3Location {
32
- bucket: string;
33
- /** https://host[:port], no path. Default: AWS's regional endpoint. */
34
- endpoint?: string;
35
- region: string;
36
- }
37
-
38
- export type S3Target = S3Location & (S3Credentials | { webIdentity: S3WebIdentity });
10
+ export { assumeRoleWithWebIdentity, S3Client, sign, type S3Credentials, type S3Location, type S3Target, type S3WebIdentity } from "@intentius/chant/object-store";
39
11
 
40
12
  /**
41
13
  * The target for `bucket` from the AWS environment variables. Credentials,
42
14
  * first match wins: static keys in `AWS_ACCESS_KEY_ID` and
43
15
  * `AWS_SECRET_ACCESS_KEY`; `AWS_ROLE_ARN` assumed with the token in
44
- * `AWS_WEB_IDENTITY_TOKEN_FILE`.
16
+ * `AWS_WEB_IDENTITY_TOKEN_FILE`, in a session named `chant-state` unless
17
+ * AWS_ROLE_SESSION_NAME names one.
45
18
  */
46
19
  export function s3FromEnv(where: { bucket: string; endpoint?: string }, env: NodeJS.ProcessEnv = process.env): S3Target {
47
- const endpoint = where.endpoint ?? env.AWS_ENDPOINT_URL_S3 ?? env.AWS_ENDPOINT_URL;
48
- const region = env.AWS_REGION || env.AWS_DEFAULT_REGION || "us-east-1";
49
- const at = { bucket: where.bucket, ...(endpoint ? { endpoint: endpoint.replace(/\/+$/, "") } : {}), region };
50
- const accessKeyId = env.AWS_ACCESS_KEY_ID;
51
- const secretAccessKey = env.AWS_SECRET_ACCESS_KEY;
52
- if (accessKeyId && secretAccessKey) return { ...at, accessKeyId, secretAccessKey, ...(env.AWS_SESSION_TOKEN ? { sessionToken: env.AWS_SESSION_TOKEN } : {}) };
53
- const tokenFile = env.AWS_WEB_IDENTITY_TOKEN_FILE;
54
- if (env.AWS_ROLE_ARN && tokenFile) {
55
- return {
56
- ...at,
57
- webIdentity: {
58
- roleArn: env.AWS_ROLE_ARN,
59
- tokenFile,
60
- sessionName: env.AWS_ROLE_SESSION_NAME || "chant-state",
61
- endpoint: (env.AWS_ENDPOINT_URL_STS || env.AWS_ENDPOINT_URL || `https://sts.${region}.amazonaws.com`).replace(/\/+$/, ""),
62
- },
63
- };
64
- }
65
- throw new StoreError("set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE");
66
- }
67
-
68
- /** STS AssumeRoleWithWebIdentity. The request carries the token and is not signed. */
69
- export async function assumeRoleWithWebIdentity(w: S3WebIdentity, fetchFn: StoreFetch, readToken: (file: string) => string = (f) => readFileSync(f, "utf-8")): Promise<S3Credentials & { expiration?: Date }> {
70
- let token: string;
71
- try {
72
- token = readToken(w.tokenFile).trim();
73
- } catch (e) {
74
- throw new StoreError(`cannot read the OIDC token in ${w.tokenFile}: ${(e as Error).message}`);
75
- }
76
- if (!token) throw new StoreError(`the OIDC token file ${w.tokenFile} is empty`);
77
- const body = new URLSearchParams({ Action: "AssumeRoleWithWebIdentity", Version: "2011-06-15", RoleArn: w.roleArn, RoleSessionName: w.sessionName, WebIdentityToken: token }).toString();
78
- const res = await fetchFn(`${w.endpoint}/`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded; charset=utf-8", accept: "application/xml" }, body });
79
- const xml = await res.text();
80
- if (!res.ok) {
81
- const why = [xmlText(xml, "Code"), xmlText(xml, "Message")].filter(Boolean).join(": ") || xml.slice(0, 300);
82
- throw new StoreError(`AssumeRoleWithWebIdentity for ${w.roleArn}: ${res.status} ${why}`);
83
- }
84
- const accessKeyId = xmlText(xml, "AccessKeyId");
85
- const secretAccessKey = xmlText(xml, "SecretAccessKey");
86
- const sessionToken = xmlText(xml, "SessionToken");
87
- if (!accessKeyId || !secretAccessKey || !sessionToken) throw new StoreError(`AssumeRoleWithWebIdentity for ${w.roleArn} answered without credentials`);
88
- const expires = xmlText(xml, "Expiration");
89
- return { accessKeyId, secretAccessKey, sessionToken, ...(expires && !Number.isNaN(Date.parse(expires)) ? { expiration: new Date(expires) } : {}) };
90
- }
91
-
92
- const sha256 = (b: string | Uint8Array): string => createHash("sha256").update(b).digest("hex");
93
- const hmac = (key: string | Buffer, s: string): Buffer => createHmac("sha256", key).update(s).digest();
94
-
95
- /** Signature Version 4 headers for a request; its query string, if any, is signed in canonical order. `extra` headers are signed too. */
96
- export function sign(t: Pick<S3Location, "region"> & S3Credentials, method: string, rawUrl: string, extra: Record<string, string>, payload: string, now: Date, service = "s3"): Record<string, string> {
97
- const url = new URL(rawUrl);
98
- const amzDate = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
99
- const day = amzDate.slice(0, 8);
100
- const headers: Record<string, string> = {
101
- host: url.host,
102
- "x-amz-content-sha256": payload,
103
- "x-amz-date": amzDate,
104
- ...Object.fromEntries(Object.entries(extra).map(([k, v]) => [k.toLowerCase(), v])),
105
- ...(t.sessionToken ? { "x-amz-security-token": t.sessionToken } : {}),
106
- };
107
- const names = Object.keys(headers).sort();
108
- const query = [...url.searchParams].map(([k, v]) => `${encodeStrict(k)}=${encodeStrict(v)}`).sort().join("&");
109
- const canonical = [method, url.pathname, query, ...names.map((n) => `${n}:${headers[n]!.trim()}`), "", names.join(";"), payload].join("\n");
110
- const scope = `${day}/${t.region}/${service}/aws4_request`;
111
- const toSign = ["AWS4-HMAC-SHA256", amzDate, scope, sha256(canonical)].join("\n");
112
- const key4 = hmac(hmac(hmac(hmac(`AWS4${t.secretAccessKey}`, day), t.region), service), "aws4_request");
113
- const signature = createHmac("sha256", key4).update(toSign).digest("hex");
114
- const { host: _host, ...sent } = headers;
115
- return { ...sent, authorization: `AWS4-HMAC-SHA256 Credential=${t.accessKeyId}/${scope}, SignedHeaders=${names.join(";")}, Signature=${signature}` };
116
- }
117
-
118
- /** The address of `key` in the bucket: path-style on a custom endpoint, virtual-hosted on AWS. */
119
- const objectUrl = (t: S3Location, key: string): string => `${t.endpoint ? `${t.endpoint}/${t.bucket}` : `https://${t.bucket}.s3.${t.region}.amazonaws.com`}/${encodePath(key)}`;
120
-
121
- export class S3Client {
122
- private creds?: Promise<S3Credentials & { expiration?: Date }>;
123
-
124
- constructor(readonly target: S3Target, private readonly fetchFn: StoreFetch = defaultFetch) {}
125
-
126
- private where(key: string): string {
127
- return `s3://${this.target.bucket}/${key}`;
128
- }
129
-
130
- /** Static keys as given; a web identity's keys once per client, again a minute before they expire. */
131
- private async signer(): Promise<S3Location & S3Credentials> {
132
- const t = this.target;
133
- if (!("webIdentity" in t)) return t;
134
- const assume = (): Promise<S3Credentials & { expiration?: Date }> =>
135
- (this.creds = assumeRoleWithWebIdentity(t.webIdentity, this.fetchFn).catch((e: unknown) => {
136
- this.creds = undefined;
137
- throw e;
138
- }));
139
- let held = await (this.creds ?? assume());
140
- if (held.expiration && held.expiration.getTime() - Date.now() < 60_000) held = await assume();
141
- const { accessKeyId, secretAccessKey, sessionToken } = held;
142
- return { bucket: t.bucket, ...(t.endpoint ? { endpoint: t.endpoint } : {}), region: t.region, accessKeyId, secretAccessKey, ...(sessionToken ? { sessionToken } : {}) };
143
- }
144
-
145
- private async request(method: string, key: string, body: string, extra: Record<string, string> = {}) {
146
- const t = await this.signer();
147
- const url = objectUrl(t, key);
148
- return this.fetchFn(url, { method, headers: sign(t, method, url, extra, sha256(body), new Date()), ...(method === "PUT" ? { body } : {}) });
149
- }
150
-
151
- /** Write an object, unconditionally. */
152
- async put(key: string, body: string, contentType: string): Promise<void> {
153
- const res = await this.request("PUT", key, body, { "content-type": contentType });
154
- if (!res.ok) throw new StoreError(`PUT ${this.where(key)}: ${res.status} ${(await res.text()).slice(0, 300)}`);
155
- }
156
-
157
- /** The object's text, or undefined when there is none. */
158
- async get(key: string): Promise<string | undefined> {
159
- const res = await this.request("GET", key, "");
160
- if (res.status === 404) return undefined;
161
- if (!res.ok) throw new StoreError(`GET ${this.where(key)}: ${res.status} ${(await res.text()).slice(0, 300)}`);
162
- return res.text();
163
- }
164
-
165
- /** The object's metadata and never its body: whether it exists, its ETag and its version id. */
166
- async head(key: string): Promise<{ exists: boolean; etag?: string; versionId?: string }> {
167
- const res = await this.request("HEAD", key, "");
168
- if (res.status === 404) return { exists: false };
169
- if (!res.ok) throw new StoreError(`HEAD ${this.where(key)}: ${res.status}`);
170
- const etag = res.headers?.get("etag") ?? undefined;
171
- const version = res.headers?.get("x-amz-version-id") ?? undefined;
172
- return { exists: true, ...(etag ? { etag } : {}), ...(version && version !== "null" ? { versionId: version } : {}) };
173
- }
174
-
175
- /**
176
- * Write an object only when there is none (If-None-Match: *): true when
177
- * this write made it, false when one was there. A store that refuses the
178
- * condition fails the write: a lock is never taken unconditionally.
179
- */
180
- async putIfAbsent(key: string, body: string, contentType: string): Promise<boolean> {
181
- const res = await this.request("PUT", key, body, { "content-type": contentType, "if-none-match": "*" });
182
- if (res.status === 412 || res.status === 409) return false;
183
- if (res.status === 501) throw new StoreError(`PUT ${this.where(key)}: the store does not take a conditional write (501)`);
184
- if (!res.ok) throw new StoreError(`PUT ${this.where(key)}: ${res.status} ${(await res.text()).slice(0, 300)}`);
185
- return true;
186
- }
187
-
188
- /** Delete an object. One that is not there is deleted already. */
189
- async remove(key: string): Promise<void> {
190
- const res = await this.request("DELETE", key, "");
191
- if (!res.ok && res.status !== 404) throw new StoreError(`DELETE ${this.where(key)}: ${res.status} ${(await res.text()).slice(0, 300)}`);
192
- }
20
+ return coreS3FromEnv({ ...where, sessionName: "chant-state", setting: "the state bucket" }, env);
193
21
  }
@@ -99,7 +99,7 @@ function s3Store(o: S3State, fetchFn?: StoreFetch): StateStore {
99
99
  lock: (info) => client.putIfAbsent(lockKey, info, JSON_TYPE),
100
100
  unlock: () => client.remove(lockKey),
101
101
  readBeside: (suffix) => client.get(`${o.key}${suffix}`),
102
- writeBeside: (suffix, body) => client.put(`${o.key}${suffix}`, body, JSON_TYPE),
102
+ writeBeside: async (suffix, body) => void (await client.put(`${o.key}${suffix}`, body, JSON_TYPE)),
103
103
  removeBeside: (suffix) => client.remove(`${o.key}${suffix}`),
104
104
  besideLocation: (suffix) => `s3://${o.bucket}/${o.key}${suffix}`,
105
105
  adopt: () => {},
@@ -122,7 +122,7 @@ function gcsStore(o: GcsState, fetchFn?: StoreFetch): StateStore {
122
122
  lock: (info) => client.putIfAbsent(o.lock, info, JSON_TYPE),
123
123
  unlock: () => client.remove(o.lock),
124
124
  readBeside: (suffix) => client.get(`${o.key}${suffix}`),
125
- writeBeside: (suffix, body) => client.put(`${o.key}${suffix}`, body, JSON_TYPE),
125
+ writeBeside: async (suffix, body) => void (await client.put(`${o.key}${suffix}`, body, JSON_TYPE)),
126
126
  removeBeside: (suffix) => client.remove(`${o.key}${suffix}`),
127
127
  besideLocation: (suffix) => `gs://${o.bucket}/${o.key}${suffix}`,
128
128
  adopt: () => {},