@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.
- package/README.md +7 -4
- package/dist/ci-setup.d.ts +11 -0
- package/dist/ci-setup.d.ts.map +1 -0
- package/dist/ci-setup.js +41 -0
- package/dist/components/capability-plugin.d.ts +40 -6
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +174 -30
- package/dist/components/compose-group.d.ts +1 -1
- package/dist/components/compose-group.d.ts.map +1 -1
- package/dist/components/compose-group.js +1 -1
- package/dist/components/release-inputs.d.ts +29 -0
- package/dist/components/release-inputs.d.ts.map +1 -0
- package/dist/components/release-inputs.js +57 -0
- package/dist/components/trace-facts.d.ts +16 -0
- package/dist/components/trace-facts.d.ts.map +1 -0
- package/dist/components/trace-facts.js +19 -0
- package/dist/cone.d.ts +53 -0
- package/dist/cone.d.ts.map +1 -0
- package/dist/cone.js +119 -0
- package/dist/cost-hold.d.ts +27 -0
- package/dist/cost-hold.d.ts.map +1 -0
- package/dist/cost-hold.js +21 -0
- package/dist/drift-rows.d.ts +61 -0
- package/dist/drift-rows.d.ts.map +1 -0
- package/dist/drift-rows.js +108 -0
- package/dist/exit-codes.d.ts +55 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/integrity.json +7 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-catalog.js +27 -3
- package/dist/lint/audit-lineage.d.ts +14 -2
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/index.js +4 -0
- package/dist/lint/post-synth/record-store.d.ts +54 -0
- package/dist/lint/post-synth/record-store.d.ts.map +1 -0
- package/dist/lint/post-synth/record-store.js +104 -0
- package/dist/lint/post-synth/tf001.d.ts +2 -2
- package/dist/lint/post-synth/tf001.js +2 -2
- package/dist/lint/post-synth/tf024.d.ts +35 -11
- package/dist/lint/post-synth/tf024.d.ts.map +1 -1
- package/dist/lint/post-synth/tf024.js +92 -20
- package/dist/lint/post-synth/tf046.d.ts +64 -0
- package/dist/lint/post-synth/tf046.d.ts.map +1 -0
- package/dist/lint/post-synth/tf046.js +175 -0
- package/dist/lint/post-synth/tf047.d.ts +36 -0
- package/dist/lint/post-synth/tf047.d.ts.map +1 -0
- package/dist/lint/post-synth/tf047.js +94 -0
- package/dist/lock-rows.d.ts +36 -0
- package/dist/lock-rows.d.ts.map +1 -0
- package/dist/lock-rows.js +60 -0
- package/dist/lock-timeout.d.ts +22 -0
- package/dist/lock-timeout.d.ts.map +1 -0
- package/dist/lock-timeout.js +32 -0
- package/dist/manifest.json +1 -1
- package/dist/module-catalog/index.d.ts +69 -0
- package/dist/module-catalog/index.d.ts.map +1 -0
- package/dist/module-catalog/index.js +110 -0
- package/dist/module-catalog/refresh-cli.d.ts +4 -0
- package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
- package/dist/module-catalog/refresh-cli.js +64 -0
- package/dist/module-catalog/static-tier.d.ts +6 -0
- package/dist/module-catalog/static-tier.d.ts.map +1 -0
- package/dist/module-catalog/static-tier.js +1650 -0
- package/dist/module-catalog/types.d.ts +65 -0
- package/dist/module-catalog/types.d.ts.map +1 -0
- package/dist/module-catalog/types.js +33 -0
- package/dist/okf/index.md +3 -1
- package/dist/okf/rules/TF024.md +2 -2
- package/dist/okf/rules/TF046.md +11 -0
- package/dist/okf/rules/TF047.md +11 -0
- package/dist/op/activities/terraform.d.ts +29 -0
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +19 -4
- package/dist/pin/consumers.d.ts +35 -0
- package/dist/pin/consumers.d.ts.map +1 -0
- package/dist/pin/consumers.js +70 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +5 -0
- package/dist/registry/address.d.ts +63 -0
- package/dist/registry/address.d.ts.map +1 -0
- package/dist/registry/address.js +65 -0
- package/dist/registry/catalog-read.d.ts +14 -0
- package/dist/registry/catalog-read.d.ts.map +1 -0
- package/dist/registry/catalog-read.js +58 -0
- package/dist/registry/deprecated.d.ts +37 -0
- package/dist/registry/deprecated.d.ts.map +1 -0
- package/dist/registry/deprecated.js +43 -0
- package/dist/registry/docs.d.ts +96 -0
- package/dist/registry/docs.d.ts.map +1 -0
- package/dist/registry/docs.js +210 -0
- package/dist/registry/edge/edge.d.ts +46 -0
- package/dist/registry/edge/edge.d.ts.map +1 -0
- package/dist/registry/edge/edge.js +194 -0
- package/dist/registry/edge/index.d.ts +11 -0
- package/dist/registry/edge/index.d.ts.map +1 -0
- package/dist/registry/edge/index.js +10 -0
- package/dist/registry/edge/lambda-edge.d.ts +76 -0
- package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
- package/dist/registry/edge/lambda-edge.js +95 -0
- package/dist/registry/edge/sigv4-web.d.ts +33 -0
- package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
- package/dist/registry/edge/sigv4-web.js +56 -0
- package/dist/registry/edge/token-source.d.ts +73 -0
- package/dist/registry/edge/token-source.d.ts.map +1 -0
- package/dist/registry/edge/token-source.js +115 -0
- package/dist/registry/edge/worker.d.ts +45 -0
- package/dist/registry/edge/worker.d.ts.map +1 -0
- package/dist/registry/edge/worker.js +26 -0
- package/dist/registry/handler.d.ts +25 -0
- package/dist/registry/handler.d.ts.map +1 -0
- package/dist/registry/handler.js +75 -0
- package/dist/registry/headers-file.d.ts +21 -0
- package/dist/registry/headers-file.d.ts.map +1 -0
- package/dist/registry/headers-file.js +57 -0
- package/dist/registry/index.d.ts +31 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +30 -0
- package/dist/registry/mirror-paths.d.ts +9 -0
- package/dist/registry/mirror-paths.d.ts.map +1 -0
- package/dist/registry/mirror-paths.js +8 -0
- package/dist/registry/mirror.d.ts +130 -0
- package/dist/registry/mirror.d.ts.map +1 -0
- package/dist/registry/mirror.js +319 -0
- package/dist/registry/proxies.d.ts +73 -0
- package/dist/registry/proxies.d.ts.map +1 -0
- package/dist/registry/proxies.js +111 -0
- package/dist/registry/reader.d.ts +57 -0
- package/dist/registry/reader.d.ts.map +1 -0
- package/dist/registry/reader.js +198 -0
- package/dist/registry/semver.d.ts +16 -0
- package/dist/registry/semver.d.ts.map +1 -0
- package/dist/registry/semver.js +24 -0
- package/dist/registry/serve.d.ts +24 -0
- package/dist/registry/serve.d.ts.map +1 -0
- package/dist/registry/serve.js +64 -0
- package/dist/registry/source-forms.d.ts +79 -0
- package/dist/registry/source-forms.d.ts.map +1 -0
- package/dist/registry/source-forms.js +202 -0
- package/dist/registry/targets/artifactory.d.ts +28 -0
- package/dist/registry/targets/artifactory.d.ts.map +1 -0
- package/dist/registry/targets/artifactory.js +206 -0
- package/dist/registry/targets/fake-artifactory.d.ts +30 -0
- package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
- package/dist/registry/targets/fake-artifactory.js +142 -0
- package/dist/registry/targets/fake-gitlab.d.ts +39 -0
- package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
- package/dist/registry/targets/fake-gitlab.js +136 -0
- package/dist/registry/targets/fake-hcp.d.ts +50 -0
- package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
- package/dist/registry/targets/fake-hcp.js +202 -0
- package/dist/registry/targets/fake-http.d.ts +31 -0
- package/dist/registry/targets/fake-http.d.ts.map +1 -0
- package/dist/registry/targets/fake-http.js +87 -0
- package/dist/registry/targets/fake-nexus.d.ts +27 -0
- package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
- package/dist/registry/targets/fake-nexus.js +137 -0
- package/dist/registry/targets/gitlab.d.ts +53 -0
- package/dist/registry/targets/gitlab.d.ts.map +1 -0
- package/dist/registry/targets/gitlab.js +187 -0
- package/dist/registry/targets/hcp.d.ts +29 -0
- package/dist/registry/targets/hcp.d.ts.map +1 -0
- package/dist/registry/targets/hcp.js +310 -0
- package/dist/registry/targets/hosted.d.ts +62 -0
- package/dist/registry/targets/hosted.d.ts.map +1 -0
- package/dist/registry/targets/hosted.js +74 -0
- package/dist/registry/targets/index.d.ts +18 -0
- package/dist/registry/targets/index.d.ts.map +1 -0
- package/dist/registry/targets/index.js +17 -0
- package/dist/registry/targets/module-target.d.ts +68 -0
- package/dist/registry/targets/module-target.d.ts.map +1 -0
- package/dist/registry/targets/module-target.js +63 -0
- package/dist/registry/targets/nexus.d.ts +43 -0
- package/dist/registry/targets/nexus.d.ts.map +1 -0
- package/dist/registry/targets/nexus.js +250 -0
- package/dist/registry/targets/zip.d.ts +55 -0
- package/dist/registry/targets/zip.d.ts.map +1 -0
- package/dist/registry/targets/zip.js +278 -0
- package/dist/registry/writer.d.ts +101 -0
- package/dist/registry/writer.d.ts.map +1 -0
- package/dist/registry/writer.js +109 -0
- package/dist/rules/record-store.ts +122 -0
- package/dist/rules/tf001.ts +2 -2
- package/dist/rules/tf024.ts +105 -22
- package/dist/rules/tf046.ts +204 -0
- package/dist/rules/tf047.ts +114 -0
- package/dist/state/azure-blob.d.ts +8 -67
- package/dist/state/azure-blob.d.ts.map +1 -1
- package/dist/state/azure-blob.js +6 -199
- package/dist/state/gcs.d.ts +7 -61
- package/dist/state/gcs.d.ts.map +1 -1
- package/dist/state/gcs.js +5 -222
- package/dist/state/object-store.d.ts +4 -33
- package/dist/state/object-store.d.ts.map +1 -1
- package/dist/state/object-store.js +4 -17
- package/dist/state/s3.d.ts +11 -58
- package/dist/state/s3.d.ts.map +1 -1
- package/dist/state/s3.js +10 -161
- package/dist/state/store.js +2 -2
- package/dist/state-ops.d.ts +255 -0
- package/dist/state-ops.d.ts.map +1 -0
- package/dist/state-ops.js +305 -0
- package/dist/state-reads.d.ts +57 -0
- package/dist/state-reads.d.ts.map +1 -0
- package/dist/state-reads.js +104 -0
- package/dist/used-sources.d.ts +54 -0
- package/dist/used-sources.d.ts.map +1 -0
- package/dist/used-sources.js +157 -0
- package/package.json +19 -3
- package/src/__fixtures__/drift-record/README.md +5 -0
- package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
- package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
- package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
- package/src/__fixtures__/used-sources/root/main.tf +31 -0
- package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
- package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
- package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
- package/src/ci-setup.ts +42 -0
- package/src/components/capability-plugin.test.ts +26 -14
- package/src/components/capability-plugin.ts +185 -31
- package/src/components/compose-group.ts +1 -1
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
- package/src/components/pr-loop.tofu.test.ts +1 -1
- package/src/components/release-inputs.ts +69 -0
- package/src/components/trace-facts.test.ts +21 -0
- package/src/components/trace-facts.ts +27 -0
- package/src/cone.ts +138 -0
- package/src/cost-hold.ts +29 -0
- package/src/describe-resources.engine-facts.test.ts +2 -3
- package/src/drift-rows.test.ts +135 -0
- package/src/drift-rows.ts +137 -0
- package/src/exit-codes.test.ts +23 -0
- package/src/exit-codes.ts +59 -0
- package/src/index.ts +3 -0
- package/src/lint/audit-catalog.ts +29 -3
- package/src/lint/audit-lineage.ts +14 -2
- package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
- package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
- package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
- package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
- package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/record-store.ts +122 -0
- package/src/lint/post-synth/tf001.ts +2 -2
- package/src/lint/post-synth/tf024.test.ts +26 -5
- package/src/lint/post-synth/tf024.ts +105 -22
- package/src/lint/post-synth/tf046.test.ts +137 -0
- package/src/lint/post-synth/tf046.ts +204 -0
- package/src/lint/post-synth/tf047.test.ts +56 -0
- package/src/lint/post-synth/tf047.ts +114 -0
- package/src/lock-rows.test.ts +70 -0
- package/src/lock-rows.ts +82 -0
- package/src/lock-timeout.ts +34 -0
- package/src/module-catalog/index.test.ts +93 -0
- package/src/module-catalog/index.ts +159 -0
- package/src/module-catalog/refresh-cli.ts +68 -0
- package/src/module-catalog/static-tier.ts +1659 -0
- package/src/module-catalog/types.ts +88 -0
- package/src/op/activities/terraform.ts +46 -5
- package/src/op-wave-step.test.ts +109 -0
- package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
- package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
- package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
- package/src/pin/consumers.test.ts +57 -0
- package/src/pin/consumers.ts +95 -0
- package/src/plugin.ts +6 -0
- package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
- package/src/registry/__fixtures__/docs/documented/README.md +1 -0
- package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
- package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
- package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
- package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
- package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
- package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
- package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
- package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
- package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
- package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
- package/src/registry/address.ts +100 -0
- package/src/registry/catalog-read.ts +56 -0
- package/src/registry/deprecated.test.ts +139 -0
- package/src/registry/deprecated.ts +76 -0
- package/src/registry/docs.test.ts +140 -0
- package/src/registry/docs.ts +299 -0
- package/src/registry/edge/bundle.test.ts +50 -0
- package/src/registry/edge/edge.test.ts +176 -0
- package/src/registry/edge/edge.ts +211 -0
- package/src/registry/edge/index.ts +10 -0
- package/src/registry/edge/lambda-edge.ts +127 -0
- package/src/registry/edge/sigv4-web.ts +79 -0
- package/src/registry/edge/token-source.test.ts +258 -0
- package/src/registry/edge/token-source.ts +169 -0
- package/src/registry/edge/worker.ts +61 -0
- package/src/registry/handler.ts +89 -0
- package/src/registry/headers-file.ts +61 -0
- package/src/registry/index.ts +31 -0
- package/src/registry/mirror-paths.ts +10 -0
- package/src/registry/mirror.test.ts +198 -0
- package/src/registry/mirror.ts +395 -0
- package/src/registry/proxies.test.ts +153 -0
- package/src/registry/proxies.ts +132 -0
- package/src/registry/reader.ts +225 -0
- package/src/registry/registry.test.ts +262 -0
- package/src/registry/semver.ts +34 -0
- package/src/registry/serve.ts +82 -0
- package/src/registry/source-forms.test.ts +87 -0
- package/src/registry/source-forms.ts +216 -0
- package/src/registry/targets/artifactory.test.ts +248 -0
- package/src/registry/targets/artifactory.ts +257 -0
- package/src/registry/targets/fake-artifactory.ts +158 -0
- package/src/registry/targets/fake-gitlab.ts +161 -0
- package/src/registry/targets/fake-hcp.ts +233 -0
- package/src/registry/targets/fake-http.ts +97 -0
- package/src/registry/targets/fake-nexus.ts +152 -0
- package/src/registry/targets/gitlab.test.ts +208 -0
- package/src/registry/targets/gitlab.ts +223 -0
- package/src/registry/targets/hcp.test.ts +230 -0
- package/src/registry/targets/hcp.ts +360 -0
- package/src/registry/targets/hosted.ts +117 -0
- package/src/registry/targets/index.ts +17 -0
- package/src/registry/targets/module-target.ts +131 -0
- package/src/registry/targets/nexus.test.ts +220 -0
- package/src/registry/targets/nexus.ts +314 -0
- package/src/registry/targets/zip.test.ts +65 -0
- package/src/registry/targets/zip.ts +287 -0
- package/src/registry/writer.ts +155 -0
- package/src/state/azure-blob.ts +6 -221
- package/src/state/gcs.ts +5 -252
- package/src/state/object-store.ts +4 -32
- package/src/state/s3.ts +10 -182
- package/src/state/store.ts +2 -2
- package/src/state-ops.test.ts +182 -0
- package/src/state-ops.ts +462 -0
- package/src/state-reads.ts +116 -0
- package/src/used-sources.test.ts +75 -0
- package/src/used-sources.ts +204 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The edge function in front of a bucket-hosted module registry (chant#4566),
|
|
3
|
+
* built on `registryHandler`. It does three things:
|
|
4
|
+
*
|
|
5
|
+
* - Adds `X-Terraform-Get` to every download answer, which is what Terraform
|
|
6
|
+
* installs from (terragucci#1287). Always on.
|
|
7
|
+
* - With `auth` set, checks the bearer token Terraform and OpenTofu send for
|
|
8
|
+
* the host (`TF_TOKEN_<host>` or credentials.tfrc.json) on discovery,
|
|
9
|
+
* `versions`, `download` and every other file, and answers 401 when it is
|
|
10
|
+
* missing or wrong. Off by default.
|
|
11
|
+
* - With `auth` set, a download whose location is a tarball behind this same
|
|
12
|
+
* host is answered with a short-lived signed URL, so the CLIs' tarball fetch
|
|
13
|
+
* (go-getter, which sends no token) needs no `.netrc`. With auth off the
|
|
14
|
+
* location is the tarball's plain URL, as stored.
|
|
15
|
+
* - With `auth` set, a provider mirror's `<version>.json` (chant#4564) is
|
|
16
|
+
* answered with each archive's URL signed the same way, so the CLIs'
|
|
17
|
+
* archive fetch passes too.
|
|
18
|
+
*
|
|
19
|
+
* Only web-standard APIs (Request, Response, URL, crypto.subtle,
|
|
20
|
+
* TextEncoder), so the same code runs in a Cloudflare Worker, Lambda@Edge on
|
|
21
|
+
* Node 20, Deno, or a test. Adapters for the first two are in ./worker and
|
|
22
|
+
* ./lambda-edge.
|
|
23
|
+
*/
|
|
24
|
+
import { DISCOVERY, keyFor } from "../address";
|
|
25
|
+
import { registryHandler, type RegistryFiles } from "../handler";
|
|
26
|
+
import { isMirrorVersionPath } from "../mirror-paths";
|
|
27
|
+
import { downloadAnswer, downloadLocation, isDownloadPath } from "../writer";
|
|
28
|
+
|
|
29
|
+
/** Read auth: the tokens accepted and how tarball URLs are signed. */
|
|
30
|
+
export interface EdgeAuth {
|
|
31
|
+
/** Accepted bearer tokens. Keep two while one is rotated out. */
|
|
32
|
+
tokens: string[];
|
|
33
|
+
/** The key tarball URLs are signed with. Default: the first token. */
|
|
34
|
+
signingKey?: string;
|
|
35
|
+
/** Seconds a signed tarball URL works. Default 300. */
|
|
36
|
+
ttl?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RegistryEdgeOptions {
|
|
40
|
+
/** Where the registry's files are read: the bucket behind the function. */
|
|
41
|
+
files: RegistryFiles;
|
|
42
|
+
/** The registry's prefix in the bucket. */
|
|
43
|
+
prefix?: string;
|
|
44
|
+
/** Read auth. Absent (the default) means anyone who can reach the host reads. */
|
|
45
|
+
auth?: EdgeAuth;
|
|
46
|
+
/** Milliseconds since the epoch; a test fixes it. Default `Date.now`. */
|
|
47
|
+
now?: () => number;
|
|
48
|
+
/** Called once per request: method, path and the status sent. Never given a token. */
|
|
49
|
+
log?: (line: string) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The query parameters a signed tarball URL carries. */
|
|
53
|
+
export const SIGNED_EXPIRES = "expires";
|
|
54
|
+
export const SIGNED_SIGNATURE = "signature";
|
|
55
|
+
/** Default lifetime of a signed tarball URL, in seconds. */
|
|
56
|
+
export const DEFAULT_SIGNED_TTL = 300;
|
|
57
|
+
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
|
|
60
|
+
const base64url = (bytes: ArrayBuffer): string => {
|
|
61
|
+
let s = "";
|
|
62
|
+
for (const b of new Uint8Array(bytes)) s += String.fromCharCode(b);
|
|
63
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const hmac = async (key: string, text: string): Promise<string> => {
|
|
67
|
+
const k = await crypto.subtle.importKey("raw", encoder.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
68
|
+
return base64url(await crypto.subtle.sign("HMAC", k, encoder.encode(text)));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Compares two strings in time that depends only on their lengths. */
|
|
72
|
+
const sameText = (a: string, b: string): boolean => {
|
|
73
|
+
const x = encoder.encode(a);
|
|
74
|
+
const y = encoder.encode(b);
|
|
75
|
+
let diff = x.length ^ y.length;
|
|
76
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
|
|
77
|
+
return diff === 0;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const signingKeyOf = (auth: EdgeAuth): string | undefined => auth.signingKey || auth.tokens.find((t) => !!t);
|
|
81
|
+
|
|
82
|
+
/** What a signature covers: the path and the expiry, so a URL can't be moved to another file or extended. */
|
|
83
|
+
const signedText = (pathname: string, expires: number): string => `${pathname}\n${expires}`;
|
|
84
|
+
|
|
85
|
+
/** A location with `expires` and `signature` added, valid for `ttl` seconds from `nowMs`. */
|
|
86
|
+
export async function signLocation(location: string, key: string, nowMs: number, ttl = DEFAULT_SIGNED_TTL): Promise<string> {
|
|
87
|
+
const url = new URL(location);
|
|
88
|
+
const expires = Math.floor(nowMs / 1000) + ttl;
|
|
89
|
+
url.searchParams.delete(SIGNED_EXPIRES);
|
|
90
|
+
url.searchParams.delete(SIGNED_SIGNATURE);
|
|
91
|
+
url.searchParams.set(SIGNED_EXPIRES, String(expires));
|
|
92
|
+
url.searchParams.set(SIGNED_SIGNATURE, await hmac(key, signedText(url.pathname, expires)));
|
|
93
|
+
return url.toString();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Whether a URL carries a signature this key made for its path that has not expired. */
|
|
97
|
+
export async function verifySignedUrl(url: URL, key: string, nowMs: number): Promise<boolean> {
|
|
98
|
+
const expires = Number(url.searchParams.get(SIGNED_EXPIRES));
|
|
99
|
+
const signature = url.searchParams.get(SIGNED_SIGNATURE);
|
|
100
|
+
if (!signature || !Number.isInteger(expires) || expires * 1000 < nowMs) return false;
|
|
101
|
+
return sameText(signature, await hmac(key, signedText(url.pathname, expires)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether a request carries one of the accepted bearer tokens. */
|
|
105
|
+
export function bearerAccepted(request: Request, tokens: string[]): boolean {
|
|
106
|
+
const m = /^Bearer\s+(.+)$/i.exec(request.headers.get("authorization") ?? "");
|
|
107
|
+
if (!m) return false;
|
|
108
|
+
const given = m[1]!.trim();
|
|
109
|
+
// Check every token, so the time taken doesn't say which one matched.
|
|
110
|
+
let ok = false;
|
|
111
|
+
for (const t of tokens) if (t && sameText(given, t)) ok = true;
|
|
112
|
+
return ok;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const unauthorized = (): Response => new Response(`${JSON.stringify({ errors: ["a token is required: set TF_TOKEN_<host> or a credentials block for this host"] })}\n`, { status: 401, headers: { "content-type": "application/json", "www-authenticate": 'Bearer realm="terraform-registry"' } });
|
|
116
|
+
|
|
117
|
+
/** A tarball behind this host: the location's origin is the request's, and it names an archive. */
|
|
118
|
+
const isOwnTarball = (location: URL, request: URL): boolean => location.origin === request.origin && /\.(tar\.gz|tgz|zip)$/.test(location.pathname);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The part of the function that decides before the bucket is read: a 401, a
|
|
122
|
+
* download answer, or undefined for "serve this file from the bucket as it is".
|
|
123
|
+
* Lambda@Edge uses this on its own and lets CloudFront fetch the rest.
|
|
124
|
+
*/
|
|
125
|
+
export function registryEdgeGate(options: RegistryEdgeOptions): (request: Request) => Promise<Response | undefined> {
|
|
126
|
+
const now = options.now ?? Date.now;
|
|
127
|
+
return async (request) => {
|
|
128
|
+
const url = new URL(request.url);
|
|
129
|
+
const method = request.method.toUpperCase();
|
|
130
|
+
const done = (res: Response): Response => {
|
|
131
|
+
options.log?.(`${method} ${url.pathname} -> ${res.status}`);
|
|
132
|
+
return res;
|
|
133
|
+
};
|
|
134
|
+
const path = url.pathname.replace(/^\/+/, "");
|
|
135
|
+
const auth = options.auth;
|
|
136
|
+
if (auth) {
|
|
137
|
+
const protocolFile = path === DISCOVERY || isDownloadPath(path) || /(^|\/)versions$/.test(path);
|
|
138
|
+
let allowed = bearerAccepted(request, auth.tokens);
|
|
139
|
+
// A signed URL opens one file that is not a protocol answer: the tarball the download named.
|
|
140
|
+
const key = signingKeyOf(auth);
|
|
141
|
+
if (!allowed && !protocolFile && key) allowed = await verifySignedUrl(url, key, now());
|
|
142
|
+
if (!allowed) return done(unauthorized());
|
|
143
|
+
}
|
|
144
|
+
if (method !== "GET" && method !== "HEAD") return undefined;
|
|
145
|
+
if (auth && isMirrorVersionPath(path)) return signedMirrorVersion(request, url, path, options, auth, now(), done);
|
|
146
|
+
if (!isDownloadPath(path)) return undefined;
|
|
147
|
+
let file: { body?: string | Uint8Array };
|
|
148
|
+
try {
|
|
149
|
+
file = await options.files(keyFor(options.prefix, path));
|
|
150
|
+
} catch {
|
|
151
|
+
return done(new Response(null, { status: 502 }));
|
|
152
|
+
}
|
|
153
|
+
if (file.body === undefined) return done(new Response(null, { status: 404 }));
|
|
154
|
+
let location = downloadLocation(typeof file.body === "string" ? file.body : new TextDecoder().decode(file.body));
|
|
155
|
+
if (!location) return undefined;
|
|
156
|
+
const key = auth ? signingKeyOf(auth) : undefined;
|
|
157
|
+
if (auth && key) {
|
|
158
|
+
const resolved = new URL(location, url);
|
|
159
|
+
if (isOwnTarball(resolved, url)) location = await signLocation(resolved.toString(), key, now(), auth.ttl);
|
|
160
|
+
}
|
|
161
|
+
const answer = downloadAnswer(location);
|
|
162
|
+
return done(new Response(method === "HEAD" ? null : answer.body, { status: answer.status, headers: answer.headers }));
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A mirror version file with each archive on this host signed; undefined (served as stored) when it is not the protocol's JSON. */
|
|
167
|
+
async function signedMirrorVersion(request: Request, url: URL, path: string, options: RegistryEdgeOptions, auth: EdgeAuth, nowMs: number, done: (res: Response) => Response): Promise<Response | undefined> {
|
|
168
|
+
const key = signingKeyOf(auth);
|
|
169
|
+
if (!key) return undefined;
|
|
170
|
+
let file: { body?: string | Uint8Array };
|
|
171
|
+
try {
|
|
172
|
+
file = await options.files(keyFor(options.prefix, path));
|
|
173
|
+
} catch {
|
|
174
|
+
return done(new Response(null, { status: 502 }));
|
|
175
|
+
}
|
|
176
|
+
if (file.body === undefined) return done(new Response(null, { status: 404 }));
|
|
177
|
+
let doc: { archives?: Record<string, { url?: unknown }> };
|
|
178
|
+
try {
|
|
179
|
+
doc = JSON.parse(typeof file.body === "string" ? file.body : new TextDecoder().decode(file.body));
|
|
180
|
+
} catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
for (const archive of Object.values(doc.archives ?? {})) {
|
|
184
|
+
if (!archive || typeof archive.url !== "string") continue;
|
|
185
|
+
const resolved = new URL(archive.url, url);
|
|
186
|
+
if (resolved.origin === url.origin) archive.url = await signLocation(resolved.toString(), key, nowMs, auth.ttl);
|
|
187
|
+
}
|
|
188
|
+
const body = `${JSON.stringify(doc)}\n`;
|
|
189
|
+
return done(new Response(request.method.toUpperCase() === "HEAD" ? null : body, { status: 200, headers: { "content-type": "application/json" } }));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The whole function: the gate, then every other file served from the bucket by `registryHandler`. */
|
|
193
|
+
export function registryEdge(options: RegistryEdgeOptions): (request: Request) => Promise<Response> {
|
|
194
|
+
const gate = registryEdgeGate(options);
|
|
195
|
+
const serve = registryHandler(options.files, { prefix: options.prefix, log: options.log });
|
|
196
|
+
return async (request) => (await gate(request)) ?? serve(request);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Registry files read from an origin over HTTP: a public bucket endpoint, a website endpoint, or a test server. */
|
|
200
|
+
export function originFiles(base: string, fetchFn: (url: string, init?: RequestInit) => Promise<Response> = (u, i) => fetch(u, i)): RegistryFiles {
|
|
201
|
+
const root = base.replace(/\/+$/, "");
|
|
202
|
+
return async (key) => {
|
|
203
|
+
const res = await fetchFn(`${root}/${key.split("/").map(encodeURIComponent).join("/")}`, { method: "GET" });
|
|
204
|
+
if (res.status === 404 || res.status === 403) return {};
|
|
205
|
+
if (!res.ok) throw new Error(`origin answered ${res.status}`);
|
|
206
|
+
return { body: new Uint8Array(await res.arrayBuffer()) };
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Accepted tokens from a secret's text: one per line or comma separated, blanks dropped. */
|
|
211
|
+
export const tokensFromSecret = (text: string | undefined): string[] => (text ?? "").split(/[\n,]/).map((t) => t.trim()).filter((t) => !!t);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The edge function for bucket-hosted module registries (chant#4566): the
|
|
3
|
+
* download header always, and with `auth` a bearer token check and signed
|
|
4
|
+
* tarball URLs. Platform adapters for a Cloudflare Worker and Lambda@Edge.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./edge";
|
|
7
|
+
export * from "./worker";
|
|
8
|
+
export * from "./lambda-edge";
|
|
9
|
+
export * from "./token-source";
|
|
10
|
+
export * from "./sigv4-web";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry edge function as a Lambda@Edge origin-request handler on
|
|
3
|
+
* CloudFront (chant#4566), Node 20 or later. Lambda@Edge has no environment
|
|
4
|
+
* variables, so the options are fixed when the function is built:
|
|
5
|
+
*
|
|
6
|
+
* export const handler = lambdaEdgeHandler({ files: originFiles("https://<bucket endpoint>"), auth: { tokens: [...] } });
|
|
7
|
+
*
|
|
8
|
+
* A request the gate refuses gets the 401 from the function. A download gets
|
|
9
|
+
* its answer, with `X-Terraform-Get` and, with auth on, a signed tarball URL.
|
|
10
|
+
* Every other request goes on to the bucket, without the query string when
|
|
11
|
+
* auth is on. CloudFront must pass
|
|
12
|
+
* the query string and the Authorization header to the function (an origin
|
|
13
|
+
* request policy) for the token check and signed URLs to work.
|
|
14
|
+
*
|
|
15
|
+
* A CloudFront Function cannot read the bucket's `download` body, so it
|
|
16
|
+
* cannot add the header; this needs Lambda@Edge.
|
|
17
|
+
*
|
|
18
|
+
* The accepted tokens come from `auth.source` (chant#4590): built into the
|
|
19
|
+
* zip as `auth.tokens` (`{ kind: "zip" }`, the default), or read from SSM
|
|
20
|
+
* Parameter Store or Secrets Manager at cold start and kept for a bounded
|
|
21
|
+
* time (./token-source.ts):
|
|
22
|
+
*
|
|
23
|
+
* lambdaEdgeHandler({ files, auth: { source: { kind: "ssm", name: "/registry/tokens", region: "us-east-1" } } });
|
|
24
|
+
*
|
|
25
|
+
* While a store read fails, every request gets a 503.
|
|
26
|
+
*/
|
|
27
|
+
import { registryEdgeGate, type EdgeAuth, type RegistryEdgeOptions } from "./edge";
|
|
28
|
+
import { storedTokenCache, type EdgeTokenSource, type StoredTokens } from "./token-source";
|
|
29
|
+
|
|
30
|
+
/** The parts of a CloudFront request event this handler reads. */
|
|
31
|
+
export interface CloudFrontRequest {
|
|
32
|
+
uri: string;
|
|
33
|
+
querystring?: string;
|
|
34
|
+
method: string;
|
|
35
|
+
headers: Record<string, { key?: string; value: string }[]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CloudFrontRequestEvent {
|
|
39
|
+
Records: { cf: { config?: { distributionDomainName?: string }; request: CloudFrontRequest } }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A response Lambda@Edge generates in place of the origin's. */
|
|
43
|
+
export interface CloudFrontResponse {
|
|
44
|
+
status: string;
|
|
45
|
+
statusDescription?: string;
|
|
46
|
+
headers: Record<string, { key: string; value: string }[]>;
|
|
47
|
+
body?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The web Request a CloudFront request stands for. */
|
|
51
|
+
export function requestFromCloudFront(event: CloudFrontRequestEvent): Request {
|
|
52
|
+
const { config, request } = event.Records[0]!.cf;
|
|
53
|
+
const host = request.headers.host?.[0]?.value ?? config?.distributionDomainName ?? "localhost";
|
|
54
|
+
const headers = new Headers();
|
|
55
|
+
for (const values of Object.values(request.headers)) for (const h of values) if (h.key) headers.append(h.key, h.value);
|
|
56
|
+
return new Request(`https://${host}${request.uri}${request.querystring ? `?${request.querystring}` : ""}`, { method: request.method, headers });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A web Response as a Lambda@Edge generated response. */
|
|
60
|
+
export async function responseToCloudFront(res: Response): Promise<CloudFrontResponse> {
|
|
61
|
+
const headers: CloudFrontResponse["headers"] = {};
|
|
62
|
+
res.headers.forEach((value, key) => {
|
|
63
|
+
headers[key.toLowerCase()] = [{ key, value }];
|
|
64
|
+
});
|
|
65
|
+
const body = res.body ? await res.text() : undefined;
|
|
66
|
+
return { status: String(res.status), headers, ...(body !== undefined ? { body } : {}) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Read auth on Lambda@Edge: `EdgeAuth`, with the tokens from `source`. */
|
|
70
|
+
export interface LambdaEdgeAuth extends Omit<EdgeAuth, "tokens"> {
|
|
71
|
+
/** The tokens built into the function, for `source` zip (the default). Ignored with a store source. */
|
|
72
|
+
tokens?: string[];
|
|
73
|
+
/** Where the tokens come from. Default `{ kind: "zip" }`. */
|
|
74
|
+
source?: EdgeTokenSource;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LambdaEdgeOptions extends Omit<RegistryEdgeOptions, "auth"> {
|
|
78
|
+
/** Read auth. Absent (the default) means anyone who can reach the host reads. */
|
|
79
|
+
auth?: LambdaEdgeAuth;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The answer while the token store cannot be read: the function fails closed, and CloudFront does not cache it. */
|
|
83
|
+
const storeUnavailable = (): CloudFrontResponse => ({
|
|
84
|
+
status: "503",
|
|
85
|
+
statusDescription: "Service Unavailable",
|
|
86
|
+
headers: { "content-type": [{ key: "Content-Type", value: "application/json" }], "cache-control": [{ key: "Cache-Control", value: "no-store" }] },
|
|
87
|
+
body: `${JSON.stringify({ errors: ["the registry cannot check tokens right now"] })}\n`,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** An origin-request handler: a generated response, or the request passed on to the bucket. */
|
|
91
|
+
export function lambdaEdgeHandler(options: LambdaEdgeOptions): (event: CloudFrontRequestEvent) => Promise<CloudFrontRequest | CloudFrontResponse> {
|
|
92
|
+
const { auth, ...rest } = options;
|
|
93
|
+
const source = auth?.source ?? { kind: "zip" };
|
|
94
|
+
const passOn = (event: CloudFrontRequestEvent): CloudFrontRequest => {
|
|
95
|
+
// The signature has been checked; the bucket gets the plain object key, so its own query auth never sees ours.
|
|
96
|
+
const request = event.Records[0]!.cf.request;
|
|
97
|
+
return auth ? { ...request, querystring: "" } : request;
|
|
98
|
+
};
|
|
99
|
+
const edgeAuth = (tokens: string[], signingKey?: string): EdgeAuth => ({
|
|
100
|
+
tokens,
|
|
101
|
+
// A signing key kept in the store rotates with it, so it comes before one built into the zip.
|
|
102
|
+
...(signingKey || auth?.signingKey ? { signingKey: signingKey || auth?.signingKey } : {}),
|
|
103
|
+
...(auth?.ttl !== undefined ? { ttl: auth.ttl } : {}),
|
|
104
|
+
});
|
|
105
|
+
if (!auth || source.kind === "zip") {
|
|
106
|
+
const gate = registryEdgeGate({ ...rest, ...(auth ? { auth: edgeAuth(auth.tokens ?? []) } : {}) });
|
|
107
|
+
return async (event) => {
|
|
108
|
+
const answer = await gate(requestFromCloudFront(event));
|
|
109
|
+
return answer ? responseToCloudFront(answer) : passOn(event);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const cache = storedTokenCache(source, rest.now);
|
|
113
|
+
// One gate per read, so a request between rotations is checked against one consistent token set.
|
|
114
|
+
let gateFor: { stored: StoredTokens; gate: ReturnType<typeof registryEdgeGate> } | undefined;
|
|
115
|
+
return async (event) => {
|
|
116
|
+
let stored: StoredTokens;
|
|
117
|
+
try {
|
|
118
|
+
stored = await cache.get();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
rest.log?.(`token store: ${(e as Error).message}`);
|
|
121
|
+
return storeUnavailable();
|
|
122
|
+
}
|
|
123
|
+
if (gateFor?.stored !== stored) gateFor = { stored, gate: registryEdgeGate({ ...rest, auth: edgeAuth(stored.tokens, stored.signingKey) }) };
|
|
124
|
+
const answer = await gateFor.gate(requestFromCloudFront(event));
|
|
125
|
+
return answer ? responseToCloudFront(answer) : passOn(event);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Signature Version 4 with Web Crypto, for the registry edge function's
|
|
3
|
+
* token reads (chant#4590). chant's one AWS signer
|
|
4
|
+
* (packages/core/src/cloud-lister/sigv4.ts) hashes with node:crypto, which a
|
|
5
|
+
* browser-platform bundle of registry/edge must not reach (chant#4589), so
|
|
6
|
+
* this is the same canonical request built on crypto.subtle. It signs only
|
|
7
|
+
* what the edge sends: a JSON POST to an AWS query endpoint, with `host`,
|
|
8
|
+
* `x-amz-date`, `x-amz-target`, `content-type` and the session token.
|
|
9
|
+
*
|
|
10
|
+
* Its signatures match core's signer for the same input; the test checks
|
|
11
|
+
* the two against each other.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The credentials a request is signed with: the function's execution role on Lambda@Edge. */
|
|
15
|
+
export interface EdgeAwsCredentials {
|
|
16
|
+
accessKeyId: string;
|
|
17
|
+
secretAccessKey: string;
|
|
18
|
+
sessionToken?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
|
|
23
|
+
const hex = (bytes: ArrayBuffer): string => [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
|
|
25
|
+
const sha256Hex = async (text: string): Promise<string> => hex(await crypto.subtle.digest("SHA-256", encoder.encode(text)));
|
|
26
|
+
|
|
27
|
+
async function hmac(key: string | ArrayBuffer, text: string): Promise<ArrayBuffer> {
|
|
28
|
+
const raw: BufferSource = typeof key === "string" ? encoder.encode(key) : key;
|
|
29
|
+
const k = await crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
30
|
+
return crypto.subtle.sign("HMAC", k, encoder.encode(text));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const encodeRfc3986 = (value: string): string => encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
34
|
+
|
|
35
|
+
function canonicalQuery(url: URL): string {
|
|
36
|
+
const pairs: Array<[string, string]> = [];
|
|
37
|
+
url.searchParams.forEach((v, k) => pairs.push([encodeRfc3986(k), encodeRfc3986(v)]));
|
|
38
|
+
pairs.sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : a[0] < b[0] ? -1 : 1));
|
|
39
|
+
return pairs.map(([k, v]) => `${k}=${v}`).join("&");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The headers to send with `body`, `authorization` included, in core's
|
|
44
|
+
* signer's shape: lower-case names, `host` and `x-amz-date` added.
|
|
45
|
+
*/
|
|
46
|
+
export async function signV4Web(input: {
|
|
47
|
+
method: string;
|
|
48
|
+
url: string;
|
|
49
|
+
headers: Record<string, string>;
|
|
50
|
+
body: string;
|
|
51
|
+
region: string;
|
|
52
|
+
service: string;
|
|
53
|
+
credentials: EdgeAwsCredentials;
|
|
54
|
+
now: Date;
|
|
55
|
+
}): Promise<Record<string, string>> {
|
|
56
|
+
const url = new URL(input.url);
|
|
57
|
+
const amzDate = input.now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
58
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
59
|
+
const payloadHash = await sha256Hex(input.body);
|
|
60
|
+
const headers: Record<string, string> = {};
|
|
61
|
+
for (const [k, v] of Object.entries(input.headers)) headers[k.toLowerCase()] = v.trim().replace(/\s+/g, " ");
|
|
62
|
+
headers.host = url.host;
|
|
63
|
+
headers["x-amz-date"] = amzDate;
|
|
64
|
+
if (input.credentials.sessionToken) headers["x-amz-security-token"] = input.credentials.sessionToken;
|
|
65
|
+
|
|
66
|
+
const names = Object.keys(headers).sort();
|
|
67
|
+
const canonicalHeaders = names.map((n) => `${n}:${headers[n]}\n`).join("");
|
|
68
|
+
const signedHeaders = names.join(";");
|
|
69
|
+
const canonicalRequest = [input.method.toUpperCase(), url.pathname || "/", canonicalQuery(url), canonicalHeaders, signedHeaders, payloadHash].join("\n");
|
|
70
|
+
|
|
71
|
+
const scope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
|
|
72
|
+
const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, await sha256Hex(canonicalRequest)].join("\n");
|
|
73
|
+
let key = await hmac(`AWS4${input.credentials.secretAccessKey}`, dateStamp);
|
|
74
|
+
for (const part of [input.region, input.service, "aws4_request"]) key = await hmac(key, part);
|
|
75
|
+
const signature = hex(await hmac(key, stringToSign));
|
|
76
|
+
|
|
77
|
+
headers.authorization = `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
78
|
+
return headers;
|
|
79
|
+
}
|