Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: copy duplex option from Request object #25

Merged
merged 21 commits into from
Dec 30, 2024
Merged
33 changes: 27 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ jobs:
- name: Lint
run: pnpm lint

test:
name: Test
unit-test:
name: Unit Test
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -54,7 +54,7 @@ jobs:
run: pnpm build

- name: Test
run: pnpm test
run: pnpm test:unit

coverage:
name: Coverage
Expand All @@ -71,14 +71,35 @@ jobs:
run: pnpm build

- name: Test
run: pnpm test:coverage
run: pnpm test:unit --coverage

- name: Coverage
if: ${{ steps.install.conclusion == 'success' }}
uses: davelosert/vitest-coverage-report-action@v2
with:
vite-config-path: vitest.config.unit.ts

e2e-test:
name: E2E Test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup and Install
id: install
uses: zirkelc/setup-and-install@v1

- name: Build
run: pnpm build

- name: Deploy
working-directory: test/aws
run: pnpm run deploy

- name: Test
run: pnpm test:e2e

preview:
name: Preview
runs-on: ubuntu-latest
Expand All @@ -97,12 +118,12 @@ jobs:
run: pnpm build

- name: Publish Preview
run: npx pkg-pr-new publish
run: npx pkg-pr-new publish --pnpm --packageManager=pnpm

release:
name: Release
runs-on: ubuntu-latest
needs: [lint,test]
needs: [lint, unit-test, e2e-test]
if: github.ref == 'refs/heads/main'

