acrylblends 0.7.3 → 0.7.4
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/docs/framework/README.md +4 -3
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/getting-started/aws-lambda.md +115 -0
- package/docs/framework/getting-started/bun.md +1 -3
- package/docs/framework/getting-started/cloudflare-pages.md +53 -0
- package/docs/framework/getting-started/cloudflare-workers.md +67 -0
- package/docs/framework/getting-started/lambda-edge.md +41 -0
- package/docs/framework/getting-started/netlify.md +25 -0
- package/docs/framework/getting-started/other-runtimes.md +20 -9
- package/docs/framework/getting-started/service-worker.md +31 -0
- package/docs/framework/getting-started/vercel.md +28 -0
- package/docs/framework/guides/create-acrylblends.md +40 -2
- package/docs/framework/guides/faq.md +2 -2
- package/docs/framework/helpers/conninfo.md +1 -1
- package/docs/framework/helpers/ssg.md +1 -1
- package/docs/framework/helpers/streaming.md +3 -2
- package/docs/framework/helpers/websocket.md +2 -2
- package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
- package/docs/framework/upstream.json +3 -3
- package/lib/aws-lambda.d.ts +324 -0
- package/lib/aws-lambda.js +357 -0
- package/lib/bun.d.ts +75 -0
- package/lib/bun.js +134 -0
- package/lib/cloudflare-pages.d.ts +58 -0
- package/lib/cloudflare-pages.js +78 -0
- package/lib/cloudflare-workers.d.ts +21 -0
- package/lib/cloudflare-workers.js +80 -0
- package/lib/index-CseujaYI.d.ts +31 -0
- package/lib/index-DaJtL7ci.d.ts +156 -0
- package/lib/lambda-edge.d.ts +92 -0
- package/lib/lambda-edge.js +105 -0
- package/lib/netlify.d.ts +29 -0
- package/lib/netlify.js +30 -0
- package/lib/node-server/register.js +1 -1
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.js +3 -3
- package/lib/serve-static-Cp5adzGo.js +77 -0
- package/lib/serve-static.d.ts +1 -30
- package/lib/serve-static.js +1 -76
- package/lib/service-worker.d.ts +48 -0
- package/lib/service-worker.js +38 -0
- package/lib/ssg.d.ts +1 -155
- package/lib/vercel.d.ts +10 -0
- package/lib/vercel.js +9 -0
- package/package.json +33 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/upstream.json +1 -1
- package/plugins/acryl-router/src/core/adapter/aws-lambda/conninfo.ts +73 -0
- package/plugins/acryl-router/src/core/adapter/aws-lambda/handler.ts +682 -0
- package/plugins/acryl-router/src/core/adapter/aws-lambda/index.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/aws-lambda/types.ts +173 -0
- package/plugins/acryl-router/src/core/adapter/bun/conninfo.ts +43 -0
- package/plugins/acryl-router/src/core/adapter/bun/index.ts +11 -0
- package/plugins/acryl-router/src/core/adapter/bun/serve-static.ts +32 -0
- package/plugins/acryl-router/src/core/adapter/bun/server.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/bun/ssg.ts +26 -0
- package/plugins/acryl-router/src/core/adapter/bun/websocket.ts +113 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-pages/conninfo.ts +26 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-pages/handler.ts +122 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-pages/index.ts +11 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/conninfo.ts +7 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/index.ts +8 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/serve-static-module.ts +12 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/serve-static.ts +41 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/utils.ts +50 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/websocket.ts +52 -0
- package/plugins/acryl-router/src/core/adapter/lambda-edge/conninfo.ts +15 -0
- package/plugins/acryl-router/src/core/adapter/lambda-edge/handler.ts +239 -0
- package/plugins/acryl-router/src/core/adapter/lambda-edge/index.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/netlify/conninfo.ts +57 -0
- package/plugins/acryl-router/src/core/adapter/netlify/handler.ts +10 -0
- package/plugins/acryl-router/src/core/adapter/netlify/index.ts +6 -0
- package/plugins/acryl-router/src/core/adapter/netlify/mod.ts +2 -0
- package/plugins/acryl-router/src/core/adapter/service-worker/handler.ts +37 -0
- package/plugins/acryl-router/src/core/adapter/service-worker/index.ts +36 -0
- package/plugins/acryl-router/src/core/adapter/service-worker/types.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/vercel/conninfo.ts +8 -0
- package/plugins/acryl-router/src/core/adapter/vercel/handler.ts +8 -0
- package/plugins/acryl-router/src/core/adapter/vercel/index.ts +7 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/upstream.json +1 -1
- package/templates/todo/upstream.json +1 -1
- package/lib/{serve-static-B14MF1Va.js → serve-static-B849HlX2.js} +1 -1
- package/lib/{transpile-CTnWof4z.js → transpile-QUK9b3eG.js} +1 -1
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { pipeline } from 'node:stream/promises'
|
|
2
|
+
import type { Acryl } from '../../app.ts'
|
|
3
|
+
import type { Env, Schema } from '../../types.ts'
|
|
4
|
+
import { decodeBase64, encodeBase64 } from '../../utils/encode.ts'
|
|
5
|
+
import type {
|
|
6
|
+
ALBRequestContext,
|
|
7
|
+
ApiGatewayRequestContext,
|
|
8
|
+
ApiGatewayRequestContextV2,
|
|
9
|
+
Handler,
|
|
10
|
+
LambdaContext,
|
|
11
|
+
LatticeRequestContextV2,
|
|
12
|
+
} from './types.ts'
|
|
13
|
+
|
|
14
|
+
function sanitizeHeaderValue(value: string): string {
|
|
15
|
+
// Check if the value contains non-ASCII characters (char codes > 127)
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
const hasNonAscii = /[^\x00-\x7F]/.test(value)
|
|
18
|
+
if (!hasNonAscii) {
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
return encodeURIComponent(value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type LambdaEvent =
|
|
25
|
+
| APIGatewayProxyEvent
|
|
26
|
+
| APIGatewayProxyEventV2
|
|
27
|
+
| ALBProxyEvent
|
|
28
|
+
| LatticeProxyEventV2
|
|
29
|
+
|
|
30
|
+
export interface LatticeProxyEventV2 {
|
|
31
|
+
version: string
|
|
32
|
+
path: string
|
|
33
|
+
method: string
|
|
34
|
+
headers: Record<string, string[] | undefined>
|
|
35
|
+
queryStringParameters: Record<string, string[] | undefined>
|
|
36
|
+
body: string | null
|
|
37
|
+
isBase64Encoded: boolean
|
|
38
|
+
requestContext: LatticeRequestContextV2
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// When calling HTTP API or Lambda directly through function urls
|
|
42
|
+
export interface APIGatewayProxyEventV2 {
|
|
43
|
+
version: string
|
|
44
|
+
routeKey: string
|
|
45
|
+
headers: Record<string, string | undefined>
|
|
46
|
+
multiValueHeaders?: undefined
|
|
47
|
+
cookies?: string[]
|
|
48
|
+
rawPath: string
|
|
49
|
+
rawQueryString: string
|
|
50
|
+
body: string | null
|
|
51
|
+
isBase64Encoded: boolean
|
|
52
|
+
requestContext: ApiGatewayRequestContextV2
|
|
53
|
+
queryStringParameters?: {
|
|
54
|
+
[name: string]: string | undefined
|
|
55
|
+
}
|
|
56
|
+
pathParameters?: {
|
|
57
|
+
[name: string]: string | undefined
|
|
58
|
+
}
|
|
59
|
+
stageVariables?: {
|
|
60
|
+
[name: string]: string | undefined
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// When calling Lambda through an API Gateway
|
|
65
|
+
export interface APIGatewayProxyEvent {
|
|
66
|
+
version: string
|
|
67
|
+
httpMethod: string
|
|
68
|
+
headers: Record<string, string | undefined>
|
|
69
|
+
multiValueHeaders?: {
|
|
70
|
+
[headerKey: string]: string[]
|
|
71
|
+
}
|
|
72
|
+
path: string
|
|
73
|
+
body: string | null
|
|
74
|
+
isBase64Encoded: boolean
|
|
75
|
+
queryStringParameters?: Record<string, string | undefined>
|
|
76
|
+
requestContext: ApiGatewayRequestContext
|
|
77
|
+
resource: string
|
|
78
|
+
multiValueQueryStringParameters?: {
|
|
79
|
+
[parameterKey: string]: string[]
|
|
80
|
+
}
|
|
81
|
+
pathParameters?: Record<string, string>
|
|
82
|
+
stageVariables?: Record<string, string>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// When calling Lambda through an Application Load Balancer
|
|
86
|
+
export interface ALBProxyEvent {
|
|
87
|
+
httpMethod: string
|
|
88
|
+
headers?: Record<string, string | undefined>
|
|
89
|
+
multiValueHeaders?: Record<string, string[] | undefined>
|
|
90
|
+
path: string
|
|
91
|
+
body: string | null
|
|
92
|
+
isBase64Encoded: boolean
|
|
93
|
+
queryStringParameters?: Record<string, string | undefined>
|
|
94
|
+
multiValueQueryStringParameters?: {
|
|
95
|
+
[parameterKey: string]: string[]
|
|
96
|
+
}
|
|
97
|
+
requestContext: ALBRequestContext
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type WithHeaders = {
|
|
101
|
+
headers: Record<string, string | string[]>
|
|
102
|
+
multiValueHeaders?: undefined
|
|
103
|
+
}
|
|
104
|
+
type WithMultiValueHeaders = {
|
|
105
|
+
headers?: undefined
|
|
106
|
+
multiValueHeaders: Record<string, string[]>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type APIGatewayProxyResult = {
|
|
110
|
+
statusCode: number
|
|
111
|
+
statusDescription?: string
|
|
112
|
+
body: string
|
|
113
|
+
cookies?: string[]
|
|
114
|
+
isBase64Encoded: boolean
|
|
115
|
+
} & (WithHeaders | WithMultiValueHeaders)
|
|
116
|
+
|
|
117
|
+
const getRequestContext = (
|
|
118
|
+
event: LambdaEvent
|
|
119
|
+
):
|
|
120
|
+
| ApiGatewayRequestContext
|
|
121
|
+
| ApiGatewayRequestContextV2
|
|
122
|
+
| ALBRequestContext
|
|
123
|
+
| LatticeRequestContextV2 => {
|
|
124
|
+
return event.requestContext
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function* readWebStream(
|
|
128
|
+
reader: ReadableStreamDefaultReader<Uint8Array>
|
|
129
|
+
): AsyncGenerator<Uint8Array> {
|
|
130
|
+
let readResult = await reader.read()
|
|
131
|
+
while (!readResult.done) {
|
|
132
|
+
yield readResult.value
|
|
133
|
+
readResult = await reader.read()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const streamToNodeStream = (
|
|
138
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
139
|
+
writer: NodeJS.WritableStream
|
|
140
|
+
): Promise<void> => pipeline(readWebStream(reader), writer)
|
|
141
|
+
|
|
142
|
+
export const streamHandle = <
|
|
143
|
+
E extends Env = Env,
|
|
144
|
+
S extends Schema = {},
|
|
145
|
+
BasePath extends string = '/',
|
|
146
|
+
>(
|
|
147
|
+
app: Acryl<E, S, BasePath>
|
|
148
|
+
): Handler => {
|
|
149
|
+
// @ts-expect-error awslambda is not a standard API
|
|
150
|
+
return awslambda.streamifyResponse(
|
|
151
|
+
async (event: LambdaEvent, responseStream: NodeJS.WritableStream, context: LambdaContext) => {
|
|
152
|
+
const processor = getProcessor(event)
|
|
153
|
+
try {
|
|
154
|
+
const req = processor.createRequest(event)
|
|
155
|
+
const requestContext = getRequestContext(event)
|
|
156
|
+
|
|
157
|
+
const res = await app.fetch(req, {
|
|
158
|
+
event,
|
|
159
|
+
requestContext,
|
|
160
|
+
context,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const headers: Record<string, string> = {}
|
|
164
|
+
const cookies: string[] = []
|
|
165
|
+
res.headers.forEach((value, name) => {
|
|
166
|
+
if (name === 'set-cookie') {
|
|
167
|
+
cookies.push(value)
|
|
168
|
+
} else {
|
|
169
|
+
headers[name] = value
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Check content type
|
|
174
|
+
const httpResponseMetadata = {
|
|
175
|
+
statusCode: res.status,
|
|
176
|
+
headers,
|
|
177
|
+
cookies,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update response stream
|
|
181
|
+
// @ts-expect-error awslambda is not a standard API
|
|
182
|
+
responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata)
|
|
183
|
+
|
|
184
|
+
if (res.body) {
|
|
185
|
+
await streamToNodeStream(res.body.getReader(), responseStream)
|
|
186
|
+
} else {
|
|
187
|
+
responseStream.write('')
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('Error processing request:', error)
|
|
191
|
+
responseStream.write('Internal Server Error')
|
|
192
|
+
} finally {
|
|
193
|
+
responseStream.end()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type HandleOptions = {
|
|
200
|
+
isContentTypeBinary: ((contentType: string) => boolean) | undefined
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Converts a Acryl application to an AWS Lambda handler.
|
|
205
|
+
*
|
|
206
|
+
* Accepts events from API Gateway (v1 and v2), Application Load Balancer (ALB),
|
|
207
|
+
* and Lambda Function URLs.
|
|
208
|
+
*
|
|
209
|
+
* @param app - The Acryl application instance
|
|
210
|
+
* @param options - Optional configuration
|
|
211
|
+
* @param options.isContentTypeBinary - A function to determine if the content type is binary.
|
|
212
|
+
* If not provided, the default function will be used.
|
|
213
|
+
* @returns Lambda handler function
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```js
|
|
217
|
+
* import { Acryl } from 'acrylblends'
|
|
218
|
+
* import { handle } from 'acrylblends/aws-lambda'
|
|
219
|
+
*
|
|
220
|
+
* const app = new Acryl()
|
|
221
|
+
*
|
|
222
|
+
* app.get('/', (c) => c.text('Hello from Lambda'))
|
|
223
|
+
* app.get('/json', (c) => c.json({ message: 'Hello JSON' }))
|
|
224
|
+
*
|
|
225
|
+
* export const handler = handle(app)
|
|
226
|
+
* ```
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```js
|
|
230
|
+
* // With custom binary content type detection
|
|
231
|
+
* import { handle, defaultIsContentTypeBinary } from 'acrylblends/aws-lambda'
|
|
232
|
+
* export const handler = handle(app, {
|
|
233
|
+
* isContentTypeBinary: (contentType) => {
|
|
234
|
+
* if (defaultIsContentTypeBinary(contentType)) {
|
|
235
|
+
* // default logic same as prior to v4.8.4
|
|
236
|
+
* return true
|
|
237
|
+
* }
|
|
238
|
+
* return contentType.startsWith('image/') || contentType === 'application/pdf'
|
|
239
|
+
* }
|
|
240
|
+
* })
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
export const handle = <E extends Env = Env, S extends Schema = {}, BasePath extends string = '/'>(
|
|
244
|
+
app: Acryl<E, S, BasePath>,
|
|
245
|
+
{ isContentTypeBinary }: HandleOptions = { isContentTypeBinary: undefined }
|
|
246
|
+
): (<L extends LambdaEvent>(
|
|
247
|
+
event: L,
|
|
248
|
+
lambdaContext?: LambdaContext
|
|
249
|
+
) => Promise<
|
|
250
|
+
APIGatewayProxyResult &
|
|
251
|
+
(L extends { multiValueHeaders: Record<string, string[]> }
|
|
252
|
+
? WithMultiValueHeaders
|
|
253
|
+
: WithHeaders)
|
|
254
|
+
>) => {
|
|
255
|
+
// @ts-expect-error conditional return type is not inferable
|
|
256
|
+
return async (event, lambdaContext?) => {
|
|
257
|
+
const processor = getProcessor(event)
|
|
258
|
+
|
|
259
|
+
let req, requestContext
|
|
260
|
+
try {
|
|
261
|
+
req = processor.createRequest(event)
|
|
262
|
+
requestContext = getRequestContext(event)
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('Error processing request:', error)
|
|
265
|
+
const errorResponse =
|
|
266
|
+
error instanceof TypeError
|
|
267
|
+
? new Response('Invalid request', { status: 400 })
|
|
268
|
+
: new Response('Internal Server Error', { status: 500 })
|
|
269
|
+
return processor.createResult(event, errorResponse, { isContentTypeBinary })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const res = await app.fetch(req, {
|
|
273
|
+
event,
|
|
274
|
+
requestContext,
|
|
275
|
+
lambdaContext,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return processor.createResult(event, res, { isContentTypeBinary })
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export abstract class EventProcessor<E extends LambdaEvent> {
|
|
283
|
+
protected abstract getPath(event: E): string
|
|
284
|
+
|
|
285
|
+
protected abstract getMethod(event: E): string
|
|
286
|
+
|
|
287
|
+
protected abstract getQueryString(event: E): string
|
|
288
|
+
|
|
289
|
+
protected abstract getHeaders(event: E): Headers
|
|
290
|
+
|
|
291
|
+
protected abstract getCookies(event: E, headers: Headers): void
|
|
292
|
+
|
|
293
|
+
protected abstract setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void
|
|
294
|
+
|
|
295
|
+
protected getHeaderValue(headers: E['headers'], key: string): string | undefined {
|
|
296
|
+
const value = headers
|
|
297
|
+
? Array.isArray(headers[key])
|
|
298
|
+
? headers[key][0]
|
|
299
|
+
: headers[key]
|
|
300
|
+
: undefined
|
|
301
|
+
|
|
302
|
+
return value
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
protected getDomainName(event: E): string | undefined {
|
|
306
|
+
if (event.requestContext && 'domainName' in event.requestContext) {
|
|
307
|
+
return event.requestContext.domainName
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const hostFromHeaders = this.getHeaderValue(event.headers, 'host')
|
|
311
|
+
|
|
312
|
+
if (hostFromHeaders) {
|
|
313
|
+
return hostFromHeaders
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const multiValueHeaders = 'multiValueHeaders' in event ? event.multiValueHeaders : {}
|
|
317
|
+
const hostFromMultiValueHeaders = this.getHeaderValue(multiValueHeaders, 'host')
|
|
318
|
+
|
|
319
|
+
return hostFromMultiValueHeaders
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
createRequest(event: E): Request {
|
|
323
|
+
const queryString = this.getQueryString(event)
|
|
324
|
+
const domainName = this.getDomainName(event)
|
|
325
|
+
const path = this.getPath(event)
|
|
326
|
+
const urlPath = `https://${domainName}${path}`
|
|
327
|
+
const url = queryString ? `${urlPath}?${queryString}` : urlPath
|
|
328
|
+
|
|
329
|
+
const headers = this.getHeaders(event)
|
|
330
|
+
|
|
331
|
+
const method = this.getMethod(event)
|
|
332
|
+
const requestInit: RequestInit = {
|
|
333
|
+
headers,
|
|
334
|
+
method,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (event.body) {
|
|
338
|
+
const body = event.isBase64Encoded
|
|
339
|
+
? decodeBase64(event.body)
|
|
340
|
+
: new TextEncoder().encode(event.body)
|
|
341
|
+
requestInit.body = body
|
|
342
|
+
headers.set('content-length', body.length.toString())
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return new Request(url, requestInit)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async createResult(
|
|
349
|
+
event: E,
|
|
350
|
+
res: Response,
|
|
351
|
+
options: Pick<HandleOptions, 'isContentTypeBinary'>
|
|
352
|
+
): Promise<APIGatewayProxyResult> {
|
|
353
|
+
// determine whether the response body should be base64 encoded
|
|
354
|
+
const contentType = res.headers.get('content-type')
|
|
355
|
+
const isContentTypeBinary = options.isContentTypeBinary ?? defaultIsContentTypeBinary // overwrite default function if provided
|
|
356
|
+
let isBase64Encoded = contentType && isContentTypeBinary(contentType) ? true : false
|
|
357
|
+
|
|
358
|
+
if (!isBase64Encoded) {
|
|
359
|
+
const contentEncoding = res.headers.get('content-encoding')
|
|
360
|
+
isBase64Encoded = isContentEncodingBinary(contentEncoding)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const body = isBase64Encoded ? encodeBase64(await res.arrayBuffer()) : await res.text()
|
|
364
|
+
|
|
365
|
+
const result: APIGatewayProxyResult = {
|
|
366
|
+
body: body,
|
|
367
|
+
statusCode: res.status,
|
|
368
|
+
isBase64Encoded,
|
|
369
|
+
...('multiValueHeaders' in event && event.multiValueHeaders
|
|
370
|
+
? {
|
|
371
|
+
multiValueHeaders: {},
|
|
372
|
+
}
|
|
373
|
+
: {
|
|
374
|
+
headers: {},
|
|
375
|
+
}),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.setCookies(event, res, result)
|
|
379
|
+
if (result.multiValueHeaders) {
|
|
380
|
+
res.headers.forEach((value, key) => {
|
|
381
|
+
result.multiValueHeaders[key] = [value]
|
|
382
|
+
})
|
|
383
|
+
} else {
|
|
384
|
+
res.headers.forEach((value, key) => {
|
|
385
|
+
result.headers[key] = value
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return result
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
setCookies(_event: E, res: Response, result: APIGatewayProxyResult) {
|
|
393
|
+
if (res.headers.has('set-cookie')) {
|
|
394
|
+
const cookies = res.headers.getSetCookie
|
|
395
|
+
? res.headers.getSetCookie()
|
|
396
|
+
: Array.from(res.headers.entries())
|
|
397
|
+
.filter(([k]) => k === 'set-cookie')
|
|
398
|
+
.map(([, v]) => v)
|
|
399
|
+
|
|
400
|
+
if (Array.isArray(cookies)) {
|
|
401
|
+
this.setCookiesToResult(result, cookies)
|
|
402
|
+
res.headers.delete('set-cookie')
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export class EventV2Processor extends EventProcessor<APIGatewayProxyEventV2> {
|
|
409
|
+
protected getPath(event: APIGatewayProxyEventV2): string {
|
|
410
|
+
return event.rawPath
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
protected getMethod(event: APIGatewayProxyEventV2): string {
|
|
414
|
+
return event.requestContext.http.method
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
protected getQueryString(event: APIGatewayProxyEventV2): string {
|
|
418
|
+
return event.rawQueryString
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
protected getCookies(event: APIGatewayProxyEventV2, headers: Headers): void {
|
|
422
|
+
if (Array.isArray(event.cookies)) {
|
|
423
|
+
headers.set('Cookie', event.cookies.join('; '))
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
protected setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void {
|
|
428
|
+
result.cookies = cookies
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
protected getHeaders(event: APIGatewayProxyEventV2): Headers {
|
|
432
|
+
const headers = new Headers()
|
|
433
|
+
this.getCookies(event, headers)
|
|
434
|
+
if (event.headers) {
|
|
435
|
+
for (const [k, v] of Object.entries(event.headers)) {
|
|
436
|
+
if (v) {
|
|
437
|
+
headers.set(k, v)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return headers
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const v2Processor: EventV2Processor = new EventV2Processor()
|
|
446
|
+
|
|
447
|
+
export class EventV1Processor extends EventProcessor<APIGatewayProxyEvent> {
|
|
448
|
+
protected getPath(event: APIGatewayProxyEvent): string {
|
|
449
|
+
return event.path
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
protected getMethod(event: APIGatewayProxyEvent): string {
|
|
453
|
+
return event.httpMethod
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
protected getQueryString(event: APIGatewayProxyEvent): string {
|
|
457
|
+
// In the case of gateway Integration either queryStringParameters or multiValueQueryStringParameters can be present not both
|
|
458
|
+
// API Gateway passes decoded values, so we need to re-encode them to preserve the original URL
|
|
459
|
+
if (event.multiValueQueryStringParameters) {
|
|
460
|
+
return Object.entries(event.multiValueQueryStringParameters || {})
|
|
461
|
+
.filter(([, value]) => value)
|
|
462
|
+
.map(([key, values]) =>
|
|
463
|
+
values.map((value) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&')
|
|
464
|
+
)
|
|
465
|
+
.join('&')
|
|
466
|
+
} else {
|
|
467
|
+
return Object.entries(event.queryStringParameters || {})
|
|
468
|
+
.filter(([, value]) => value !== undefined)
|
|
469
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value || '')}`)
|
|
470
|
+
.join('&')
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
protected getCookies(_event: APIGatewayProxyEvent, _headers: Headers): void {
|
|
475
|
+
// nop
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
protected getHeaders(event: APIGatewayProxyEvent): Headers {
|
|
479
|
+
const headers = new Headers()
|
|
480
|
+
this.getCookies(event, headers)
|
|
481
|
+
if (event.multiValueHeaders) {
|
|
482
|
+
for (const [k, values] of Object.entries(event.multiValueHeaders)) {
|
|
483
|
+
if (values) {
|
|
484
|
+
values.forEach((v) => headers.append(k, sanitizeHeaderValue(v)))
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (event.headers) {
|
|
489
|
+
for (const [k, v] of Object.entries(event.headers)) {
|
|
490
|
+
if (v && !headers.has(k)) {
|
|
491
|
+
headers.set(k, sanitizeHeaderValue(v))
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return headers
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
protected setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void {
|
|
499
|
+
result.multiValueHeaders = {
|
|
500
|
+
'set-cookie': cookies,
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const v1Processor: EventV1Processor = new EventV1Processor()
|
|
506
|
+
|
|
507
|
+
export class ALBProcessor extends EventProcessor<ALBProxyEvent> {
|
|
508
|
+
protected getHeaders(event: ALBProxyEvent): Headers {
|
|
509
|
+
const headers = new Headers()
|
|
510
|
+
// if multiValueHeaders is present the ALB will use it instead of the headers field
|
|
511
|
+
// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers
|
|
512
|
+
if (event.multiValueHeaders) {
|
|
513
|
+
for (const [key, values] of Object.entries(event.multiValueHeaders)) {
|
|
514
|
+
if (values && Array.isArray(values)) {
|
|
515
|
+
// https://www.rfc-editor.org/rfc/rfc9110.html#name-common-rules-for-defining-f
|
|
516
|
+
const sanitizedValue = sanitizeHeaderValue(values.join('; '))
|
|
517
|
+
headers.set(key, sanitizedValue)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
for (const [key, value] of Object.entries(event.headers ?? {})) {
|
|
522
|
+
if (value) {
|
|
523
|
+
headers.set(key, sanitizeHeaderValue(value))
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return headers
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
protected getPath(event: ALBProxyEvent): string {
|
|
531
|
+
return event.path
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
protected getMethod(event: ALBProxyEvent): string {
|
|
535
|
+
return event.httpMethod
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
protected getQueryString(event: ALBProxyEvent): string {
|
|
539
|
+
// In the case of ALB Integration either queryStringParameters or multiValueQueryStringParameters can be present not both
|
|
540
|
+
/*
|
|
541
|
+
In other cases like when using the serverless framework, the event object does contain both queryStringParameters and multiValueQueryStringParameters:
|
|
542
|
+
Below is an example event object for this URL: /payment/b8c55e69?select=amount&select=currency
|
|
543
|
+
{
|
|
544
|
+
...
|
|
545
|
+
queryStringParameters: { select: 'currency' },
|
|
546
|
+
multiValueQueryStringParameters: { select: [ 'amount', 'currency' ] },
|
|
547
|
+
}
|
|
548
|
+
The expected results is for select to be an array with two items. However the pre-fix code is only returning one item ('currency') in the array.
|
|
549
|
+
A simple fix would be to invert the if statement and check the multiValueQueryStringParameters first.
|
|
550
|
+
*/
|
|
551
|
+
if (event.multiValueQueryStringParameters) {
|
|
552
|
+
return Object.entries(event.multiValueQueryStringParameters || {})
|
|
553
|
+
.filter(([, value]) => value)
|
|
554
|
+
.map(([key, value]) => `${key}=${value.join(`&${key}=`)}`)
|
|
555
|
+
.join('&')
|
|
556
|
+
} else {
|
|
557
|
+
return Object.entries(event.queryStringParameters || {})
|
|
558
|
+
.filter(([, value]) => value !== undefined)
|
|
559
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
560
|
+
.join('&')
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
protected getCookies(event: ALBProxyEvent, headers: Headers): void {
|
|
565
|
+
let cookie
|
|
566
|
+
if (event.multiValueHeaders) {
|
|
567
|
+
cookie = event.multiValueHeaders['cookie']?.join('; ')
|
|
568
|
+
} else {
|
|
569
|
+
cookie = event.headers ? event.headers['cookie'] : undefined
|
|
570
|
+
}
|
|
571
|
+
if (cookie) {
|
|
572
|
+
headers.append('Cookie', cookie)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
protected setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void {
|
|
577
|
+
// when multi value headers is enabled
|
|
578
|
+
if (result.multiValueHeaders) {
|
|
579
|
+
result.multiValueHeaders['set-cookie'] = cookies
|
|
580
|
+
} else {
|
|
581
|
+
result.headers['set-cookie'] = cookies[0]
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const albProcessor: ALBProcessor = new ALBProcessor()
|
|
587
|
+
|
|
588
|
+
export class LatticeV2Processor extends EventProcessor<LatticeProxyEventV2> {
|
|
589
|
+
protected getPath(event: LatticeProxyEventV2): string {
|
|
590
|
+
return event.path
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
protected getMethod(event: LatticeProxyEventV2): string {
|
|
594
|
+
return event.method
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
protected getQueryString(): string {
|
|
598
|
+
return ''
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
protected getHeaders(event: LatticeProxyEventV2): Headers {
|
|
602
|
+
const headers = new Headers()
|
|
603
|
+
|
|
604
|
+
if (event.headers) {
|
|
605
|
+
for (const [k, values] of Object.entries(event.headers)) {
|
|
606
|
+
if (values) {
|
|
607
|
+
values.forEach((v) => headers.append(k, sanitizeHeaderValue(v)))
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return headers
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
protected getCookies(): void {
|
|
616
|
+
// nop
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
protected setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void {
|
|
620
|
+
result.headers = {
|
|
621
|
+
...result.headers,
|
|
622
|
+
'set-cookie': cookies,
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const latticeV2Processor: LatticeV2Processor = new LatticeV2Processor()
|
|
628
|
+
|
|
629
|
+
export const getProcessor = (event: LambdaEvent): EventProcessor<LambdaEvent> => {
|
|
630
|
+
if (isProxyEventALB(event)) {
|
|
631
|
+
return albProcessor
|
|
632
|
+
}
|
|
633
|
+
if (isProxyEventV2(event)) {
|
|
634
|
+
return v2Processor
|
|
635
|
+
}
|
|
636
|
+
if (isLatticeEventV2(event)) {
|
|
637
|
+
return latticeV2Processor
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return v1Processor
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const isProxyEventALB = (event: LambdaEvent): event is ALBProxyEvent => {
|
|
644
|
+
if (event.requestContext) {
|
|
645
|
+
return Object.hasOwn(event.requestContext, 'elb')
|
|
646
|
+
}
|
|
647
|
+
return false
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const isProxyEventV2 = (event: LambdaEvent): event is APIGatewayProxyEventV2 => {
|
|
651
|
+
// A V1 (REST API) event behind a custom domain base path mapping also carries a
|
|
652
|
+
// `rawPath`, so `rawPath` alone is not enough to identify a V2 event. Every V2
|
|
653
|
+
// (HTTP API / function URL) event has an `http` object on its request context.
|
|
654
|
+
return Object.hasOwn(event, 'rawPath') && Object.hasOwn(event.requestContext ?? {}, 'http')
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const isLatticeEventV2 = (event: LambdaEvent): event is LatticeProxyEventV2 => {
|
|
658
|
+
if (event.requestContext) {
|
|
659
|
+
return Object.hasOwn(event.requestContext, 'serviceArn')
|
|
660
|
+
}
|
|
661
|
+
return false
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Check if the given content type is binary.
|
|
666
|
+
* This is a default function and may be overwritten by the user via `isContentTypeBinary` option in handler().
|
|
667
|
+
* @param contentType The content type to check.
|
|
668
|
+
* @returns True if the content type is binary, false otherwise.
|
|
669
|
+
*/
|
|
670
|
+
export const defaultIsContentTypeBinary = (contentType: string): boolean => {
|
|
671
|
+
if (/^application\/vnd\.(?:apple\.installer|mozilla\.xul)\+xml\s*(?:;|$)/i.test(contentType)) {
|
|
672
|
+
return true
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return !/^text\/(?:plain|html|css|javascript|csv)|(?:\/|\+)(?:json|xml)\s*(?:;|$)/i.test(
|
|
676
|
+
contentType
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export const isContentEncodingBinary = (contentEncoding: string | null) => {
|
|
681
|
+
return !!contentEncoding && !/^identity$/i.test(contentEncoding)
|
|
682
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* AWS Lambda Adapter for Acryl.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { handle, streamHandle, defaultIsContentTypeBinary } from './handler.ts'
|
|
7
|
+
export { getConnInfo } from './conninfo.ts'
|
|
8
|
+
export type { APIGatewayProxyResult, LambdaEvent } from './handler.ts'
|
|
9
|
+
export type {
|
|
10
|
+
ApiGatewayRequestContext,
|
|
11
|
+
ApiGatewayRequestContextV2,
|
|
12
|
+
ALBRequestContext,
|
|
13
|
+
LambdaContext,
|
|
14
|
+
} from './types.ts'
|