-
Notifications
You must be signed in to change notification settings - Fork 165
Expand file tree
/
Copy pathwebhook.ts
More file actions
123 lines (113 loc) · 3.35 KB
/
Copy pathwebhook.ts
File metadata and controls
123 lines (113 loc) · 3.35 KB
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
import z from "zod";
import { mergeHeaderValue, type ParsedHttpRequest } from "./parse";
export const FUSOR_DELIVERY_CE_TYPE = "dev.spctrm.fusor.delivery";
const CANONICAL_BASE64 =
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
const canonicalBase64Schema = z.string().refine((value) => {
if (!CANONICAL_BASE64.test(value)) {
return false;
}
try {
const binary = atob(value);
return btoa(binary) === value;
} catch {
return false;
}
}, "expected canonical padded base64");
const requestFields = {
method: z.string().min(1),
path: z.string().min(1),
headers: z.record(z.string(), z.string()),
rawBodyBase64: canonicalBase64Schema,
};
const fusorWebhookRequestSchema = z.discriminatedUnion("bodyEncoding", [
z.looseObject({
...requestFields,
bodyEncoding: z.literal("json"),
body: z.json(),
}),
z.looseObject({
...requestFields,
bodyEncoding: z.literal("form"),
body: z.record(z.string(), z.union([z.string(), z.array(z.string())])),
}),
z.looseObject({
...requestFields,
bodyEncoding: z.literal("text"),
body: z.string(),
}),
z.looseObject({
...requestFields,
bodyEncoding: z.literal("base64"),
body: canonicalBase64Schema,
}),
]);
const fusorWebhookEnvelopeSchema = z
.looseObject({
schemaVersion: z.literal(1),
eventId: z.string().min(1),
projectId: z.string().min(1),
platform: z.string().min(1),
receivedAt: z.iso.datetime({ offset: true }).optional(),
sourceId: z.string().min(1).optional(),
prevSubjectSeq: z.number().int().nonnegative().safe(),
request: fusorWebhookRequestSchema,
})
.superRefine((envelope, context) => {
if (
envelope.request.bodyEncoding === "base64" &&
envelope.request.body !== envelope.request.rawBodyBase64
) {
context.addIssue({
code: "custom",
path: ["request", "body"],
message: "base64 body must equal rawBodyBase64",
});
}
});
export interface FusorWebhookEvent {
eventId: string;
platform: string;
request: ParsedHttpRequest;
}
const decodeCanonicalBase64 = (value: string): Uint8Array => {
const binary = atob(value);
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
};
const normalizeHeaders = (
input: Record<string, string>
): Record<string, string> => {
const headers: Record<string, string> = Object.create(null) as Record<
string,
string
>;
for (const [name, value] of Object.entries(input)) {
mergeHeaderValue(headers, name, value);
}
return headers;
};
/**
* Parses the versioned JSON envelope delivered by Fusor over HTTP. The
* normalized `body` arm is validation/debugging data; provider verification
* always receives the exact original bytes from `rawBodyBase64`.
*/
export const decodeFusorWebhookEvent = (
bodyBytes: Uint8Array
): FusorWebhookEvent | null => {
try {
const json = new TextDecoder("utf-8", { fatal: true }).decode(bodyBytes);
const envelope = fusorWebhookEnvelopeSchema.parse(JSON.parse(json));
return {
eventId: envelope.eventId,
platform: envelope.platform,
request: {
method: envelope.request.method,
path: envelope.request.path,
headers: normalizeHeaders(envelope.request.headers),
rawBody: decodeCanonicalBase64(envelope.request.rawBodyBase64),
},
};
} catch {
return null;
}
};