diff --git a/.projen/deps.json b/.projen/deps.json index ff76964..dd0a0e6 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -150,10 +150,6 @@ "name": "@types/aws-lambda", "type": "runtime" }, - { - "name": "axios", - "type": "runtime" - }, { "name": "constructs", "type": "runtime" diff --git a/.projen/tasks.json b/.projen/tasks.json index 2eac037..9c6bd6e 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -254,13 +254,13 @@ }, "steps": [ { - "exec": "npx npm-check-updates@20 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@aws-sdk/client-cognito-identity-provider,@aws-sdk/client-dynamodb,@aws-sdk/client-s3,@aws-sdk/lib-dynamodb,@hapi/boom,@types/jest,@types/js-yaml,@types/jsonwebtoken,@types/jwk-to-pem,@types/lambda-log,@types/node,eslint-import-resolver-typescript,eslint-plugin-import,jest,projen,ts-jest,ts-node,typedoc,typescript,openapi-typescript,@types/aws-lambda,axios,constructs,date-fns,js-yaml,jsonwebtoken,jwk-to-pem,lambda-log" + "exec": "npx npm-check-updates@20 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@aws-sdk/client-cognito-identity-provider,@aws-sdk/client-dynamodb,@aws-sdk/client-s3,@aws-sdk/lib-dynamodb,@hapi/boom,@types/jest,@types/js-yaml,@types/jsonwebtoken,@types/jwk-to-pem,@types/lambda-log,@types/node,eslint-import-resolver-typescript,eslint-plugin-import,jest,projen,ts-jest,ts-node,typedoc,typescript,openapi-typescript,@types/aws-lambda,constructs,date-fns,js-yaml,jsonwebtoken,jwk-to-pem,lambda-log" }, { "exec": "npm install" }, { - "exec": "npm update @aws-sdk/client-cognito-identity-provider @aws-sdk/client-dynamodb @aws-sdk/client-s3 @aws-sdk/lib-dynamodb @hapi/boom @stylistic/eslint-plugin @types/jest @types/js-yaml @types/jsonwebtoken @types/jwk-to-pem @types/lambda-log @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version constructs eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit projen ts-jest ts-node typedoc typescript aws-cdk-lib dynamodb-onetable openapi-typescript @types/aws-lambda axios date-fns js-yaml jsonwebtoken jwk-to-pem lambda-log" + "exec": "npm update @aws-sdk/client-cognito-identity-provider @aws-sdk/client-dynamodb @aws-sdk/client-s3 @aws-sdk/lib-dynamodb @hapi/boom @stylistic/eslint-plugin @types/jest @types/js-yaml @types/jsonwebtoken @types/jwk-to-pem @types/lambda-log @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version constructs eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit projen ts-jest ts-node typedoc typescript aws-cdk-lib dynamodb-onetable openapi-typescript @types/aws-lambda date-fns js-yaml jsonwebtoken jwk-to-pem lambda-log" }, { "exec": "npx projen" diff --git a/.projenrc.ts b/.projenrc.ts index b37c8c9..5c29d09 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -17,7 +17,6 @@ const project = new typescript.TypeScriptProject({ 'js-yaml', 'jsonwebtoken', 'jwk-to-pem', - 'axios', 'lambda-log', 'constructs', ], diff --git a/README.md b/README.md index 69fbeda..f2aeef2 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,70 @@ await test.cleanupItems(); await test.removeUser('test@example.com'); ``` +## Breaking Change: `axios` Removed + +CDK Serverless no longer depends on `axios`. The library now uses the +platform-native `fetch` API (stable in Node.js 18+; the Lambda functions +created by this library use `Runtime.NODEJS_LATEST`, currently Node 22). + +This removes one third-party runtime dependency from every Lambda bundle that +imports from `cdk-serverless/lambda` and from every test workspace that +imports from `cdk-serverless/tests`. It was motivated by repeated axios +security advisories — landing this as a deliberate breaking change so the +fix is permanent rather than chasing CVE upgrades. + +### Impact on Lambda handlers (`cdk-serverless/lambda`) + +None. The internal JWKS / well-known-issuer fetches in the JWT authorizers +were the only axios call sites in the Lambda runtime code, and their public +behavior is unchanged. Errors now surface as `Error` (or a `TimeoutError` +from `AbortSignal.timeout`) instead of `AxiosError`; if you catch errors +inside the authorizer, the message text is similar but the type guard is +different. + +### Impact on `IntegTestUtil` (`cdk-serverless/tests`) + +`IntegTestUtil.getClient()` and `IntegTestUtil.getAuthenticatedClient()` +previously returned an `Axios` instance. They now return a small +`HttpClient` exported from `cdk-serverless/tests`. The migration is +mechanical: + +```typescript +// Before +const client = await test.getAuthenticatedClient('test@example.com'); +const response = await client.get('/items'); +// response.data is the parsed JSON, response.status is the HTTP status code +const items = response.data.items; + +// After +const client = await test.getAuthenticatedClient('test@example.com'); +const response = await client.get('/items'); +// response.body is the raw string, response.json() parses it, response.ok +// reports 2xx, response.status is the HTTP status code +const items = response.json<{ items: Item[] }>().items; +``` + +The `HttpClient` exposes the methods that integration tests in this +ecosystem actually use: + +- `get(path, options?)` +- `post(path, body?, options?)` — `body` may be a string or any JSON-serializable + value; objects are stringified and `Content-Type: application/json` is added + automatically if the caller did not set it. +- `put(path, body?, options?)`, `patch(path, body?, options?)`, `delete(path, options?)` + +Configuration accepted by `HttpClient` and by `getClient(config)`: + +- `baseURL` — prepended to relative paths. +- `headers` — default headers applied to every request; per-request `headers` + override these on collision. + +If you relied on axios-specific features (interceptors, `defaults`, +`transformRequest` / `transformResponse`, automatic `data` parsing), implement +the equivalent in your test code or wrap `HttpClient`. If your use case +needs richer client features and we should expose them, please open an +issue. + ## Contribute ### How to contribute to CDK Serverless diff --git a/docs/constructs/assets/highlight.css b/docs/constructs/assets/highlight.css index 0022ac6..72a5329 100644 --- a/docs/constructs/assets/highlight.css +++ b/docs/constructs/assets/highlight.css @@ -15,10 +15,10 @@ --dark-hl-6: #6A9955; --light-hl-7: #0070C1; --dark-hl-7: #4FC1FF; - --light-hl-8: #098658; - --dark-hl-8: #B5CEA8; - --light-hl-9: #267F99; - --dark-hl-9: #4EC9B0; + --light-hl-8: #267F99; + --dark-hl-8: #4EC9B0; + --light-hl-9: #098658; + --dark-hl-9: #B5CEA8; --light-code-background: #FFFFFF; --dark-code-background: #1E1E1E; } diff --git a/docs/constructs/classes/LambdaFunction.html b/docs/constructs/classes/LambdaFunction.html index 8068e89..af3b3a4 100644 --- a/docs/constructs/classes/LambdaFunction.html +++ b/docs/constructs/classes/LambdaFunction.html @@ -2,7 +2,7 @@ This construct facilitates setting up a Lambda function with various custom options, environment variables, and permissions required to interact with AWS services such as DynamoDB, Cognito, and S3. It supports setting up Lambda functions with pre-defined bundles, custom handlers, and integrations with AWS services. The construct also provides methods to dynamically update the configuration and permissions of the Lambda function after it has been created.

-
const lambdaFunction = new LambdaFunction(this, 'MyLambdaFunction', {
entry: 'path/to/lambda/handler.ts',
handler: 'main',
stageName: 'dev',
table: myDynamoDBTable,
userPool: myCognitoUserPool,
assetBucket: myS3Bucket,
lambdaOptions: {
timeout: Duration.seconds(30),
memorySize: 512,
},
});

// Add additional permissions
lambdaFunction.grantSendEmails();
lambdaFunction.grantTableWrite(); +
const lambdaFunction = new LambdaFunction(this, 'MyLambdaFunction', {
entry: 'path/to/lambda/handler.ts',
handler: 'main',
stageName: 'dev',
table: myDynamoDBTable,
userPool: myCognitoUserPool,
assetBucket: myS3Bucket,
lambdaOptions: {
timeout: Duration.seconds(30),
memorySize: 512,
},
});

// Add additional permissions
lambdaFunction.grantSendEmails();
lambdaFunction.grantTableWrite();

Hierarchy

Index

Constructors

constructor @@ -145,7 +145,7 @@

Returns Stack

Methods

  • Defines an alias for this function.

    The alias will automatically be updated to point to the latest version of the function as it is being updated during a deployment.

    -
    declare const fn: lambda.Function;

    fn.addAlias('Live');

    // Is equivalent to

    new lambda.Alias(this, 'AliasLive', {
    aliasName: 'Live',
    version: fn.currentVersion,
    }); +
    declare const fn: lambda.Function;

    fn.addAlias('Live');

    // Is equivalent to

    new lambda.Alias(this, 'AliasLive', {
    aliasName: 'Live',
    version: fn.currentVersion,
    });

    Parameters

    • aliasName: string

      The name of the alias

      @@ -282,27 +282,27 @@

      Parameters

      • construct: IConstruct

      Returns boolean

  • Check whether the given construct is a Resource

    Parameters

    • construct: IConstruct

    Returns construct is Resource

  • Return the given named metric for this Lambda

    Parameters

    • metricName: string
    • Optionalprops: MetricOptions

    Returns Metric

  • Metric for the number of concurrent executions across all Lambdas

    -

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    max over 5 minutes
    +

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    max over 5 minutes
     
  • Metric for the Duration executing all Lambdas

    -

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    average over 5 minutes
    +

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    average over 5 minutes
     
  • Metric for the number of Errors executing all Lambdas

    -

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    sum over 5 minutes
    +

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    sum over 5 minutes
     
  • Metric for the number of invocations of all Lambdas

    -

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    sum over 5 minutes
    +

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    sum over 5 minutes
     
  • Metric for the number of throttled invocations of all Lambdas

    -

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    sum over 5 minutes
    +

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    sum over 5 minutes
     
  • Metric for the number of unreserved concurrent executions across all Lambdas

    -

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    max over 5 minutes
    +

    Parameters

    • Optionalprops: MetricOptions

    Returns Metric

    max over 5 minutes
     
diff --git a/docs/constructs/index.html b/docs/constructs/index.html index 40ae622..56722cd 100644 --- a/docs/constructs/index.html +++ b/docs/constructs/index.html @@ -55,6 +55,50 @@ +

None. The internal JWKS / well-known-issuer fetches in the JWT authorizers +were the only axios call sites in the Lambda runtime code, and their public +behavior is unchanged. Errors now surface as Error (or a TimeoutError +from AbortSignal.timeout) instead of AxiosError; if you catch errors +inside the authorizer, the message text is similar but the type guard is +different.

+ +

IntegTestUtil.getClient() and IntegTestUtil.getAuthenticatedClient() +previously returned an Axios instance. They now return a small +HttpClient exported from cdk-serverless/tests. The migration is +mechanical:

+
// Before
const client = await test.getAuthenticatedClient('test@example.com');
const response = await client.get('/items');
// response.data is the parsed JSON, response.status is the HTTP status code
const items = response.data.items;

// After
const client = await test.getAuthenticatedClient('test@example.com');
const response = await client.get('/items');
// response.body is the raw string, response.json() parses it, response.ok
// reports 2xx, response.status is the HTTP status code
const items = response.json<{ items: Item[] }>().items; +
+ +

The HttpClient exposes the methods that integration tests in this +ecosystem actually use:

+ +

Configuration accepted by HttpClient and by getClient(config):

+ +

If you relied on axios-specific features (interceptors, defaults, +transformRequest / transformResponse, automatic data parsing), implement +the equivalent in your test code or wrap HttpClient. If your use case +needs richer client features and we should expose them, please open an +issue.

@@ -92,4 +136,4 @@

Authors

Brought to you by Taimos

-
+
diff --git a/docs/lambda/assets/highlight.css b/docs/lambda/assets/highlight.css index c794a8f..72a5329 100644 --- a/docs/lambda/assets/highlight.css +++ b/docs/lambda/assets/highlight.css @@ -15,8 +15,10 @@ --dark-hl-6: #6A9955; --light-hl-7: #0070C1; --dark-hl-7: #4FC1FF; - --light-hl-8: #098658; - --dark-hl-8: #B5CEA8; + --light-hl-8: #267F99; + --dark-hl-8: #4EC9B0; + --light-hl-9: #098658; + --dark-hl-9: #B5CEA8; --light-code-background: #FFFFFF; --dark-code-background: #1E1E1E; } @@ -31,6 +33,7 @@ --hl-6: var(--light-hl-6); --hl-7: var(--light-hl-7); --hl-8: var(--light-hl-8); + --hl-9: var(--light-hl-9); --code-background: var(--light-code-background); } } @@ -44,6 +47,7 @@ --hl-6: var(--dark-hl-6); --hl-7: var(--dark-hl-7); --hl-8: var(--dark-hl-8); + --hl-9: var(--dark-hl-9); --code-background: var(--dark-code-background); } } @@ -57,6 +61,7 @@ --hl-6: var(--light-hl-6); --hl-7: var(--light-hl-7); --hl-8: var(--light-hl-8); + --hl-9: var(--light-hl-9); --code-background: var(--light-code-background); } @@ -70,6 +75,7 @@ --hl-6: var(--dark-hl-6); --hl-7: var(--dark-hl-7); --hl-8: var(--dark-hl-8); + --hl-9: var(--dark-hl-9); --code-background: var(--dark-code-background); } @@ -82,4 +88,5 @@ .hl-6 { color: var(--hl-6); } .hl-7 { color: var(--hl-7); } .hl-8 { color: var(--hl-8); } +.hl-9 { color: var(--hl-9); } pre, code { background: var(--code-background); } diff --git a/docs/lambda/classes/errors.BadRequestError.html b/docs/lambda/classes/errors.BadRequestError.html index b996506..76bf61b 100644 --- a/docs/lambda/classes/errors.BadRequestError.html +++ b/docs/lambda/classes/errors.BadRequestError.html @@ -30,7 +30,7 @@ generated stack trace.

The constructorOpt argument is useful for hiding implementation details of error generation from the user. For instance:

-
function a() {
b();
}

function b() {
c();
}

function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;

// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}

a(); +
function a() {
b();
}

function b() {
c();
}

function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;

// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}

a();

Parameters

  • targetObject: object
  • OptionalconstructorOpt: Function

Returns void

  • Parameters

    • err: Error
    • stackTraces: CallSite[]

    Returns any

    https://v8.dev/docs/stack-trace-api#customizing-stack-traces

    diff --git a/docs/lambda/classes/errors.ForbiddenError.html b/docs/lambda/classes/errors.ForbiddenError.html index fa91675..2e86e61 100644 --- a/docs/lambda/classes/errors.ForbiddenError.html +++ b/docs/lambda/classes/errors.ForbiddenError.html @@ -30,7 +30,7 @@ generated stack trace.

    The constructorOpt argument is useful for hiding implementation details of error generation from the user. For instance:

    -
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a(); +
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a();

    Parameters

    • targetObject: object
    • OptionalconstructorOpt: Function

    Returns void

  • Parameters

    • err: Error
    • stackTraces: CallSite[]

    Returns any

    https://v8.dev/docs/stack-trace-api#customizing-stack-traces

    diff --git a/docs/lambda/classes/errors.HttpError.html b/docs/lambda/classes/errors.HttpError.html index d18074b..54d51b9 100644 --- a/docs/lambda/classes/errors.HttpError.html +++ b/docs/lambda/classes/errors.HttpError.html @@ -31,7 +31,7 @@ generated stack trace.

    The constructorOpt argument is useful for hiding implementation details of error generation from the user. For instance:

    -
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a(); +
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a();

    Parameters

    • targetObject: object
    • OptionalconstructorOpt: Function

    Returns void

  • Parameters

    • err: Error
    • stackTraces: CallSite[]

    Returns any

    https://v8.dev/docs/stack-trace-api#customizing-stack-traces

    diff --git a/docs/lambda/classes/errors.NotFoundError.html b/docs/lambda/classes/errors.NotFoundError.html index 17f1695..7f34c72 100644 --- a/docs/lambda/classes/errors.NotFoundError.html +++ b/docs/lambda/classes/errors.NotFoundError.html @@ -30,7 +30,7 @@ generated stack trace.

    The constructorOpt argument is useful for hiding implementation details of error generation from the user. For instance:

    -
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a(); +
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a();

    Parameters

    • targetObject: object
    • OptionalconstructorOpt: Function

    Returns void

  • Parameters

    • err: Error
    • stackTraces: CallSite[]

    Returns any

    https://v8.dev/docs/stack-trace-api#customizing-stack-traces

    diff --git a/docs/lambda/classes/errors.UnauthenticatedError.html b/docs/lambda/classes/errors.UnauthenticatedError.html index f2055c2..787dfc8 100644 --- a/docs/lambda/classes/errors.UnauthenticatedError.html +++ b/docs/lambda/classes/errors.UnauthenticatedError.html @@ -30,7 +30,7 @@ generated stack trace.

    The constructorOpt argument is useful for hiding implementation details of error generation from the user. For instance:

    -
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a(); +
    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a();

    Parameters

    • targetObject: object
    • OptionalconstructorOpt: Function

    Returns void

+ +

CDK Serverless no longer depends on axios. The library now uses the +platform-native fetch API (stable in Node.js 18+; the Lambda functions +created by this library use Runtime.NODEJS_LATEST, currently Node 22).

+

This removes one third-party runtime dependency from every Lambda bundle that +imports from cdk-serverless/lambda and from every test workspace that +imports from cdk-serverless/tests. It was motivated by repeated axios +security advisories — landing this as a deliberate breaking change so the +fix is permanent rather than chasing CVE upgrades.

+ +

None. The internal JWKS / well-known-issuer fetches in the JWT authorizers +were the only axios call sites in the Lambda runtime code, and their public +behavior is unchanged. Errors now surface as Error (or a TimeoutError +from AbortSignal.timeout) instead of AxiosError; if you catch errors +inside the authorizer, the message text is similar but the type guard is +different.

+ +

IntegTestUtil.getClient() and IntegTestUtil.getAuthenticatedClient() +previously returned an Axios instance. They now return a small +HttpClient exported from cdk-serverless/tests. The migration is +mechanical:

+
// Before
const client = await test.getAuthenticatedClient('test@example.com');
const response = await client.get('/items');
// response.data is the parsed JSON, response.status is the HTTP status code
const items = response.data.items;

// After
const client = await test.getAuthenticatedClient('test@example.com');
const response = await client.get('/items');
// response.body is the raw string, response.json() parses it, response.ok
// reports 2xx, response.status is the HTTP status code
const items = response.json<{ items: Item[] }>().items; +
+ +

The HttpClient exposes the methods that integration tests in this +ecosystem actually use:

+ +

Configuration accepted by HttpClient and by getClient(config):

+ +

If you relied on axios-specific features (interceptors, defaults, +transformRequest / transformResponse, automatic data parsing), implement +the equivalent in your test code or wrap HttpClient. If your use case +needs richer client features and we should expose them, please open an +issue.

@@ -92,4 +136,4 @@

Authors

Brought to you by Taimos

-
+
diff --git a/docs/projen/assets/highlight.css b/docs/projen/assets/highlight.css index e38fdbc..4399cff 100644 --- a/docs/projen/assets/highlight.css +++ b/docs/projen/assets/highlight.css @@ -15,12 +15,14 @@ --dark-hl-6: #6A9955; --light-hl-7: #0070C1; --dark-hl-7: #4FC1FF; - --light-hl-8: #098658; - --dark-hl-8: #B5CEA8; - --light-hl-9: #CD3131; - --dark-hl-9: #F44747; - --light-hl-10: #000000; - --dark-hl-10: #C8C8C8; + --light-hl-8: #267F99; + --dark-hl-8: #4EC9B0; + --light-hl-9: #098658; + --dark-hl-9: #B5CEA8; + --light-hl-10: #CD3131; + --dark-hl-10: #F44747; + --light-hl-11: #000000; + --dark-hl-11: #C8C8C8; --light-code-background: #FFFFFF; --dark-code-background: #1E1E1E; } @@ -37,6 +39,7 @@ --hl-8: var(--light-hl-8); --hl-9: var(--light-hl-9); --hl-10: var(--light-hl-10); + --hl-11: var(--light-hl-11); --code-background: var(--light-code-background); } } @@ -52,6 +55,7 @@ --hl-8: var(--dark-hl-8); --hl-9: var(--dark-hl-9); --hl-10: var(--dark-hl-10); + --hl-11: var(--dark-hl-11); --code-background: var(--dark-code-background); } } @@ -67,6 +71,7 @@ --hl-8: var(--light-hl-8); --hl-9: var(--light-hl-9); --hl-10: var(--light-hl-10); + --hl-11: var(--light-hl-11); --code-background: var(--light-code-background); } @@ -82,6 +87,7 @@ --hl-8: var(--dark-hl-8); --hl-9: var(--dark-hl-9); --hl-10: var(--dark-hl-10); + --hl-11: var(--dark-hl-11); --code-background: var(--dark-code-background); } @@ -96,4 +102,5 @@ .hl-8 { color: var(--hl-8); } .hl-9 { color: var(--hl-9); } .hl-10 { color: var(--hl-10); } +.hl-11 { color: var(--hl-11); } pre, code { background: var(--code-background); } diff --git a/docs/projen/index.html b/docs/projen/index.html index 61fcf26..baafa6f 100644 --- a/docs/projen/index.html +++ b/docs/projen/index.html @@ -55,6 +55,50 @@
+

None. The internal JWKS / well-known-issuer fetches in the JWT authorizers +were the only axios call sites in the Lambda runtime code, and their public +behavior is unchanged. Errors now surface as Error (or a TimeoutError +from AbortSignal.timeout) instead of AxiosError; if you catch errors +inside the authorizer, the message text is similar but the type guard is +different.

+ +

IntegTestUtil.getClient() and IntegTestUtil.getAuthenticatedClient() +previously returned an Axios instance. They now return a small +HttpClient exported from cdk-serverless/tests. The migration is +mechanical:

+
// Before
const client = await test.getAuthenticatedClient('test@example.com');
const response = await client.get('/items');
// response.data is the parsed JSON, response.status is the HTTP status code
const items = response.data.items;

// After
const client = await test.getAuthenticatedClient('test@example.com');
const response = await client.get('/items');
// response.body is the raw string, response.json() parses it, response.ok
// reports 2xx, response.status is the HTTP status code
const items = response.json<{ items: Item[] }>().items; +
+ +

The HttpClient exposes the methods that integration tests in this +ecosystem actually use:

+ +

Configuration accepted by HttpClient and by getClient(config):

+ +

If you relied on axios-specific features (interceptors, defaults, +transformRequest / transformResponse, automatic data parsing), implement +the equivalent in your test code or wrap HttpClient. If your use case +needs richer client features and we should expose them, please open an +issue.

@@ -92,4 +136,4 @@

Authors

Brought to you by Taimos

-
+
diff --git a/docs/projen/interfaces/ServerlessProjectOptions.html b/docs/projen/interfaces/ServerlessProjectOptions.html index 3bf5545..0e37e64 100644 --- a/docs/projen/interfaces/ServerlessProjectOptions.html +++ b/docs/projen/interfaces/ServerlessProjectOptions.html @@ -297,7 +297,7 @@
cdkAssert?: boolean

Warning: NodeJS only. Install the @aws-cdk/assert library?

-
- will be included by default for AWS CDK >= 1.0.0 < 2.0.0
+
- will be included by default for AWS CDK >= 1.0.0 < 2.0.0
 

The @aws-cdk/assert library is deprecated in favor of @@ -305,7 +305,7 @@

cdkAssertions?: boolean

Install the assertions library?

Only needed for CDK 1.x. If using CDK 2.x then assertions is already included in 'aws-cdk-lib'

-
- will be included by default for AWS CDK >= 1.111.0 < 2.0.0
+
- will be included by default for AWS CDK >= 1.111.0 < 2.0.0
 
cdkCliVersion?: string

Version range of the AWS CDK CLI to depend on.

@@ -483,7 +483,7 @@
entrypointTypes?: string

The .d.ts file that includes the type declarations for this module.

-
- .d.ts file derived from the project's entrypoint (usually lib/index.d.ts)
+
- .d.ts file derived from the project's entrypoint (usually lib/index.d.ts)
 
eslint?: boolean

Setup eslint.

@@ -839,7 +839,7 @@
readme?: SampleReadmeProps

The README setup.

-
- { filename: 'README.md', contents: '# replace this' }
+
- { filename: 'README.md', contents: '# replace this' }
 
"{ filename: 'readme.md', contents: '# title' }"
diff --git a/package-lock.json b/package-lock.json
index fae07a1..90ceb8c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,6 @@
       "license": "Apache-2.0",
       "dependencies": {
         "@types/aws-lambda": "^8.10.161",
-        "axios": "^1.15.0",
         "constructs": "^10.5.1",
         "date-fns": "^4.1.0",
         "js-yaml": "^4.1.1",
@@ -4969,12 +4968,6 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "license": "MIT"
-    },
     "node_modules/available-typed-arrays": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -5421,17 +5414,6 @@
         "node": ">= 6"
       }
     },
-    "node_modules/axios": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
-      "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
-      "license": "MIT",
-      "dependencies": {
-        "follow-redirects": "^1.15.11",
-        "form-data": "^4.0.5",
-        "proxy-from-env": "^2.1.0"
-      }
-    },
     "node_modules/babel-jest": {
       "version": "29.7.0",
       "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -5786,6 +5768,7 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
       "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "es-errors": "^1.3.0",
@@ -5983,18 +5966,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "license": "MIT",
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
     "node_modules/commit-and-tag-version": {
       "version": "12.7.1",
       "resolved": "https://registry.npmjs.org/commit-and-tag-version/-/commit-and-tag-version-12.7.1.tgz",
@@ -6657,15 +6628,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
     "node_modules/detect-indent": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
@@ -6840,6 +6802,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
       "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.1",
@@ -7013,6 +6976,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
       "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -7022,6 +6986,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
       "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -7031,6 +6996,7 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
       "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "es-errors": "^1.3.0"
@@ -7043,6 +7009,7 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
       "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "es-errors": "^1.3.0",
@@ -7823,26 +7790,6 @@
       "dev": true,
       "license": "ISC"
     },
-    "node_modules/follow-redirects": {
-      "version": "1.15.11",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
-      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://github.com/sponsors/RubenVerborgh"
-        }
-      ],
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.0"
-      },
-      "peerDependenciesMeta": {
-        "debug": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/for-each": {
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -7859,22 +7806,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/form-data": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
-      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
-      "license": "MIT",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "es-set-tostringtag": "^2.1.0",
-        "hasown": "^2.0.2",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -7901,6 +7832,7 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
       "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
       "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -7971,6 +7903,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
       "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.2",
@@ -8055,6 +7988,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
       "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "dunder-proto": "^1.0.1",
@@ -8260,6 +8194,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
       "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -8363,6 +8298,7 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
       "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -8375,6 +8311,7 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
       "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "has-symbols": "^1.0.3"
@@ -8400,6 +8337,7 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
       "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "function-bind": "^1.1.2"
@@ -11340,6 +11278,7 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
       "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -11565,27 +11504,6 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "license": "MIT",
-      "dependencies": {
-        "mime-db": "1.52.0"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
     "node_modules/mimic-fn": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -13434,15 +13352,6 @@
         "node": ">= 6"
       }
     },
