@awsless/cli 0.0.45 → 0.0.46-next.0

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.
@@ -0,0 +1,534 @@
1
+ // src/feature/bundle/server/handle.ts
2
+ import { patch, unpatch } from "@awsless/json";
3
+ import { ExpectedError, invoke, isErrorResponse } from "@awsless/lambda";
4
+ import { formatRoutePayload, getCurrentRoute, withRoute } from "awsless";
5
+
6
+ // src/feature/bundle/server/preview.ts
7
+ import { GetObjectCommand, NoSuchKey } from "@aws-sdk/client-s3";
8
+ import { s3Client } from "@awsless/s3";
9
+ var getPossibleRouteKeys = (path) => {
10
+ if (path === "" || path === "/") {
11
+ return ["/", "/*"];
12
+ }
13
+ const parts = path.split("/");
14
+ const root = path.startsWith("/") ? parts[1] : parts[0];
15
+ const file = parts[parts.length - 1].includes(".");
16
+ if (root.includes(".")) {
17
+ return [path, "/*.", "/*"];
18
+ }
19
+ if (file) {
20
+ return [path, "/" + root + "/*.", "/" + root + "/*", "/*.", "/*"];
21
+ }
22
+ return [path, "/" + root + "/*", "/*"];
23
+ };
24
+ var findRoute = (props, path, method) => {
25
+ for (const key of getPossibleRouteKeys(path)) {
26
+ const route = props.routes[`${props.router}:${key}`];
27
+ if (!route) {
28
+ continue;
29
+ }
30
+ if (route.type === "s3" && method !== "GET" && method !== "HEAD") {
31
+ continue;
32
+ }
33
+ return route;
34
+ }
35
+ return;
36
+ };
37
+ var rewritePath = (route, path) => {
38
+ if (!route.rewrite) {
39
+ return path;
40
+ }
41
+ if (route.rewrite.regex) {
42
+ return path.replace(new RegExp(route.rewrite.regex), route.rewrite.to);
43
+ }
44
+ return route.rewrite.to;
45
+ };
46
+ var serveObject = async (route, path, method) => {
47
+ const bucket = route.domainName.split(".s3")[0];
48
+ const key = rewritePath(route, path).replace(/^\//, "");
49
+ let result;
50
+ try {
51
+ result = await s3Client().send(new GetObjectCommand({
52
+ Bucket: bucket,
53
+ Key: key
54
+ }));
55
+ } catch (error) {
56
+ if (error instanceof NoSuchKey) {
57
+ return {
58
+ statusCode: 404
59
+ };
60
+ }
61
+ throw error;
62
+ }
63
+ const headers = {};
64
+ if (result.ContentType) {
65
+ headers["content-type"] = result.ContentType;
66
+ }
67
+ if (result.CacheControl) {
68
+ headers["cache-control"] = result.CacheControl;
69
+ }
70
+ if (result.ETag) {
71
+ headers["etag"] = result.ETag;
72
+ }
73
+ if (method === "HEAD") {
74
+ return { statusCode: 200, headers };
75
+ }
76
+ return {
77
+ statusCode: 200,
78
+ headers,
79
+ body: await result.Body.transformToString("base64"),
80
+ isBase64Encoded: true
81
+ };
82
+ };
83
+ var createPreviewHandler = (props) => {
84
+ return async (event) => {
85
+ const method = event.requestContext.http.method;
86
+ const headers = event.headers ?? {};
87
+ let path = event.rawPath;
88
+ try {
89
+ path = decodeURIComponent(path);
90
+ } catch {}
91
+ if (method === "OPTIONS") {
92
+ return {
93
+ statusCode: 204,
94
+ headers: {
95
+ "access-control-allow-origin": "*",
96
+ "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
97
+ "access-control-allow-headers": "*",
98
+ "access-control-max-age": "86400"
99
+ }
100
+ };
101
+ }
102
+ const route = findRoute(props, path, method);
103
+ if (!route) {
104
+ return {
105
+ statusCode: 404
106
+ };
107
+ }
108
+ if (route.type === "s3") {
109
+ return serveObject(route, path, method);
110
+ }
111
+ for (const [name, value] of Object.entries(route.requestHeaders ?? {})) {
112
+ headers[name] = value;
113
+ }
114
+ if (headers.authorization) {
115
+ headers["x-awsless-authorization"] = headers.authorization;
116
+ } else {
117
+ delete headers["x-awsless-authorization"];
118
+ }
119
+ if (route.forwardHost && headers.host) {
120
+ headers["x-forwarded-host"] = headers.host;
121
+ }
122
+ event.headers = headers;
123
+ event.rawPath = rewritePath(route, path);
124
+ return props.dispatch(event);
125
+ };
126
+ };
127
+
128
+ // src/feature/bundle/server/resource/util.ts
129
+ var asyncRoute = (key, payload) => {
130
+ process.env.THROW_EXPECTED_ERRORS = "1";
131
+ return { key, payload };
132
+ };
133
+ var webRoute = (key, event) => {
134
+ const authorization = event.headers?.["x-awsless-authorization"];
135
+ if (typeof authorization === "string") {
136
+ event.headers.authorization = authorization;
137
+ delete event.headers["x-awsless-authorization"];
138
+ }
139
+ return { key, payload: event };
140
+ };
141
+
142
+ // src/feature/bundle/server/resource/cron.ts
143
+ var cronHandler = (event) => {
144
+ const route = event?.["$awsless-route"];
145
+ if (typeof route === "string" && route.split(":")[1] === "cron") {
146
+ return asyncRoute(route, event.event);
147
+ }
148
+ return;
149
+ };
150
+
151
+ // src/feature/bundle/server/resource/function.ts
152
+ var functionHandler = (event) => {
153
+ const route = event?.["$awsless-route"];
154
+ if (typeof route === "string" && route.split(":")[1] === "function") {
155
+ return {
156
+ key: route,
157
+ payload: event.event
158
+ };
159
+ }
160
+ return;
161
+ };
162
+
163
+ // src/feature/bundle/server/resource/icon.ts
164
+ var iconHandler = (event) => {
165
+ const route = event?.["$awsless-route"];
166
+ if (typeof route === "string") {
167
+ if (route.split(":")[1] === "icon") {
168
+ return {
169
+ key: route,
170
+ payload: event.event
171
+ };
172
+ }
173
+ return;
174
+ }
175
+ const requestRoute = event?.headers?.["x-awsless-route"];
176
+ if (typeof requestRoute === "string" && requestRoute.split(":")[1] === "icon") {
177
+ return webRoute(requestRoute, event);
178
+ }
179
+ return;
180
+ };
181
+
182
+ // src/feature/bundle/server/resource/image.ts
183
+ var imageHandler = (event) => {
184
+ const route = event?.["$awsless-route"];
185
+ if (typeof route === "string") {
186
+ if (route.split(":")[1] === "image") {
187
+ return {
188
+ key: route,
189
+ payload: event.event
190
+ };
191
+ }
192
+ return;
193
+ }
194
+ const requestRoute = event?.headers?.["x-awsless-route"];
195
+ if (typeof requestRoute === "string" && requestRoute.split(":")[1] === "image") {
196
+ return webRoute(requestRoute, event);
197
+ }
198
+ return;
199
+ };
200
+
201
+ // src/feature/bundle/server/resource/log.ts
202
+ var logHandler = (event) => {
203
+ const route = event?.["$awsless-route"];
204
+ if (typeof route === "string") {
205
+ if (route.split(":")[1] === "on-error-log") {
206
+ return asyncRoute(route, event.event);
207
+ }
208
+ return;
209
+ }
210
+ if (typeof event?.awslogs?.data === "string") {
211
+ return asyncRoute(`${process.env.APP}:on-error-log:handler`, event);
212
+ }
213
+ return;
214
+ };
215
+
216
+ // src/feature/bundle/server/resource/metric.ts
217
+ var metricHandler = (event) => {
218
+ if (typeof event?.["$awsless-route"] === "string" || typeof event?.headers?.["x-awsless-route"] === "string") {
219
+ return;
220
+ }
221
+ if (event?.source === "aws.cloudwatch" && typeof event.alarmArn === "string") {
222
+ const alarmName = event.alarmArn.split(":alarm:").at(-1);
223
+ const route = alarmName.slice(process.env.APP.length + 2).split("--").join(":");
224
+ if (route.split(":")[1] === "metric") {
225
+ return asyncRoute(route, event);
226
+ }
227
+ }
228
+ return;
229
+ };
230
+
231
+ // src/feature/bundle/server/resource/on-failure.ts
232
+ var onFailureHandler = (event) => {
233
+ const route = event?.["$awsless-route"];
234
+ if (typeof route === "string") {
235
+ if (route.split(":")[1] === "on-failure") {
236
+ return asyncRoute(route, event.event);
237
+ }
238
+ return;
239
+ }
240
+ if (typeof event?.headers?.["x-awsless-route"] === "string") {
241
+ return;
242
+ }
243
+ const record = event?.Records?.[0];
244
+ const eventSourceArn = record?.eventSourceARN;
245
+ if (record?.eventSource === "aws:sqs" && typeof eventSourceArn === "string" && eventSourceArn.endsWith(":" + process.env.APP + "--on-failure--failure")) {
246
+ return asyncRoute(`${process.env.APP}:on-failure:normalizer`, event);
247
+ }
248
+ return;
249
+ };
250
+
251
+ // src/feature/bundle/server/resource/pubsub.ts
252
+ var eventTypes = ["connected", "disconnected", "subscribed", "unsubscribed"];
253
+ var pubsubHandler = (event, routes) => {
254
+ const route = event?.["$awsless-route"];
255
+ if (typeof route === "string") {
256
+ if (route.split(":")[1] === "pubsub") {
257
+ if (eventTypes.some((type) => route.endsWith(`-${type}`))) {
258
+ return asyncRoute(route, event.event);
259
+ }
260
+ return {
261
+ key: route,
262
+ payload: event.event
263
+ };
264
+ }
265
+ return;
266
+ }
267
+ if (typeof event?.headers?.["x-awsless-route"] === "string") {
268
+ return;
269
+ }
270
+ const record = event?.Records?.[0];
271
+ if (record?.EventSource !== "aws:sns" || typeof record?.Sns?.TopicArn !== "string") {
272
+ return;
273
+ }
274
+ const topicName = record.Sns.TopicArn.split(":").at(-1);
275
+ const [, resourceType, pubsubId] = topicName.split("--");
276
+ if (resourceType !== "pubsub-events") {
277
+ return;
278
+ }
279
+ const eventType = record.Sns.MessageAttributes?.event?.Value;
280
+ const listeners = routes.filter((route2) => {
281
+ const [, type, id] = route2.split(":");
282
+ return type === "pubsub" && id === `${pubsubId}-${eventType}`;
283
+ });
284
+ if (!listeners.length) {
285
+ throw new Error(`Unknown bundle pubsub event: ${pubsubId}-${eventType}`);
286
+ }
287
+ if (listeners.length === 1) {
288
+ return asyncRoute(listeners[0], event);
289
+ }
290
+ return listeners.map((key) => ({ key, payload: event }));
291
+ };
292
+
293
+ // src/feature/bundle/server/resource/queue.ts
294
+ var queueHandler = (event) => {
295
+ const route = event?.["$awsless-route"];
296
+ if (typeof route === "string") {
297
+ if (route.split(":")[1] === "queue") {
298
+ return asyncRoute(route, event.event);
299
+ }
300
+ return;
301
+ }
302
+ if (typeof event?.headers?.["x-awsless-route"] === "string") {
303
+ return;
304
+ }
305
+ const record = event?.Records?.[0];
306
+ if (record?.eventSource === "aws:sqs" && typeof record.eventSourceARN === "string") {
307
+ const resourceName = record.eventSourceARN.split(":").at(-1);
308
+ const route2 = resourceName.replace(/\.fifo$/, "").slice(process.env.APP.length + 2).split("--").join(":");
309
+ if (route2.split(":")[1] === "queue") {
310
+ return asyncRoute(route2, event);
311
+ }
312
+ }
313
+ return;
314
+ };
315
+
316
+ // src/feature/bundle/server/resource/rest.ts
317
+ var restHandler = (event) => {
318
+ if (typeof event?.["$awsless-route"] === "string") {
319
+ return;
320
+ }
321
+ const route = event?.headers?.["x-awsless-route"];
322
+ if (typeof route === "string" && route.split(":")[1] === "rest") {
323
+ return webRoute(route, event);
324
+ }
325
+ return;
326
+ };
327
+
328
+ // src/feature/bundle/server/resource/rpc.ts
329
+ var rpcHandler = (event) => {
330
+ const route = event?.["$awsless-route"];
331
+ if (typeof route === "string") {
332
+ if (route.split(":")[1] === "rpc") {
333
+ return {
334
+ key: route,
335
+ payload: event.event
336
+ };
337
+ }
338
+ return;
339
+ }
340
+ const requestRoute = event?.headers?.["x-awsless-route"];
341
+ if (typeof requestRoute === "string" && requestRoute.split(":")[1] === "rpc") {
342
+ return webRoute(requestRoute, event);
343
+ }
344
+ return;
345
+ };
346
+
347
+ // src/feature/bundle/server/resource/site.ts
348
+ var siteHandler = (event) => {
349
+ if (typeof event?.["$awsless-route"] === "string") {
350
+ return;
351
+ }
352
+ const route = event?.headers?.["x-awsless-route"];
353
+ if (typeof route === "string" && route.split(":")[1] === "site") {
354
+ return webRoute(route, event);
355
+ }
356
+ return;
357
+ };
358
+
359
+ // src/feature/bundle/server/resource/store.ts
360
+ var storeHandler = (event) => {
361
+ const requestedRoute = event?.["$awsless-route"];
362
+ if (typeof requestedRoute === "string") {
363
+ if (requestedRoute.split(":")[1] === "store") {
364
+ return asyncRoute(requestedRoute, event.event);
365
+ }
366
+ return;
367
+ }
368
+ if (typeof event?.headers?.["x-awsless-route"] === "string") {
369
+ return;
370
+ }
371
+ const record = event?.Records?.[0];
372
+ const route = record?.s3?.configurationId;
373
+ if (record?.eventSource === "aws:s3" && typeof route === "string" && route.split(":")[1] === "store") {
374
+ return asyncRoute(route, event);
375
+ }
376
+ return;
377
+ };
378
+
379
+ // src/feature/bundle/server/resource/table.ts
380
+ var tableHandler = (event) => {
381
+ if (typeof event?.["$awsless-route"] === "string" || typeof event?.headers?.["x-awsless-route"] === "string") {
382
+ return;
383
+ }
384
+ const record = event?.Records?.[0];
385
+ if (record?.eventSource === "aws:dynamodb" && typeof record.eventSourceARN === "string") {
386
+ const tableName = record.eventSourceARN.split("/")[1];
387
+ const route = tableName.slice(process.env.APP.length + 2).split("--").join(":");
388
+ if (route.split(":")[1] === "table") {
389
+ return asyncRoute(route, event);
390
+ }
391
+ }
392
+ return;
393
+ };
394
+
395
+ // src/feature/bundle/server/resource/task.ts
396
+ var taskHandler = (event) => {
397
+ const route = event?.["$awsless-route"];
398
+ if (typeof route === "string" && route.split(":")[1] === "task") {
399
+ return asyncRoute(route, event.event);
400
+ }
401
+ return;
402
+ };
403
+
404
+ // src/feature/bundle/server/resource/topic.ts
405
+ var topicHandler = (event, routes) => {
406
+ const route = event?.["$awsless-route"];
407
+ if (typeof route === "string") {
408
+ if (route.split(":")[1] === "topic") {
409
+ return asyncRoute(route, event.event);
410
+ }
411
+ return;
412
+ }
413
+ if (typeof event?.headers?.["x-awsless-route"] === "string") {
414
+ return;
415
+ }
416
+ const record = event?.Records?.[0];
417
+ if (record?.EventSource !== "aws:sns" || typeof record?.Sns?.TopicArn !== "string") {
418
+ return;
419
+ }
420
+ const topicId = record.Sns.TopicArn.split(":").at(-1).split("--").at(-1);
421
+ const subscribers = routes.filter((route2) => {
422
+ const [, type, id] = route2.split(":");
423
+ return type === "topic" && id === topicId;
424
+ });
425
+ if (!subscribers.length) {
426
+ throw new Error(`Unknown bundle topic: ${topicId}`);
427
+ }
428
+ if (subscribers.length === 1) {
429
+ return asyncRoute(subscribers[0], event);
430
+ }
431
+ return subscribers.map((key) => ({ key, payload: event }));
432
+ };
433
+
434
+ // src/feature/bundle/server/handle.ts
435
+ var createBundle = (handlers) => {
436
+ const routes = Object.keys(handlers);
437
+ let previewConfig;
438
+ const matchers = [
439
+ functionHandler,
440
+ cronHandler,
441
+ iconHandler,
442
+ imageHandler,
443
+ metricHandler,
444
+ onFailureHandler,
445
+ logHandler,
446
+ queueHandler,
447
+ pubsubHandler,
448
+ topicHandler,
449
+ taskHandler,
450
+ restHandler,
451
+ rpcHandler,
452
+ siteHandler,
453
+ storeHandler,
454
+ tableHandler
455
+ ];
456
+ const matchRoute = (event) => {
457
+ delete process.env.THROW_EXPECTED_ERRORS;
458
+ for (const matcher of matchers) {
459
+ const match = matcher(event, routes);
460
+ if (match) {
461
+ return match;
462
+ }
463
+ }
464
+ const route = event?.["$awsless-route"] ?? event?.headers?.["x-awsless-route"];
465
+ throw new Error("Unknown bundle route: " + route);
466
+ };
467
+ return async (event, context) => {
468
+ const handleRoute = (match2) => {
469
+ const load = handlers[match2.key];
470
+ if (!load) {
471
+ throw new Error("Unknown bundle route: " + match2.key);
472
+ }
473
+ process.env.AWSLESS_ROUTE = match2.key;
474
+ return withRoute(match2.key, invokeRoute, async () => {
475
+ const handle = await load();
476
+ return handle(match2.payload ?? {}, context);
477
+ });
478
+ };
479
+ const invokeRoute = async (key, payload) => {
480
+ const caller = getCurrentRoute();
481
+ const throwExpectedErrors = process.env.THROW_EXPECTED_ERRORS;
482
+ let result;
483
+ try {
484
+ const match2 = matchRoute(formatRoutePayload(key, unpatch(payload ?? {})));
485
+ if (Array.isArray(match2)) {
486
+ throw new Error("Unknown bundle route: " + key);
487
+ }
488
+ result = await handleRoute(match2);
489
+ } finally {
490
+ if (caller) {
491
+ process.env.AWSLESS_ROUTE = caller;
492
+ if (throwExpectedErrors) {
493
+ process.env.THROW_EXPECTED_ERRORS = throwExpectedErrors;
494
+ } else {
495
+ delete process.env.THROW_EXPECTED_ERRORS;
496
+ }
497
+ }
498
+ }
499
+ const response = result === undefined ? undefined : patch(result);
500
+ if (isErrorResponse(response)) {
501
+ throw new ExpectedError(response.__error__.type, response.__error__.message);
502
+ }
503
+ return response;
504
+ };
505
+ const raw = event;
506
+ if (process.env.AWSLESS_PREVIEW && raw?.requestContext?.http && !event?.headers?.["x-awsless-route"]) {
507
+ previewConfig ??= JSON.parse(process.env.AWSLESS_PREVIEW);
508
+ return createPreviewHandler({
509
+ ...previewConfig,
510
+ dispatch: async (event2) => {
511
+ const match2 = matchRoute(event2);
512
+ if (Array.isArray(match2)) {
513
+ throw new Error("Unknown bundle route");
514
+ }
515
+ return handleRoute(match2);
516
+ }
517
+ })(event);
518
+ }
519
+ const match = matchRoute(event);
520
+ if (Array.isArray(match)) {
521
+ const name = `${process.env.AWS_LAMBDA_FUNCTION_NAME}:${process.env.AWS_LAMBDA_FUNCTION_VERSION}`;
522
+ await Promise.all(match.map((route) => invoke({
523
+ name,
524
+ type: "Event",
525
+ payload: formatRoutePayload(route.key, route.payload)
526
+ })));
527
+ return;
528
+ }
529
+ return handleRoute(match);
530
+ };
531
+ };
532
+ export {
533
+ createBundle
534
+ };
@@ -0,0 +1,104 @@
1
+ // src/feature/icon/server/handle.ts
2
+ import { getObject, putObject } from "@awsless/s3";
3
+ import { getRouteEnv, invokeRoute } from "awsless";
4
+ import { optimize } from "svgo/browser";
5
+ import svgstore from "svgstore";
6
+ var handle_default = async (event) => {
7
+ try {
8
+ const path = event.rawPath.startsWith("/") ? event.rawPath.slice(1) : event.rawPath;
9
+ const bucket = getRouteEnv("ICON_BUCKET");
10
+ const folder = getRouteEnv("ICON_FOLDER") ?? "";
11
+ const cacheKey = `${folder}cache/${path}`;
12
+ if (bucket) {
13
+ const cachedIcon = await getObject({
14
+ bucket,
15
+ key: cacheKey
16
+ });
17
+ if (cachedIcon) {
18
+ const cachedIconData = await cachedIcon.body.transformToByteArray();
19
+ return {
20
+ statusCode: 200,
21
+ body: Buffer.from(cachedIconData).toString("base64"),
22
+ isBase64Encoded: true,
23
+ headers: {
24
+ "Content-Type": "image/svg+xml",
25
+ "Cache-Control": "public, max-age=31536000, immutable"
26
+ }
27
+ };
28
+ }
29
+ }
30
+ const configsEnv = getRouteEnv("ICON_CONFIG");
31
+ if (!configsEnv) {
32
+ throw new Error("Icon config not found in environment variables");
33
+ }
34
+ const config = JSON.parse(configsEnv);
35
+ let baseIcon;
36
+ if (getRouteEnv("ICON_ORIGIN_S3")) {
37
+ const result = await getObject({
38
+ bucket,
39
+ key: `${folder}origin/${path}`
40
+ });
41
+ if (result?.body) {
42
+ const data2 = await result.body.transformToByteArray();
43
+ baseIcon = Buffer.from(data2);
44
+ }
45
+ }
46
+ const originRoute = getRouteEnv("ICON_ORIGIN");
47
+ if (!baseIcon && originRoute) {
48
+ const result = await invokeRoute(originRoute, { path });
49
+ if (typeof result === "string") {
50
+ baseIcon = Buffer.from(result);
51
+ } else if (result === undefined) {
52
+ return { statusCode: 404 };
53
+ } else {
54
+ throw new Error("Invalid response from icon origin lambda");
55
+ }
56
+ }
57
+ if (!baseIcon) {
58
+ return { statusCode: 404 };
59
+ }
60
+ const { data } = optimize(baseIcon.toString("utf-8"), {
61
+ multipass: true,
62
+ plugins: [
63
+ {
64
+ name: "preset-default",
65
+ params: {
66
+ overrides: {
67
+ ...config.preserveIds ? {
68
+ cleanupIds: false
69
+ } : {}
70
+ }
71
+ }
72
+ }
73
+ ]
74
+ });
75
+ let icon = data;
76
+ if (config.symbols) {
77
+ const symbols = svgstore();
78
+ symbols.add("default", data);
79
+ icon = symbols.toString({ inline: true });
80
+ }
81
+ await putObject({
82
+ bucket,
83
+ key: cacheKey,
84
+ body: icon,
85
+ contentType: "image/svg+xml",
86
+ cacheControl: "public, max-age=31536000, immutable"
87
+ });
88
+ return {
89
+ statusCode: 200,
90
+ body: Buffer.from(icon).toString("base64"),
91
+ isBase64Encoded: true,
92
+ headers: {
93
+ "Content-Type": "image/svg+xml",
94
+ "Cache-Control": "public, max-age=31536000, immutable"
95
+ }
96
+ };
97
+ } catch (error) {
98
+ console.error(error);
99
+ return { statusCode: 500 };
100
+ }
101
+ };
102
+ export {
103
+ handle_default as default
104
+ };