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.
Files changed (87) hide show
  1. package/docs/framework/README.md +4 -3
  2. package/docs/framework/UPSTREAM.md +1 -1
  3. package/docs/framework/getting-started/aws-lambda.md +115 -0
  4. package/docs/framework/getting-started/bun.md +1 -3
  5. package/docs/framework/getting-started/cloudflare-pages.md +53 -0
  6. package/docs/framework/getting-started/cloudflare-workers.md +67 -0
  7. package/docs/framework/getting-started/lambda-edge.md +41 -0
  8. package/docs/framework/getting-started/netlify.md +25 -0
  9. package/docs/framework/getting-started/other-runtimes.md +20 -9
  10. package/docs/framework/getting-started/service-worker.md +31 -0
  11. package/docs/framework/getting-started/vercel.md +28 -0
  12. package/docs/framework/guides/create-acrylblends.md +40 -2
  13. package/docs/framework/guides/faq.md +2 -2
  14. package/docs/framework/helpers/conninfo.md +1 -1
  15. package/docs/framework/helpers/ssg.md +1 -1
  16. package/docs/framework/helpers/streaming.md +3 -2
  17. package/docs/framework/helpers/websocket.md +2 -2
  18. package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
  19. package/docs/framework/upstream.json +3 -3
  20. package/lib/aws-lambda.d.ts +324 -0
  21. package/lib/aws-lambda.js +357 -0
  22. package/lib/bun.d.ts +75 -0
  23. package/lib/bun.js +134 -0
  24. package/lib/cloudflare-pages.d.ts +58 -0
  25. package/lib/cloudflare-pages.js +78 -0
  26. package/lib/cloudflare-workers.d.ts +21 -0
  27. package/lib/cloudflare-workers.js +80 -0
  28. package/lib/index-CseujaYI.d.ts +31 -0
  29. package/lib/index-DaJtL7ci.d.ts +156 -0
  30. package/lib/lambda-edge.d.ts +92 -0
  31. package/lib/lambda-edge.js +105 -0
  32. package/lib/netlify.d.ts +29 -0
  33. package/lib/netlify.js +30 -0
  34. package/lib/node-server/register.js +1 -1
  35. package/lib/node-server/serve-static.js +1 -1
  36. package/lib/node-server.js +3 -3
  37. package/lib/serve-static-Cp5adzGo.js +77 -0
  38. package/lib/serve-static.d.ts +1 -30
  39. package/lib/serve-static.js +1 -76
  40. package/lib/service-worker.d.ts +48 -0
  41. package/lib/service-worker.js +38 -0
  42. package/lib/ssg.d.ts +1 -155
  43. package/lib/vercel.d.ts +10 -0
  44. package/lib/vercel.js +9 -0
  45. package/package.json +33 -1
  46. package/plugins/acryl-inspect/upstream.json +1 -1
  47. package/plugins/acryl-node-server/upstream.json +1 -1
  48. package/plugins/acryl-router/src/core/adapter/aws-lambda/conninfo.ts +73 -0
  49. package/plugins/acryl-router/src/core/adapter/aws-lambda/handler.ts +682 -0
  50. package/plugins/acryl-router/src/core/adapter/aws-lambda/index.ts +14 -0
  51. package/plugins/acryl-router/src/core/adapter/aws-lambda/types.ts +173 -0
  52. package/plugins/acryl-router/src/core/adapter/bun/conninfo.ts +43 -0
  53. package/plugins/acryl-router/src/core/adapter/bun/index.ts +11 -0
  54. package/plugins/acryl-router/src/core/adapter/bun/serve-static.ts +32 -0
  55. package/plugins/acryl-router/src/core/adapter/bun/server.ts +14 -0
  56. package/plugins/acryl-router/src/core/adapter/bun/ssg.ts +26 -0
  57. package/plugins/acryl-router/src/core/adapter/bun/websocket.ts +113 -0
  58. package/plugins/acryl-router/src/core/adapter/cloudflare-pages/conninfo.ts +26 -0
  59. package/plugins/acryl-router/src/core/adapter/cloudflare-pages/handler.ts +122 -0
  60. package/plugins/acryl-router/src/core/adapter/cloudflare-pages/index.ts +11 -0
  61. package/plugins/acryl-router/src/core/adapter/cloudflare-workers/conninfo.ts +7 -0
  62. package/plugins/acryl-router/src/core/adapter/cloudflare-workers/index.ts +8 -0
  63. package/plugins/acryl-router/src/core/adapter/cloudflare-workers/serve-static-module.ts +12 -0
  64. package/plugins/acryl-router/src/core/adapter/cloudflare-workers/serve-static.ts +41 -0
  65. package/plugins/acryl-router/src/core/adapter/cloudflare-workers/utils.ts +50 -0
  66. package/plugins/acryl-router/src/core/adapter/cloudflare-workers/websocket.ts +52 -0
  67. package/plugins/acryl-router/src/core/adapter/lambda-edge/conninfo.ts +15 -0
  68. package/plugins/acryl-router/src/core/adapter/lambda-edge/handler.ts +239 -0
  69. package/plugins/acryl-router/src/core/adapter/lambda-edge/index.ts +14 -0
  70. package/plugins/acryl-router/src/core/adapter/netlify/conninfo.ts +57 -0
  71. package/plugins/acryl-router/src/core/adapter/netlify/handler.ts +10 -0
  72. package/plugins/acryl-router/src/core/adapter/netlify/index.ts +6 -0
  73. package/plugins/acryl-router/src/core/adapter/netlify/mod.ts +2 -0
  74. package/plugins/acryl-router/src/core/adapter/service-worker/handler.ts +37 -0
  75. package/plugins/acryl-router/src/core/adapter/service-worker/index.ts +36 -0
  76. package/plugins/acryl-router/src/core/adapter/service-worker/types.ts +14 -0
  77. package/plugins/acryl-router/src/core/adapter/vercel/conninfo.ts +8 -0
  78. package/plugins/acryl-router/src/core/adapter/vercel/handler.ts +8 -0
  79. package/plugins/acryl-router/src/core/adapter/vercel/index.ts +7 -0
  80. package/plugins/acryl-router/upstream.json +2 -2
  81. package/plugins/acryl-router-http/upstream.json +1 -1
  82. package/plugins/acryl-state/upstream.json +1 -1
  83. package/plugins/desktop/upstream.json +1 -1
  84. package/plugins/desktop-web/upstream.json +1 -1
  85. package/templates/todo/upstream.json +1 -1
  86. package/lib/{serve-static-B14MF1Va.js → serve-static-B849HlX2.js} +1 -1
  87. 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'