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,324 @@
|
|
|
1
|
+
import { Y as Schema, v as Env } from "./context-DS3fYyG-.js";
|
|
2
|
+
import { r as GetConnInfo } from "./types-Dv_nV2cv.js";
|
|
3
|
+
import { t as Acryl } from "./app-D8riatvv.js";
|
|
4
|
+
|
|
5
|
+
//#region ../../plugins/acryl-router/src/core/adapter/aws-lambda/types.d.ts
|
|
6
|
+
interface CognitoIdentity {
|
|
7
|
+
cognitoIdentityId: string;
|
|
8
|
+
cognitoIdentityPoolId: string;
|
|
9
|
+
}
|
|
10
|
+
interface ClientContext {
|
|
11
|
+
client: ClientContextClient;
|
|
12
|
+
Custom?: any;
|
|
13
|
+
env: ClientContextEnv;
|
|
14
|
+
}
|
|
15
|
+
interface ClientContextClient {
|
|
16
|
+
installationId: string;
|
|
17
|
+
appTitle: string;
|
|
18
|
+
appVersionName: string;
|
|
19
|
+
appVersionCode: string;
|
|
20
|
+
appPackageName: string;
|
|
21
|
+
}
|
|
22
|
+
interface ClientContextEnv {
|
|
23
|
+
platformVersion: string;
|
|
24
|
+
platform: string;
|
|
25
|
+
make: string;
|
|
26
|
+
model: string;
|
|
27
|
+
locale: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* {@link Handler} context parameter.
|
|
31
|
+
* See {@link https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html AWS documentation}.
|
|
32
|
+
*/
|
|
33
|
+
interface LambdaContext {
|
|
34
|
+
callbackWaitsForEmptyEventLoop: boolean;
|
|
35
|
+
functionName: string;
|
|
36
|
+
functionVersion: string;
|
|
37
|
+
invokedFunctionArn: string;
|
|
38
|
+
memoryLimitInMB: string;
|
|
39
|
+
awsRequestId: string;
|
|
40
|
+
logGroupName: string;
|
|
41
|
+
logStreamName: string;
|
|
42
|
+
identity?: CognitoIdentity | undefined;
|
|
43
|
+
clientContext?: ClientContext | undefined;
|
|
44
|
+
getRemainingTimeInMillis(): number;
|
|
45
|
+
}
|
|
46
|
+
type Callback<TResult = any> = (error?: Error | string | null, result?: TResult) => void;
|
|
47
|
+
type Handler<TEvent = any, TResult = any> = (event: TEvent, context: LambdaContext, callback: Callback<TResult>) => void | Promise<TResult>;
|
|
48
|
+
interface ClientCert {
|
|
49
|
+
clientCertPem: string;
|
|
50
|
+
subjectDN: string;
|
|
51
|
+
issuerDN: string;
|
|
52
|
+
serialNumber: string;
|
|
53
|
+
validity: {
|
|
54
|
+
notBefore: string;
|
|
55
|
+
notAfter: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
interface Identity {
|
|
59
|
+
accessKey?: string;
|
|
60
|
+
accountId?: string;
|
|
61
|
+
caller?: string;
|
|
62
|
+
cognitoAuthenticationProvider?: string;
|
|
63
|
+
cognitoAuthenticationType?: string;
|
|
64
|
+
cognitoIdentityId?: string;
|
|
65
|
+
cognitoIdentityPoolId?: string;
|
|
66
|
+
principalOrgId?: string;
|
|
67
|
+
sourceIp: string;
|
|
68
|
+
user?: string;
|
|
69
|
+
userAgent: string;
|
|
70
|
+
userArn?: string;
|
|
71
|
+
clientCert?: ClientCert;
|
|
72
|
+
}
|
|
73
|
+
interface ApiGatewayRequestContext {
|
|
74
|
+
accountId: string;
|
|
75
|
+
apiId: string;
|
|
76
|
+
authorizer: {
|
|
77
|
+
claims?: unknown;
|
|
78
|
+
scopes?: unknown;
|
|
79
|
+
};
|
|
80
|
+
domainName: string;
|
|
81
|
+
domainPrefix: string;
|
|
82
|
+
extendedRequestId: string;
|
|
83
|
+
httpMethod: string;
|
|
84
|
+
identity: Identity;
|
|
85
|
+
path: string;
|
|
86
|
+
protocol: string;
|
|
87
|
+
requestId: string;
|
|
88
|
+
requestTime: string;
|
|
89
|
+
requestTimeEpoch: number;
|
|
90
|
+
resourceId?: string;
|
|
91
|
+
resourcePath: string;
|
|
92
|
+
stage: string;
|
|
93
|
+
}
|
|
94
|
+
interface Authorizer {
|
|
95
|
+
iam?: {
|
|
96
|
+
accessKey: string;
|
|
97
|
+
accountId: string;
|
|
98
|
+
callerId: string;
|
|
99
|
+
cognitoIdentity: null;
|
|
100
|
+
principalOrgId: null;
|
|
101
|
+
userArn: string;
|
|
102
|
+
userId: string;
|
|
103
|
+
};
|
|
104
|
+
jwt?: {
|
|
105
|
+
claims: Record<string, string | number | boolean | string[]>;
|
|
106
|
+
scopes: string[] | null;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* The `context` object returned by a Lambda (REQUEST) authorizer.
|
|
110
|
+
* It is `null` when the authorizer returns no context.
|
|
111
|
+
*/
|
|
112
|
+
lambda?: Record<string, unknown> | null;
|
|
113
|
+
}
|
|
114
|
+
interface ApiGatewayRequestContextV2 {
|
|
115
|
+
accountId: string;
|
|
116
|
+
apiId: string;
|
|
117
|
+
authentication: null;
|
|
118
|
+
authorizer: Authorizer;
|
|
119
|
+
domainName: string;
|
|
120
|
+
domainPrefix: string;
|
|
121
|
+
http: {
|
|
122
|
+
method: string;
|
|
123
|
+
path: string;
|
|
124
|
+
protocol: string;
|
|
125
|
+
sourceIp: string;
|
|
126
|
+
userAgent: string;
|
|
127
|
+
};
|
|
128
|
+
requestId: string;
|
|
129
|
+
routeKey: string;
|
|
130
|
+
stage: string;
|
|
131
|
+
time: string;
|
|
132
|
+
timeEpoch: number;
|
|
133
|
+
}
|
|
134
|
+
interface ALBRequestContext {
|
|
135
|
+
elb: {
|
|
136
|
+
targetGroupArn: string;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
interface LatticeRequestContextV2 {
|
|
140
|
+
serviceNetworkArn: string;
|
|
141
|
+
serviceArn: string;
|
|
142
|
+
targetGroupArn: string;
|
|
143
|
+
region: string;
|
|
144
|
+
timeEpoch: string;
|
|
145
|
+
identity: {
|
|
146
|
+
sourceVpcArn?: string;
|
|
147
|
+
type?: string;
|
|
148
|
+
principal?: string;
|
|
149
|
+
principalOrgID?: string;
|
|
150
|
+
sessionName?: string;
|
|
151
|
+
x509IssuerOu?: string;
|
|
152
|
+
x509SanDns?: string;
|
|
153
|
+
x509SanNameCn?: string;
|
|
154
|
+
x509SanUri?: string;
|
|
155
|
+
x509SubjectCn?: string;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region ../../plugins/acryl-router/src/core/adapter/aws-lambda/handler.d.ts
|
|
160
|
+
type LambdaEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2 | ALBProxyEvent | LatticeProxyEventV2;
|
|
161
|
+
interface LatticeProxyEventV2 {
|
|
162
|
+
version: string;
|
|
163
|
+
path: string;
|
|
164
|
+
method: string;
|
|
165
|
+
headers: Record<string, string[] | undefined>;
|
|
166
|
+
queryStringParameters: Record<string, string[] | undefined>;
|
|
167
|
+
body: string | null;
|
|
168
|
+
isBase64Encoded: boolean;
|
|
169
|
+
requestContext: LatticeRequestContextV2;
|
|
170
|
+
}
|
|
171
|
+
interface APIGatewayProxyEventV2 {
|
|
172
|
+
version: string;
|
|
173
|
+
routeKey: string;
|
|
174
|
+
headers: Record<string, string | undefined>;
|
|
175
|
+
multiValueHeaders?: undefined;
|
|
176
|
+
cookies?: string[];
|
|
177
|
+
rawPath: string;
|
|
178
|
+
rawQueryString: string;
|
|
179
|
+
body: string | null;
|
|
180
|
+
isBase64Encoded: boolean;
|
|
181
|
+
requestContext: ApiGatewayRequestContextV2;
|
|
182
|
+
queryStringParameters?: {
|
|
183
|
+
[name: string]: string | undefined;
|
|
184
|
+
};
|
|
185
|
+
pathParameters?: {
|
|
186
|
+
[name: string]: string | undefined;
|
|
187
|
+
};
|
|
188
|
+
stageVariables?: {
|
|
189
|
+
[name: string]: string | undefined;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
interface APIGatewayProxyEvent {
|
|
193
|
+
version: string;
|
|
194
|
+
httpMethod: string;
|
|
195
|
+
headers: Record<string, string | undefined>;
|
|
196
|
+
multiValueHeaders?: {
|
|
197
|
+
[headerKey: string]: string[];
|
|
198
|
+
};
|
|
199
|
+
path: string;
|
|
200
|
+
body: string | null;
|
|
201
|
+
isBase64Encoded: boolean;
|
|
202
|
+
queryStringParameters?: Record<string, string | undefined>;
|
|
203
|
+
requestContext: ApiGatewayRequestContext;
|
|
204
|
+
resource: string;
|
|
205
|
+
multiValueQueryStringParameters?: {
|
|
206
|
+
[parameterKey: string]: string[];
|
|
207
|
+
};
|
|
208
|
+
pathParameters?: Record<string, string>;
|
|
209
|
+
stageVariables?: Record<string, string>;
|
|
210
|
+
}
|
|
211
|
+
interface ALBProxyEvent {
|
|
212
|
+
httpMethod: string;
|
|
213
|
+
headers?: Record<string, string | undefined>;
|
|
214
|
+
multiValueHeaders?: Record<string, string[] | undefined>;
|
|
215
|
+
path: string;
|
|
216
|
+
body: string | null;
|
|
217
|
+
isBase64Encoded: boolean;
|
|
218
|
+
queryStringParameters?: Record<string, string | undefined>;
|
|
219
|
+
multiValueQueryStringParameters?: {
|
|
220
|
+
[parameterKey: string]: string[];
|
|
221
|
+
};
|
|
222
|
+
requestContext: ALBRequestContext;
|
|
223
|
+
}
|
|
224
|
+
type WithHeaders = {
|
|
225
|
+
headers: Record<string, string | string[]>;
|
|
226
|
+
multiValueHeaders?: undefined;
|
|
227
|
+
};
|
|
228
|
+
type WithMultiValueHeaders = {
|
|
229
|
+
headers?: undefined;
|
|
230
|
+
multiValueHeaders: Record<string, string[]>;
|
|
231
|
+
};
|
|
232
|
+
type APIGatewayProxyResult = {
|
|
233
|
+
statusCode: number;
|
|
234
|
+
statusDescription?: string;
|
|
235
|
+
body: string;
|
|
236
|
+
cookies?: string[];
|
|
237
|
+
isBase64Encoded: boolean;
|
|
238
|
+
} & (WithHeaders | WithMultiValueHeaders);
|
|
239
|
+
declare const streamHandle: <E extends Env = Env, S extends Schema = {}, BasePath extends string = "/">(app: Acryl<E, S, BasePath>) => Handler;
|
|
240
|
+
type HandleOptions = {
|
|
241
|
+
isContentTypeBinary: ((contentType: string) => boolean) | undefined;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Converts a Acryl application to an AWS Lambda handler.
|
|
245
|
+
*
|
|
246
|
+
* Accepts events from API Gateway (v1 and v2), Application Load Balancer (ALB),
|
|
247
|
+
* and Lambda Function URLs.
|
|
248
|
+
*
|
|
249
|
+
* @param app - The Acryl application instance
|
|
250
|
+
* @param options - Optional configuration
|
|
251
|
+
* @param options.isContentTypeBinary - A function to determine if the content type is binary.
|
|
252
|
+
* If not provided, the default function will be used.
|
|
253
|
+
* @returns Lambda handler function
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```js
|
|
257
|
+
* import { Acryl } from 'acrylblends'
|
|
258
|
+
* import { handle } from 'acrylblends/aws-lambda'
|
|
259
|
+
*
|
|
260
|
+
* const app = new Acryl()
|
|
261
|
+
*
|
|
262
|
+
* app.get('/', (c) => c.text('Hello from Lambda'))
|
|
263
|
+
* app.get('/json', (c) => c.json({ message: 'Hello JSON' }))
|
|
264
|
+
*
|
|
265
|
+
* export const handler = handle(app)
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```js
|
|
270
|
+
* // With custom binary content type detection
|
|
271
|
+
* import { handle, defaultIsContentTypeBinary } from 'acrylblends/aws-lambda'
|
|
272
|
+
* export const handler = handle(app, {
|
|
273
|
+
* isContentTypeBinary: (contentType) => {
|
|
274
|
+
* if (defaultIsContentTypeBinary(contentType)) {
|
|
275
|
+
* // default logic same as prior to v4.8.4
|
|
276
|
+
* return true
|
|
277
|
+
* }
|
|
278
|
+
* return contentType.startsWith('image/') || contentType === 'application/pdf'
|
|
279
|
+
* }
|
|
280
|
+
* })
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
declare const handle: <E extends Env = Env, S extends Schema = {}, BasePath extends string = "/">(app: Acryl<E, S, BasePath>, {
|
|
284
|
+
isContentTypeBinary
|
|
285
|
+
}?: HandleOptions) => (<L extends LambdaEvent>(event: L, lambdaContext?: LambdaContext) => Promise<APIGatewayProxyResult & (L extends {
|
|
286
|
+
multiValueHeaders: Record<string, string[]>;
|
|
287
|
+
} ? WithMultiValueHeaders : WithHeaders)>);
|
|
288
|
+
/**
|
|
289
|
+
* Check if the given content type is binary.
|
|
290
|
+
* This is a default function and may be overwritten by the user via `isContentTypeBinary` option in handler().
|
|
291
|
+
* @param contentType The content type to check.
|
|
292
|
+
* @returns True if the content type is binary, false otherwise.
|
|
293
|
+
*/
|
|
294
|
+
declare const defaultIsContentTypeBinary: (contentType: string) => boolean;
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region ../../plugins/acryl-router/src/core/adapter/aws-lambda/conninfo.d.ts
|
|
297
|
+
/**
|
|
298
|
+
* Get connection information from AWS Lambda
|
|
299
|
+
*
|
|
300
|
+
* Extracts client IP from various Lambda event sources:
|
|
301
|
+
* - API Gateway v1 (REST API): requestContext.identity.sourceIp
|
|
302
|
+
* - API Gateway v2 (HTTP API/Function URLs): requestContext.http.sourceIp
|
|
303
|
+
* - ALB: Falls back to x-forwarded-for header
|
|
304
|
+
*
|
|
305
|
+
* @param c - Context
|
|
306
|
+
* @returns Connection information including remote address
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* import { Acryl } from 'acrylblends'
|
|
310
|
+
* import { handle, getConnInfo } from 'acrylblends/aws-lambda'
|
|
311
|
+
*
|
|
312
|
+
* const app = new Acryl()
|
|
313
|
+
*
|
|
314
|
+
* app.get('/', (c) => {
|
|
315
|
+
* const info = getConnInfo(c)
|
|
316
|
+
* return c.text(`Your IP: ${info.remote.address}`)
|
|
317
|
+
* })
|
|
318
|
+
*
|
|
319
|
+
* export const handler = handle(app)
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
declare const getConnInfo: GetConnInfo;
|
|
323
|
+
//#endregion
|
|
324
|
+
export { type ALBRequestContext, type APIGatewayProxyResult, type ApiGatewayRequestContext, type ApiGatewayRequestContextV2, type LambdaContext, type LambdaEvent, defaultIsContentTypeBinary, getConnInfo, handle, streamHandle };
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { decodeBase64, encodeBase64 } from "./utils/encode.js";
|
|
2
|
+
import { pipeline } from "node:stream/promises";
|
|
3
|
+
//#region ../../plugins/acryl-router/src/core/adapter/aws-lambda/handler.ts
|
|
4
|
+
function sanitizeHeaderValue(value) {
|
|
5
|
+
if (!/[^\x00-\x7F]/.test(value)) return value;
|
|
6
|
+
return encodeURIComponent(value);
|
|
7
|
+
}
|
|
8
|
+
const getRequestContext = (event) => {
|
|
9
|
+
return event.requestContext;
|
|
10
|
+
};
|
|
11
|
+
async function* readWebStream(reader) {
|
|
12
|
+
let readResult = await reader.read();
|
|
13
|
+
while (!readResult.done) {
|
|
14
|
+
yield readResult.value;
|
|
15
|
+
readResult = await reader.read();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const streamToNodeStream = (reader, writer) => pipeline(readWebStream(reader), writer);
|
|
19
|
+
const streamHandle = (app) => {
|
|
20
|
+
return awslambda.streamifyResponse(async (event, responseStream, context) => {
|
|
21
|
+
const processor = getProcessor(event);
|
|
22
|
+
try {
|
|
23
|
+
const req = processor.createRequest(event);
|
|
24
|
+
const requestContext = getRequestContext(event);
|
|
25
|
+
const res = await app.fetch(req, {
|
|
26
|
+
event,
|
|
27
|
+
requestContext,
|
|
28
|
+
context
|
|
29
|
+
});
|
|
30
|
+
const headers = {};
|
|
31
|
+
const cookies = [];
|
|
32
|
+
res.headers.forEach((value, name) => {
|
|
33
|
+
if (name === "set-cookie") cookies.push(value);
|
|
34
|
+
else headers[name] = value;
|
|
35
|
+
});
|
|
36
|
+
const httpResponseMetadata = {
|
|
37
|
+
statusCode: res.status,
|
|
38
|
+
headers,
|
|
39
|
+
cookies
|
|
40
|
+
};
|
|
41
|
+
responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata);
|
|
42
|
+
if (res.body) await streamToNodeStream(res.body.getReader(), responseStream);
|
|
43
|
+
else responseStream.write("");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("Error processing request:", error);
|
|
46
|
+
responseStream.write("Internal Server Error");
|
|
47
|
+
} finally {
|
|
48
|
+
responseStream.end();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Converts a Acryl application to an AWS Lambda handler.
|
|
54
|
+
*
|
|
55
|
+
* Accepts events from API Gateway (v1 and v2), Application Load Balancer (ALB),
|
|
56
|
+
* and Lambda Function URLs.
|
|
57
|
+
*
|
|
58
|
+
* @param app - The Acryl application instance
|
|
59
|
+
* @param options - Optional configuration
|
|
60
|
+
* @param options.isContentTypeBinary - A function to determine if the content type is binary.
|
|
61
|
+
* If not provided, the default function will be used.
|
|
62
|
+
* @returns Lambda handler function
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```js
|
|
66
|
+
* import { Acryl } from 'acrylblends'
|
|
67
|
+
* import { handle } from 'acrylblends/aws-lambda'
|
|
68
|
+
*
|
|
69
|
+
* const app = new Acryl()
|
|
70
|
+
*
|
|
71
|
+
* app.get('/', (c) => c.text('Hello from Lambda'))
|
|
72
|
+
* app.get('/json', (c) => c.json({ message: 'Hello JSON' }))
|
|
73
|
+
*
|
|
74
|
+
* export const handler = handle(app)
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```js
|
|
79
|
+
* // With custom binary content type detection
|
|
80
|
+
* import { handle, defaultIsContentTypeBinary } from 'acrylblends/aws-lambda'
|
|
81
|
+
* export const handler = handle(app, {
|
|
82
|
+
* isContentTypeBinary: (contentType) => {
|
|
83
|
+
* if (defaultIsContentTypeBinary(contentType)) {
|
|
84
|
+
* // default logic same as prior to v4.8.4
|
|
85
|
+
* return true
|
|
86
|
+
* }
|
|
87
|
+
* return contentType.startsWith('image/') || contentType === 'application/pdf'
|
|
88
|
+
* }
|
|
89
|
+
* })
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
const handle = (app, { isContentTypeBinary } = { isContentTypeBinary: void 0 }) => {
|
|
93
|
+
return async (event, lambdaContext) => {
|
|
94
|
+
const processor = getProcessor(event);
|
|
95
|
+
let req, requestContext;
|
|
96
|
+
try {
|
|
97
|
+
req = processor.createRequest(event);
|
|
98
|
+
requestContext = getRequestContext(event);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error("Error processing request:", error);
|
|
101
|
+
const errorResponse = error instanceof TypeError ? new Response("Invalid request", { status: 400 }) : new Response("Internal Server Error", { status: 500 });
|
|
102
|
+
return processor.createResult(event, errorResponse, { isContentTypeBinary });
|
|
103
|
+
}
|
|
104
|
+
const res = await app.fetch(req, {
|
|
105
|
+
event,
|
|
106
|
+
requestContext,
|
|
107
|
+
lambdaContext
|
|
108
|
+
});
|
|
109
|
+
return processor.createResult(event, res, { isContentTypeBinary });
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
var EventProcessor = class {
|
|
113
|
+
getHeaderValue(headers, key) {
|
|
114
|
+
return headers ? Array.isArray(headers[key]) ? headers[key][0] : headers[key] : void 0;
|
|
115
|
+
}
|
|
116
|
+
getDomainName(event) {
|
|
117
|
+
if (event.requestContext && "domainName" in event.requestContext) return event.requestContext.domainName;
|
|
118
|
+
const hostFromHeaders = this.getHeaderValue(event.headers, "host");
|
|
119
|
+
if (hostFromHeaders) return hostFromHeaders;
|
|
120
|
+
const multiValueHeaders = "multiValueHeaders" in event ? event.multiValueHeaders : {};
|
|
121
|
+
return this.getHeaderValue(multiValueHeaders, "host");
|
|
122
|
+
}
|
|
123
|
+
createRequest(event) {
|
|
124
|
+
const queryString = this.getQueryString(event);
|
|
125
|
+
const urlPath = `https://${this.getDomainName(event)}${this.getPath(event)}`;
|
|
126
|
+
const url = queryString ? `${urlPath}?${queryString}` : urlPath;
|
|
127
|
+
const headers = this.getHeaders(event);
|
|
128
|
+
const requestInit = {
|
|
129
|
+
headers,
|
|
130
|
+
method: this.getMethod(event)
|
|
131
|
+
};
|
|
132
|
+
if (event.body) {
|
|
133
|
+
const body = event.isBase64Encoded ? decodeBase64(event.body) : new TextEncoder().encode(event.body);
|
|
134
|
+
requestInit.body = body;
|
|
135
|
+
headers.set("content-length", body.length.toString());
|
|
136
|
+
}
|
|
137
|
+
return new Request(url, requestInit);
|
|
138
|
+
}
|
|
139
|
+
async createResult(event, res, options) {
|
|
140
|
+
const contentType = res.headers.get("content-type");
|
|
141
|
+
const isContentTypeBinary = options.isContentTypeBinary ?? defaultIsContentTypeBinary;
|
|
142
|
+
let isBase64Encoded = contentType && isContentTypeBinary(contentType) ? true : false;
|
|
143
|
+
if (!isBase64Encoded) {
|
|
144
|
+
const contentEncoding = res.headers.get("content-encoding");
|
|
145
|
+
isBase64Encoded = isContentEncodingBinary(contentEncoding);
|
|
146
|
+
}
|
|
147
|
+
const result = {
|
|
148
|
+
body: isBase64Encoded ? encodeBase64(await res.arrayBuffer()) : await res.text(),
|
|
149
|
+
statusCode: res.status,
|
|
150
|
+
isBase64Encoded,
|
|
151
|
+
..."multiValueHeaders" in event && event.multiValueHeaders ? { multiValueHeaders: {} } : { headers: {} }
|
|
152
|
+
};
|
|
153
|
+
this.setCookies(event, res, result);
|
|
154
|
+
if (result.multiValueHeaders) res.headers.forEach((value, key) => {
|
|
155
|
+
result.multiValueHeaders[key] = [value];
|
|
156
|
+
});
|
|
157
|
+
else res.headers.forEach((value, key) => {
|
|
158
|
+
result.headers[key] = value;
|
|
159
|
+
});
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
setCookies(_event, res, result) {
|
|
163
|
+
if (res.headers.has("set-cookie")) {
|
|
164
|
+
const cookies = res.headers.getSetCookie ? res.headers.getSetCookie() : Array.from(res.headers.entries()).filter(([k]) => k === "set-cookie").map(([, v]) => v);
|
|
165
|
+
if (Array.isArray(cookies)) {
|
|
166
|
+
this.setCookiesToResult(result, cookies);
|
|
167
|
+
res.headers.delete("set-cookie");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var EventV2Processor = class extends EventProcessor {
|
|
173
|
+
getPath(event) {
|
|
174
|
+
return event.rawPath;
|
|
175
|
+
}
|
|
176
|
+
getMethod(event) {
|
|
177
|
+
return event.requestContext.http.method;
|
|
178
|
+
}
|
|
179
|
+
getQueryString(event) {
|
|
180
|
+
return event.rawQueryString;
|
|
181
|
+
}
|
|
182
|
+
getCookies(event, headers) {
|
|
183
|
+
if (Array.isArray(event.cookies)) headers.set("Cookie", event.cookies.join("; "));
|
|
184
|
+
}
|
|
185
|
+
setCookiesToResult(result, cookies) {
|
|
186
|
+
result.cookies = cookies;
|
|
187
|
+
}
|
|
188
|
+
getHeaders(event) {
|
|
189
|
+
const headers = new Headers();
|
|
190
|
+
this.getCookies(event, headers);
|
|
191
|
+
if (event.headers) {
|
|
192
|
+
for (const [k, v] of Object.entries(event.headers)) if (v) headers.set(k, v);
|
|
193
|
+
}
|
|
194
|
+
return headers;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const v2Processor = new EventV2Processor();
|
|
198
|
+
var EventV1Processor = class extends EventProcessor {
|
|
199
|
+
getPath(event) {
|
|
200
|
+
return event.path;
|
|
201
|
+
}
|
|
202
|
+
getMethod(event) {
|
|
203
|
+
return event.httpMethod;
|
|
204
|
+
}
|
|
205
|
+
getQueryString(event) {
|
|
206
|
+
if (event.multiValueQueryStringParameters) return Object.entries(event.multiValueQueryStringParameters || {}).filter(([, value]) => value).map(([key, values]) => values.map((value) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&")).join("&");
|
|
207
|
+
else return Object.entries(event.queryStringParameters || {}).filter(([, value]) => value !== void 0).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value || "")}`).join("&");
|
|
208
|
+
}
|
|
209
|
+
getCookies(_event, _headers) {}
|
|
210
|
+
getHeaders(event) {
|
|
211
|
+
const headers = new Headers();
|
|
212
|
+
this.getCookies(event, headers);
|
|
213
|
+
if (event.multiValueHeaders) {
|
|
214
|
+
for (const [k, values] of Object.entries(event.multiValueHeaders)) if (values) values.forEach((v) => headers.append(k, sanitizeHeaderValue(v)));
|
|
215
|
+
}
|
|
216
|
+
if (event.headers) {
|
|
217
|
+
for (const [k, v] of Object.entries(event.headers)) if (v && !headers.has(k)) headers.set(k, sanitizeHeaderValue(v));
|
|
218
|
+
}
|
|
219
|
+
return headers;
|
|
220
|
+
}
|
|
221
|
+
setCookiesToResult(result, cookies) {
|
|
222
|
+
result.multiValueHeaders = { "set-cookie": cookies };
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const v1Processor = new EventV1Processor();
|
|
226
|
+
var ALBProcessor = class extends EventProcessor {
|
|
227
|
+
getHeaders(event) {
|
|
228
|
+
const headers = new Headers();
|
|
229
|
+
if (event.multiValueHeaders) {
|
|
230
|
+
for (const [key, values] of Object.entries(event.multiValueHeaders)) if (values && Array.isArray(values)) {
|
|
231
|
+
const sanitizedValue = sanitizeHeaderValue(values.join("; "));
|
|
232
|
+
headers.set(key, sanitizedValue);
|
|
233
|
+
}
|
|
234
|
+
} else for (const [key, value] of Object.entries(event.headers ?? {})) if (value) headers.set(key, sanitizeHeaderValue(value));
|
|
235
|
+
return headers;
|
|
236
|
+
}
|
|
237
|
+
getPath(event) {
|
|
238
|
+
return event.path;
|
|
239
|
+
}
|
|
240
|
+
getMethod(event) {
|
|
241
|
+
return event.httpMethod;
|
|
242
|
+
}
|
|
243
|
+
getQueryString(event) {
|
|
244
|
+
if (event.multiValueQueryStringParameters) return Object.entries(event.multiValueQueryStringParameters || {}).filter(([, value]) => value).map(([key, value]) => `${key}=${value.join(`&${key}=`)}`).join("&");
|
|
245
|
+
else return Object.entries(event.queryStringParameters || {}).filter(([, value]) => value !== void 0).map(([key, value]) => `${key}=${value}`).join("&");
|
|
246
|
+
}
|
|
247
|
+
getCookies(event, headers) {
|
|
248
|
+
let cookie;
|
|
249
|
+
if (event.multiValueHeaders) cookie = event.multiValueHeaders["cookie"]?.join("; ");
|
|
250
|
+
else cookie = event.headers ? event.headers["cookie"] : void 0;
|
|
251
|
+
if (cookie) headers.append("Cookie", cookie);
|
|
252
|
+
}
|
|
253
|
+
setCookiesToResult(result, cookies) {
|
|
254
|
+
if (result.multiValueHeaders) result.multiValueHeaders["set-cookie"] = cookies;
|
|
255
|
+
else result.headers["set-cookie"] = cookies[0];
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const albProcessor = new ALBProcessor();
|
|
259
|
+
var LatticeV2Processor = class extends EventProcessor {
|
|
260
|
+
getPath(event) {
|
|
261
|
+
return event.path;
|
|
262
|
+
}
|
|
263
|
+
getMethod(event) {
|
|
264
|
+
return event.method;
|
|
265
|
+
}
|
|
266
|
+
getQueryString() {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
getHeaders(event) {
|
|
270
|
+
const headers = new Headers();
|
|
271
|
+
if (event.headers) {
|
|
272
|
+
for (const [k, values] of Object.entries(event.headers)) if (values) values.forEach((v) => headers.append(k, sanitizeHeaderValue(v)));
|
|
273
|
+
}
|
|
274
|
+
return headers;
|
|
275
|
+
}
|
|
276
|
+
getCookies() {}
|
|
277
|
+
setCookiesToResult(result, cookies) {
|
|
278
|
+
result.headers = {
|
|
279
|
+
...result.headers,
|
|
280
|
+
"set-cookie": cookies
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const latticeV2Processor = new LatticeV2Processor();
|
|
285
|
+
const getProcessor = (event) => {
|
|
286
|
+
if (isProxyEventALB(event)) return albProcessor;
|
|
287
|
+
if (isProxyEventV2(event)) return v2Processor;
|
|
288
|
+
if (isLatticeEventV2(event)) return latticeV2Processor;
|
|
289
|
+
return v1Processor;
|
|
290
|
+
};
|
|
291
|
+
const isProxyEventALB = (event) => {
|
|
292
|
+
if (event.requestContext) return Object.hasOwn(event.requestContext, "elb");
|
|
293
|
+
return false;
|
|
294
|
+
};
|
|
295
|
+
const isProxyEventV2 = (event) => {
|
|
296
|
+
return Object.hasOwn(event, "rawPath") && Object.hasOwn(event.requestContext ?? {}, "http");
|
|
297
|
+
};
|
|
298
|
+
const isLatticeEventV2 = (event) => {
|
|
299
|
+
if (event.requestContext) return Object.hasOwn(event.requestContext, "serviceArn");
|
|
300
|
+
return false;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Check if the given content type is binary.
|
|
304
|
+
* This is a default function and may be overwritten by the user via `isContentTypeBinary` option in handler().
|
|
305
|
+
* @param contentType The content type to check.
|
|
306
|
+
* @returns True if the content type is binary, false otherwise.
|
|
307
|
+
*/
|
|
308
|
+
const defaultIsContentTypeBinary = (contentType) => {
|
|
309
|
+
if (/^application\/vnd\.(?:apple\.installer|mozilla\.xul)\+xml\s*(?:;|$)/i.test(contentType)) return true;
|
|
310
|
+
return !/^text\/(?:plain|html|css|javascript|csv)|(?:\/|\+)(?:json|xml)\s*(?:;|$)/i.test(contentType);
|
|
311
|
+
};
|
|
312
|
+
const isContentEncodingBinary = (contentEncoding) => {
|
|
313
|
+
return !!contentEncoding && !/^identity$/i.test(contentEncoding);
|
|
314
|
+
};
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region ../../plugins/acryl-router/src/core/adapter/aws-lambda/conninfo.ts
|
|
317
|
+
/**
|
|
318
|
+
* Get connection information from AWS Lambda
|
|
319
|
+
*
|
|
320
|
+
* Extracts client IP from various Lambda event sources:
|
|
321
|
+
* - API Gateway v1 (REST API): requestContext.identity.sourceIp
|
|
322
|
+
* - API Gateway v2 (HTTP API/Function URLs): requestContext.http.sourceIp
|
|
323
|
+
* - ALB: Falls back to x-forwarded-for header
|
|
324
|
+
*
|
|
325
|
+
* @param c - Context
|
|
326
|
+
* @returns Connection information including remote address
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* import { Acryl } from 'acrylblends'
|
|
330
|
+
* import { handle, getConnInfo } from 'acrylblends/aws-lambda'
|
|
331
|
+
*
|
|
332
|
+
* const app = new Acryl()
|
|
333
|
+
*
|
|
334
|
+
* app.get('/', (c) => {
|
|
335
|
+
* const info = getConnInfo(c)
|
|
336
|
+
* return c.text(`Your IP: ${info.remote.address}`)
|
|
337
|
+
* })
|
|
338
|
+
*
|
|
339
|
+
* export const handler = handle(app)
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
const getConnInfo = (c) => {
|
|
343
|
+
const requestContext = c.env.requestContext;
|
|
344
|
+
let address;
|
|
345
|
+
if ("identity" in requestContext && requestContext.identity?.sourceIp) address = requestContext.identity.sourceIp;
|
|
346
|
+
else if ("http" in requestContext && requestContext.http?.sourceIp) address = requestContext.http.sourceIp;
|
|
347
|
+
else {
|
|
348
|
+
const xff = c.req.header("x-forwarded-for");
|
|
349
|
+
if (xff) {
|
|
350
|
+
const ips = xff.split(",");
|
|
351
|
+
address = ips[ips.length - 1].trim();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { remote: { address } };
|
|
355
|
+
};
|
|
356
|
+
//#endregion
|
|
357
|
+
export { defaultIsContentTypeBinary, getConnInfo, handle, streamHandle };
|