-    "node_modules/proxy-from-env": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
-      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
diff --git a/package.json b/package.json
index 0508fc1..f245085 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,6 @@
   },
   "dependencies": {
     "@types/aws-lambda": "^8.10.161",
-    "axios": "^1.15.0",
     "constructs": "^10.5.1",
     "date-fns": "^4.1.0",
     "js-yaml": "^4.1.1",
diff --git a/specs/remove-axios-dependency.md b/specs/remove-axios-dependency.md
new file mode 100644
index 0000000..d63e292
--- /dev/null
+++ b/specs/remove-axios-dependency.md
@@ -0,0 +1,240 @@
+# Spec: Remove the `axios` Runtime Dependency
+
+Status: Proposed
+Author: open-constructs
+Target: `cdk-serverless@next` (minor or major release — see Breaking Changes)
+
+## Summary
+
+Drop `axios` as a runtime dependency of `cdk-serverless` by replacing every
+call site with the platform-native `fetch` API. `axios` is currently declared
+in `dependencies` (`^1.15.0`, `package.json:77`) and therefore ships into
+every Lambda bundle that pulls in `cdk-serverless/lambda`, and into every
+test workspace that pulls in `cdk-serverless/tests`.
+
+The Node.js `fetch` global has been stable since Node 21 and available since
+Node 18. The Lambda functions created by this library use
+`Runtime.NODEJS_LATEST` (`src/constructs/func.ts:145`), which today resolves
+to Node 22, so the runtime requirement is already met.
+
+## Motivation
+
+1. **Smaller Lambda bundles.** `axios` is bundled into the JWT-verifying code
+   path that ships with every cdk-serverless Lambda using `auth.ts`. The
+   minified+gzipped cost of axios is ~15 KB; with `fetch` that drops to 0.
+2. **Smaller dependency surface.** One fewer third-party runtime dependency
+   to track for supply-chain and CVE management. axios has had several
+   security advisories (CVE-2023-45857, CVE-2024-39338, etc.) which forced
+   downstream upgrades in this library before.
+3. **Cleaner peer story.** With `axios` gone, the `dependencies` section
+   only contains things consumers genuinely need at runtime. `@types/aws-lambda`
+   stays for typed handler shapes, and we keep the small set of crypto/JWT
+   libs.
+4. **Alignment with platform.** AWS Lambda's managed Node runtime ships
+   `fetch` and `undici` natively; using them avoids re-shipping HTTP client
+   code that the runtime already provides.
+
+## Non-Goals
+
+- Replacing `jsonwebtoken` / `jwk-to-pem`. These do crypto, not HTTP, and
+  there is no equivalent built-in.
+- Replacing the AWS SDK clients used in `IntegTestUtil` (Cognito, DynamoDB).
+- Adding a new HTTP abstraction layer. `fetch` is the abstraction.
+- Changing the JWT verification flow itself (token discovery, JWKS caching
+  behavior, claims shape).
+
+## Current Usage
+
+`axios` is imported from exactly two source files:
+
+### 1. `src/lambda/auth.ts` — runtime (ships in Lambda bundles)
+
+```ts
+import Axios from 'axios';
+// ...
+const publicKeys: PublicKeys =
+  (await Axios.get(jwksUrl)).data;                    // line 47
+// ...
+const issuerMetadata =
+  (await Axios.get(wellKnownUri)).data;           // line 65
+```
+
+Both calls are plain GETs against well-known JSON endpoints (the issuer's
+`/.well-known` discovery document and the JWKS URL). Response is parsed as
+JSON. No retry, no interceptors, no custom transports — trivially mappable
+to `fetch(url).then(r => r.json())`.
+
+### 2. `src/tests/integ-test-util.ts` — test utility (consumer-facing API)
+
+```ts
+import { Axios, AxiosRequestConfig, HttpStatusCode } from 'axios';
+
+public getClient(config?: AxiosRequestConfig) { return new Axios({...}); }   // line 78
+public async getAuthenticatedClient(email, password?, config?) { ... }       // line 99
+protected async loginUser(email, password) {
+  const cognitoClient = new Axios({ baseURL: '...' });                       // line 204
+  const auth = await cognitoClient.post('/', JSON.stringify({...}), {...});
+}
+```
+
+This file is exported via `src/tests/index.ts` and is part of the **public
+API** of `cdk-serverless/tests`. `getClient()` and `getAuthenticatedClient()`
+both **return an `Axios` instance** and accept an `AxiosRequestConfig`.
+Consumers writing integration tests call `.get()` / `.post()` etc. on the
+returned client.
+
+## Proposed Changes
+
+### Phase 1 — `src/lambda/auth.ts` (no breaking change)
+
+Replace the two `Axios.get` calls with `fetch`. The function signatures and
+exported types in `auth.ts` do not expose axios, so this is a pure internal
+refactor.
+
+```ts
+const getPublicKeys = async (jwksUrl: string): Promise => {
+  if (!cacheKeys) {
+    const res = await fetch(jwksUrl);
+    if (!res.ok) throw new Error(`JWKS fetch failed: ${res.status}`);
+    const publicKeys = (await res.json()) as PublicKeys;
+    cacheKeys = publicKeys.keys.reduce(/* ... unchanged ... */);
+  }
+  return cacheKeys;
+};
+
+const getJwksUri = async (discoveryUri: string, jwksUri?: string): Promise => {
+  if (jwksUri) return jwksUri;
+  const res = await fetch(`${discoveryUri}/.well-known`);
+  if (!res.ok) throw new Error(`Issuer metadata fetch failed: ${res.status}`);
+  const issuerMetadata = (await res.json()) as IssuerMetadata;
+  if (!issuerMetadata.jwks_uri) throw new Error('Issuer does not offer JWKS endpoint');
+  return issuerMetadata.jwks_uri;
+};
+```
+
+Behavior delta vs. axios:
+- axios throws on non-2xx by default; `fetch` does not. We add an explicit
+  `if (!res.ok)` check at each call site to preserve the prior behavior.
+- axios JSON-parses based on response `Content-Type`; we call `res.json()`
+  explicitly. The two endpoints (JWKS, well-known) are spec-defined to
+  return JSON.
+- Error shape changes (no `AxiosError`). Callers in this file already wrap
+  with try/catch and log via `lambda-log`, so the visible behavior is the
+  same.
+
+### Phase 2 — `src/tests/integ-test-util.ts` (breaking change for tests/integ consumers)
+
+Two options, choose one before implementation:
+
+**Option A — Replace return type with a thin `fetch`-based client (breaking).**
+Introduce a small internal `HttpClient` class with the methods consumers
+actually use (`get`, `post`, optional `put`/`delete`/`patch`), backed by
+`fetch`. Update the exported signatures:
+
+```ts
+public getClient(config?: HttpClientConfig): HttpClient { ... }
+public async getAuthenticatedClient(email, password?, config?): Promise { ... }
+```
+
+`HttpClientConfig` covers the fields actually used today: `baseURL`,
+`headers`, `transformRequest`, `transformResponse`. The `loginUser` flow
+is rewritten internally to call `fetch` directly against the Cognito
+endpoint.
+
+This is the cleanest end state. It is a breaking change for anyone
+chaining axios-specific methods (`interceptors`, `defaults`, etc.) on
+the returned client — but the existing public surface in this repo only
+uses `.get`/`.post` with `baseURL` and headers.
+
+**Option B — Keep axios in `IntegTestUtil`, move to `devDependencies`/`peerDependencies`.**
+Remove axios only from `auth.ts`. Move axios from `dependencies` to
+`peerDependencies` (optional) in `package.json` so it is no longer pulled
+in transitively by consumers who only use the Lambda runtime helpers.
+Consumers who use `cdk-serverless/tests` add `axios` to their own
+`devDependencies` (standard for test utilities).
+
+This is **lower risk** and still removes axios from production Lambda
+bundles, but leaves an axios dep in the picture for test consumers.
+
+**Recommendation: Option A.** The public surface of the integ-test client
+is small, the migration is mechanical (replace `client.get('/x').then(r => r.data)`
+with `client.get('/x').then(r => r.json())`), and the end state — zero
+axios — is clean. Bundle as a `feat!`/major in the next breaking release.
+
+### Phase 3 — Cleanup
+
+- Remove `axios` from `dependencies` in `package.json`.
+- Remove `'axios'` from `deps` in `.projenrc.ts:20`.
+- Run `npx projen` to regenerate `package.json` and lockfile.
+- Verify no transitive axios in the produced Lambda bundles via
+  `esbuild --analyze` or `npm ls axios` showing empty.
+
+## API Impact
+
+| Symbol | Before | After (Option A) | Breaking? |
+|---|---|---|---|
+| `auth.ApiGatewayv1JwtAuthorizer` and friends | — | — | No (internal only) |
+| `tests.IntegTestUtil#getClient` | returns `Axios` | returns `HttpClient` | **Yes** |
+| `tests.IntegTestUtil#getAuthenticatedClient` | returns `Promise` | returns `Promise` | **Yes** |
+| `tests.IntegTestUtil#getClient(config)` | `AxiosRequestConfig` | `HttpClientConfig` (subset) | **Yes** |
+
+Migration note for consumers of `IntegTestUtil`:
+- `client.get(path).then(r => r.data)` → `client.get(path).then(r => r.json())`
+- `client.post(path, JSON.stringify(body), { headers })` → same shape, but
+  the response shape differs (no `.data`, no `.status` — use `.ok`, `.status`,
+  `.json()`).
+
+## Testing
+
+1. **Unit tests** — exercise `auth.ts` against a local HTTP server that
+   serves a mock `/.well-known` document and JWKS. Confirm:
+   - Successful key fetch and PEM conversion (cache populated once).
+   - 5xx from JWKS endpoint surfaces as an error (existing behavior).
+   - Missing `jwks_uri` in metadata throws.
+2. **Lambda test utilities** — update existing `lambda-test-utils.ts` tests
+   if any depend on the axios import path (none today, but verify).
+3. **Integ tests** — re-run any integ test in this repo that uses
+   `IntegTestUtil` against a real Cognito user pool to confirm
+   `getClient`/`getAuthenticatedClient`/`loginUser` paths work with the
+   new client.
+4. **Bundle size check** — run `esbuild` over `src/lambda/handler.ts` before
+   and after to record the delta. Include the number in the PR description.
+
+## Alternatives Considered
+
+1. **`node-fetch` or `undici` directly.** `node-fetch` is a polyfill we
+   don't need; `undici` is what `fetch` is built on, but using it directly
+   adds a runtime dep without benefit. Native `fetch` is the right call.
+2. **Keep axios, pin tighter, add interceptors for fetch-like ergonomics.**
+   Doesn't address bundle size or supply-chain motivations.
+3. **Vendor a tiny `http`/`https` wrapper.** Works on older Node, but the
+   minimum supported runtime here (`NODEJS_LATEST`) makes `fetch` strictly
+   better.
+
+## Rollout
+
+1. Land Phase 1 (`auth.ts` only) as a non-breaking `chore:` or `refactor:`
+   commit. This already removes axios from the **Lambda runtime** code path
+   even before the API is changed.
+2. Land Phase 2 (`IntegTestUtil`) as a `feat!:` commit gated to a major
+   bump. Document the migration in the release notes / CHANGELOG.
+3. Land Phase 3 (`package.json` / `.projenrc.ts` cleanup) together with
+   Phase 2.
+
+If we choose Option B instead, Phase 2 becomes a `chore:` (move axios to
+peerDependencies, document in README) and there is no major bump.
+
+## Open Questions
+
+1. **Option A vs. Option B** — accept a breaking change in `IntegTestUtil`,
+   or keep axios there and just move it to `peerDependencies`? (Recommendation
+   above is A.)
+2. **Cache invalidation on fetch failure** — should we clear `cacheKeys` if
+   a JWKS rotation makes a previously-cached `kid` invalid? Out of scope for
+   this spec, but worth filing as a follow-up if not already tracked.
+3. **Timeout behavior** — `fetch` has no default timeout. Should we wrap
+   the JWKS/well-known fetches with `AbortSignal.timeout(5000)`? axios also
+   had no default timeout in our usage, so behavior parity is preserved
+   without it — but adding one is cheap and prevents a hang on a dead JWKS
+   host from blocking a cold start. Recommendation: add
+   `AbortSignal.timeout(5000)`.
diff --git a/src/lambda/auth.ts b/src/lambda/auth.ts
index c462571..e7f8a56 100644
--- a/src/lambda/auth.ts
+++ b/src/lambda/auth.ts
@@ -1,5 +1,4 @@
 import { env } from 'process';
-import Axios from 'axios';
 import { verify, JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
 import jwkToPem from 'jwk-to-pem';
 import logger from 'lambda-log';
@@ -42,9 +41,17 @@ interface MapOfKidToPublicKey {
 
 let cacheKeys: MapOfKidToPublicKey | undefined;
 
+const fetchJson = async (url: string): Promise => {
+  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
+  if (!res.ok) {
+    throw new Error(`Request to ${url} failed: ${res.status} ${res.statusText}`);
+  }
+  return (await res.json()) as T;
+};
+
 const getPublicKeys = async (jwksUrl: string): Promise => {
   if (!cacheKeys) {
-    const publicKeys: PublicKeys = (await Axios.get(jwksUrl)).data;
+    const publicKeys = await fetchJson(jwksUrl);
     cacheKeys = publicKeys.keys.reduce((agg, current) => {
       const pem = jwkToPem(current as jwkToPem.JWK);
       agg[current.kid] = { instance: current, pem };
@@ -62,7 +69,7 @@ const getJwksUri = async (discoveryUri: string, jwksUri?: string): Promise(wellKnownUri)).data;
+  const issuerMetadata = await fetchJson(wellKnownUri);
   if (!issuerMetadata.jwks_uri) {
     throw new Error('Issuer does not offer JWKS endpoint');
   }
diff --git a/src/tests/http-client.ts b/src/tests/http-client.ts
new file mode 100644
index 0000000..2aa09cf
--- /dev/null
+++ b/src/tests/http-client.ts
@@ -0,0 +1,115 @@
+/**
+ * A minimal HTTP client used by IntegTestUtil, backed by the native `fetch`
+ * API. It exists to keep the integration-test API stable while removing the
+ * `axios` runtime dependency.
+ */
+
+export interface HttpClientConfig {
+  /**
+   * Base URL prepended to relative paths passed to client methods.
+   */
+  readonly baseURL?: string;
+
+  /**
+   * Default headers applied to every request issued by this client.
+   * Per-request headers override these on collision.
+   */
+  readonly headers?: Record;
+}
+
+export interface HttpClientRequestOptions {
+  /**
+   * Per-request headers, merged on top of the client's default headers.
+   */
+  readonly headers?: Record;
+}
+
+/**
+ * Response wrapper returned by every `HttpClient` request method.
+ */
+export class HttpClientResponse {
+  public readonly status: number;
+  public readonly ok: boolean;
+  public readonly headers: Headers;
+  public readonly body: string;
+
+  constructor(status: number, ok: boolean, headers: Headers, body: string) {
+    this.status = status;
+    this.ok = ok;
+    this.headers = headers;
+    this.body = body;
+  }
+
+  /**
+   * Parse the response body as JSON. Throws if the body is not valid JSON.
+   */
+  public json(): T {
+    return JSON.parse(this.body) as T;
+  }
+}
+
+export class HttpClient {
+  private readonly baseURL?: string;
+  private readonly defaultHeaders: Record;
+
+  constructor(config: HttpClientConfig = {}) {
+    this.baseURL = config.baseURL;
+    this.defaultHeaders = { ...(config.headers ?? {}) };
+  }
+
+  public get(path: string, options?: HttpClientRequestOptions): Promise {
+    return this.request('GET', path, undefined, options);
+  }
+
+  public delete(path: string, options?: HttpClientRequestOptions): Promise {
+    return this.request('DELETE', path, undefined, options);
+  }
+
+  public post(path: string, body?: unknown, options?: HttpClientRequestOptions): Promise {
+    return this.request('POST', path, body, options);
+  }
+
+  public put(path: string, body?: unknown, options?: HttpClientRequestOptions): Promise {
+    return this.request('PUT', path, body, options);
+  }
+
+  public patch(path: string, body?: unknown, options?: HttpClientRequestOptions): Promise {
+    return this.request('PATCH', path, body, options);
+  }
+
+  private async request(method: string, path: string, body: unknown, options?: HttpClientRequestOptions): Promise {
+    const url = this.resolveUrl(path);
+    const headers: Record = {
+      ...this.defaultHeaders,
+      ...(options?.headers ?? {}),
+    };
+
+    let requestBody: BodyInit | undefined;
+    if (body !== undefined && body !== null) {
+      if (typeof body === 'string') {
+        requestBody = body;
+      } else {
+        requestBody = JSON.stringify(body);
+        if (!Object.keys(headers).some((h) => h.toLowerCase() === 'content-type')) {
+          headers['Content-Type'] = 'application/json';
+        }
+      }
+    }
+
+    const res = await fetch(url, { method, headers, body: requestBody });
+    const responseBody = await res.text();
+    return new HttpClientResponse(res.status, res.ok, res.headers, responseBody);
+  }
+
+  private resolveUrl(path: string): string {
+    if (!this.baseURL) {
+      return path;
+    }
+    if (/^https?:\/\//i.test(path)) {
+      return path;
+    }
+    const base = this.baseURL.endsWith('/') ? this.baseURL.slice(0, -1) : this.baseURL;
+    const suffix = path.startsWith('/') ? path : `/${path}`;
+    return `${base}${suffix}`;
+  }
+}
diff --git a/src/tests/index.ts b/src/tests/index.ts
index 66d8bc5..d10ea9d 100644
--- a/src/tests/index.ts
+++ b/src/tests/index.ts
@@ -1,2 +1,3 @@
+export * from './http-client';
 export * from './integ-test-util';
 export * from './lambda-test-utils';
\ No newline at end of file
diff --git a/src/tests/integ-test-util.ts b/src/tests/integ-test-util.ts
index 45d8245..c52e8ca 100644
--- a/src/tests/integ-test-util.ts
+++ b/src/tests/integ-test-util.ts
@@ -1,10 +1,12 @@
 import { AdminAddUserToGroupCommand, AdminCreateUserCommand, AdminDeleteUserCommand, AdminSetUserPasswordCommand, CognitoIdentityProviderClient, MessageActionType } from '@aws-sdk/client-cognito-identity-provider';
 import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
 import { DeleteCommand, DynamoDBDocumentClient, NativeAttributeValue } from '@aws-sdk/lib-dynamodb';
-import { Axios, AxiosRequestConfig, HttpStatusCode } from 'axios';
 import { decode, JwtPayload } from 'jsonwebtoken';
+import { HttpClient, HttpClientConfig } from './http-client';
 import { CFN_OUTPUT_SUFFIX_AUTH_IDENTITYPOOL_ID, CFN_OUTPUT_SUFFIX_AUTH_USERPOOLID, CFN_OUTPUT_SUFFIX_AUTH_USERPOOL_CLIENTID, CFN_OUTPUT_SUFFIX_DATASTORE_TABLENAME, CFN_OUTPUT_SUFFIX_RESTAPI_URL } from '../shared/outputs';
 
+export { HttpClient, HttpClientConfig, HttpClientResponse } from './http-client';
+
 export interface IntegTestUtilOptions {
 
   readonly region: string;
@@ -75,28 +77,14 @@ export class IntegTestUtil {
 
   // AUTH
 
-  public getClient(config?: AxiosRequestConfig) {
-    return new Axios({
+  public getClient(config?: HttpClientConfig): HttpClient {
+    return new HttpClient({
       baseURL: this.options.apiOptions?.baseURL,
-      transformResponse: (data) => {
-        try {
-          return JSON.parse(data);
-        } catch (error) {
-          return data;
-        }
-      },
-      transformRequest: (data) => {
-        try {
-          return JSON.stringify(data);
-        } catch (error) {
-          return data;
-        }
-      },
       ...config,
     });
   }
 
-  public async getAuthenticatedClient(email: string, password?: string, config?: AxiosRequestConfig) {
+  public async getAuthenticatedClient(email: string, password?: string, config?: HttpClientConfig): Promise {
     if (!this.apiTokens[email]) {
       if (!password) {
         throw new Error('No password provided; You can only leave password blank for users created by this utility');
@@ -104,11 +92,11 @@ export class IntegTestUtil {
       await this.loginUser(email, password);
     }
     return this.getClient({
+      ...config,
       headers: {
         Authorization: `Bearer ${this.apiTokens[email].token}`,
         ...(config?.headers ?? {}),
       },
-      ...config,
     });
   }
 
@@ -201,29 +189,29 @@ export class IntegTestUtil {
     if (!this.options.authOptions?.userPoolClientId) {
       throw new Error('No userPoolClientId configured');
     }
-    const cognitoClient = new Axios({
+    const cognitoClient = new HttpClient({
       baseURL: `https://cognito-idp.${this.options.region}.amazonaws.com/`,
     });
-    const auth = await cognitoClient.post('/', JSON.stringify({
+    const auth = await cognitoClient.post('/', {
       AuthParameters: {
         USERNAME: email,
         PASSWORD: password,
       },
       AuthFlow: 'USER_PASSWORD_AUTH',
       ClientId: this.options.authOptions.userPoolClientId,
-    }), {
+    }, {
       headers: {
         'Content-Type': 'application/x-amz-json-1.1',
         'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth',
       },
     });
-    if (auth.status !== HttpStatusCode.Ok) {
+    if (!auth.ok) {
       throw new Error(`Failed to authenticate user ${email}`);
     }
-    const token = JSON.parse(auth.data).AuthenticationResult.IdToken;
+    const token = (await auth.json<{ AuthenticationResult: { IdToken: string } }>()).AuthenticationResult.IdToken;
     this.apiTokens[email] = {
       token,
-      payload: decode(this.apiTokens[email].token) as JwtPayload,
+      payload: decode(token) as JwtPayload,
     };
   }
 }
\ No newline at end of file