serverless-spy 0.0.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.
- package/.eslintrc.yml +97 -0
- package/.gitattributes +24 -0
- package/.husky/pre-commit +4 -0
- package/.jsii +3199 -0
- package/.prettierignore +1 -0
- package/.prettierrc.json +5 -0
- package/API.md +123 -0
- package/LICENSE +202 -0
- package/README.md +1 -0
- package/cli/cli.ts +97 -0
- package/cli/index.html +43 -0
- package/cli/package-lock.json +2356 -0
- package/cli/package.json +14 -0
- package/cli/serverlessSpy.js +73 -0
- package/cli/spy_log.json +0 -0
- package/cli/style.css +43 -0
- package/cli/ws.ts +79 -0
- package/common/getWebSocketUrl.ts +68 -0
- package/common/package.json +22 -0
- package/common/spyEvents/DynamoDBSpyEvent.ts +12 -0
- package/common/spyEvents/EventBridgeRuleSpyEvent.ts +13 -0
- package/common/spyEvents/EventBridgeSpyEvent.ts +13 -0
- package/common/spyEvents/FunctionConsoleSpyEvent.ts +11 -0
- package/common/spyEvents/FunctionContext.ts +8 -0
- package/common/spyEvents/FunctionErrorSpyEvent.ts +9 -0
- package/common/spyEvents/FunctionRequestSpyEvent.ts +8 -0
- package/common/spyEvents/FunctionResponseSpyEvent.ts +10 -0
- package/common/spyEvents/S3SpyEvent.ts +9 -0
- package/common/spyEvents/SnsSubscriptionSpyEvent.ts +12 -0
- package/common/spyEvents/SnsTopicSpyEvent.ts +12 -0
- package/common/spyEvents/SpyEvent.ts +3 -0
- package/common/spyEvents/SpyMessage.ts +7 -0
- package/common/spyEvents/SqsSpyEvent.ts +8 -0
- package/functions/onConnect.ts +29 -0
- package/functions/onDisconnect.ts +28 -0
- package/functions/sendMessage.ts +277 -0
- package/functions/tsconfig.dev.json +16 -0
- package/functions/tsconfig.json +16 -0
- package/index.ts +2 -0
- package/lambda-extension/aws/Common.ts +88 -0
- package/lambda-extension/aws/Errors.ts +140 -0
- package/lambda-extension/aws/UserFunction.ts +169 -0
- package/lambda-extension/interceptor.ts +146 -0
- package/lambda-extension/spy-wrapper +4 -0
- package/lambda-extension/tsconfig.json +16 -0
- package/lib/ServerlessSpy.d.ts +27 -0
- package/lib/ServerlessSpy.js +301 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +14 -0
- package/listener/PrettifyForDisplay.ts +5 -0
- package/listener/RecursivePartial.ts +9 -0
- package/listener/SpyHandlers.ts.ts +190 -0
- package/listener/SpyListener.ts +104 -0
- package/listener/createServerlessSpyListener.ts +258 -0
- package/listener/index.ts +1 -0
- package/listener/matchers.ts +53 -0
- package/listener/package.json +19 -0
- package/listener/setup.ts +21 -0
- package/listener/tsconfig.dev.json +16 -0
- package/listener/tsconfig.json +16 -0
- package/package.json +132 -0
- package/scripts/run-task +1836 -0
- package/sst-esbuild/esbuild.js +57 -0
- package/x_jest.config.ts +203 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiGatewayManagementApi,
|
|
3
|
+
PostToConnectionCommand,
|
|
4
|
+
} from '@aws-sdk/client-apigatewaymanagementapi';
|
|
5
|
+
import {
|
|
6
|
+
AttributeValue,
|
|
7
|
+
DeleteItemCommand,
|
|
8
|
+
DynamoDBClient,
|
|
9
|
+
ScanCommand,
|
|
10
|
+
} from '@aws-sdk/client-dynamodb';
|
|
11
|
+
import { unmarshall } from '@aws-sdk/util-dynamodb';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
DynamoDBStreamEvent,
|
|
15
|
+
S3Event,
|
|
16
|
+
SNSEvent,
|
|
17
|
+
EventBridgeEvent,
|
|
18
|
+
SQSEvent,
|
|
19
|
+
} from 'aws-lambda';
|
|
20
|
+
import { DynamoDBSpyEvent } from '../common/spyEvents/DynamoDBSpyEvent';
|
|
21
|
+
import { EventBridgeRuleSpyEvent } from '../common/spyEvents/EventBridgeRuleSpyEvent';
|
|
22
|
+
import { EventBridgeSpyEvent } from '../common/spyEvents/EventBridgeSpyEvent';
|
|
23
|
+
import { S3SpyEvent } from '../common/spyEvents/S3SpyEvent';
|
|
24
|
+
import { SnsSubscriptionSpyEvent } from '../common/spyEvents/SnsSubscriptionSpyEvent';
|
|
25
|
+
import { SnsTopicSpyEvent } from '../common/spyEvents/SnsTopicSpyEvent';
|
|
26
|
+
import { SpyMessage } from '../common/spyEvents/SpyMessage';
|
|
27
|
+
import { SqsSpyEvent } from '../common/spyEvents/SqsSpyEvent';
|
|
28
|
+
|
|
29
|
+
const ddb = new DynamoDBClient({
|
|
30
|
+
region: process.env.AWS_REGION,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { TABLE_NAME } = process.env;
|
|
34
|
+
const endpoint = process.env.WS_ENDPOINT!;
|
|
35
|
+
|
|
36
|
+
const apigwManagementApi = new ApiGatewayManagementApi({
|
|
37
|
+
apiVersion: '2018-11-29',
|
|
38
|
+
endpoint,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let connections: Record<string, AttributeValue>[] | undefined;
|
|
42
|
+
|
|
43
|
+
export const handler = async (event: any, context: any) => {
|
|
44
|
+
console.log('EVENT', JSON.stringify(event));
|
|
45
|
+
// console.log("CONTEXT", JSON.stringify(context));
|
|
46
|
+
|
|
47
|
+
const mapping = JSON.parse(process.env.INFRA_MAPPING!);
|
|
48
|
+
console.log('mapping', JSON.stringify(mapping));
|
|
49
|
+
|
|
50
|
+
let connectionData;
|
|
51
|
+
|
|
52
|
+
const scanParams = new ScanCommand({
|
|
53
|
+
TableName: process.env.TABLE_NAME as string,
|
|
54
|
+
ProjectionExpression: 'connectionId',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
connectionData = await ddb.send(scanParams);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(e);
|
|
61
|
+
return { statusCode: 500, body: (e as Error)?.stack };
|
|
62
|
+
}
|
|
63
|
+
connections = connectionData.Items;
|
|
64
|
+
|
|
65
|
+
const postDataPromises: Promise<any>[] = [];
|
|
66
|
+
|
|
67
|
+
if (event?.Records && event.Records[0]?.Sns) {
|
|
68
|
+
console.log('*** SNS ***');
|
|
69
|
+
const eventSns = event as SNSEvent;
|
|
70
|
+
for (const record of eventSns.Records) {
|
|
71
|
+
const subscriptionArn = record.EventSubscriptionArn;
|
|
72
|
+
|
|
73
|
+
let serviceKey: string;
|
|
74
|
+
if (mapping[subscriptionArn]) {
|
|
75
|
+
// subscription event that could contain filter based on existing subscription
|
|
76
|
+
serviceKey = mapping[subscriptionArn];
|
|
77
|
+
} else {
|
|
78
|
+
// catch all subscription
|
|
79
|
+
const topicArn = record.Sns.TopicArn;
|
|
80
|
+
serviceKey = mapping[topicArn];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let message: string;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
message = JSON.parse(record.Sns.Message);
|
|
87
|
+
} catch {
|
|
88
|
+
message = record.Sns.Message;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const spyEventType = getSpyEventType(serviceKey) as
|
|
92
|
+
| 'FunctionSnsTopic'
|
|
93
|
+
| 'FunctionSnsSubscription';
|
|
94
|
+
|
|
95
|
+
const data: SnsTopicSpyEvent | SnsSubscriptionSpyEvent = {
|
|
96
|
+
spyEventType,
|
|
97
|
+
message,
|
|
98
|
+
subject: record.Sns.Subject,
|
|
99
|
+
timestamp: record.Sns.Timestamp,
|
|
100
|
+
topicArn: record.Sns.TopicArn,
|
|
101
|
+
messageId: record.Sns.MessageId,
|
|
102
|
+
messageAttributes: record.Sns.MessageAttributes,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const fluentEvent: Omit<SpyMessage, 'timestamp'> = {
|
|
106
|
+
data,
|
|
107
|
+
serviceKey,
|
|
108
|
+
};
|
|
109
|
+
postDataPromises.push(postData(fluentEvent));
|
|
110
|
+
}
|
|
111
|
+
} else if (event?.Records && event.Records[0]?.eventSource === 'aws:sqs') {
|
|
112
|
+
console.log('*** SQS ***');
|
|
113
|
+
const eventSqs = event as SQSEvent;
|
|
114
|
+
for (const record of eventSqs.Records) {
|
|
115
|
+
const subscriptionArn = record.eventSourceARN;
|
|
116
|
+
|
|
117
|
+
const serviceKey = mapping[subscriptionArn];
|
|
118
|
+
let body: string;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
body = JSON.parse(record.body);
|
|
122
|
+
} catch {
|
|
123
|
+
body = record.body;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const data: SqsSpyEvent = {
|
|
127
|
+
spyEventType: 'Sqs',
|
|
128
|
+
body,
|
|
129
|
+
messageAttributes: record.messageAttributes,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const fluentEvent: Omit<SpyMessage, 'timestamp'> = {
|
|
133
|
+
data,
|
|
134
|
+
serviceKey,
|
|
135
|
+
};
|
|
136
|
+
postDataPromises.push(postData(fluentEvent));
|
|
137
|
+
}
|
|
138
|
+
} else if (event?.Records && event.Records[0]?.s3) {
|
|
139
|
+
console.log('*** S3 ***');
|
|
140
|
+
const eventS3 = event as S3Event;
|
|
141
|
+
for (const record of eventS3.Records) {
|
|
142
|
+
const bucketArn = record.s3.bucket.arn;
|
|
143
|
+
|
|
144
|
+
const data: S3SpyEvent = {
|
|
145
|
+
spyEventType: 'S3',
|
|
146
|
+
eventName: record.eventName,
|
|
147
|
+
eventTime: record.eventTime,
|
|
148
|
+
bucket: record.s3.bucket.name,
|
|
149
|
+
key: record.s3.object.key,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const fluentEvent: Omit<SpyMessage, 'timestamp'> = {
|
|
153
|
+
data,
|
|
154
|
+
serviceKey: mapping[bucketArn],
|
|
155
|
+
};
|
|
156
|
+
postDataPromises.push(postData(fluentEvent));
|
|
157
|
+
}
|
|
158
|
+
} else if (event.Records && event.Records[0]?.dynamodb) {
|
|
159
|
+
console.log('*** DYNAMODB ***');
|
|
160
|
+
const eventDynamoDB = event as DynamoDBStreamEvent;
|
|
161
|
+
for (const record of eventDynamoDB.Records) {
|
|
162
|
+
let arn = record.eventSourceARN!;
|
|
163
|
+
arn = arn.substring(0, arn.indexOf('/stream/'));
|
|
164
|
+
|
|
165
|
+
const data: DynamoDBSpyEvent = {
|
|
166
|
+
spyEventType: 'DynamoDB',
|
|
167
|
+
eventName: record.eventName,
|
|
168
|
+
newImage: unmarshall(record.dynamodb?.NewImage as any),
|
|
169
|
+
keys: unmarshall(record.dynamodb?.Keys as any),
|
|
170
|
+
oldImage: record.dynamodb?.OldImage
|
|
171
|
+
? unmarshall(record.dynamodb?.OldImage as any)
|
|
172
|
+
: undefined,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const fluentEvent: Omit<SpyMessage, 'timestamp'> = {
|
|
176
|
+
data,
|
|
177
|
+
serviceKey: mapping[arn],
|
|
178
|
+
};
|
|
179
|
+
postDataPromises.push(postData(fluentEvent));
|
|
180
|
+
}
|
|
181
|
+
} else if (
|
|
182
|
+
event.detail &&
|
|
183
|
+
event['detail-type'] &&
|
|
184
|
+
event.version &&
|
|
185
|
+
event.source
|
|
186
|
+
) {
|
|
187
|
+
console.log('*** EventBridge ***');
|
|
188
|
+
const eventEb = event as EventBridgeEvent<any, any>;
|
|
189
|
+
|
|
190
|
+
const serviceKey = mapping.eventBridge; // the is new lambda for each subscription
|
|
191
|
+
|
|
192
|
+
const spyEventType = getSpyEventType(serviceKey) as
|
|
193
|
+
| 'EventBridge'
|
|
194
|
+
| 'EventBridgeRule';
|
|
195
|
+
|
|
196
|
+
const message = eventEb.detail;
|
|
197
|
+
|
|
198
|
+
const data: EventBridgeSpyEvent | EventBridgeRuleSpyEvent = {
|
|
199
|
+
spyEventType,
|
|
200
|
+
detail: message,
|
|
201
|
+
detailType: eventEb['detail-type'],
|
|
202
|
+
source: eventEb.source,
|
|
203
|
+
time: eventEb.time,
|
|
204
|
+
account: eventEb.account,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const fluentEvent: Omit<SpyMessage, 'timestamp'> = {
|
|
208
|
+
data,
|
|
209
|
+
serviceKey,
|
|
210
|
+
};
|
|
211
|
+
postDataPromises.push(postData(fluentEvent));
|
|
212
|
+
} else {
|
|
213
|
+
console.log('*** OTHER ***');
|
|
214
|
+
const fluentEvent: Omit<SpyMessage, 'timestamp'> = event;
|
|
215
|
+
postDataPromises.push(postData(fluentEvent));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await Promise.all(postDataPromises);
|
|
219
|
+
|
|
220
|
+
return { statusCode: 200, body: 'Data sent.' };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
async function postData(spyMessage: Omit<SpyMessage, 'timestamp'>) {
|
|
224
|
+
// const postData = JSON.parse(event.body!).data;
|
|
225
|
+
console.log('postData', JSON.stringify(spyMessage));
|
|
226
|
+
|
|
227
|
+
if (!connections) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const postCalls = connections.map(async ({ connectionId }) => {
|
|
232
|
+
console.log(`Sending message to: ${connectionId.S}`);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const postToConnectionCommand = new PostToConnectionCommand({
|
|
236
|
+
ConnectionId: connectionId.S,
|
|
237
|
+
Data: JSON.stringify({
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
serviceKey: spyMessage.serviceKey,
|
|
240
|
+
data: spyMessage.data,
|
|
241
|
+
}) as any,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await apigwManagementApi.send(postToConnectionCommand);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.error(`Fails sending message to: ${connectionId.S}`, e);
|
|
247
|
+
console.error(`Status code: ${(e as any).statusCode}`);
|
|
248
|
+
if ((e as any).$metadata.httpStatusCode === 410) {
|
|
249
|
+
console.log(`Found stale connection, deleting ${connectionId}`);
|
|
250
|
+
|
|
251
|
+
const deleteParams = new DeleteItemCommand({
|
|
252
|
+
TableName: TABLE_NAME,
|
|
253
|
+
Key: { connectionId },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await ddb.send(deleteParams);
|
|
257
|
+
} else {
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await Promise.all(postCalls);
|
|
265
|
+
console.log('Send message finish');
|
|
266
|
+
} catch (e) {
|
|
267
|
+
return { statusCode: 500, body: (e as Error)?.stack };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getSpyEventType(serviceKey: string) {
|
|
272
|
+
if (!serviceKey) {
|
|
273
|
+
throw new Error('Missing serviceKey');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return serviceKey.substring(0, serviceKey.indexOf('#'));
|
|
277
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "Node 16",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"lib": ["es2021"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"target": "es2021",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"resolveJsonModule": true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "Node 16",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"lib": ["es2021"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"target": "es2021",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"resolveJsonModule": true
|
|
15
|
+
}
|
|
16
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This code was copied from:
|
|
6
|
+
* https://raw.githubusercontent.com/aws/aws-lambda-nodejs-runtime-interface-client/main/src/Common/index.ts
|
|
7
|
+
*
|
|
8
|
+
* This module defines types, enums and interfaces common to the other modules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { IncomingHttpHeaders } from 'http';
|
|
12
|
+
|
|
13
|
+
export interface InvocationResponse {
|
|
14
|
+
bodyJson: string;
|
|
15
|
+
headers: IncomingHttpHeaders;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NativeClient {
|
|
19
|
+
initializeClient: (userAgent: string) => void;
|
|
20
|
+
done: (id: string, bodyString: string) => void;
|
|
21
|
+
error: (id: string, bodyString: string, xrayString: string) => void;
|
|
22
|
+
next: () => Promise<InvocationResponse>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export enum INVOKE_HEADER {
|
|
26
|
+
ClientContext = 'lambda-runtime-client-context',
|
|
27
|
+
CognitoIdentity = 'lambda-runtime-cognito-identity',
|
|
28
|
+
ARN = 'lambda-runtime-invoked-function-arn',
|
|
29
|
+
AWSRequestId = 'lambda-runtime-aws-request-id',
|
|
30
|
+
DeadlineMs = 'lambda-runtime-deadline-ms',
|
|
31
|
+
XRayTrace = 'lambda-runtime-trace-id',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IEnvironmentData {
|
|
35
|
+
functionVersion?: string;
|
|
36
|
+
functionName?: string;
|
|
37
|
+
memoryLimitInMB?: string;
|
|
38
|
+
logGroupName?: string;
|
|
39
|
+
logStreamName?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IHeaderData {
|
|
43
|
+
clientContext?: string;
|
|
44
|
+
identity?: string;
|
|
45
|
+
invokedFunctionArn?: string;
|
|
46
|
+
awsRequestId?: string;
|
|
47
|
+
getRemainingTimeInMillis: () => number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ErrorStringOrUndefined = Error | string | undefined;
|
|
51
|
+
|
|
52
|
+
export type ErrorStringOrUndefinedOrNull = ErrorStringOrUndefined | null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
*/
|
|
57
|
+
export interface ICallbackContext {
|
|
58
|
+
callbackWaitsForEmptyEventLoop: boolean;
|
|
59
|
+
succeed: (result: unknown) => void;
|
|
60
|
+
fail: (err: ErrorStringOrUndefinedOrNull) => void;
|
|
61
|
+
done: (err: ErrorStringOrUndefinedOrNull, result?: unknown) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type CallbackFunction = (
|
|
65
|
+
err: ErrorStringOrUndefinedOrNull,
|
|
66
|
+
result?: unknown
|
|
67
|
+
) => void;
|
|
68
|
+
|
|
69
|
+
export interface IBeforeExitListener {
|
|
70
|
+
invoke: () => void;
|
|
71
|
+
reset: () => () => void;
|
|
72
|
+
set: (listener: () => void) => () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface IErrorCallbacks {
|
|
76
|
+
uncaughtException: (err: Error) => void;
|
|
77
|
+
unhandledRejection: (err: Error) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type HandlerFunction = (
|
|
81
|
+
body: unknown,
|
|
82
|
+
data: IEnvironmentData & IHeaderData,
|
|
83
|
+
callback: CallbackFunction
|
|
84
|
+
) => PromiseLike<unknown> | unknown;
|
|
85
|
+
|
|
86
|
+
export function isHandlerFunction(value: any): value is HandlerFunction {
|
|
87
|
+
return typeof value === 'function';
|
|
88
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/**
|
|
4
|
+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
5
|
+
*
|
|
6
|
+
* This code was copied from:
|
|
7
|
+
* https://raw.githubusercontent.com/aws/aws-lambda-nodejs-runtime-interface-client/main/src/Errors/index.ts
|
|
8
|
+
*
|
|
9
|
+
* Defines custom error types throwable by the runtime.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
import util from 'util';
|
|
15
|
+
|
|
16
|
+
export function isError(obj: any): obj is Error {
|
|
17
|
+
return (
|
|
18
|
+
obj &&
|
|
19
|
+
obj.name &&
|
|
20
|
+
obj.message &&
|
|
21
|
+
obj.stack &&
|
|
22
|
+
typeof obj.name === 'string' &&
|
|
23
|
+
typeof obj.message === 'string' &&
|
|
24
|
+
typeof obj.stack === 'string'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RuntimeErrorResponse {
|
|
29
|
+
errorType: string;
|
|
30
|
+
errorMessage: string;
|
|
31
|
+
trace: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Attempt to convert an object into a response object.
|
|
36
|
+
* This method accounts for failures when serializing the error object.
|
|
37
|
+
*/
|
|
38
|
+
export function toRuntimeResponse(error: unknown): RuntimeErrorResponse {
|
|
39
|
+
try {
|
|
40
|
+
if (util.types.isNativeError(error) || isError(error)) {
|
|
41
|
+
if (!error.stack) {
|
|
42
|
+
throw new Error('Error stack is missing.');
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
errorType: error.name,
|
|
46
|
+
errorMessage: error.message,
|
|
47
|
+
trace: error.stack.split('\n') || [],
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
return {
|
|
51
|
+
errorType: typeof error,
|
|
52
|
+
errorMessage: (error as any).toString(),
|
|
53
|
+
trace: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
} catch (_err) {
|
|
57
|
+
return {
|
|
58
|
+
errorType: 'handled',
|
|
59
|
+
errorMessage:
|
|
60
|
+
'callback called with Error argument, but there was a problem while retrieving one or more of its message, name, and stack',
|
|
61
|
+
trace: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format an error with the expected properties.
|
|
68
|
+
* For compatability, the error string always starts with a tab.
|
|
69
|
+
*/
|
|
70
|
+
export const toFormatted = (error: unknown): string => {
|
|
71
|
+
try {
|
|
72
|
+
return (
|
|
73
|
+
'\t' + JSON.stringify(error, (_k, v) => _withEnumerableProperties(v))
|
|
74
|
+
);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return '\t' + JSON.stringify(toRuntimeResponse(error));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Error name, message, code, and stack are all members of the superclass, which
|
|
82
|
+
* means they aren't enumerable and don't normally show up in JSON.stringify.
|
|
83
|
+
* This method ensures those interesting properties are available along with any
|
|
84
|
+
* user-provided enumerable properties.
|
|
85
|
+
*/
|
|
86
|
+
function _withEnumerableProperties(error: any) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
const extendedError: ExtendedError = <ExtendedError>(<any>error);
|
|
89
|
+
const ret: any = Object.assign(
|
|
90
|
+
{
|
|
91
|
+
errorType: extendedError.name,
|
|
92
|
+
errorMessage: extendedError.message,
|
|
93
|
+
code: extendedError.code,
|
|
94
|
+
},
|
|
95
|
+
extendedError
|
|
96
|
+
);
|
|
97
|
+
if (typeof extendedError.stack === 'string') {
|
|
98
|
+
ret.stack = extendedError.stack.split('\n');
|
|
99
|
+
}
|
|
100
|
+
return ret;
|
|
101
|
+
} else {
|
|
102
|
+
return error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class ExtendedError extends Error {
|
|
107
|
+
code?: number;
|
|
108
|
+
custom?: string;
|
|
109
|
+
reason?: string;
|
|
110
|
+
promise?: Promise<any>;
|
|
111
|
+
|
|
112
|
+
constructor(reason?: string) {
|
|
113
|
+
super(reason); // 'Error' breaks prototype chain here
|
|
114
|
+
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class ImportModuleError extends ExtendedError {}
|
|
119
|
+
export class HandlerNotFound extends ExtendedError {}
|
|
120
|
+
export class MalformedHandlerName extends ExtendedError {}
|
|
121
|
+
export class UserCodeSyntaxError extends ExtendedError {}
|
|
122
|
+
export class UnhandledPromiseRejection extends ExtendedError {
|
|
123
|
+
constructor(reason?: string, promise?: Promise<any>) {
|
|
124
|
+
super(reason);
|
|
125
|
+
this.reason = reason;
|
|
126
|
+
this.promise = promise;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const errorClasses = [
|
|
131
|
+
ImportModuleError,
|
|
132
|
+
HandlerNotFound,
|
|
133
|
+
MalformedHandlerName,
|
|
134
|
+
UserCodeSyntaxError,
|
|
135
|
+
UnhandledPromiseRejection,
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
errorClasses.forEach((e) => {
|
|
139
|
+
e.prototype.name = `Runtime.${e.name}`;
|
|
140
|
+
});
|