-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathanswers-webhook-handler.ts
159 lines (150 loc) · 4.15 KB
/
answers-webhook-handler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { APIGatewayProxyHandler } from 'aws-lambda';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
import * as z from 'zod';
import { createHmac } from 'crypto';
export const answersWebhookHandler: APIGatewayProxyHandler = async (event) => {
if (!event.body) {
return {
statusCode: 400,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
error: 'Request body is required',
}),
};
}
let body: string;
const bodyBuffer = Buffer.from(event.body, 'base64');
try {
if (event.isBase64Encoded) {
const decodedBody = bodyBuffer.toString('utf-8');
body = JSON.parse(decodedBody);
} else {
body = JSON.parse(event.body);
}
} catch {
return {
statusCode: 400,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
error: 'Request body must be valid JSON',
}),
};
}
if (
process.env.FORMSORT_WEBHOOK_SIGNING_KEY &&
event.headers['x-formsort-secure'] === 'sign'
) {
const expectedSignature = event.headers['x-formsort-signature'];
const key = Buffer.from(process.env.FORMSORT_WEBHOOK_SIGNING_KEY, 'utf8');
const actualSignature = createHmac('sha256', key)
.update(bodyBuffer)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
if (actualSignature !== expectedSignature) {
return {
statusCode: 400,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
detail: {
expectedSignature,
actualSignature,
},
error:
'Request body does not match signature. Please check that you have the correct formsortWebhookSigningKey set in your pulumi config, otherwise remove it from config to disable signature checking.',
}),
};
}
}
const answersWebhookPayloadSchema = z.object({
answers: z.any(),
flow_label: z.string(),
responder_uuid: z.string().uuid(),
variant_label: z.string(),
variant_uuid: z.string().uuid(),
finalized: z.boolean(),
created_at: z.string().datetime({ offset: true }),
});
const parseResult = answersWebhookPayloadSchema.safeParse(body);
if (!parseResult.success) {
return {
statusCode: 400,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
error:
'Error when parsing the request body as a Formsort webhook payload',
detail: parseResult.error,
}),
};
}
const { data } = parseResult;
// Upload the body to S3
if (!process.env.ANSWERS_BUCKET_NAME) {
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
error: 'ANSWERS_BUCKET_NAME must be defined in the lambda environment',
}),
};
}
const s3 = new S3Client({ region: process.env.AWS_REGION });
await s3.send(
new PutObjectCommand({
Bucket: process.env.ANSWERS_BUCKET_NAME,
Key: 'my-answers.json',
Body: JSON.stringify(body),
ContentType: 'application/json',
})
);
// Write the body to Dynamo as well
if (!process.env.ANSWERS_DYNAMO_TABLE_NAME) {
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
error:
'ANSWERS_DYNAMO_TABLE_NAME must be defined in the lambda environment',
}),
};
}
try {
const dynamoDBClient = new DynamoDBClient({
region: process.env.AWS_REGION,
});
await dynamoDBClient.send(
new PutItemCommand({
TableName: process.env.ANSWERS_DYNAMO_TABLE_NAME,
Item: marshall(data),
})
);
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({
error: 'Error writing to DynamoDB',
detail: error?.message,
}),
};
}
return {
statusCode: 204,
body: '',
};
};
export default answersWebhookHandler;