diff --git a/examples/snippets/event-handler/http/advanced_fine_grained_responses.ts b/examples/snippets/event-handler/http/advanced_fine_grained_responses.ts index 009907c59e..8e8df435d8 100644 --- a/examples/snippets/event-handler/http/advanced_fine_grained_responses.ts +++ b/examples/snippets/event-handler/http/advanced_fine_grained_responses.ts @@ -24,7 +24,7 @@ app.get('/todos', async () => { }); app.post('/todos', async ({ req }) => { - const body = await req.json(); + const body = (await req.json()) as { title: string }; const todo = await createTodo(body.title); return new Response(JSON.stringify(todo), { diff --git a/examples/snippets/parameters/customProviderVault.ts b/examples/snippets/parameters/customProviderVault.ts index 5c2e02a6d9..1d509baebf 100644 --- a/examples/snippets/parameters/customProviderVault.ts +++ b/examples/snippets/parameters/customProviderVault.ts @@ -84,7 +84,9 @@ class HashiCorpVaultProvider extends BaseProvider { if (!res.ok) { throw new GetParameterError(`Failed to fetch secret ${res.statusText}`); } - const response = await res.json(); + const response = (await res.json()) as { + data: { data: Record }; + }; return response.data.data; } diff --git a/package-lock.json b/package-lock.json index ccddf17f65..fd8f79d6b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "typedoc": "^0.28.19", "typedoc-plugin-missing-exports": "^4.1.3", "typescript": "^6.0.3", + "undici-types": "^7.24.6", "vitest": "^4.1.2" }, "engines": { diff --git a/package.json b/package.json index d1c7d7d10d..c59e89c98c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "typedoc": "^0.28.19", "typedoc-plugin-missing-exports": "^4.1.3", "typescript": "^6.0.3", + "undici-types": "^7.24.6", "vitest": "^4.1.2" }, "lint-staged": { diff --git a/packages/event-handler/src/http/converters.ts b/packages/event-handler/src/http/converters.ts index cf525a4751..863402cf50 100644 --- a/packages/event-handler/src/http/converters.ts +++ b/packages/event-handler/src/http/converters.ts @@ -8,6 +8,7 @@ import type { APIGatewayProxyResult, APIGatewayProxyStructuredResultV2, } from 'aws-lambda'; +import type { BodyInit } from 'undici-types'; import type { ExtendedAPIGatewayProxyResult, ExtendedAPIGatewayProxyResultBody, diff --git a/packages/event-handler/tests/e2e/httpRouter.test.FunctionCode.ts b/packages/event-handler/tests/e2e/httpRouter.test.FunctionCode.ts index 4198724150..4242e2b6c2 100644 --- a/packages/event-handler/tests/e2e/httpRouter.test.FunctionCode.ts +++ b/packages/event-handler/tests/e2e/httpRouter.test.FunctionCode.ts @@ -1,3 +1,4 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { Router } from '@aws-lambda-powertools/event-handler/http'; import type { Context } from 'aws-lambda'; import { binaryRouter } from './routers/binaryRouter.js'; @@ -27,7 +28,7 @@ const app = new Router() // Request body parsing and headers app.post('/echo', async ({ req }) => { - const body = await req.json(); + const body = (await req.json()) as JSONValue; const contentType = req.headers.get('content-type'); const customHeader = req.headers.get('x-custom-header'); const multiHeader = req.headers.get('x-multi-header'); diff --git a/packages/event-handler/tests/e2e/httpRouter.test.ts b/packages/event-handler/tests/e2e/httpRouter.test.ts index af3d5f3dd3..ef3c0326d3 100644 --- a/packages/event-handler/tests/e2e/httpRouter.test.ts +++ b/packages/event-handler/tests/e2e/httpRouter.test.ts @@ -56,7 +56,7 @@ describe('REST Event Handler E2E tests', () => { // Act // Prepare const response = await fetch(`${apiUrl}/methods`); - const data = await response.json(); + const data = (await response.json()) as { method: string }; // Assess @@ -73,7 +73,7 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch(`${apiUrl}/methods`, { method: 'POST', }); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('POST'); @@ -84,7 +84,7 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch(`${apiUrl}/methods`, { method: 'PUT', }); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('PUT'); @@ -95,7 +95,7 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch(`${apiUrl}/methods`, { method: 'PATCH', }); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('PATCH'); @@ -106,7 +106,7 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch(`${apiUrl}/methods`, { method: 'DELETE', }); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('DELETE'); @@ -132,7 +132,7 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch(`${apiUrl}/methods`, { method: 'OPTIONS', }); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('OPTIONS'); @@ -146,7 +146,10 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/users/${userId}`); - const data = await response.json(); + const data = (await response.json()) as { + userId: string; + postId: string; + }; expect(response.status).toBe(200); expect(response.headers.get('content-type')).toContain( @@ -164,7 +167,10 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch( `${apiUrl}/params/users/${userId}/posts/${postId}` ); - const data = await response.json(); + const data = (await response.json()) as { + userId: string; + postId: string; + }; expect(response.status).toBe(200); expect(data.userId).toBe(userId); @@ -178,7 +184,10 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/users/${encodedUserId}`); - const data = await response.json(); + const data = (await response.json()) as { + userId: string; + postId: string; + }; expect(response.status).toBe(200); expect(data.userId).toBe(userId); @@ -191,7 +200,10 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/users/${encodedUserId}`); - const data = await response.json(); + const data = (await response.json()) as { + userId: string; + postId: string; + }; expect(response.status).toBe(200); expect(data.userId).toBe(userId); @@ -205,7 +217,11 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/search?q=${searchQuery}`); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBe(searchQuery); @@ -220,7 +236,11 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch( `${apiUrl}/params/search?q=${searchQuery}&limit=${limit}` ); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBe(searchQuery); @@ -236,7 +256,11 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch( `${apiUrl}/params/search?q=${searchQuery}&filter=${filters[0]}&filter=${filters[1]}` ); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBe(searchQuery); @@ -246,7 +270,11 @@ describe('REST Event Handler E2E tests', () => { it('handles missing query parameters', async () => { // Prepare const response = await fetch(`${apiUrl}/params/search`); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBeNull(); @@ -261,7 +289,11 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/search?q=${encodedQuery}`); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBe(searchQuery); @@ -274,7 +306,11 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/search?q=${encodedQuery}`); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBe(searchQuery); @@ -286,7 +322,11 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/search?q=&limit=${limit}`); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBe(''); @@ -296,7 +336,11 @@ describe('REST Event Handler E2E tests', () => { it('handles single-value array parameter', async () => { // Act const response = await fetch(`${apiUrl}/params/search?filter=active`); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.filters).toEqual(['active']); @@ -307,7 +351,11 @@ describe('REST Event Handler E2E tests', () => { it('returns 400 for bad request errors', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/400`); - const data = await response.json(); + const data = (await response.json()) as { + error: string; + message: string; + custom: boolean; + }; expect(response.status).toBe(400); expect(response.headers.get('content-type')).toContain( @@ -321,7 +369,11 @@ describe('REST Event Handler E2E tests', () => { it('returns 401 for unauthorized errors', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/401`); - const data = await response.json(); + const data = (await response.json()) as { + statusCode: number; + error: string; + message: string; + }; expect(response.status).toBe(401); expect(data.statusCode).toBe(401); @@ -332,7 +384,11 @@ describe('REST Event Handler E2E tests', () => { it('returns 403 for forbidden errors', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/403`); - const data = await response.json(); + const data = (await response.json()) as { + statusCode: number; + error: string; + message: string; + }; expect(response.status).toBe(403); expect(data.statusCode).toBe(403); @@ -343,7 +399,11 @@ describe('REST Event Handler E2E tests', () => { it('returns 404 for not found errors', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/404`); - const data = await response.json(); + const data = (await response.json()) as { + statusCode: number; + error: string; + message: string; + }; // Route exists and throws NotFoundError, which is caught by custom notFound handler expect(response.status).toBe(404); @@ -355,7 +415,11 @@ describe('REST Event Handler E2E tests', () => { it('returns 405 for method not allowed errors', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/405`); - const data = await response.json(); + const data = (await response.json()) as { + statusCode: number; + error: string; + message: string; + }; expect(response.status).toBe(405); expect(data.statusCode).toBe(405); @@ -366,7 +430,11 @@ describe('REST Event Handler E2E tests', () => { it('returns 500 for internal server errors', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/500`); - const data = await response.json(); + const data = (await response.json()) as { + statusCode: number; + error: string; + message: string; + }; expect(response.status).toBe(500); expect(data.statusCode).toBe(500); @@ -377,7 +445,11 @@ describe('REST Event Handler E2E tests', () => { it('returns 500 for generic errors', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/generic`); - const data = await response.json(); + const data = (await response.json()) as { + statusCode: number; + error: string; + message: string; + }; expect(response.status).toBe(500); expect(data.statusCode).toBe(500); @@ -388,7 +460,11 @@ describe('REST Event Handler E2E tests', () => { it('applies custom error handler for specific error type', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/custom`); - const data = await response.json(); + const data = (await response.json()) as { + error: string; + message: string; + custom: boolean; + }; expect(response.status).toBe(400); expect(data.error).toBe('Bad Request'); @@ -399,7 +475,10 @@ describe('REST Event Handler E2E tests', () => { it('applies custom not found handler for unmatched routes', async () => { // Prepare const response = await fetch(`${apiUrl}/errors/nonexistent-route`); - const data = await response.json(); + const data = (await response.json()) as { + error: string; + message: string; + }; expect(response.status).toBe(404); expect(data.error).toBe('Not Found'); @@ -411,7 +490,7 @@ describe('REST Event Handler E2E tests', () => { it('handles GET request to nested router', async () => { // Prepare const response = await fetch(`${apiUrl}/nested/info`); - const data = await response.json(); + const data = (await response.json()) as { nested: boolean; path: string }; expect(response.status).toBe(200); expect(data.nested).toBe(true); @@ -428,7 +507,10 @@ describe('REST Event Handler E2E tests', () => { body: JSON.stringify(testData), headers: { 'Content-Type': 'application/json' }, }); - const data = await response.json(); + const data = (await response.json()) as { + nested: boolean; + created: unknown; + }; expect(response.status).toBe(200); expect(data.nested).toBe(true); @@ -444,7 +526,7 @@ describe('REST Event Handler E2E tests', () => { Origin: 'https://example.com', }, }); - const data = await response.json(); + const data = (await response.json()) as { message: string }; expect(response.status).toBe(200); expect(response.headers.get('content-type')).toContain( @@ -472,7 +554,7 @@ describe('REST Event Handler E2E tests', () => { Origin: 'https://example.com', }, }); - const data = await response.json(); + const data = (await response.json()) as { received: unknown }; expect(response.status).toBe(200); expect(data.received).toEqual(testData); @@ -518,7 +600,7 @@ describe('REST Event Handler E2E tests', () => { const response = await fetch(`${apiUrl}/compress/large`, { headers: { 'Accept-Encoding': 'gzip' }, }); - const data = await response.json(); + const data = (await response.json()) as { message: string; data: string }; expect(response.status).toBe(200); expect(data.message).toContain('compressed'); @@ -532,7 +614,7 @@ describe('REST Event Handler E2E tests', () => { headers: { 'Accept-Encoding': 'gzip' }, }); // Act - const data = await response.json(); + const data = (await response.json()) as { message: string }; // Assess expect(response.status).toBe(200); @@ -557,7 +639,10 @@ describe('REST Event Handler E2E tests', () => { 'X-Custom-Header': customHeaderValue, }, }); - const data = await response.json(); + const data = (await response.json()) as { + body: unknown; + headers: Record; + }; expect(response.status).toBe(200); expect(data.body).toEqual(testData); @@ -578,7 +663,10 @@ describe('REST Event Handler E2E tests', () => { 'X-Multi-Header': 'value1, value2', }, }); - const data = await response.json(); + const data = (await response.json()) as { + body: unknown; + headers: Record; + }; expect(response.status).toBe(200); expect(data.headers['x-multi-header']).toBeDefined(); @@ -601,7 +689,11 @@ describe('REST Event Handler E2E tests', () => { 'Content-Type': 'application/x-www-form-urlencoded', }, }); - const data = await response.json(); + const data = (await response.json()) as { + contentType: string; + received: boolean; + bodyLength: number; + }; expect(response.status).toBe(200); expect(data.contentType).toBe('application/x-www-form-urlencoded'); @@ -632,7 +724,11 @@ describe('REST Event Handler E2E tests', () => { 'Content-Type': `multipart/form-data; boundary=----${boundary}`, }, }); - const data = await response.json(); + const data = (await response.json()) as { + contentType: string; + received: boolean; + bodyLength: number; + }; expect(response.status).toBe(200); expect(data.contentType).toContain('multipart/form-data'); @@ -661,7 +757,7 @@ describe('REST Event Handler E2E tests', () => { it('returns multiple Set-Cookie headers', async () => { // Prepare const response = await fetch(`${apiUrl}/multi-headers/set-cookies`); - const data = await response.json(); + const data = (await response.json()) as { message: string }; expect(response.status).toBe(200); expect(data.message).toBe('Multiple cookies set'); @@ -685,7 +781,11 @@ describe('REST Event Handler E2E tests', () => { 'Content-Type': 'application/octet-stream', }, }); - const data = await response.json(); + const data = (await response.json()) as { + contentType: string; + received: boolean; + bodyLength: number; + }; expect(response.status).toBe(200); expect(data.received).toBe(true); @@ -708,7 +808,11 @@ describe('REST Event Handler E2E tests', () => { 'Content-Type': 'image/png', }, }); - const data = await response.json(); + const data = (await response.json()) as { + contentType: string; + received: boolean; + bodyLength: number; + }; expect(response.status).toBe(200); expect(data.received).toBe(true); @@ -731,7 +835,7 @@ describe('REST Event Handler E2E tests', () => { it('returns custom status code and headers', async () => { // Prepare const response = await fetch(`${apiUrl}/custom-response`); - const data = await response.json(); + const data = (await response.json()) as { message: string }; expect(response.status).toBe(201); expect(response.headers.get('content-type')).toContain( @@ -747,7 +851,10 @@ describe('REST Event Handler E2E tests', () => { it('handles root path GET request', async () => { // Prepare const response = await fetch(`${apiUrl}/`); - const data = await response.json(); + const data = (await response.json()) as { + message: string; + version: string; + }; expect(response.status).toBe(200); expect(data.message).toBe('Root path'); @@ -757,7 +864,7 @@ describe('REST Event Handler E2E tests', () => { it('normalizes a trailing slash to match a route registered without one', async () => { // Prepare const response = await fetch(`${apiUrl}/methods/`); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('GET'); @@ -766,7 +873,7 @@ describe('REST Event Handler E2E tests', () => { it('handles path without trailing slash', async () => { // Prepare const response = await fetch(`${apiUrl}/methods`); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('GET'); @@ -778,7 +885,11 @@ describe('REST Event Handler E2E tests', () => { // Act const response = await fetch(`${apiUrl}/params/search?q=${searchQuery}`); - const data = await response.json(); + const data = (await response.json()) as { + query: string | null; + limit: string | null; + filters?: string[]; + }; expect(response.status).toBe(200); expect(data.query).toBe(searchQuery); @@ -787,7 +898,7 @@ describe('REST Event Handler E2E tests', () => { it('handles path with fragment in URL', async () => { // Prepare const response = await fetch(`${apiUrl}/methods#fragment`); - const data = await response.json(); + const data = (await response.json()) as { method: string }; expect(response.status).toBe(200); expect(data.method).toBe('GET'); @@ -796,7 +907,10 @@ describe('REST Event Handler E2E tests', () => { it('treats path with leading double slash as protocol-relative URL', async () => { // Prepare const response = await fetch(`${apiUrl}//methods`); - const data = await response.json(); + const data = (await response.json()) as { + message: string; + version: string; + }; expect(response.status).toBe(200); // Double slash at the beginning is treated as protocol-relative URL by the URL constructor @@ -872,9 +986,9 @@ describe('REST Event Handler E2E tests', () => { it('returns a unique request-scoped value per invocation', async () => { // Act const response1 = await fetch(`${apiUrl}/store/request`); - const data1 = await response1.json(); + const data1 = (await response1.json()) as { requestId: string }; const response2 = await fetch(`${apiUrl}/store/request`); - const data2 = await response2.json(); + const data2 = (await response2.json()) as { requestId: string }; // Assess expect(response1.status).toBe(200); @@ -895,7 +1009,11 @@ describe('REST Event Handler E2E tests', () => { it('returns both request and shared store values', async () => { // Act const response = await fetch(`${apiUrl}/store/both`); - const data = await response.json(); + const data = (await response.json()) as { + requestId: string; + appName: string; + version: number; + }; // Assess expect(response.status).toBe(200); diff --git a/packages/event-handler/tests/e2e/routers/corsRouter.ts b/packages/event-handler/tests/e2e/routers/corsRouter.ts index 45f1ab0cb2..7003006c99 100644 --- a/packages/event-handler/tests/e2e/routers/corsRouter.ts +++ b/packages/event-handler/tests/e2e/routers/corsRouter.ts @@ -1,3 +1,4 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { Router } from '@aws-lambda-powertools/event-handler/http'; import { cors } from '@aws-lambda-powertools/event-handler/http/middleware'; @@ -17,7 +18,7 @@ corsRouter.get('/data', () => ({ })); corsRouter.post('/data', async ({ req }) => { - const body = await req.json(); + const body = (await req.json()) as JSONValue; return { received: body }; }); diff --git a/packages/event-handler/tests/e2e/routers/nestedRouter.ts b/packages/event-handler/tests/e2e/routers/nestedRouter.ts index 1ca9e13e49..4493116047 100644 --- a/packages/event-handler/tests/e2e/routers/nestedRouter.ts +++ b/packages/event-handler/tests/e2e/routers/nestedRouter.ts @@ -1,3 +1,4 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { Router } from '@aws-lambda-powertools/event-handler/http'; const nestedRouter = new Router(); @@ -8,7 +9,7 @@ nestedRouter.get('/info', ({ req }) => ({ })); nestedRouter.post('/create', async ({ req }) => { - const body = await req.json(); + const body = (await req.json()) as JSONValue; return { nested: true, created: body }; }); diff --git a/packages/event-handler/tests/unit/http/Router/basic-routing.test.ts b/packages/event-handler/tests/unit/http/Router/basic-routing.test.ts index 199e64b5cd..c359d68d57 100644 --- a/packages/event-handler/tests/unit/http/Router/basic-routing.test.ts +++ b/packages/event-handler/tests/unit/http/Router/basic-routing.test.ts @@ -1,4 +1,5 @@ import { Readable } from 'node:stream'; +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import context from '@aws-lambda-powertools/testing-utils/context'; import { describe, expect, it, vi } from 'vitest'; import { HttpStatusText } from '../../../../src/http/constants.js'; @@ -446,7 +447,7 @@ describe('Class: Router - ALB Support', () => { // Prepare const app = new Router(); app.post('/test', async ({ req }) => { - const body = await req.json(); + const body = (await req.json()) as JSONValue; return { received: body }; }); diff --git a/packages/event-handler/tests/unit/http/Router/middleware.test.ts b/packages/event-handler/tests/unit/http/Router/middleware.test.ts index 19848ba0ea..76b1a2e224 100644 --- a/packages/event-handler/tests/unit/http/Router/middleware.test.ts +++ b/packages/event-handler/tests/unit/http/Router/middleware.test.ts @@ -788,7 +788,7 @@ describe('Class: Router - Middleware', () => { async ({ reqCtx, next }) => { await next(); const clonedRes = reqCtx.res.clone(); - message = (await clonedRes.json()).message; + message = ((await clonedRes.json()) as { message: string }).message; }, async () => { return { message: 'Middleware applied' }; diff --git a/tsconfig.json b/tsconfig.json index a64c3cc925..ccab8d6e89 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,10 +5,9 @@ "target": "ES2024", // Node.js 22 // `Disposable`/`Symbol.dispose` are not part of any yearly ES lib yet (not ES2024 nor // ES2025 as of TS 6.x), so we pull them in explicitly. Compile-time only: it doesn't - // change emitted code or require a runtime polyfill. DOM libs must be listed too: - // setting `lib` replaces the target's implicit default (ES2024 + DOM + DOM.Iterable), - // and the codebase relies on DOM types like `BodyInit` and `Response`. - "lib": ["ES2024", "DOM", "DOM.Iterable", "ESNext.Disposable"], + // change emitted code or require a runtime polyfill. No DOM libs: this is a Node.js + // library, so fetch/Request/Response/Headers come from @types/node (undici-types). + "lib": ["ES2024", "ESNext.Disposable"], "experimentalDecorators": true, "module": "NodeNext", "moduleResolution": "NodeNext",