steps:
Expand Down
143 changes: 103 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![npm](https://img.shields.io/npm/dt/aws-sigv4-fetch)](https://www.npmjs.com/package/aws-sigv4-fetch)

# aws-sigv4-fetch
AWS SignatureV4 fetch API function to automatically sign HTTP request with given AWS credentials. Built entirely on the newest version of the official [AWS SDK for JS](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html).
A lightweight wrapper around the fetch API that automatically signs HTTP requests with AWS Signature Version 4 (SigV4) authentication. Built on the official AWS SDK for JS v3.

## Signature Version 4
> Signature Version 4 (SigV4) is the process to add authentication information to AWS API requests sent by HTTP. For security, most requests to AWS must be signed with an access key. The access key consists of an access key ID and secret access key, which are commonly referred to as your security credentials
Expand All @@ -13,14 +13,10 @@ AWS SignatureV4 fetch API function to automatically sign HTTP request with given
## Install
```sh
npm install --save aws-sigv4-fetch

yarn add aws-sigv4-fetch

pnpm add aws-sigv4-fetch
```

## ESM and CommonJS
This package ships with ES Module and CommonJS support. That means you can `import` or `require` the package in your project depending on your mdoule format.
This package ships with ES Module and CommonJS support. That means you can `import` or `require` the package in your project depending on your module format.

```ts
// ESM
Expand All @@ -31,57 +27,67 @@ const { createSignedFetcher } = require('aws-sigv4-fetch');
```

## Usage
This package exports a function `createSignedFetcher` that returns a `fetch` function to automatically sign HTTP requests with AWS Signature V4 for the given AWS service and region. The credentials can be passed to the function directly, or they will be retrieved from the environment by `defaultProvider()` from package [`@aws-sdk/credential-provider-node`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_provider_node.html).
This package exports a function `createSignedFetcher` that returns a `fetch` function to automatically sign HTTP requests with AWS Signature V4 for the given AWS service and region.
The returned `signedFetch` function accepts the same arguments as the default `fetch` function.


```ts
import { createSignedFetcher } from 'aws-sigv4-fetch';

const signedFetch = createSignedFetcher({ service: 'appsync', region: 'eu-west-1' });
const url = 'https://mygraphqlapi.appsync-api.eu-west-1.amazonaws.com/graphql';
const signedFetch = createSignedFetcher({ service: 's3', region: 'eu-west-1' });

const body = { a: 1 };
// signedFetch(input: string)
const response = await signedFetch('https://s3.eu-west-1.amazonaws.com/my-bucket/my-key.json');

const response = await signedFetch(url, {
method: 'post',
body: JSON.stringify(body),
headers: {'Content-Type': 'application/json'}
});
// signedFetch(input: URL)
const response = await signedFetch(new URL('https://s3.eu-west-1.amazonaws.com/my-bucket/my-key.json'));

// signedFetch(input: Request)
const response = await signedFetch(new Request('https://s3.eu-west-1.amazonaws.com/my-bucket/my-key.json'));

const data = await response.json();
// signedFetch(input: string, init?: RequestInit)
const response = await signedFetch('https://s3.eu-west-1.amazonaws.com/my-bucket/my-key.json', {
method: 'POST',
body: JSON.stringify({ a: 1 }),
headers: { 'Content-Type': 'application/json' }
});
```

### Sign GraphQL Requests with `graphql-request`
If you are using [`graphql-request`](https://www.npmjs.com/package/graphql-request) as GraphQL library, you can easily sign all HTTP requests. The library has `fetch`option to pass a [custom `fetch` method](https://github.com/prisma-labs/graphql-request#using-a-custom-fetch-method):
## API
The `createSignedFetcher(options: SignedFetcherOptions)` function accepts the following options:

```ts
import { createSignedFetcher } from 'aws-sigv4-fetch';
import { GraphQLClient } from 'graphql-request';
type SignedFetcherOptions = {
service: string;
region?: string;
credentials?: AwsCredentialIdentity;
fetch?: typeof fetch;
};
```

const query = `
mutation CreateItem($input: CreateItemInput!) {
createItem(input: $input) {
id
createdAt
updatedAt
name
}
}
`;
### Service
The `service` is required and must match the AWS service you are signing requests for.
If it doesn't match, the request will fail with an error like:
> Credential should be scoped to correct service: 'service'

const variables = {
input: {
name,
},
};
### Region
The `region` is optional and defaults to `us-east-1` if not provided. Some services like IAM are global and don't require a region.

const client = new GraphQLClient('https://mygraphqlapi.appsync-api.eu-west-1.amazonaws.com/graphql', {
fetch: createSignedFetcher({ service: 'appsync', region: 'eu-west-1' }),
});
### Credentials
The `credentials` is optional. If not provided, the credentials will be retrieved from the environment by the package [`@aws-sdk/credential-provider-node`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_provider_node.html).

const result = await client.request(query, variables);
```ts
import { createSignedFetcher } from 'aws-sigv4-fetch';

// credentials will be retrieved from the environment
const signedFetch = createSignedFetcher({ service: 's3', region: 'eu-west-1' });

// credentials will be passed directly to the function
const signedFetch = createSignedFetcher({ service: 's3', region: 'eu-west-1', credentials: { accessKeyId: '...', secretAccessKey: '...' } });
```

### Fetch
By default, `createSignedFetcher` uses the `fetch` function from the environment. Native `fetch` is supported in Node.js >= v18. If you are running in an environment where native `fetch` is **not** available, the `fetch` function must be polyfilled or provided as an argument to `createSignedFetcher`. This allows to use the same `fetch` function that is already used in your application. There are several ways to do this:
The `fetch` function is optional. If not provided, the `fetch` function from the environment will be used. Native `fetch` is supported in Node.js >= v18. If you are running in an environment where native `fetch` is **not** available, the `fetch` function must be polyfilled or provided as an argument to `createSignedFetcher`. This allows to use the same `fetch` function that is already used in your application. There are several ways to do this:

#### Native `fetch`
If native `fetch` is available, you don't have to pass it as argument to `createSignedFetcher`.
Expand Down Expand Up @@ -115,7 +121,64 @@ import { createSignedFetcher } from 'aws-sigv4-fetch';
const signedFetch = createSignedFetcher({ service: 'iam', region: 'eu-west-1', fetch });
```

## Examples

### AWS
Here are some examples of common AWS services.

```ts
// API Gateway
const signedFetch = createSignedFetcher({ service: 'execute-api', region: 'eu-west-1' });
const response = await signedFetch('https://myapi.execute-api.eu-west-1.amazonaws.com/my-stage/my-resource');

// Lambda Function URL
const signedFetch = createSignedFetcher({ service: 'lambda', region: 'eu-west-1' });
const response = await signedFetch(new URL('https://mylambda.lambda-url.eu-west-1.on.aws/'));

// AppSync
const signedFetch = createSignedFetcher({ service: 'appsync', region: 'eu-west-1' });
const response = await signedFetch('https://mygraphqlapi.appsync-api.eu-west-1.amazonaws.com/graphql', {
method: 'POST',
body: JSON.stringify({ a: 1 }),
headers: {'Content-Type': 'application/json'}
});
```

### Automatically sign GraphQL Requests with `graphql-request`
If you are using [`graphql-request`](https://www.npmjs.com/package/graphql-request) as GraphQL library, you can easily sign all HTTP requests. The library has `fetch` option to pass a [custom `fetch` method](https://github.com/prisma-labs/graphql-request#using-a-custom-fetch-method):

```ts
import { createSignedFetcher } from 'aws-sigv4-fetch';
import { GraphQLClient } from 'graphql-request';

const query = `
mutation CreateItem($input: CreateItemInput!) {
createItem(input: $input) {
id
createdAt
updatedAt
name
}
}
`;

const variables = {
input: {
name,
},
};

const client = new GraphQLClient('https://mygraphqlapi.appsync-api.eu-west-1.amazonaws.com/graphql', {
fetch: createSignedFetcher({ service: 'appsync', region: 'eu-west-1' }),
});

const result = await client.request(query, variables);
```

## Resources
- [Sign GraphQL Request with AWS IAM and Signature V4](https://dev.to/zirkelc/sign-graphql-request-with-aws-iam-and-signature-v4-2il6)
- [Amplify Signing a request from Lambda](https://docs.amplify.aws/lib/graphqlapi/graphql-from-nodejs/q/platform/js/#signing-a-request-from-lambda)
- [Signing HTTP requests to Amazon OpenSearch Service](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/request-signing.html#request-signing-node)

## License
MIT
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"noForEach": "off"
},
"style": {
"noNonNullAssertion": "off"
"noNonNullAssertion": "off",
"noUnusedTemplateLiteral": "off"
}
}
},
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
"dist"
],
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage.enabled --coverage.provider=v8 --coverage.reporter=json-summary --coverage.reporter=json --coverage.reporter=text-summary --coverage.reporter=html",
"test:unit": "vitest --project=unit",
"test:e2e": "vitest --project=e2e/*",
"build": "tsup && attw --pack .",
"watch": "tsup --watch src",
"dev": "tsup ./src/index.ts --watch",
"prepublishOnly": "pnpm build",
"lint": "pnpm biome check --write --no-errors-on-unmatched src test"
"lint": "pnpm biome check --write --no-errors-on-unmatched ."
},
"repository": {
"type": "git",
Expand All @@ -56,12 +56,15 @@
"@types/node": "^22.9.0",
"@vitest/coverage-v8": "^2.0.4",
"@vitest/ui": "^2.0.5",
"cdk": "^2.173.2",
"cross-fetch": "^4.0.0",
"np": "^10.0.6",
"pkg-pr-new": "^0.0.30",
"tsup": "^8.1.0",
"tsx": "^4.17.0",
"typescript": "^5.5.3",
"undici": "^7.1.0",
"undici-types": "^7.1.0",
"vitest": "^2.0.5",
"vitest-github-actions-reporter": "^0.11.1"
}
Expand Down
Loading
Loading