itty-lambda 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Evan Kaufman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # `itty-lambda` - AWS Lambda support for [itty-router]
2
+
3
+ [
4
+ ![ci status](https://github.com/EvanK/itty-lambda/actions/workflows/ci.yaml/badge.svg)
5
+ ](https://github.com/EvanK/itty-lambda/actions/workflows/ci.yaml)
6
+ [
7
+ ![node.js support as of v20](https://img.shields.io/badge/Node.js-v20-yellow)
8
+ ](https://nodejs.org/docs/latest-v20.x/api/)
9
+
10
+
11
+ AWS Lambda functions can respond to HTTP requests via one of the following invocation types:
12
+ - [Function urls][lambda-function-urls]
13
+ - [API gateways][api-gateways]
14
+ - [Application load balancers][albs].
15
+
16
+ This library aims to maintain support for running [itty-router] in any such environment.
17
+
18
+
19
+ ## Example
20
+
21
+ ```js
22
+ // supports CJS -or- ESM
23
+ // const { url, ag, alb } = require('itty-lambda');
24
+ // import { url, ag, alb } from 'itty-lambda';
25
+
26
+ // for just API Gateway support, use 'itty-lambda/ag'
27
+ // for just Application Load Balancer support, use 'itty-lambda/alb'
28
+ // for just Lambda function url support, use 'itty-lambda/url'
29
+
30
+ import { AutoRouter } from 'itty-router';
31
+
32
+ // using an app load balancer...
33
+ import { eventToRequest, responseToResult } from 'itty-lambda/alb';
34
+
35
+ export async function handler (event) {
36
+ const router = AutoRouter();
37
+ router.get('/foo', () => ({ success: true }));
38
+
39
+ const request = await eventToRequest(event);
40
+ // { path: string, httpMethod: string, headers: object, ... }
41
+ // ->
42
+ // { url: string, method: string, headers: Headers, ... }
43
+
44
+ const response = await router.fetch(request);
45
+
46
+ const result = await responseToResult(response);
47
+ // { status: number, headers: Headers, body: ReadableStream, ... }
48
+ // ->
49
+ // { statusCode: number, body: string, headers: object, ... }
50
+
51
+ return result;
52
+ }
53
+ ```
54
+
55
+ Each implementation provides these two named exports, for you to plug in before and after routing...
56
+
57
+ For more information, see [the documentation](https://EvanK.github.io/itty-lambda/).
58
+
59
+ ## eventToRequest
60
+
61
+ Accepts an AWS Lambda event for the invocation type, and resolves to a well formed `RequestLike`.
62
+
63
+ ```ts
64
+ eventToRequest(
65
+ event: APIGatewayProxyEvent | ALBEvent | LambdaFunctionURLEvent,
66
+ options: EventOptions | undefined
67
+ ) : Promise<RequestLike>
68
+ ```
69
+
70
+ ### EventOptions
71
+
72
+ | Name | Type(s) | Default Value | Description |
73
+ | -- | -- | -- | -- |
74
+ | **defaultMethod** | `string` | `GET` | HTTP method if none provided from event |
75
+
76
+
77
+ ## responseToResult
78
+
79
+ Accepts a router `Response`, and resolves to a result for the invocation type.
80
+
81
+ ```ts
82
+ responseToResult(
83
+ response: Response | undefined,
84
+ options : ResponseOptions | undefined
85
+ ) : Promise<APIGatewayProxyResult | ALBResult | LambdaFunctionURLResult>
86
+ ```
87
+
88
+ ### ResponseOptions
89
+
90
+ | Name | Type(s) | Default Value | Description |
91
+ | -- | -- | -- | -- |
92
+ | **base64Encode** | `boolean` | `false` | Encode response body |
93
+ | **fallbackStatus** | `number` | `404` | Status if no response provided from router |
94
+ | **multiValueHeaders** | `boolean` | `false` | Split response headers with multiple values |
95
+
96
+
97
+ <!-- footnotes and urls -->
98
+ [itty-router]: https://itty.dev/itty-router/
99
+ [lambda-function-urls]: https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html
100
+ [api-gateways]: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integrations.html
101
+ [albs]: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * API gateway implemention
4
+ *
5
+ * @module ag
6
+ *
7
+ * @example
8
+ * import { eventToRequest, responseToResult } from 'itty-lambda/ag';
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.eventToRequest = eventToRequest;
12
+ exports.responseToResult = responseToResult;
13
+ const common_1 = require("./common");
14
+ /**
15
+ * Accepts an event from an AWS Lambda function invocation by way of an
16
+ * API gateway proxy integration, and formats it into a request suitable
17
+ * for routing through itty-router.
18
+ *
19
+ * @param event
20
+ * @param options
21
+ */
22
+ async function eventToRequest(event, options) {
23
+ return await (0, common_1.eventToRequest)(common_1.RoutingMode.Ag, event, options);
24
+ }
25
+ /**
26
+ * Accepts a response from itty-router (or undefined if no route was matched),
27
+ * and formats it into a result suitable to return to the Lambda service and
28
+ * subsequently to an API gateway proxy integration.
29
+ *
30
+ * If no response was provided, an error response is built with the given
31
+ * fallback HTTP status, defaulting to 404.
32
+ *
33
+ * @param response
34
+ * @param options
35
+ */
36
+ async function responseToResult(response, options) {
37
+ return (await (0, common_1.responseToResult)(common_1.RoutingMode.Ag, response, options));
38
+ }
39
+ /** @ignore */
40
+ exports.default = {
41
+ eventToRequest,
42
+ responseToResult,
43
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Application load balancer implemention
4
+ *
5
+ * @module alb
6
+ *
7
+ * @example
8
+ * import { eventToRequest, responseToResult } from 'itty-lambda/alb';
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.eventToRequest = eventToRequest;
12
+ exports.responseToResult = responseToResult;
13
+ const common_1 = require("./common");
14
+ /**
15
+ * Accepts an event from an AWS Lambda function invocation by way of an
16
+ * application load balancer target, and formats it into a request
17
+ * suitable for routing through itty-router.
18
+ *
19
+ * @param event
20
+ * @param options
21
+ */
22
+ async function eventToRequest(event, options) {
23
+ return await (0, common_1.eventToRequest)(common_1.RoutingMode.Alb, event, options);
24
+ }
25
+ /**
26
+ * Accepts a response from itty-router (or undefined if no route was matched),
27
+ * and formats it into a result suitable to return to the Lambda service and
28
+ * subsequently to an application load balancer.
29
+ *
30
+ * If no response was provided, an error response is built with the given
31
+ * fallback HTTP status, defaulting to 404.
32
+ *
33
+ * @param response
34
+ * @param options
35
+ */
36
+ async function responseToResult(response, options) {
37
+ return (await (0, common_1.responseToResult)(common_1.RoutingMode.Alb, response, options));
38
+ }
39
+ /** @ignore */
40
+ exports.default = {
41
+ eventToRequest,
42
+ responseToResult,
43
+ };
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ /**
3
+ * Common shared functionality between implementations.
4
+ *
5
+ * @module common
6
+ * @protected
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.RoutingMode = void 0;
10
+ exports.eventToRequest = eventToRequest;
11
+ exports.responseToResult = responseToResult;
12
+ const consumers_1 = require("node:stream/consumers");
13
+ const itty_router_1 = require("itty-router");
14
+ /** @ignore */
15
+ var RoutingMode;
16
+ (function (RoutingMode) {
17
+ RoutingMode["Ag"] = "api-gateway";
18
+ RoutingMode["Alb"] = "application-load-balancer";
19
+ RoutingMode["Url"] = "lambda-function-url";
20
+ })(RoutingMode || (exports.RoutingMode = RoutingMode = {}));
21
+ /** @ignore */
22
+ async function eventToRequest(mode, event, options) {
23
+ const output = { method: '', url: '' };
24
+ output.headers = objectsToHeaders(event?.headers,
25
+ // account for lambda function urls not supporting multi values
26
+ (mode != RoutingMode.Url ? event.multiValueHeaders : undefined));
27
+ // infer protocol from headers when available
28
+ const proto = output.headers.get('x-forwarded-proto')
29
+ ?? output.headers.get('forwarded')?.match(/proto=(\w+)/)?.[1]
30
+ ?? 'http';
31
+ // infer host when available
32
+ const host = (
33
+ // ag or url may have requestContext.domainName
34
+ (mode != RoutingMode.Alb ? event.requestContext?.domainName : undefined))
35
+ ?? output.headers.get('host')
36
+ ?? output.headers.get('x-forwarded-host')
37
+ ?? output.headers.get('forwarded')?.match(/host=([^;]+)/)?.[1]
38
+ ?? 'localhost.localdomain';
39
+ // infer path when available
40
+ const path = (
41
+ // ag or alb may have path
42
+ (mode != RoutingMode.Url ? event.path : undefined))
43
+ ?? (
44
+ // url may have rawPath
45
+ (mode == RoutingMode.Url ? event.rawPath : undefined))
46
+ ?? (
47
+ // ag may have requestContext.path
48
+ (mode == RoutingMode.Ag ? event.requestContext?.path : undefined))
49
+ ?? (
50
+ // url may have requestContext.http.path
51
+ (mode == RoutingMode.Url ? event.requestContext?.http?.path : undefined))
52
+ ?? '';
53
+ // infer querystring and convert to string as necessary
54
+ const queryString = (
55
+ // url may have rawQueryString
56
+ (mode == RoutingMode.Url ? event.rawQueryString : undefined))
57
+ ?? objectsToQueryString(event?.queryStringParameters, (
58
+ // ag or alb may have multiValueQueryStringParameters
59
+ (mode != RoutingMode.Url ? event.multiValueQueryStringParameters : undefined)));
60
+ // assemble well-formed url from inferred values above
61
+ output.url = `${proto}://${host}${path}?${queryString}`;
62
+ // infer or default http method
63
+ output.method =
64
+ (
65
+ // ag or alb may have httpMethod
66
+ (mode != RoutingMode.Url ? event.httpMethod : undefined))
67
+ ?? (
68
+ // ag may have requestContext.httpMethod
69
+ (mode != RoutingMode.Ag ? event.requestContext?.httpMethod : undefined))
70
+ ?? (
71
+ // url may have requestContext.http.method
72
+ (mode == RoutingMode.Url ? event.requestContext?.http?.method : undefined))
73
+ ?? options?.defaultMethod
74
+ ?? 'GET';
75
+ // get and decode as necessary any request body
76
+ if (event?.body) {
77
+ output.body = event?.isBase64Encoded
78
+ ? Buffer.from(`${event?.body}`, 'base64').toString('ascii')
79
+ : event?.body;
80
+ }
81
+ return output;
82
+ }
83
+ /** @ignore */
84
+ async function responseToResult(mode, response, options) {
85
+ options = Object.assign({ base64Encode: false, fallbackStatus: 404, multiValueHeaders: false }, options);
86
+ // function urls do not support multi values
87
+ if (options && mode === RoutingMode.Url) {
88
+ options.multiValueHeaders = false;
89
+ }
90
+ try {
91
+ return await parseResponseOrError(mode, response ?? (0, itty_router_1.error)(options.fallbackStatus, 'Response not found'), options);
92
+ }
93
+ catch (err) { // eslint-disable-line @typescript-eslint/no-explicit-any
94
+ return await parseResponseOrError(mode, (0, itty_router_1.error)(err), options);
95
+ }
96
+ }
97
+ async function parseResponseOrError(mode, input, options) {
98
+ let output;
99
+ switch (mode) {
100
+ case RoutingMode.Ag:
101
+ output = { statusCode: 200, isBase64Encoded: !!options.base64Encode, body: '' };
102
+ break;
103
+ case RoutingMode.Alb:
104
+ output = { statusCode: 200, isBase64Encoded: !!options.base64Encode };
105
+ break;
106
+ case RoutingMode.Url:
107
+ output = { statusCode: 200, isBase64Encoded: !!options.base64Encode };
108
+ break;
109
+ }
110
+ // destructure just what we need
111
+ const { status, headers, body } = input;
112
+ output.statusCode = status;
113
+ // handle single or multi headers
114
+ const { headers: singleHeaders, multiValueHeaders } = headersToObjects(headers, options.multiValueHeaders);
115
+ output.headers = {};
116
+ for (const [key, value] of Object.entries(singleHeaders)) {
117
+ if (undefined !== value)
118
+ output.headers[key] = value;
119
+ }
120
+ if (mode !== RoutingMode.Url) {
121
+ output.multiValueHeaders = {};
122
+ if ('multiValueHeaders' in output && output.multiValueHeaders) {
123
+ for (const [key, value] of Object.entries(multiValueHeaders)) {
124
+ if (undefined !== value)
125
+ output.multiValueHeaders[key] = value;
126
+ }
127
+ }
128
+ }
129
+ // un-streamify body as necessary
130
+ if (body) {
131
+ output.body = await (0, consumers_1.text)(body);
132
+ if (options.base64Encode) {
133
+ output.body = Buffer.from(output.body, 'utf-8').toString('base64');
134
+ }
135
+ }
136
+ return output;
137
+ }
138
+ /**
139
+ * Combine single and multi value header objects and convert to standardized
140
+ * Headers instance.
141
+ */
142
+ function objectsToHeaders(single, multi) {
143
+ const input = {};
144
+ // add each single value header, wrapped in array
145
+ if (single) {
146
+ for (const [key, value] of Object.entries(single)) {
147
+ if (value)
148
+ input[key] = [value];
149
+ }
150
+ }
151
+ // and each multi value header
152
+ if (multi) {
153
+ for (const [key, values] of Object.entries(multi)) {
154
+ if (values) {
155
+ // merge with any existing arrays of values
156
+ if (undefined === input?.[key]) {
157
+ input[key] = values;
158
+ }
159
+ else {
160
+ for (const value of values) {
161
+ if (!input[key].includes(value))
162
+ input[key].push(value);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ // append each normalized array to new Headers instance
169
+ const output = new Headers();
170
+ for (const [key, values] of Object.entries(input)) {
171
+ for (const value of values) {
172
+ output.append(key, value);
173
+ }
174
+ }
175
+ return output;
176
+ }
177
+ /**
178
+ * Split Headers instance into single and (when explicitly enabled) multi value
179
+ * header objects.
180
+ */
181
+ function headersToObjects(headers, splitIntoMultiValues = false) {
182
+ const single = {};
183
+ const multi = {};
184
+ for (const [key, value] of headers.entries()) {
185
+ if (splitIntoMultiValues && value.includes(',')) {
186
+ multi[key] = value.split(/\s*,\s*/);
187
+ }
188
+ else {
189
+ single[key] = value;
190
+ }
191
+ }
192
+ return { headers: single, multiValueHeaders: multi };
193
+ }
194
+ /**
195
+ * Combine single and multi value query string parameters into a well formed
196
+ * query string, as produced by the URLSearchParams global.
197
+ */
198
+ function objectsToQueryString(single, multi) {
199
+ const output = new URLSearchParams();
200
+ // include all single value query params by default
201
+ if (single) {
202
+ for (const [key, value] of Object.entries(single)) {
203
+ if (undefined !== value)
204
+ output.append(key, value);
205
+ }
206
+ }
207
+ // and all multi value query params that don't already contain same value
208
+ if (multi) {
209
+ for (const [key, value] of Object.entries(multi)) {
210
+ if (Array.isArray(value)) {
211
+ for (const v of value) {
212
+ if (!output.has(key, v))
213
+ output.append(key, v);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return output.toString();
219
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ /**
3
+ * Each implements the following functions:
4
+ *
5
+ * @see eventToRequest
6
+ * Expects an event as provided by the AWS Lambda service to an invoked
7
+ * function, and returns a request-like object for use by itty-router.
8
+ *
9
+ * @see responseToResult
10
+ * Expects a response as returned by itty-router (or undefined if no route
11
+ * match found), and returns a result as expected by the Lambda service.
12
+ *
13
+ * The format of each incoming event and outgoing result vary depending on
14
+ * how the Lambda has been invoked, hence the multiple implementations.
15
+ *
16
+ * @module index
17
+ * @ignore
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.url = exports.alb = exports.ag = void 0;
24
+ // API Gateways
25
+ const ag_1 = __importDefault(require("./ag"));
26
+ exports.ag = ag_1.default;
27
+ // Application Load Balancers
28
+ const alb_1 = __importDefault(require("./alb"));
29
+ exports.alb = alb_1.default;
30
+ // Lambda function urls
31
+ const url_1 = __importDefault(require("./url"));
32
+ exports.url = url_1.default;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Lambda function url implemention
4
+ *
5
+ * @module url
6
+ *
7
+ * @example
8
+ * import { eventToRequest, responseToResult } from 'itty-lambda/url';
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.eventToRequest = eventToRequest;
12
+ exports.responseToResult = responseToResult;
13
+ const common_1 = require("./common");
14
+ /**
15
+ * Accepts an event from an AWS Lambda function invocation by way of a
16
+ * function url, and formats it into a request suitable for routing through
17
+ * itty-router.
18
+ *
19
+ * @param event
20
+ * @param options
21
+ */
22
+ async function eventToRequest(event, options) {
23
+ return await (0, common_1.eventToRequest)(common_1.RoutingMode.Url, event, options);
24
+ }
25
+ /**
26
+ * Accepts a response from itty-router (or undefined if no route was matched),
27
+ * and formats it into a result suitable to return to the Lambda service and
28
+ * subsequently to the client invoking the function url.
29
+ *
30
+ * If no response was provided, an error response is built with the given
31
+ * fallback HTTP status, defaulting to 404.
32
+ *
33
+ * @param response
34
+ * @param options
35
+ */
36
+ async function responseToResult(response, options) {
37
+ return (await (0, common_1.responseToResult)(common_1.RoutingMode.Url, response, options));
38
+ }
39
+ /** @ignore */
40
+ exports.default = {
41
+ eventToRequest,
42
+ responseToResult,
43
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * API gateway implemention
4
+ *
5
+ * @module ag
6
+ *
7
+ * @example
8
+ * import { eventToRequest, responseToResult } from 'itty-lambda/ag';
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.eventToRequest = eventToRequest;
12
+ exports.responseToResult = responseToResult;
13
+ const common_1 = require("./common");
14
+ /**
15
+ * Accepts an event from an AWS Lambda function invocation by way of an
16
+ * API gateway proxy integration, and formats it into a request suitable
17
+ * for routing through itty-router.
18
+ *
19
+ * @param event
20
+ * @param options
21
+ */
22
+ async function eventToRequest(event, options) {
23
+ return await (0, common_1.eventToRequest)(common_1.RoutingMode.Ag, event, options);
24
+ }
25
+ /**
26
+ * Accepts a response from itty-router (or undefined if no route was matched),
27
+ * and formats it into a result suitable to return to the Lambda service and
28
+ * subsequently to an API gateway proxy integration.
29
+ *
30
+ * If no response was provided, an error response is built with the given
31
+ * fallback HTTP status, defaulting to 404.
32
+ *
33
+ * @param response
34
+ * @param options
35
+ */
36
+ async function responseToResult(response, options) {
37
+ return (await (0, common_1.responseToResult)(common_1.RoutingMode.Ag, response, options));
38
+ }
39
+ /** @ignore */
40
+ exports.default = {
41
+ eventToRequest,
42
+ responseToResult,
43
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Application load balancer implemention
4
+ *
5
+ * @module alb
6
+ *
7
+ * @example
8
+ * import { eventToRequest, responseToResult } from 'itty-lambda/alb';
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.eventToRequest = eventToRequest;
12
+ exports.responseToResult = responseToResult;
13
+ const common_1 = require("./common");
14
+ /**
15
+ * Accepts an event from an AWS Lambda function invocation by way of an
16
+ * application load balancer target, and formats it into a request
17
+ * suitable for routing through itty-router.
18
+ *
19
+ * @param event
20
+ * @param options
21
+ */
22
+ async function eventToRequest(event, options) {
23
+ return await (0, common_1.eventToRequest)(common_1.RoutingMode.Alb, event, options);
24
+ }
25
+ /**
26
+ * Accepts a response from itty-router (or undefined if no route was matched),
27
+ * and formats it into a result suitable to return to the Lambda service and
28
+ * subsequently to an application load balancer.
29
+ *
30
+ * If no response was provided, an error response is built with the given
31
+ * fallback HTTP status, defaulting to 404.
32
+ *
33
+ * @param response
34
+ * @param options
35
+ */
36
+ async function responseToResult(response, options) {
37
+ return (await (0, common_1.responseToResult)(common_1.RoutingMode.Alb, response, options));
38
+ }
39
+ /** @ignore */
40
+ exports.default = {
41
+ eventToRequest,
42
+ responseToResult,
43
+ };
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ /**
3
+ * Common shared functionality between implementations.
4
+ *
5
+ * @module common
6
+ * @protected
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.RoutingMode = void 0;
10
+ exports.eventToRequest = eventToRequest;
11
+ exports.responseToResult = responseToResult;
12
+ const consumers_1 = require("node:stream/consumers");
13
+ const itty_router_1 = require("itty-router");
14
+ /** @ignore */
15
+ var RoutingMode;
16
+ (function (RoutingMode) {
17
+ RoutingMode["Ag"] = "api-gateway";
18
+ RoutingMode["Alb"] = "application-load-balancer";
19
+ RoutingMode["Url"] = "lambda-function-url";
20
+ })(RoutingMode || (exports.RoutingMode = RoutingMode = {}));
21
+ /** @ignore */
22
+ async function eventToRequest(mode, event, options) {
23
+ const output = { method: '', url: '' };
24
+ output.headers = objectsToHeaders(event?.headers,
25
+ // account for lambda function urls not supporting multi values
26
+ (mode != RoutingMode.Url ? event.multiValueHeaders : undefined));
27
+ // infer protocol from headers when available
28
+ const proto = output.headers.get('x-forwarded-proto')
29
+ ?? output.headers.get('forwarded')?.match(/proto=(\w+)/)?.[1]
30
+ ?? 'http';
31
+ // infer host when available
32
+ const host = (
33
+ // ag or url may have requestContext.domainName
34
+ (mode != RoutingMode.Alb ? event.requestContext?.domainName : undefined))
35
+ ?? output.headers.get('host')
36
+ ?? output.headers.get('x-forwarded-host')
37
+ ?? output.headers.get('forwarded')?.match(/host=([^;]+)/)?.[1]
38
+ ?? 'localhost.localdomain';
39
+ // infer path when available
40
+ const path = (
41
+ // ag or alb may have path
42
+ (mode != RoutingMode.Url ? event.path : undefined))
43
+ ?? (
44
+ // url may have rawPath
45
+ (mode == RoutingMode.Url ? event.rawPath : undefined))
46
+ ?? (
47
+ // ag may have requestContext.path
48
+ (mode == RoutingMode.Ag ? event.requestContext?.path : undefined))
49
+ ?? (
50
+ // url may have requestContext.http.path
51
+ (mode == RoutingMode.Url ? event.requestContext?.http?.path : undefined))
52
+ ?? '';
53
+ // infer querystring and convert to string as necessary
54
+ const queryString = (
55
+ // url may have rawQueryString
56
+ (mode == RoutingMode.Url ? event.rawQueryString : undefined))
57
+ ?? objectsToQueryString(event?.queryStringParameters, (
58
+ // ag or alb may have multiValueQueryStringParameters
59
+ (mode != RoutingMode.Url ? event.multiValueQueryStringParameters : undefined)));
60
+ // assemble well-formed url from inferred values above
61
+ output.url = `${proto}://${host}${path}?${queryString}`;
62
+ // infer or default http method
63
+ output.method =
64
+ (
65
+ // ag or alb may have httpMethod
66
+ (mode != RoutingMode.Url ? event.httpMethod : undefined))
67
+ ?? (
68
+ // ag may have requestContext.httpMethod
69
+ (mode != RoutingMode.Ag ? event.requestContext?.httpMethod : undefined))
70
+ ?? (
71
+ // url may have requestContext.http.method
72
+ (mode == RoutingMode.Url ? event.requestContext?.http?.method : undefined))
73
+ ?? options?.defaultMethod
74
+ ?? 'GET';
75
+ // get and decode as necessary any request body
76
+ if (event?.body) {
77
+ output.body = event?.isBase64Encoded
78
+ ? Buffer.from(`${event?.body}`, 'base64').toString('ascii')
79
+ : event?.body;
80
+ }
81
+ return output;
82
+ }
83
+ /** @ignore */
84
+ async function responseToResult(mode, response, options) {
85
+ options = Object.assign({ base64Encode: false, fallbackStatus: 404, multiValueHeaders: false }, options);
86
+ // function urls do not support multi values
87
+ if (options && mode === RoutingMode.Url) {
88
+ options.multiValueHeaders = false;
89
+ }
90
+ try {
91
+ return await parseResponseOrError(mode, response ?? (0, itty_router_1.error)(options.fallbackStatus, 'Response not found'), options);
92
+ }
93
+ catch (err) { // eslint-disable-line @typescript-eslint/no-explicit-any
94
+ return await parseResponseOrError(mode, (0, itty_router_1.error)(err), options);
95
+ }
96
+ }
97
+ async function parseResponseOrError(mode, input, options) {
98
+ let output;
99
+ switch (mode) {
100
+ case RoutingMode.Ag:
101
+ output = { statusCode: 200, isBase64Encoded: !!options.base64Encode, body: '' };
102
+ break;
103
+ case RoutingMode.Alb:
104
+ output = { statusCode: 200, isBase64Encoded: !!options.base64Encode };
105
+ break;
106
+ case RoutingMode.Url:
107
+ output = { statusCode: 200, isBase64Encoded: !!options.base64Encode };
108
+ break;
109
+ }
110
+ // destructure just what we need
111
+ const { status, headers, body } = input;
112
+ output.statusCode = status;
113
+ // handle single or multi headers
114
+ const { headers: singleHeaders, multiValueHeaders } = headersToObjects(headers, options.multiValueHeaders);
115
+ output.headers = {};
116
+ for (const [key, value] of Object.entries(singleHeaders)) {
117
+ if (undefined !== value)
118
+ output.headers[key] = value;
119
+ }
120
+ if (mode !== RoutingMode.Url) {
121
+ output.multiValueHeaders = {};
122
+ if ('multiValueHeaders' in output && output.multiValueHeaders) {
123
+ for (const [key, value] of Object.entries(multiValueHeaders)) {
124
+ if (undefined !== value)
125
+ output.multiValueHeaders[key] = value;
126
+ }
127
+ }
128
+ }
129
+ // un-streamify body as necessary
130
+ if (body) {
131
+ output.body = await (0, consumers_1.text)(body);
132
+ if (options.base64Encode) {
133
+ output.body = Buffer.from(output.body, 'utf-8').toString('base64');
134
+ }
135
+ }
136
+ return output;
137
+ }
138
+ /**
139
+ * Combine single and multi value header objects and convert to standardized
140
+ * Headers instance.
141
+ */
142
+ function objectsToHeaders(single, multi) {
143
+ const input = {};
144
+ // add each single value header, wrapped in array
145
+ if (single) {
146
+ for (const [key, value] of Object.entries(single)) {
147
+ if (value)
148
+ input[key] = [value];
149
+ }
150
+ }
151
+ // and each multi value header
152
+ if (multi) {
153
+ for (const [key, values] of Object.entries(multi)) {
154
+ if (values) {
155
+ // merge with any existing arrays of values
156
+ if (undefined === input?.[key]) {
157
+ input[key] = values;
158
+ }
159
+ else {
160
+ for (const value of values) {
161
+ if (!input[key].includes(value))
162
+ input[key].push(value);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ // append each normalized array to new Headers instance
169
+ const output = new Headers();
170
+ for (const [key, values] of Object.entries(input)) {
171
+ for (const value of values) {
172
+ output.append(key, value);
173
+ }
174
+ }
175
+ return output;
176
+ }
177
+ /**
178
+ * Split Headers instance into single and (when explicitly enabled) multi value
179
+ * header objects.
180
+ */
181
+ function headersToObjects(headers, splitIntoMultiValues = false) {
182
+ const single = {};
183
+ const multi = {};
184
+ for (const [key, value] of headers.entries()) {
185
+ if (splitIntoMultiValues && value.includes(',')) {
186
+ multi[key] = value.split(/\s*,\s*/);
187
+ }
188
+ else {
189
+ single[key] = value;
190
+ }
191
+ }
192
+ return { headers: single, multiValueHeaders: multi };
193
+ }
194
+ /**
195
+ * Combine single and multi value query string parameters into a well formed
196
+ * query string, as produced by the URLSearchParams global.
197
+ */
198
+ function objectsToQueryString(single, multi) {
199
+ const output = new URLSearchParams();
200
+ // include all single value query params by default
201
+ if (single) {
202
+ for (const [key, value] of Object.entries(single)) {
203
+ if (undefined !== value)
204
+ output.append(key, value);
205
+ }
206
+ }
207
+ // and all multi value query params that don't already contain same value
208
+ if (multi) {
209
+ for (const [key, value] of Object.entries(multi)) {
210
+ if (Array.isArray(value)) {
211
+ for (const v of value) {
212
+ if (!output.has(key, v))
213
+ output.append(key, v);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return output.toString();
219
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ /**
3
+ * Each implements the following functions:
4
+ *
5
+ * @see eventToRequest
6
+ * Expects an event as provided by the AWS Lambda service to an invoked
7
+ * function, and returns a request-like object for use by itty-router.
8
+ *
9
+ * @see responseToResult
10
+ * Expects a response as returned by itty-router (or undefined if no route
11
+ * match found), and returns a result as expected by the Lambda service.
12
+ *
13
+ * The format of each incoming event and outgoing result vary depending on
14
+ * how the Lambda has been invoked, hence the multiple implementations.
15
+ *
16
+ * @module index
17
+ * @ignore
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.url = exports.alb = exports.ag = void 0;
24
+ // API Gateways
25
+ const ag_1 = __importDefault(require("./ag"));
26
+ exports.ag = ag_1.default;
27
+ // Application Load Balancers
28
+ const alb_1 = __importDefault(require("./alb"));
29
+ exports.alb = alb_1.default;
30
+ // Lambda function urls
31
+ const url_1 = __importDefault(require("./url"));
32
+ exports.url = url_1.default;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Lambda function url implemention
4
+ *
5
+ * @module url
6
+ *
7
+ * @example
8
+ * import { eventToRequest, responseToResult } from 'itty-lambda/url';
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.eventToRequest = eventToRequest;
12
+ exports.responseToResult = responseToResult;
13
+ const common_1 = require("./common");
14
+ /**
15
+ * Accepts an event from an AWS Lambda function invocation by way of a
16
+ * function url, and formats it into a request suitable for routing through
17
+ * itty-router.
18
+ *
19
+ * @param event
20
+ * @param options
21
+ */
22
+ async function eventToRequest(event, options) {
23
+ return await (0, common_1.eventToRequest)(common_1.RoutingMode.Url, event, options);
24
+ }
25
+ /**
26
+ * Accepts a response from itty-router (or undefined if no route was matched),
27
+ * and formats it into a result suitable to return to the Lambda service and
28
+ * subsequently to the client invoking the function url.
29
+ *
30
+ * If no response was provided, an error response is built with the given
31
+ * fallback HTTP status, defaulting to 404.
32
+ *
33
+ * @param response
34
+ * @param options
35
+ */
36
+ async function responseToResult(response, options) {
37
+ return (await (0, common_1.responseToResult)(common_1.RoutingMode.Url, response, options));
38
+ }
39
+ /** @ignore */
40
+ exports.default = {
41
+ eventToRequest,
42
+ responseToResult,
43
+ };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "itty-lambda",
3
+ "version": "1.0.1",
4
+ "description": "AWS Lambda support for itty-router",
5
+ "author": "Evan Kaufman <evan@evanskaufman.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/EvanK/itty-lambda.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/EvanK/itty-lambda/issues"
13
+ },
14
+ "homepage": "https://github.com/EvanK/itty-lambda",
15
+ "keywords": [
16
+ "itty-router",
17
+ "router",
18
+ "aws-lambda"
19
+ ],
20
+ "files": [
21
+ "build/**/*.js"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "require": "./build/cjs/index.js",
26
+ "import": "./build/esm/index.js"
27
+ },
28
+ "./url": {
29
+ "require": "./build/cjs/url.js",
30
+ "import": "./build/esm/url.js"
31
+ },
32
+ "./ag": {
33
+ "require": "./build/cjs/ag.js",
34
+ "import": "./build/esm/ag.js"
35
+ },
36
+ "./alb": {
37
+ "require": "./build/cjs/alb.js",
38
+ "import": "./build/esm/alb.js"
39
+ }
40
+ },
41
+ "scripts": {
42
+ "docs": "npm run docs-html && npm run docs-md",
43
+ "docs-html": "typedoc --options typedoc.html.json",
44
+ "docs-md": "typedoc --options typedoc.md.json",
45
+ "link": "npm link && npm link itty-lambda",
46
+ "lint": "eslint ./src/",
47
+ "test": "nyc mocha ./test/unit/*.test.*js",
48
+ "tsc": "npm run tsc-cjs && npm run tsc-esm",
49
+ "tsc-cjs": "tsc --module commonjs --outDir ./build/cjs/",
50
+ "tsc-esm": "tsc --module nodenext --outDir ./build/esm/"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/js": "^9.29.0",
54
+ "@types/aws-lambda": "^8.10.150",
55
+ "@types/node": "^24.2.0",
56
+ "chai": "^5.2.1",
57
+ "eslint": "^9.29.0",
58
+ "mocha": "^11.7.1",
59
+ "nyc": "^17.1.0",
60
+ "sinon": "^21.0.0",
61
+ "typedoc": "^0.28.8",
62
+ "typedoc-github-theme": "^0.3.0",
63
+ "typedoc-plugin-markdown": "^4.8.0",
64
+ "typedoc-plugin-mdn-links": "^5.0.7",
65
+ "typescript": "^5.8.3",
66
+ "typescript-eslint": "^8.34.1"
67
+ },
68
+ "dependencies": {
69
+ "itty-router": "^5.0.0"
70
+ }
71
+ }