Skip to content

Commit c60691d

Browse files
author
Aleks Marchenko
committed
feat: add AWS Signature v4 for HttpClientRequest
1 parent 3ebb775 commit c60691d

File tree

4 files changed

+86
-0
lines changed

4 files changed

+86
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { HttpBody, HttpClient } from '@effect/platform'
2+
import { RequestError } from '@effect/platform/HttpClientError'
3+
import * as HttpClientRequest from '@effect/platform/HttpClientRequest'
4+
import { AwsV4Signer } from 'aws4fetch'
5+
import * as Effect from 'effect/Effect'
6+
import * as Match from 'effect/Match'
7+
import * as Option from 'effect/Option'
8+
import * as Ref from 'effect/Ref'
9+
import { Credentials } from './Credentials.js'
10+
import { Signer } from './Signer.js'
11+
12+
export class Aws4Fetch extends Effect.Service<Aws4Fetch>()(`@effect-aws/signature-v4/Signer`, {
13+
effect: Effect.gen(function* () {
14+
const getBody = Match.type<HttpBody.HttpBody>().pipe(
15+
Match.tag(`Empty`, (): XMLHttpRequestBodyInit => ``),
16+
Match.tag(`FormData`, (body): XMLHttpRequestBodyInit => body.formData),
17+
Match.tag(`Raw`, (body): XMLHttpRequestBodyInit => body.body as string),
18+
Match.tag(`Stream`, (): XMLHttpRequestBodyInit => ``),
19+
Match.tag(`Uint8Array`, (body): XMLHttpRequestBodyInit => body.body),
20+
Match.exhaustive,
21+
)
22+
23+
const signRequest = (request: HttpClientRequest.HttpClientRequest) => {
24+
const body: BodyInit = getBody(request.body)
25+
26+
return Credentials.pipe(Effect.flatMap(Ref.get)).pipe(
27+
Effect.andThen(
28+
Option.match({
29+
onNone: () => Effect.succeed(request),
30+
onSome: ({ accessKeyId, secretAccessKey, sessionToken }) => {
31+
const signer = new AwsV4Signer({
32+
method: request.method,
33+
url: request.url,
34+
headers: request.headers,
35+
body,
36+
accessKeyId,
37+
secretAccessKey,
38+
sessionToken: sessionToken!,
39+
})
40+
41+
return Effect.tryPromise(() => signer.sign()).pipe(
42+
Effect.andThen((result) =>
43+
request.pipe(HttpClientRequest.setHeaders(result.headers), HttpClientRequest.setUrl(result.url)),
44+
),
45+
Effect.mapError(() => new RequestError({ reason: `Encode`, request, description: `Failed to sign request` }))
46+
)
47+
},
48+
}),
49+
),
50+
)
51+
}
52+
53+
return {
54+
signRequest,
55+
transformClient: (client: HttpClient.HttpClient) =>
56+
client.pipe(
57+
HttpClient.mapRequestEffect((r) => signRequest(r)),
58+
),
59+
} as Signer
60+
}),
61+
}) {}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as Context from 'effect/Context'
2+
import * as Option from 'effect/Option'
3+
import * as Ref from 'effect/Ref'
4+
5+
export interface AWSCredentials {
6+
accessKeyId: string;
7+
secretAccessKey: string;
8+
sessionToken?: string;
9+
expiration?: Date;
10+
}
11+
12+
export class Credentials extends Context.Tag(`@effect-aws/signature-v4/Credentials`)<Credentials, Ref.Ref<Option.Option<AWSCredentials>>>() {}

packages/signature-v4/src/Signer.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { HttpClient, HttpClientError, HttpClientRequest } from '@effect/platform'
2+
import { RequestError } from '@effect/platform/HttpClientError'
3+
import * as Effect from 'effect/Effect'
4+
import { Credentials } from './Credentials.js'
5+
import { Scope } from 'effect/Scope'
6+
7+
export interface Signer {
8+
signRequest: (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientRequest.HttpClientRequest, RequestError, Credentials>
9+
transformClient: (client: HttpClient.HttpClient) => HttpClient.HttpClient<HttpClientError.HttpClientError, Scope | Credentials>
10+
}

packages/signature-v4/src/Smithy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/**
2+
* TBD
3+
*/

0 commit comments

Comments
 (0)