@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
@@ -0,0 +1,299 @@
1
+ /**
2
+ * The module docs extractor (chant#4598, terragucci#1289 bundle B): what a
3
+ * module's catalog page shows, read from the module's own directory.
4
+ *
5
+ * {@link moduleDocs} returns the README, the inputs (type, default,
6
+ * description, sensitivity, whether one is required), the outputs, the
7
+ * required providers and Terraform version, and the `examples/` tree. The
8
+ * structured fields always come from chant's own HCL parse
9
+ * (`parseTerraformRootDir`, ../hcl/parse.ts), `.tf` and `.tf.json` alike.
10
+ *
11
+ * When the module carries a `.terraform-docs.yml` (or `.terraform-docs.yaml`)
12
+ * and the caller passes a terraform-docs runner, the runner is called with
13
+ * that file and its markdown is kept as the page's body, since the module's
14
+ * author chose that layout. Without the file, without a runner, or when the
15
+ * runner finds no binary or fails, the page is built from the parse alone.
16
+ * `docsFrom` says which happened and `note` says why.
17
+ *
18
+ * {@link terraformCatalogFields} turns the docs into the release catalog's
19
+ * `terraform` fields (`@intentius/chant/lifecycle/release-catalog`), and
20
+ * {@link terraformCatalogSections} into the site's sections for a component
21
+ * and a version.
22
+ */
23
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import type { Hcl2Json } from "@intentius/chant/terraform/hcl2json";
26
+ import type { CatalogSiteSection, CatalogSiteSections } from "@intentius/chant/lifecycle/release-catalog-site";
27
+ import { OUTPUT_TYPE, TERRAFORM_TYPE, VARIABLE_TYPE, listTerraformFiles, parseTerraformRootDir, type TerraformEntity } from "../hcl/parse";
28
+
29
+ /** The terraform-docs config file names a module may carry, in the order they are looked for. */
30
+ export const TERRAFORM_DOCS_CONFIGS = [".terraform-docs.yml", ".terraform-docs.yaml"] as const;
31
+
32
+ /** One module input, a `variable` block. */
33
+ export interface ModuleInput {
34
+ name: string;
35
+ /** The type constraint as written, `string` or `map(object({...}))`, or null when none is declared. */
36
+ type: string | null;
37
+ /** True when the variable has no default. */
38
+ required: boolean;
39
+ /** The default as the parse reads it. Absent when required, and when the variable is sensitive. */
40
+ default?: unknown;
41
+ description: string | null;
42
+ sensitive: boolean;
43
+ /** The file that declares it. */
44
+ file: string;
45
+ }
46
+
47
+ /** One module output. */
48
+ export interface ModuleOutput {
49
+ name: string;
50
+ description: string | null;
51
+ sensitive: boolean;
52
+ file: string;
53
+ }
54
+
55
+ /** One entry of `required_providers`. */
56
+ export interface ModuleProvider {
57
+ name: string;
58
+ source: string | null;
59
+ version: string | null;
60
+ }
61
+
62
+ /** One directory under `examples/`. */
63
+ export interface ModuleExample {
64
+ name: string;
65
+ /** From the module's directory, `examples/<name>`. */
66
+ path: string;
67
+ readme: string | null;
68
+ /** Its configuration files, sorted. */
69
+ files: string[];
70
+ }
71
+
72
+ /** Everything a catalog page shows about one module version. */
73
+ export interface ModuleDocs {
74
+ readme: string | null;
75
+ inputs: ModuleInput[];
76
+ outputs: ModuleOutput[];
77
+ providers: ModuleProvider[];
78
+ requiredVersion: string | null;
79
+ examples: ModuleExample[];
80
+ /** `terraform-docs` when the body is terraform-docs's markdown, `hcl` when the page is built from the parse alone. */
81
+ docsFrom: "terraform-docs" | "hcl";
82
+ /** terraform-docs's output, when `docsFrom` is `terraform-docs`. */
83
+ markdown: string | null;
84
+ /** The terraform-docs config file found, from the module's directory. */
85
+ config: string | null;
86
+ /** Why the parse was used when a config file was found. */
87
+ note: string | null;
88
+ }
89
+
90
+ /**
91
+ * Runs terraform-docs in `dir` with `config` (a path from `dir`) and returns
92
+ * its markdown, or null when no terraform-docs binary is installed. Injected,
93
+ * so a test fakes it and a product chooses how the binary is found.
94
+ */
95
+ export type TerraformDocsRunner = (args: { dir: string; config: string }) => Promise<string | null>;
96
+
97
+ export interface ModuleDocsOptions {
98
+ /** The HCL parser; loaded on first use when absent. */
99
+ parser?: Hcl2Json;
100
+ /** Runs terraform-docs when the module has a config file. Without one the parse is used. */
101
+ terraformDocs?: TerraformDocsRunner;
102
+ /** The binary the module is written for (`terraform`, `tofu`), which decides the files read. */
103
+ binary?: string;
104
+ }
105
+
106
+ const README_NAMES = ["README.md", "readme.md", "Readme.md", "README"];
107
+
108
+ function readme(dir: string): string | null {
109
+ for (const name of README_NAMES) {
110
+ const p = join(dir, name);
111
+ if (existsSync(p) && statSync(p).isFile()) return readFileSync(p, "utf-8");
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /** `${string}` as hcl2json writes an expression, or `string` as a `.tf.json` file does: the expression either way. */
117
+ function expression(v: unknown): string | null {
118
+ if (typeof v !== "string") return null;
119
+ const m = /^\$\{([\s\S]*)\}$/.exec(v.trim());
120
+ return (m ? m[1]! : v).trim();
121
+ }
122
+
123
+ const text = (v: unknown): string | null => (typeof v === "string" ? v : null);
124
+
125
+ function firstBody(v: unknown): Record<string, unknown> | undefined {
126
+ const b = Array.isArray(v) ? v[0] : v;
127
+ return b && typeof b === "object" && !Array.isArray(b) ? (b as Record<string, unknown>) : undefined;
128
+ }
129
+
130
+ function providersOf(body: Record<string, unknown>): ModuleProvider[] {
131
+ const out: ModuleProvider[] = [];
132
+ const blocks = Array.isArray(body.required_providers) ? body.required_providers : body.required_providers ? [body.required_providers] : [];
133
+ for (const block of blocks) {
134
+ if (!block || typeof block !== "object") continue;
135
+ for (const [name, spec] of Object.entries(block as Record<string, unknown>)) {
136
+ // A bare string is the old `aws = "~> 4.0"` form: a version with no source.
137
+ if (typeof spec === "string") {
138
+ out.push({ name, source: null, version: spec });
139
+ continue;
140
+ }
141
+ const s = firstBody(spec) ?? {};
142
+ out.push({ name, source: text(s.source), version: text(s.version) });
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ function examples(dir: string, binary: string | undefined): ModuleExample[] {
149
+ const root = join(dir, "examples");
150
+ if (!existsSync(root) || !statSync(root).isDirectory()) return [];
151
+ return readdirSync(root, { withFileTypes: true })
152
+ .filter((d) => d.isDirectory())
153
+ .map((d) => d.name)
154
+ .sort()
155
+ .map((name) => {
156
+ const path = join(root, name);
157
+ return { name, path: `examples/${name}`, readme: readme(path), files: [...listTerraformFiles(path, binary)].sort() };
158
+ });
159
+ }
160
+
161
+ async function fromTerraformDocs(dir: string, runner: TerraformDocsRunner | undefined): Promise<Pick<ModuleDocs, "docsFrom" | "markdown" | "config" | "note">> {
162
+ const config = TERRAFORM_DOCS_CONFIGS.find((name) => existsSync(join(dir, name))) ?? null;
163
+ if (!config) return { docsFrom: "hcl", markdown: null, config: null, note: null };
164
+ if (!runner) return { docsFrom: "hcl", markdown: null, config, note: `${config} found, and no terraform-docs runner was given` };
165
+ try {
166
+ const markdown = await runner({ dir, config });
167
+ if (markdown === null) return { docsFrom: "hcl", markdown: null, config, note: `${config} found, and no terraform-docs binary is installed` };
168
+ return { docsFrom: "terraform-docs", markdown, config, note: null };
169
+ } catch (e) {
170
+ return { docsFrom: "hcl", markdown: null, config, note: `terraform-docs failed with ${config}: ${(e as Error).message.split("\n", 1)[0]}` };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Read one module's docs from `dir`. Throws what the HCL parse throws for a
176
+ * file it cannot read; a terraform-docs failure is never thrown, it falls
177
+ * back to the parse and says so in `note`.
178
+ */
179
+ export async function moduleDocs(dir: string, options: ModuleDocsOptions = {}): Promise<ModuleDocs> {
180
+ const entities = [...(await parseTerraformRootDir(dir, "module", options.parser, options.binary ? { binary: options.binary } : undefined)).values()] as TerraformEntity[];
181
+ const inputs: ModuleInput[] = [];
182
+ const outputs: ModuleOutput[] = [];
183
+ const providers: ModuleProvider[] = [];
184
+ let requiredVersion: string | null = null;
185
+ for (const e of entities) {
186
+ const { address, body, file } = e.props;
187
+ if (e.entityType === VARIABLE_TYPE) {
188
+ const sensitive = body.sensitive === true;
189
+ const required = !("default" in body);
190
+ inputs.push({
191
+ name: address.slice("var.".length),
192
+ type: expression(body.type),
193
+ required,
194
+ ...(!required && !sensitive ? { default: body.default } : {}),
195
+ description: text(body.description),
196
+ sensitive,
197
+ file,
198
+ });
199
+ } else if (e.entityType === OUTPUT_TYPE) {
200
+ outputs.push({ name: address.slice("output.".length), description: text(body.description), sensitive: body.sensitive === true, file });
201
+ } else if (e.entityType === TERRAFORM_TYPE) {
202
+ providers.push(...providersOf(body));
203
+ requiredVersion ??= text(body.required_version);
204
+ }
205
+ }
206
+ inputs.sort((a, b) => a.name.localeCompare(b.name));
207
+ outputs.sort((a, b) => a.name.localeCompare(b.name));
208
+ providers.sort((a, b) => a.name.localeCompare(b.name));
209
+ return {
210
+ readme: readme(dir),
211
+ inputs,
212
+ outputs,
213
+ providers,
214
+ requiredVersion,
215
+ examples: examples(dir, options.binary),
216
+ ...(await fromTerraformDocs(dir, options.terraformDocs)),
217
+ };
218
+ }
219
+
220
+ /**
221
+ * The release catalog's `terraform` fields for a module version: its
222
+ * address (`<host>/<namespace>/<name>/<system>`), the docs' structured
223
+ * parts, and the markdown when terraform-docs wrote it. Everything here is
224
+ * JSON, as the catalog's schema requires.
225
+ */
226
+ export function terraformCatalogFields(docs: ModuleDocs, address: string): Record<string, unknown> {
227
+ return {
228
+ address,
229
+ inputs: docs.inputs.map(({ file: _file, ...i }) => i),
230
+ outputs: docs.outputs.map(({ file: _file, ...o }) => o),
231
+ providers: docs.providers,
232
+ requiredVersion: docs.requiredVersion,
233
+ examples: docs.examples.map((x) => ({ name: x.name, path: x.path, files: x.files })),
234
+ docsFrom: docs.docsFrom,
235
+ ...(docs.readme !== null ? { readme: docs.readme } : {}),
236
+ ...(docs.markdown !== null ? { markdown: docs.markdown } : {}),
237
+ };
238
+ }
239
+
240
+ /** The `terraform` fields as the site reads them back from the catalog. */
241
+ interface TerraformFields {
242
+ address?: string;
243
+ inputs?: Array<Omit<ModuleInput, "file">>;
244
+ outputs?: Array<Omit<ModuleOutput, "file">>;
245
+ providers?: ModuleProvider[];
246
+ requiredVersion?: string | null;
247
+ examples?: Array<{ name: string; path: string }>;
248
+ readme?: string;
249
+ markdown?: string;
250
+ }
251
+
252
+ const show = (v: unknown): string => (v === undefined ? "" : typeof v === "string" ? JSON.stringify(v) : JSON.stringify(v) ?? "");
253
+
254
+ function versionSections(f: TerraformFields, version: string | null): CatalogSiteSection[] {
255
+ const out: CatalogSiteSection[] = [];
256
+ if (f.address) {
257
+ const pin = version ? `\n version = "${version}"` : "";
258
+ out.push({ title: "Usage", body: { kind: "code", text: `module "${f.address.split("/").at(-2) ?? "this"}" {\n source = "${f.address}"${pin}\n}` } });
259
+ }
260
+ if (f.inputs?.length) {
261
+ out.push({
262
+ title: "Inputs",
263
+ body: { kind: "table", columns: ["Name", "Type", "Default", "Required", "Description"], rows: f.inputs.map((i) => [i.name, i.type ?? "", i.sensitive ? "(sensitive)" : i.required ? "" : show(i.default), i.required ? "yes" : "no", i.description ?? ""]) },
264
+ });
265
+ }
266
+ if (f.outputs?.length) {
267
+ out.push({ title: "Outputs", body: { kind: "table", columns: ["Name", "Description", "Sensitive"], rows: f.outputs.map((o) => [o.name, o.description ?? "", o.sensitive ? "yes" : "no"]) } });
268
+ }
269
+ if (f.providers?.length || f.requiredVersion) {
270
+ const rows = (f.providers ?? []).map((p) => [p.name, p.source ?? "", p.version ?? ""]);
271
+ if (f.requiredVersion) rows.unshift(["terraform", "", f.requiredVersion]);
272
+ out.push({ title: "Requirements", body: { kind: "table", columns: ["Name", "Source", "Version"], rows } });
273
+ }
274
+ if (f.examples?.length) out.push({ title: "Examples", body: { kind: "table", columns: ["Example", "Path"], rows: f.examples.map((x) => [x.name, x.path]) } });
275
+ // terraform-docs's markdown is shown as text: the site renders no markdown, and escaping keeps a module's README from writing HTML into the page.
276
+ const body = f.markdown ?? f.readme;
277
+ if (body) out.push({ title: f.markdown ? "Documentation" : "README", body: { kind: "code", text: body } });
278
+ return out;
279
+ }
280
+
281
+ /**
282
+ * The site sections (`renderCatalogSite`'s `sections`) for Terraform
283
+ * modules: usage, inputs, outputs, requirements, examples and the README or
284
+ * terraform-docs's markdown on each version page, read from the version's
285
+ * `terraform` fields, and the usage line on the component page from the
286
+ * component's. A component without `terraform` fields gets none.
287
+ */
288
+ export function terraformCatalogSections(): CatalogSiteSections {
289
+ return {
290
+ component: (c) => {
291
+ const f = c.fields.terraform as TerraformFields | undefined;
292
+ return f?.address ? versionSections({ address: f.address }, c.latest && !c.latest.includes(":") ? c.latest : null) : [];
293
+ },
294
+ version: (_c, v) => {
295
+ const f = v.fields.terraform as TerraformFields | undefined;
296
+ return f ? versionSections(f, v.version) : [];
297
+ },
298
+ };
299
+ }
@@ -0,0 +1,50 @@
1
+ // registry/edge bundles for an edge runtime with no Node built-ins (chant#4589): a Cloudflare Worker
2
+ // without nodejs_compat cannot load node:fs or node:crypto, so a browser-platform bundle of the edge
3
+ // entry must reach none. The registry writer once pulled core's object-store clients in through
4
+ // StoreConflict; it now imports @intentius/chant/object-store/errors, which imports nothing.
5
+ import { builtinModules } from "node:module";
6
+ import { fileURLToPath } from "node:url";
7
+ import { build, type Plugin } from "esbuild";
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ const entry = fileURLToPath(new URL("./index.ts", import.meta.url));
11
+
12
+ const builtins = new Set(builtinModules.flatMap((m) => [m, `node:${m}`]));
13
+
14
+ /** Records each Node built-in an import names, with the file that imports it, and keeps it out of the bundle. */
15
+ function nodeImports(found: string[]): Plugin {
16
+ return {
17
+ name: "node-imports",
18
+ setup(b) {
19
+ b.onResolve({ filter: /.*/ }, (args) => {
20
+ const bare = args.path.split("/")[0]!;
21
+ if (!args.path.startsWith("node:") && !builtins.has(args.path) && !builtins.has(bare)) return undefined;
22
+ found.push(`${args.path} (from ${args.importer})`);
23
+ return { path: args.path, external: true };
24
+ });
25
+ },
26
+ };
27
+ }
28
+
29
+ describe("registry/edge bundle", () => {
30
+ it("bundles for platform browser with no node: import", async () => {
31
+ const found: string[] = [];
32
+ const result = await build({
33
+ entryPoints: [entry],
34
+ bundle: true,
35
+ write: false,
36
+ format: "esm",
37
+ platform: "browser",
38
+ target: "es2022",
39
+ conditions: ["import", "default"],
40
+ metafile: true,
41
+ logLevel: "silent",
42
+ plugins: [nodeImports(found)],
43
+ });
44
+ expect(result.errors).toEqual([]);
45
+ expect(found).toEqual([]);
46
+ // The object-store clients stay out: only the errors module of core's object store is reached.
47
+ const store = Object.keys(result.metafile.inputs).filter((p) => /object-store\//.test(p));
48
+ expect(store.every((p) => /object-store\/errors\.ts$/.test(p))).toBe(true);
49
+ }, 60_000);
50
+ });
@@ -0,0 +1,176 @@
1
+ // The edge function for bucket registries (chant#4566), tested as a handler in process: no CDN, no deploy.
2
+ // The reader's `reads: "terraform"` and `reads: "tofu"` stand in for the two CLIs' download reading, and
3
+ // its token lookup for the CLIs' TF_TOKEN_<host> (chant#4563).
4
+ import { gzipSync } from "node:zlib";
5
+ import { describe, expect, it } from "vitest";
6
+ import { downloadDoc, fetchTarball, handlerFetch, headersFile, readHeadersFile, registryLocation, registryVersions, type RegistryFetch } from "../index";
7
+ import { lambdaEdgeHandler, registryEdge, registryWorkerFetch, signLocation, tokensFromSecret, verifySignedUrl, type CloudFrontRequestEvent, type RegistryEdgeOptions } from "./index";
8
+
9
+ const HOST = "modules.test";
10
+ const MOD = `${HOST}/acme/net/generic`;
11
+ const DIR = "v1/modules/acme/net/generic";
12
+ const TARBALL = `https://${HOST}/${DIR}/0.1.0/net-0.1.0.tar.gz`;
13
+
14
+ /** A bucket's objects, as an edge function reads them: no headers per object. */
15
+ function bucket(prefix = ""): Map<string, string | Uint8Array> {
16
+ const p = prefix ? `${prefix}/` : "";
17
+ return new Map<string, string | Uint8Array>([
18
+ [`${p}.well-known/terraform.json`, '{"modules.v1":"/v1/modules/"}\n'],
19
+ [`${p}${DIR}/versions`, JSON.stringify({ modules: [{ versions: [{ version: "0.1.0" }] }] })],
20
+ [`${p}${DIR}/0.1.0/download`, downloadDoc(TARBALL)],
21
+ [`${p}${DIR}/0.1.0/net-0.1.0.tar.gz`, new Uint8Array(gzipSync(Buffer.from("module bytes")))],
22
+ [`${p}${DIR}/0.2.0/download`, downloadDoc("git::https://git.test/acme/net.git?ref=v0.2.0")],
23
+ ]);
24
+ }
25
+
26
+ const files = (objects: Map<string, string | Uint8Array>): RegistryEdgeOptions["files"] => async (key) => ({ body: objects.get(key) });
27
+
28
+ const edgeFetch = (options: Partial<RegistryEdgeOptions> & { objects?: Map<string, string | Uint8Array> } = {}): RegistryFetch => handlerFetch(registryEdge({ files: files(options.objects ?? bucket()), ...options }));
29
+
30
+ const noCredentials = { env: {}, readFile: () => undefined };
31
+ const withToken = (token: string) => ({ env: { TF_TOKEN_modules_test: token }, readFile: () => undefined });
32
+
33
+ describe("auth off (the default)", () => {
34
+ it("adds X-Terraform-Get, so both CLIs' readings find the plain tarball URL with no token", async () => {
35
+ const fetch = edgeFetch();
36
+ expect(await registryVersions(MOD, { fetch, credentials: noCredentials })).toEqual(["0.1.0"]);
37
+ expect(await registryLocation(MOD, "0.1.0", { fetch, credentials: noCredentials, reads: "terraform" })).toBe(TARBALL);
38
+ expect(await registryLocation(MOD, "0.1.0", { fetch, credentials: noCredentials, reads: "tofu" })).toBe(TARBALL);
39
+ expect((await fetchTarball(TARBALL, { fetch, credentials: noCredentials })).toString()).toBe("module bytes");
40
+ });
41
+
42
+ it("leaves a git location as stored", async () => {
43
+ expect(await registryLocation(MOD, "0.2.0", { fetch: edgeFetch(), credentials: noCredentials, reads: "terraform" })).toBe("git::https://git.test/acme/net.git?ref=v0.2.0");
44
+ });
45
+
46
+ it("reads the registry under a prefix in the bucket", async () => {
47
+ const fetch = edgeFetch({ objects: bucket("registry"), prefix: "registry" });
48
+ expect(await registryLocation(MOD, "0.1.0", { fetch, credentials: noCredentials, reads: "terraform" })).toBe(TARBALL);
49
+ });
50
+ });
51
+
52
+ describe("auth on", () => {
53
+ const auth = { tokens: ["old-token", "s3cret"], signingKey: "sign-key", ttl: 300 };
54
+ const now = 1_800_000_000_000;
55
+
56
+ it("refuses discovery, versions and download without the token, with 401", async () => {
57
+ const log: string[] = [];
58
+ const fetch = edgeFetch({ auth, now: () => now, log: (l) => log.push(l) });
59
+ await expect(registryVersions(MOD, { fetch, credentials: noCredentials })).rejects.toThrow("answered 401");
60
+ await expect(registryVersions(MOD, { fetch, token: "wrong" })).rejects.toThrow("refused the token");
61
+ for (const path of [".well-known/terraform.json", `${DIR}/versions`, `${DIR}/0.1.0/download`, `${DIR}/0.1.0/net-0.1.0.tar.gz`]) {
62
+ const res = await fetch(`https://${HOST}/${path}`);
63
+ expect(res.status, path).toBe(401);
64
+ expect(res.headers.get("www-authenticate")).toContain("Bearer");
65
+ }
66
+ expect(log.every((l) => !l.includes("s3cret"))).toBe(true);
67
+ });
68
+
69
+ it("answers with the token, under both readings, and accepts either token while one rotates", async () => {
70
+ const fetch = edgeFetch({ auth, now: () => now });
71
+ expect(await registryVersions(MOD, { fetch, credentials: withToken("s3cret") })).toEqual(["0.1.0"]);
72
+ expect(await registryVersions(MOD, { fetch, credentials: withToken("old-token") })).toEqual(["0.1.0"]);
73
+ const terraform = await registryLocation(MOD, "0.1.0", { fetch, credentials: withToken("s3cret"), reads: "terraform" });
74
+ const tofu = await registryLocation(MOD, "0.1.0", { fetch, credentials: withToken("s3cret"), reads: "tofu" });
75
+ expect(terraform).toBe(tofu);
76
+ expect(terraform).not.toBe(TARBALL);
77
+ expect(terraform.startsWith(`${TARBALL}?expires=`)).toBe(true);
78
+ });
79
+
80
+ it("serves the tarball through the signed URL with no token, until it expires", async () => {
81
+ let clock = now;
82
+ const fetch = edgeFetch({ auth, now: () => clock });
83
+ const signed = await registryLocation(MOD, "0.1.0", { fetch, credentials: withToken("s3cret"), reads: "terraform" });
84
+ // go-getter sends no token for the tarball.
85
+ expect((await fetchTarball(signed, { fetch, token: false })).toString()).toBe("module bytes");
86
+ expect((await fetch(signed, { method: "HEAD" })).status).toBe(200);
87
+ clock = now + 301_000;
88
+ expect((await fetch(signed)).status).toBe(401);
89
+ });
90
+
91
+ it("refuses a signature moved to another file or tampered with", async () => {
92
+ const signed = new URL(await signLocation(TARBALL, "sign-key", now, 300));
93
+ expect(await verifySignedUrl(signed, "sign-key", now)).toBe(true);
94
+ expect(await verifySignedUrl(signed, "other-key", now)).toBe(false);
95
+ const moved = new URL(signed.toString().replace("net-0.1.0", "net-0.2.0"));
96
+ expect(await verifySignedUrl(moved, "sign-key", now)).toBe(false);
97
+ const extended = new URL(signed);
98
+ extended.searchParams.set("expires", String(Number(extended.searchParams.get("expires")) + 3600));
99
+ expect(await verifySignedUrl(extended, "sign-key", now)).toBe(false);
100
+ // A signed URL opens files, never the protocol answers.
101
+ const fetch = edgeFetch({ auth, now: () => now });
102
+ const sneaky = new URL(`https://${HOST}/${DIR}/versions`);
103
+ sneaky.search = signed.search;
104
+ expect((await fetch(sneaky.toString())).status).toBe(401);
105
+ });
106
+
107
+ it("signs with the first token when no signing key is set", async () => {
108
+ const fetch = edgeFetch({ auth: { tokens: ["s3cret"] }, now: () => now });
109
+ const signed = await registryLocation(MOD, "0.1.0", { fetch, credentials: withToken("s3cret"), reads: "terraform" });
110
+ expect(await verifySignedUrl(new URL(signed), "s3cret", now)).toBe(true);
111
+ });
112
+ });
113
+
114
+ describe("Cloudflare Worker", () => {
115
+ const r2 = (objects: Map<string, string | Uint8Array>) => ({
116
+ get: async (key: string) => {
117
+ const body = objects.get(key);
118
+ if (body === undefined) return null;
119
+ const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
120
+ return { arrayBuffer: async () => bytes.slice().buffer };
121
+ },
122
+ });
123
+
124
+ it("leaves auth off unless REGISTRY_AUTH is token", async () => {
125
+ const res = await registryWorkerFetch(new Request(`https://${HOST}/${DIR}/0.1.0/download`), { REGISTRY_BUCKET: r2(bucket()) });
126
+ expect(res.status).toBe(200);
127
+ expect(res.headers.get("x-terraform-get")).toBe(TARBALL);
128
+ });
129
+
130
+ it("checks the token from REGISTRY_TOKENS when REGISTRY_AUTH is token", async () => {
131
+ const env = { REGISTRY_BUCKET: r2(bucket()), REGISTRY_AUTH: "token", REGISTRY_TOKENS: "a\ns3cret" };
132
+ expect((await registryWorkerFetch(new Request(`https://${HOST}/${DIR}/versions`), env)).status).toBe(401);
133
+ expect((await registryWorkerFetch(new Request(`https://${HOST}/${DIR}/versions`, { headers: { authorization: "Bearer s3cret" } }), env)).status).toBe(200);
134
+ expect(tokensFromSecret(" a, b \n\nc ")).toEqual(["a", "b", "c"]);
135
+ });
136
+
137
+ it("says what is missing without a bucket", async () => {
138
+ expect((await registryWorkerFetch(new Request(`https://${HOST}/x`), {})).status).toBe(500);
139
+ });
140
+ });
141
+
142
+ describe("Lambda@Edge", () => {
143
+ const event = (uri: string, headers: Record<string, string> = {}, querystring = ""): CloudFrontRequestEvent => ({
144
+ Records: [{ cf: { request: { uri, querystring, method: "GET", headers: { host: [{ key: "Host", value: HOST }], ...Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), [{ key: k, value: v }]])) } } } }],
145
+ });
146
+
147
+ it("answers a download itself and passes other files on to the bucket", async () => {
148
+ const handler = lambdaEdgeHandler({ files: files(bucket()) });
149
+ const answer = await handler(event(`/${DIR}/0.1.0/download`));
150
+ expect("status" in answer && answer.status).toBe("200");
151
+ expect("status" in answer && answer.headers["x-terraform-get"]?.[0]?.value).toBe(TARBALL);
152
+ const passed = await handler(event(`/${DIR}/versions`));
153
+ expect("uri" in passed && passed.uri).toBe(`/${DIR}/versions`);
154
+ });
155
+
156
+ it("refuses without the token and drops the signature before the bucket", async () => {
157
+ const now = 1_800_000_000_000;
158
+ const handler = lambdaEdgeHandler({ files: files(bucket()), auth: { tokens: ["s3cret"] }, now: () => now });
159
+ const refused = await handler(event(`/${DIR}/versions`));
160
+ expect("status" in refused && refused.status).toBe("401");
161
+ const signed = new URL(await signLocation(TARBALL, "s3cret", now));
162
+ const passed = await handler(event(signed.pathname, {}, signed.search.slice(1)));
163
+ expect("uri" in passed && passed.querystring).toBe("");
164
+ });
165
+ });
166
+
167
+ describe("_headers for Cloudflare Pages and Netlify", () => {
168
+ it("writes a rule per download and keeps the site's other rules", () => {
169
+ const existing = "/*\n X-Frame-Options: DENY\n\n/v1/modules/acme/net/generic/0.1.0/download\n X-Terraform-Get: https://old.test/x.tar.gz\n";
170
+ const text = headersFile([{ path: `${DIR}/0.1.0/download`, location: TARBALL }, { path: `/${DIR}/0.0.9/download`, location: "https://modules.test/y.tar.gz" }], existing);
171
+ expect(text).toContain("/*\n X-Frame-Options: DENY");
172
+ expect(text).toContain(`/${DIR}/0.1.0/download\n X-Terraform-Get: ${TARBALL}`);
173
+ expect(text).not.toContain("old.test");
174
+ expect(readHeadersFile(text).downloads.map((r) => r.path)).toEqual([`/${DIR}/0.0.9/download`, `/${DIR}/0.1.0/download`]);
175
+ });
176
+ });