@blokjs/trigger-webhook 0.2.1 → 0.6.1

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,294 @@
1
+ /**
2
+ * Webhook signature verifiers — one strategy per supported provider.
3
+ *
4
+ * Each verifier reads the raw request bytes (NOT the JSON-parsed body)
5
+ * and the provider-specific signature header, computes the expected
6
+ * HMAC against a shared secret, and constant-time compares. On match,
7
+ * returns `{ ok: true, eventId, eventType }`. On mismatch / missing
8
+ * header / drift, returns `{ ok: false, reason, message }`.
9
+ *
10
+ * Constant-time comparison via `crypto.timingSafeEqual` is mandatory —
11
+ * a naive `===` compare leaks the expected HMAC byte by byte through
12
+ * timing variance and a network-adjacent attacker can recover the
13
+ * secret in ~256 requests per byte.
14
+ *
15
+ * Built-in providers + their signature shapes:
16
+ *
17
+ * - **github**: `X-Hub-Signature-256: sha256=<hex>` over rawBody.
18
+ * Event id from `X-GitHub-Delivery`; event type
19
+ * from `X-GitHub-Event` header.
20
+ * - **stripe**: `Stripe-Signature: t=<ts>,v1=<hex>` over
21
+ * `<ts>.<rawBody>` with a 5-minute drift window.
22
+ * Event id + type from body.id / body.type.
23
+ * - **slack**: `X-Slack-Signature: v0=<hex>` over
24
+ * `v0:<X-Slack-Request-Timestamp>:<rawBody>` with a
25
+ * 5-minute drift window. Event type from
26
+ * body.event.type; event id from body.event_id.
27
+ * - **shopify**: `X-Shopify-Hmac-Sha256: <base64>` over rawBody.
28
+ * Event type from `X-Shopify-Topic`; event id from
29
+ * `X-Shopify-Webhook-Id`.
30
+ * - **svix**: Standard Webhooks. `webhook-signature:
31
+ * v1,<base64>` over `<webhook-id>.<webhook-timestamp>.<rawBody>`
32
+ * with a 5-minute drift window. Event id from
33
+ * `webhook-id`; event type from body.type.
34
+ *
35
+ * Custom (unknown provider) verifier is built dynamically by
36
+ * `buildCustomVerifier()` from the workflow's `signature` config.
37
+ */
38
+ import { createHmac, timingSafeEqual } from "node:crypto";
39
+ const DEFAULT_TOLERANCE_SEC = 300;
40
+ function safeEqualString(a, b) {
41
+ const aBuf = Buffer.from(a);
42
+ const bBuf = Buffer.from(b);
43
+ if (aBuf.length !== bBuf.length)
44
+ return false;
45
+ return timingSafeEqual(aBuf, bBuf);
46
+ }
47
+ function hmacHex(algo, secret, data) {
48
+ return createHmac(algo, secret).update(data).digest("hex");
49
+ }
50
+ function hmacBase64(algo, secret, data) {
51
+ return createHmac(algo, secret).update(data).digest("base64");
52
+ }
53
+ function isWithinTolerance(timestampSec, toleranceSec) {
54
+ const nowSec = Math.floor(Date.now() / 1000);
55
+ return Math.abs(nowSec - timestampSec) <= toleranceSec;
56
+ }
57
+ function getEventTypeFromBody(parsedBody, key = "type") {
58
+ if (!parsedBody || typeof parsedBody !== "object")
59
+ return undefined;
60
+ const value = parsedBody[key];
61
+ return typeof value === "string" ? value : undefined;
62
+ }
63
+ // =============================================================================
64
+ // Built-in providers
65
+ // =============================================================================
66
+ export const githubVerifier = {
67
+ verify({ headers, rawBody, secret }) {
68
+ if (!secret)
69
+ return { ok: false, reason: "missing_secret", message: "GitHub: secret not configured" };
70
+ const sig = headers["x-hub-signature-256"];
71
+ if (!sig)
72
+ return { ok: false, reason: "missing_signature", message: "GitHub: X-Hub-Signature-256 header missing" };
73
+ const expected = `sha256=${hmacHex("sha256", secret, rawBody)}`;
74
+ if (!safeEqualString(sig, expected)) {
75
+ return { ok: false, reason: "signature_mismatch", message: "GitHub: signature mismatch" };
76
+ }
77
+ return {
78
+ ok: true,
79
+ eventId: headers["x-github-delivery"] ?? "",
80
+ eventType: headers["x-github-event"] ?? "unknown",
81
+ };
82
+ },
83
+ };
84
+ export const stripeVerifier = {
85
+ verify({ headers, rawBody, parsedBody, secret, toleranceSec }) {
86
+ if (!secret)
87
+ return { ok: false, reason: "missing_secret", message: "Stripe: secret not configured" };
88
+ const sig = headers["stripe-signature"];
89
+ if (!sig)
90
+ return { ok: false, reason: "missing_signature", message: "Stripe: Stripe-Signature header missing" };
91
+ // Format: t=1234567890,v1=<hex>,v0=<hex>,...
92
+ const parts = Object.fromEntries(sig
93
+ .split(",")
94
+ .map((p) => p.trim())
95
+ .map((p) => {
96
+ const idx = p.indexOf("=");
97
+ return idx === -1 ? [p, ""] : [p.slice(0, idx), p.slice(idx + 1)];
98
+ }));
99
+ const ts = parts.t;
100
+ const v1 = parts.v1;
101
+ if (!ts || !v1) {
102
+ return {
103
+ ok: false,
104
+ reason: "bad_format",
105
+ message: "Stripe: signature missing t= or v1= component",
106
+ };
107
+ }
108
+ const tsNum = Number.parseInt(ts, 10);
109
+ if (!Number.isFinite(tsNum)) {
110
+ return { ok: false, reason: "bad_format", message: "Stripe: t= is not numeric" };
111
+ }
112
+ if (!isWithinTolerance(tsNum, toleranceSec || DEFAULT_TOLERANCE_SEC)) {
113
+ return {
114
+ ok: false,
115
+ reason: "timestamp_drift",
116
+ message: `Stripe: timestamp drift exceeds ${toleranceSec || DEFAULT_TOLERANCE_SEC}s tolerance`,
117
+ };
118
+ }
119
+ const expected = hmacHex("sha256", secret, `${ts}.${rawBody}`);
120
+ if (!safeEqualString(v1, expected)) {
121
+ return { ok: false, reason: "signature_mismatch", message: "Stripe: signature mismatch" };
122
+ }
123
+ const eventId = parsedBody?.id;
124
+ const eventType = getEventTypeFromBody(parsedBody);
125
+ return {
126
+ ok: true,
127
+ eventId: typeof eventId === "string" ? eventId : "",
128
+ eventType: eventType ?? "unknown",
129
+ };
130
+ },
131
+ };
132
+ export const slackVerifier = {
133
+ verify({ headers, rawBody, parsedBody, secret, toleranceSec }) {
134
+ if (!secret)
135
+ return { ok: false, reason: "missing_secret", message: "Slack: secret not configured" };
136
+ const sig = headers["x-slack-signature"];
137
+ if (!sig)
138
+ return { ok: false, reason: "missing_signature", message: "Slack: X-Slack-Signature header missing" };
139
+ const ts = headers["x-slack-request-timestamp"];
140
+ if (!ts) {
141
+ return {
142
+ ok: false,
143
+ reason: "missing_timestamp",
144
+ message: "Slack: X-Slack-Request-Timestamp header missing",
145
+ };
146
+ }
147
+ const tsNum = Number.parseInt(ts, 10);
148
+ if (!Number.isFinite(tsNum)) {
149
+ return { ok: false, reason: "bad_format", message: "Slack: timestamp is not numeric" };
150
+ }
151
+ if (!isWithinTolerance(tsNum, toleranceSec || DEFAULT_TOLERANCE_SEC)) {
152
+ return {
153
+ ok: false,
154
+ reason: "timestamp_drift",
155
+ message: `Slack: timestamp drift exceeds ${toleranceSec || DEFAULT_TOLERANCE_SEC}s tolerance`,
156
+ };
157
+ }
158
+ const expected = `v0=${hmacHex("sha256", secret, `v0:${ts}:${rawBody}`)}`;
159
+ if (!safeEqualString(sig, expected)) {
160
+ return { ok: false, reason: "signature_mismatch", message: "Slack: signature mismatch" };
161
+ }
162
+ const event = parsedBody ?? {};
163
+ const eventType = typeof event.event?.type === "string" ? event.event.type : "unknown";
164
+ const eventId = typeof event.event_id === "string" ? event.event_id : "";
165
+ return { ok: true, eventId, eventType };
166
+ },
167
+ };
168
+ export const shopifyVerifier = {
169
+ verify({ headers, rawBody, secret }) {
170
+ if (!secret)
171
+ return { ok: false, reason: "missing_secret", message: "Shopify: secret not configured" };
172
+ const sig = headers["x-shopify-hmac-sha256"];
173
+ if (!sig) {
174
+ return { ok: false, reason: "missing_signature", message: "Shopify: X-Shopify-Hmac-Sha256 header missing" };
175
+ }
176
+ const expected = hmacBase64("sha256", secret, rawBody);
177
+ if (!safeEqualString(sig, expected)) {
178
+ return { ok: false, reason: "signature_mismatch", message: "Shopify: signature mismatch" };
179
+ }
180
+ return {
181
+ ok: true,
182
+ eventId: headers["x-shopify-webhook-id"] ?? "",
183
+ eventType: headers["x-shopify-topic"] ?? "unknown",
184
+ };
185
+ },
186
+ };
187
+ export const svixVerifier = {
188
+ verify({ headers, rawBody, parsedBody, secret, toleranceSec }) {
189
+ if (!secret)
190
+ return { ok: false, reason: "missing_secret", message: "Svix: secret not configured" };
191
+ const webhookId = headers["webhook-id"];
192
+ const webhookTs = headers["webhook-timestamp"];
193
+ const webhookSig = headers["webhook-signature"];
194
+ if (!webhookId || !webhookTs || !webhookSig) {
195
+ return {
196
+ ok: false,
197
+ reason: "missing_signature",
198
+ message: "Svix/Standard Webhooks: missing webhook-id, webhook-timestamp, or webhook-signature header",
199
+ };
200
+ }
201
+ const tsNum = Number.parseInt(webhookTs, 10);
202
+ if (!Number.isFinite(tsNum)) {
203
+ return { ok: false, reason: "bad_format", message: "Svix: webhook-timestamp is not numeric" };
204
+ }
205
+ if (!isWithinTolerance(tsNum, toleranceSec || DEFAULT_TOLERANCE_SEC)) {
206
+ return {
207
+ ok: false,
208
+ reason: "timestamp_drift",
209
+ message: `Svix: timestamp drift exceeds ${toleranceSec || DEFAULT_TOLERANCE_SEC}s tolerance`,
210
+ };
211
+ }
212
+ const signed = `${webhookId}.${webhookTs}.${rawBody}`;
213
+ // Strip optional `whsec_` prefix Svix recommends for secrets.
214
+ const rawSecret = secret.startsWith("whsec_") ? secret.slice("whsec_".length) : secret;
215
+ // Svix encodes the secret as base64 — decode before HMAC.
216
+ const secretBuf = Buffer.from(rawSecret, "base64");
217
+ const expected = createHmac("sha256", secretBuf).update(signed).digest("base64");
218
+ // webhook-signature may include multiple versions: `v1,base64 v1,base64 ...`
219
+ const sigs = webhookSig.split(" ").map((s) => {
220
+ const idx = s.indexOf(",");
221
+ return idx === -1 ? s : s.slice(idx + 1);
222
+ });
223
+ const matched = sigs.some((s) => safeEqualString(s, expected));
224
+ if (!matched) {
225
+ return { ok: false, reason: "signature_mismatch", message: "Svix: signature mismatch" };
226
+ }
227
+ return {
228
+ ok: true,
229
+ eventId: webhookId,
230
+ eventType: getEventTypeFromBody(parsedBody) ?? "unknown",
231
+ };
232
+ },
233
+ };
234
+ export function buildCustomVerifier(config) {
235
+ const algo = config.scheme === "hmac-sha1" ? "sha1" : config.scheme === "hmac-sha512" ? "sha512" : "sha256";
236
+ const headerLower = config.header.toLowerCase();
237
+ const tsHeaderLower = config.timestampHeader?.toLowerCase();
238
+ return {
239
+ verify({ headers, rawBody, parsedBody, secret, toleranceSec }) {
240
+ if (!secret)
241
+ return { ok: false, reason: "missing_secret", message: `${config.header}: secret not configured` };
242
+ const sig = headers[headerLower];
243
+ if (!sig) {
244
+ return { ok: false, reason: "missing_signature", message: `${config.header}: header missing` };
245
+ }
246
+ let signedString = rawBody;
247
+ if (tsHeaderLower) {
248
+ const ts = headers[tsHeaderLower];
249
+ if (!ts) {
250
+ return {
251
+ ok: false,
252
+ reason: "missing_timestamp",
253
+ message: `${config.timestampHeader}: header missing`,
254
+ };
255
+ }
256
+ const tsNum = Number.parseInt(ts, 10);
257
+ if (!Number.isFinite(tsNum)) {
258
+ return {
259
+ ok: false,
260
+ reason: "bad_format",
261
+ message: `${config.timestampHeader}: not numeric`,
262
+ };
263
+ }
264
+ if (!isWithinTolerance(tsNum, toleranceSec || config.tolerance)) {
265
+ return {
266
+ ok: false,
267
+ reason: "timestamp_drift",
268
+ message: `Timestamp drift exceeds ${toleranceSec || config.tolerance}s tolerance`,
269
+ };
270
+ }
271
+ signedString = `${ts}.${rawBody}`;
272
+ }
273
+ const hex = hmacHex(algo, secret, signedString);
274
+ const base64 = hmacBase64(algo, secret, signedString);
275
+ const expected = config.format.replace("{hex}", hex).replace("{base64}", base64);
276
+ if (!safeEqualString(sig, expected)) {
277
+ return { ok: false, reason: "signature_mismatch", message: `${config.header}: signature mismatch` };
278
+ }
279
+ return {
280
+ ok: true,
281
+ eventId: "",
282
+ eventType: getEventTypeFromBody(parsedBody) ?? "unknown",
283
+ };
284
+ },
285
+ };
286
+ }
287
+ export const BUILTIN_VERIFIERS = {
288
+ github: githubVerifier,
289
+ stripe: stripeVerifier,
290
+ slack: slackVerifier,
291
+ shopify: shopifyVerifier,
292
+ svix: svixVerifier,
293
+ };
294
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyaWZpZXJzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL3ZlcmlmaWVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBb0NHO0FBRUgsT0FBTyxFQUFFLFVBQVUsRUFBRSxlQUFlLEVBQUUsTUFBTSxhQUFhLENBQUM7QUF5QzFELE1BQU0scUJBQXFCLEdBQUcsR0FBRyxDQUFDO0FBRWxDLFNBQVMsZUFBZSxDQUFDLENBQVMsRUFBRSxDQUFTO0lBQzVDLE1BQU0sSUFBSSxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDNUIsTUFBTSxJQUFJLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM1QixJQUFJLElBQUksQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLEtBQUssQ0FBQztJQUM5QyxPQUFPLGVBQWUsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUM7QUFDcEMsQ0FBQztBQUVELFNBQVMsT0FBTyxDQUFDLElBQWtDLEVBQUUsTUFBYyxFQUFFLElBQVk7SUFDaEYsT0FBTyxVQUFVLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDNUQsQ0FBQztBQUVELFNBQVMsVUFBVSxDQUFDLElBQWtDLEVBQUUsTUFBYyxFQUFFLElBQVk7SUFDbkYsT0FBTyxVQUFVLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7QUFDL0QsQ0FBQztBQUVELFNBQVMsaUJBQWlCLENBQUMsWUFBb0IsRUFBRSxZQUFvQjtJQUNwRSxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUks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QUFJLENBQUMsZUFBZSxDQUFDLEdBQUcsRUFBRSxRQUFRLENBQUMsRUFBRSxDQUFDO2dCQUNyQyxPQUFPLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsb0JBQW9CLEVBQUUsT0FBTyxFQUFFLEdBQUcsTUFBTSxDQUFDLE1BQU0sc0JBQXNCLEVBQUUsQ0FBQztZQUNyRyxDQUFDO1lBQ0QsT0FBTztnQkFDTixFQUFFLEVBQUUsSUFBSTtnQkFDUixPQUFPLEVBQUUsRUFBRTtnQkFDWCxTQUFTLEVBQUUsb0JBQW9CLENBQUMsVUFBVSxDQUFDLElBQUksU0FBUzthQUN4RCxDQUFDO1FBQ0gsQ0FBQztLQUNELENBQUM7QUFDSCxDQUFDO0FBRUQsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQTZCO0lBQzFELE1BQU0sRUFBRSxjQUFjO0lBQ3RCLE1BQU0sRUFBRSxjQUFjO0lBQ3RCLEtBQUssRUFBRSxhQUFhO0lBQ3BCLE9BQU8sRUFBRSxlQUFlO0lBQ3hCLElBQUksRUFBRSxZQUFZO0NBQ2xCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFdlYmhvb2sgc2lnbmF0dXJlIHZlcmlmaWVycyDigJQgb25lIHN0cmF0ZWd5IHBlciBzdXBwb3J0ZWQgcHJvdmlkZXIuXG4gKlxuICogRWFjaCB2ZXJpZmllciByZWFkcyB0aGUgcmF3IHJlcXVlc3QgYnl0ZXMgKE5PVCB0aGUgSlNPTi1wYXJzZWQgYm9keSlcbiAqIGFuZCB0aGUgcHJvdmlkZXItc3BlY2lmaWMgc2lnbmF0dXJlIGhlYWRlciwgY29tcHV0ZXMgdGhlIGV4cGVjdGVkXG4gKiBITUFDIGFnYWluc3QgYSBzaGFyZWQgc2VjcmV0LCBhbmQgY29uc3RhbnQtdGltZSBjb21wYXJlcy4gT24gbWF0Y2gsXG4gKiByZXR1cm5zIGB7IG9rOiB0cnVlLCBldmVudElkLCBldmVudFR5cGUgfWAuIE9uIG1pc21hdGNoIC8gbWlzc2luZ1xuICogaGVhZGVyIC8gZHJpZnQsIHJldHVybnMgYHsgb2s6IGZhbHNlLCByZWFzb24sIG1lc3NhZ2UgfWAuXG4gKlxuICogQ29uc3RhbnQtdGltZSBjb21wYXJpc29uIHZpYSBgY3J5cHRvLnRpbWluZ1NhZmVFcXVhbGAgaXMgbWFuZGF0b3J5IOKAlFxuICogYSBuYWl2ZSBgPT09YCBjb21wYXJlIGxlYWtzIHRoZSBleHBlY3RlZCBITUFDIGJ5dGUgYnkgYnl0ZSB0aHJvdWdoXG4gKiB0aW1pbmcgdmFyaWFuY2UgYW5kIGEgbmV0d29yay1hZGphY2VudCBhdHRhY2tlciBjYW4gcmVjb3ZlciB0aGVcbiAqIHNlY3JldCBpbiB+MjU2IHJlcXVlc3RzIHBlciBieXRlLlxuICpcbiAqIEJ1aWx0LWluIHByb3ZpZGVycyArIHRoZWlyIHNpZ25hdHVyZSBzaGFwZXM6XG4gKlxuICogICAtICoqZ2l0aHViKio6ICAgIGBYLUh1Yi1TaWduYXR1cmUtMjU2OiBzaGEyNTY9PGhleD5gIG92ZXIgcmF3Qm9keS5cbiAqICAgICAgICAgICAgICAgICAgICBFdmVudCBpZCBmcm9tIGBYLUdpdEh1Yi1EZWxpdmVyeWA7IGV2ZW50IHR5cGVcbiAqICAgICAgICAgICAgICAgICAgICBmcm9tIGBYLUdpdEh1Yi1FdmVudGAgaGVhZGVyLlxuICogICAtICoqc3RyaXBlKio6ICAgIGBTdHJpcGUtU2lnbmF0dXJlOiB0PTx0cz4sdjE9PGhleD5gIG92ZXJcbiAqICAgICAgICAgICAgICAgICAgICBgPHRzPi48cmF3Qm9keT5gIHdpdGggYSA1LW1pbnV0ZSBkcmlmdCB3aW5kb3cuXG4gKiAgICAgICAgICAgICAgICAgICAgRXZlbnQgaWQgKyB0eXBlIGZyb20gYm9keS5pZCAvIGJvZHkudHlwZS5cbiAqICAgLSAqKnNsYWNrKio6ICAgICBgWC1TbGFjay1TaWduYXR1cmU6IHYwPTxoZXg+YCBvdmVyXG4gKiAgICAgICAgICAgICAgICAgICAgYHYwOjxYLVNsYWNrLVJlcXVlc3QtVGltZXN0YW1wPjo8cmF3Qm9keT5gIHdpdGggYVxuICogICAgICAgICAgICAgICAgICAgIDUtbWludXRlIGRyaWZ0IHdpbmRvdy4gRXZlbnQgdHlwZSBmcm9tXG4gKiAgICAgICAgICAgICAgICAgICAgYm9keS5ldmVudC50eXBlOyBldmVudCBpZCBmcm9tIGJvZHkuZXZlbnRfaWQuXG4gKiAgIC0gKipzaG9waWZ5Kio6ICAgYFgtU2hvcGlmeS1IbWFjLVNoYTI1NjogPGJhc2U2ND5gIG92ZXIgcmF3Qm9keS5cbiAqICAgICAgICAgICAgICAgICAgICBFdmVudCB0eXBlIGZyb20gYFgtU2hvcGlmeS1Ub3BpY2A7IGV2ZW50IGlkIGZyb21cbiAqICAgICAgICAgICAgICAgICAgICBgWC1TaG9waWZ5LVdlYmhvb2stSWRgLlxuICogICAtICoqc3ZpeCoqOiAgICAgIFN0YW5kYXJkIFdlYmhvb2tzLiBgd2ViaG9vay1zaWduYXR1cmU6XG4gKiAgICAgICAgICAgICAgICAgICAgdjEsPGJhc2U2ND5gIG92ZXIgYDx3ZWJob29rLWlkPi48d2ViaG9vay10aW1lc3RhbXA+LjxyYXdCb2R5PmBcbiAqICAgICAgICAgICAgICAgICAgICB3aXRoIGEgNS1taW51dGUgZHJpZnQgd2luZG93LiBFdmVudCBpZCBmcm9tXG4gKiAgICAgICAgICAgICAgICAgICAgYHdlYmhvb2staWRgOyBldmVudCB0eXBlIGZyb20gYm9keS50eXBlLlxuICpcbiAqIEN1c3RvbSAodW5rbm93biBwcm92aWRlcikgdmVyaWZpZXIgaXMgYnVpbHQgZHluYW1pY2FsbHkgYnlcbiAqIGBidWlsZEN1c3RvbVZlcmlmaWVyKClgIGZyb20gdGhlIHdvcmtmbG93J3MgYHNpZ25hdHVyZWAgY29uZmlnLlxuICovXG5cbmltcG9ydCB7IGNyZWF0ZUhtYWMsIHRpbWluZ1NhZmVFcXVhbCB9IGZyb20gXCJub2RlOmNyeXB0b1wiO1xuXG4vKiogU3VjY2Vzc2Z1bCB2ZXJpZmljYXRpb24gcmVzdWx0IOKAlCB3b3JrZmxvdyBtYXkgcHJvY2VlZC4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgVmVyaWZ5T2sge1xuXHRvazogdHJ1ZTtcblx0LyoqIFByb3ZpZGVyLXNwZWNpZmljIGV2ZW50IGlkICh1c2VkIGZvciByZXBsYXktcHJvdGVjdGlvbiBjYWNoZSBrZXkpLiAqL1xuXHRldmVudElkOiBzdHJpbmc7XG5cdC8qKiBQcm92aWRlci1zcGVjaWZpYyBldmVudCB0eXBlICh1c2VkIGZvciB0aGUgYWxsb3dsaXN0IGNoZWNrKS4gKi9cblx0ZXZlbnRUeXBlOiBzdHJpbmc7XG59XG5cbi8qKiBWZXJpZmljYXRpb24gZmFpbHVyZSDigJQgdHJpZ2dlciByZXR1cm5zIDQwMSB3aXRoIHN0cnVjdHVyZWQgcmVhc29uLiAqL1xuZXhwb3J0IGludGVyZmFjZSBWZXJpZnlFcnJvciB7XG5cdG9rOiBmYWxzZTtcblx0LyoqIFN0YWJsZSBkaXNjcmltaW5hdG9yIOKAlCBsb2cvYWxlcnQgZGFzaGJvYXJkcyBicmFuY2ggb24gdGhpcy4gKi9cblx0cmVhc29uOlxuXHRcdHwgXCJtaXNzaW5nX3NpZ25hdHVyZVwiXG5cdFx0fCBcIm1pc3NpbmdfdGltZXN0YW1wXCJcblx0XHR8IFwibWlzc2luZ19zZWNyZXRcIlxuXHRcdHwgXCJiYWRfZm9ybWF0XCJcblx0XHR8IFwidGltZXN0YW1wX2RyaWZ0XCJcblx0XHR8IFwic2lnbmF0dXJlX21pc21hdGNoXCI7XG5cdC8qKiBIdW1hbi1yZWFkYWJsZSBlcnJvciBtZXNzYWdlLiBTYWZlIHRvIHN1cmZhY2UgdG8gdGhlIHNlbmRlci4gKi9cblx0bWVzc2FnZTogc3RyaW5nO1xufVxuXG5leHBvcnQgdHlwZSBWZXJpZnlSZXN1bHQgPSBWZXJpZnlPayB8IFZlcmlmeUVycm9yO1xuXG4vKiogSW5wdXRzIGV2ZXJ5IHZlcmlmaWVyIHJlY2VpdmVzLiAqL1xuZXhwb3J0IGludGVyZmFjZSBWZXJpZnlJbnB1dCB7XG5cdGhlYWRlcnM6IFJlY29yZDxzdHJpbmcsIHN0cmluZz47XG5cdHJhd0JvZHk6IHN0cmluZztcblx0cGFyc2VkQm9keTogdW5rbm93bjtcblx0c2VjcmV0OiBzdHJpbmc7XG5cdHRvbGVyYW5jZVNlYzogbnVtYmVyO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFZlcmlmaWVyIHtcblx0dmVyaWZ5KGlucHV0OiBWZXJpZnlJbnB1dCk6IFZlcmlmeVJlc3VsdDtcbn1cblxuY29uc3QgREVGQVVMVF9UT0xFUkFOQ0VfU0VDID0gMzAwO1xuXG5mdW5jdGlvbiBzYWZlRXF1YWxTdHJpbmcoYTogc3RyaW5nLCBiOiBzdHJpbmcpOiBib29sZWFuIHtcblx0Y29uc3QgYUJ1ZiA9IEJ1ZmZlci5mcm9tKGEpO1xuXHRjb25zdCBiQnVmID0gQnVmZmVyLmZyb20oYik7XG5cdGlmIChhQnVmLmxlbmd0aCAhPT0gYkJ1Zi5sZW5ndGgpIHJldHVybiBmYWxzZTtcblx0cmV0dXJuIHRpbWluZ1NhZmVFcXVhbChhQnVmLCBiQnVmKTtcbn1cblxuZnVuY3Rpb24gaG1hY0hleChhbGdvOiBcInNoYTI1NlwiIHwgXCJzaGExXCIgfCBcInNoYTUxMlwiLCBzZWNyZXQ6IHN0cmluZywgZGF0YTogc3RyaW5nKTogc3RyaW5nIHtcblx0cmV0dXJuIGNyZWF0ZUhtYWMoYWxnbywgc2VjcmV0KS51cGRhdGUoZGF0YSkuZGlnZXN0KFwiaGV4XCIpO1xufVxuXG5mdW5jdGlvbiBobWFjQmFzZTY0KGFsZ286IFwic2hhMjU2XCIgfCBcInNoYTFcIiB8IFwic2hhNTEyXCIsIHNlY3JldDogc3RyaW5nLCBkYXRhOiBzdHJpbmcpOiBzdHJpbmcge1xuXHRyZXR1cm4gY3JlYXRlSG1hYyhhbGdvLCBzZWNyZXQpLnVwZGF0ZShkYXRhKS5kaWdlc3QoXCJiYXNlNjRcIik7XG59XG5cbmZ1bmN0aW9uIGlzV2l0aGluVG9sZXJhbmNlKHRpbWVzdGFtcFNlYzogbnVtYmVyLCB0b2xlcmFuY2VTZWM6IG51bWJlcik6IGJvb2xlYW4ge1xuXHRjb25zdCBub3dTZWMgPSBNYXRoLmZsb29yKERhdGUubm93KCkgLyAxMDAwKTtcblx0cmV0dXJuIE1hdGguYWJzKG5vd1NlYyAtIHRpbWVzdGFtcFNlYykgPD0gdG9sZXJhbmNlU2VjO1xufVxuXG5mdW5jdGlvbiBnZXRFdmVudFR5cGVGcm9tQm9keShwYXJzZWRCb2R5OiB1bmtub3duLCBrZXkgPSBcInR5cGVcIik6IHN0cmluZyB8IHVuZGVmaW5lZCB7XG5cdGlmICghcGFyc2VkQm9keSB8fCB0eXBlb2YgcGFyc2VkQm9keSAhPT0gXCJvYmplY3RcIikgcmV0dXJuIHVuZGVmaW5lZDtcblx0Y29uc3QgdmFsdWUgPSAocGFyc2VkQm9keSBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPilba2V5XTtcblx0cmV0dXJuIHR5cGVvZiB2YWx1ZSA9PT0gXCJzdHJpbmdcIiA/IHZhbHVlIDogdW5kZWZpbmVkO1xufVxuXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuLy8gQnVpbHQtaW4gcHJvdmlkZXJzXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgY29uc3QgZ2l0aHViVmVyaWZpZXI6IFZlcmlmaWVyID0ge1xuXHR2ZXJpZnkoeyBoZWFkZXJzLCByYXdCb2R5LCBzZWNyZXQgfSkge1xuXHRcdGlmICghc2VjcmV0KSByZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJtaXNzaW5nX3NlY3JldFwiLCBtZXNzYWdlOiBcIkdpdEh1Yjogc2VjcmV0IG5vdCBjb25maWd1cmVkXCIgfTtcblx0XHRjb25zdCBzaWcgPSBoZWFkZXJzW1wieC1odWItc2lnbmF0dXJlLTI1NlwiXTtcblx0XHRpZiAoIXNpZykgcmV0dXJuIHsgb2s6IGZhbHNlLCByZWFzb246IFwibWlzc2luZ19zaWduYXR1cmVcIiwgbWVzc2FnZTogXCJHaXRIdWI6IFgtSHViLVNpZ25hdHVyZS0yNTYgaGVhZGVyIG1pc3NpbmdcIiB9O1xuXHRcdGNvbnN0IGV4cGVjdGVkID0gYHNoYTI1Nj0ke2htYWNIZXgoXCJzaGEyNTZcIiwgc2VjcmV0LCByYXdCb2R5KX1gO1xuXHRcdGlmICghc2FmZUVxdWFsU3RyaW5nKHNpZywgZXhwZWN0ZWQpKSB7XG5cdFx0XHRyZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJzaWduYXR1cmVfbWlzbWF0Y2hcIiwgbWVzc2FnZTogXCJHaXRIdWI6IHNpZ25hdHVyZSBtaXNtYXRjaFwiIH07XG5cdFx0fVxuXHRcdHJldHVybiB7XG5cdFx0XHRvazogdHJ1ZSxcblx0XHRcdGV2ZW50SWQ6IGhlYWRlcnNbXCJ4LWdpdGh1Yi1kZWxpdmVyeVwiXSA/PyBcIlwiLFxuXHRcdFx0ZXZlbnRUeXBlOiBoZWFkZXJzW1wieC1naXRodWItZXZlbnRcIl0gPz8gXCJ1bmtub3duXCIsXG5cdFx0fTtcblx0fSxcbn07XG5cbmV4cG9ydCBjb25zdCBzdHJpcGVWZXJpZmllcjogVmVyaWZpZXIgPSB7XG5cdHZlcmlmeSh7IGhlYWRlcnMsIHJhd0JvZHksIHBhcnNlZEJvZHksIHNlY3JldCwgdG9sZXJhbmNlU2VjIH0pIHtcblx0XHRpZiAoIXNlY3JldCkgcmV0dXJuIHsgb2s6IGZhbHNlLCByZWFzb246IFwibWlzc2luZ19zZWNyZXRcIiwgbWVzc2FnZTogXCJTdHJpcGU6IHNlY3JldCBub3QgY29uZmlndXJlZFwiIH07XG5cdFx0Y29uc3Qgc2lnID0gaGVhZGVyc1tcInN0cmlwZS1zaWduYXR1cmVcIl07XG5cdFx0aWYgKCFzaWcpIHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiBcIm1pc3Npbmdfc2lnbmF0dXJlXCIsIG1lc3NhZ2U6IFwiU3RyaXBlOiBTdHJpcGUtU2lnbmF0dXJlIGhlYWRlciBtaXNzaW5nXCIgfTtcblxuXHRcdC8vIEZvcm1hdDogdD0xMjM0NTY3ODkwLHYxPTxoZXg+LHYwPTxoZXg+LC4uLlxuXHRcdGNvbnN0IHBhcnRzID0gT2JqZWN0LmZyb21FbnRyaWVzKFxuXHRcdFx0c2lnXG5cdFx0XHRcdC5zcGxpdChcIixcIilcblx0XHRcdFx0Lm1hcCgocCkgPT4gcC50cmltKCkpXG5cdFx0XHRcdC5tYXAoKHApID0+IHtcblx0XHRcdFx0XHRjb25zdCBpZHggPSBwLmluZGV4T2YoXCI9XCIpO1xuXHRcdFx0XHRcdHJldHVybiBpZHggPT09IC0xID8gW3AsIFwiXCJdIDogW3Auc2xpY2UoMCwgaWR4KSwgcC5zbGljZShpZHggKyAxKV07XG5cdFx0XHRcdH0pLFxuXHRcdCk7XG5cdFx0Y29uc3QgdHMgPSBwYXJ0cy50O1xuXHRcdGNvbnN0IHYxID0gcGFydHMudjE7XG5cdFx0aWYgKCF0cyB8fCAhdjEpIHtcblx0XHRcdHJldHVybiB7XG5cdFx0XHRcdG9rOiBmYWxzZSxcblx0XHRcdFx0cmVhc29uOiBcImJhZF9mb3JtYXRcIixcblx0XHRcdFx0bWVzc2FnZTogXCJTdHJpcGU6IHNpZ25hdHVyZSBtaXNzaW5nIHQ9IG9yIHYxPSBjb21wb25lbnRcIixcblx0XHRcdH07XG5cdFx0fVxuXHRcdGNvbnN0IHRzTnVtID0gTnVtYmVyLnBhcnNlSW50KHRzLCAxMCk7XG5cdFx0aWYgKCFOdW1iZXIuaXNGaW5pdGUodHNOdW0pKSB7XG5cdFx0XHRyZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJiYWRfZm9ybWF0XCIsIG1lc3NhZ2U6IFwiU3RyaXBlOiB0PSBpcyBub3QgbnVtZXJpY1wiIH07XG5cdFx0fVxuXHRcdGlmICghaXNXaXRoaW5Ub2xlcmFuY2UodHNOdW0sIHRvbGVyYW5jZVNlYyB8fCBERUZBVUxUX1RPTEVSQU5DRV9TRUMpKSB7XG5cdFx0XHRyZXR1cm4ge1xuXHRcdFx0XHRvazogZmFsc2UsXG5cdFx0XHRcdHJlYXNvbjogXCJ0aW1lc3RhbXBfZHJpZnRcIixcblx0XHRcdFx0bWVzc2FnZTogYFN0cmlwZTogdGltZXN0YW1wIGRyaWZ0IGV4Y2VlZHMgJHt0b2xlcmFuY2VTZWMgfHwgREVGQVVMVF9UT0xFUkFOQ0VfU0VDfXMgdG9sZXJhbmNlYCxcblx0XHRcdH07XG5cdFx0fVxuXHRcdGNvbnN0IGV4cGVjdGVkID0gaG1hY0hleChcInNoYTI1NlwiLCBzZWNyZXQsIGAke3RzfS4ke3Jhd0JvZHl9YCk7XG5cdFx0aWYgKCFzYWZlRXF1YWxTdHJpbmcodjEsIGV4cGVjdGVkKSkge1xuXHRcdFx0cmV0dXJuIHsgb2s6IGZhbHNlLCByZWFzb246IFwic2lnbmF0dXJlX21pc21hdGNoXCIsIG1lc3NhZ2U6IFwiU3RyaXBlOiBzaWduYXR1cmUgbWlzbWF0Y2hcIiB9O1xuXHRcdH1cblx0XHRjb25zdCBldmVudElkID0gKHBhcnNlZEJvZHkgYXMgeyBpZD86IHVua25vd24gfSB8IG51bGwpPy5pZDtcblx0XHRjb25zdCBldmVudFR5cGUgPSBnZXRFdmVudFR5cGVGcm9tQm9keShwYXJzZWRCb2R5KTtcblx0XHRyZXR1cm4ge1xuXHRcdFx0b2s6IHRydWUsXG5cdFx0XHRldmVudElkOiB0eXBlb2YgZXZlbnRJZCA9PT0gXCJzdHJpbmdcIiA/IGV2ZW50SWQgOiBcIlwiLFxuXHRcdFx0ZXZlbnRUeXBlOiBldmVudFR5cGUgPz8gXCJ1bmtub3duXCIsXG5cdFx0fTtcblx0fSxcbn07XG5cbmV4cG9ydCBjb25zdCBzbGFja1ZlcmlmaWVyOiBWZXJpZmllciA9IHtcblx0dmVyaWZ5KHsgaGVhZGVycywgcmF3Qm9keSwgcGFyc2VkQm9keSwgc2VjcmV0LCB0b2xlcmFuY2VTZWMgfSkge1xuXHRcdGlmICghc2VjcmV0KSByZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJtaXNzaW5nX3NlY3JldFwiLCBtZXNzYWdlOiBcIlNsYWNrOiBzZWNyZXQgbm90IGNvbmZpZ3VyZWRcIiB9O1xuXHRcdGNvbnN0IHNpZyA9IGhlYWRlcnNbXCJ4LXNsYWNrLXNpZ25hdHVyZVwiXTtcblx0XHRpZiAoIXNpZykgcmV0dXJuIHsgb2s6IGZhbHNlLCByZWFzb246IFwibWlzc2luZ19zaWduYXR1cmVcIiwgbWVzc2FnZTogXCJTbGFjazogWC1TbGFjay1TaWduYXR1cmUgaGVhZGVyIG1pc3NpbmdcIiB9O1xuXHRcdGNvbnN0IHRzID0gaGVhZGVyc1tcIngtc2xhY2stcmVxdWVzdC10aW1lc3RhbXBcIl07XG5cdFx0aWYgKCF0cykge1xuXHRcdFx0cmV0dXJuIHtcblx0XHRcdFx0b2s6IGZhbHNlLFxuXHRcdFx0XHRyZWFzb246IFwibWlzc2luZ190aW1lc3RhbXBcIixcblx0XHRcdFx0bWVzc2FnZTogXCJTbGFjazogWC1TbGFjay1SZXF1ZXN0LVRpbWVzdGFtcCBoZWFkZXIgbWlzc2luZ1wiLFxuXHRcdFx0fTtcblx0XHR9XG5cdFx0Y29uc3QgdHNOdW0gPSBOdW1iZXIucGFyc2VJbnQodHMsIDEwKTtcblx0XHRpZiAoIU51bWJlci5pc0Zpbml0ZSh0c051bSkpIHtcblx0XHRcdHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiBcImJhZF9mb3JtYXRcIiwgbWVzc2FnZTogXCJTbGFjazogdGltZXN0YW1wIGlzIG5vdCBudW1lcmljXCIgfTtcblx0XHR9XG5cdFx0aWYgKCFpc1dpdGhpblRvbGVyYW5jZSh0c051bSwgdG9sZXJhbmNlU2VjIHx8IERFRkFVTFRfVE9MRVJBTkNFX1NFQykpIHtcblx0XHRcdHJldHVybiB7XG5cdFx0XHRcdG9rOiBmYWxzZSxcblx0XHRcdFx0cmVhc29uOiBcInRpbWVzdGFtcF9kcmlmdFwiLFxuXHRcdFx0XHRtZXNzYWdlOiBgU2xhY2s6IHRpbWVzdGFtcCBkcmlmdCBleGNlZWRzICR7dG9sZXJhbmNlU2VjIHx8IERFRkFVTFRfVE9MRVJBTkNFX1NFQ31zIHRvbGVyYW5jZWAsXG5cdFx0XHR9O1xuXHRcdH1cblx0XHRjb25zdCBleHBlY3RlZCA9IGB2MD0ke2htYWNIZXgoXCJzaGEyNTZcIiwgc2VjcmV0LCBgdjA6JHt0c306JHtyYXdCb2R5fWApfWA7XG5cdFx0aWYgKCFzYWZlRXF1YWxTdHJpbmcoc2lnLCBleHBlY3RlZCkpIHtcblx0XHRcdHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiBcInNpZ25hdHVyZV9taXNtYXRjaFwiLCBtZXNzYWdlOiBcIlNsYWNrOiBzaWduYXR1cmUgbWlzbWF0Y2hcIiB9O1xuXHRcdH1cblx0XHRjb25zdCBldmVudCA9IChwYXJzZWRCb2R5IGFzIHsgZXZlbnQ/OiB7IHR5cGU/OiB1bmtub3duIH07IGV2ZW50X2lkPzogdW5rbm93biB9IHwgbnVsbCkgPz8ge307XG5cdFx0Y29uc3QgZXZlbnRUeXBlID0gdHlwZW9mIGV2ZW50LmV2ZW50Py50eXBlID09PSBcInN0cmluZ1wiID8gZXZlbnQuZXZlbnQudHlwZSA6IFwidW5rbm93blwiO1xuXHRcdGNvbnN0IGV2ZW50SWQgPSB0eXBlb2YgZXZlbnQuZXZlbnRfaWQgPT09IFwic3RyaW5nXCIgPyBldmVudC5ldmVudF9pZCA6IFwiXCI7XG5cdFx0cmV0dXJuIHsgb2s6IHRydWUsIGV2ZW50SWQsIGV2ZW50VHlwZSB9O1xuXHR9LFxufTtcblxuZXhwb3J0IGNvbnN0IHNob3BpZnlWZXJpZmllcjogVmVyaWZpZXIgPSB7XG5cdHZlcmlmeSh7IGhlYWRlcnMsIHJhd0JvZHksIHNlY3JldCB9KSB7XG5cdFx0aWYgKCFzZWNyZXQpIHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiBcIm1pc3Npbmdfc2VjcmV0XCIsIG1lc3NhZ2U6IFwiU2hvcGlmeTogc2VjcmV0IG5vdCBjb25maWd1cmVkXCIgfTtcblx0XHRjb25zdCBzaWcgPSBoZWFkZXJzW1wieC1zaG9waWZ5LWhtYWMtc2hhMjU2XCJdO1xuXHRcdGlmICghc2lnKSB7XG5cdFx0XHRyZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJtaXNzaW5nX3NpZ25hdHVyZVwiLCBtZXNzYWdlOiBcIlNob3BpZnk6IFgtU2hvcGlmeS1IbWFjLVNoYTI1NiBoZWFkZXIgbWlzc2luZ1wiIH07XG5cdFx0fVxuXHRcdGNvbnN0IGV4cGVjdGVkID0gaG1hY0Jhc2U2NChcInNoYTI1NlwiLCBzZWNyZXQsIHJhd0JvZHkpO1xuXHRcdGlmICghc2FmZUVxdWFsU3RyaW5nKHNpZywgZXhwZWN0ZWQpKSB7XG5cdFx0XHRyZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJzaWduYXR1cmVfbWlzbWF0Y2hcIiwgbWVzc2FnZTogXCJTaG9waWZ5OiBzaWduYXR1cmUgbWlzbWF0Y2hcIiB9O1xuXHRcdH1cblx0XHRyZXR1cm4ge1xuXHRcdFx0b2s6IHRydWUsXG5cdFx0XHRldmVudElkOiBoZWFkZXJzW1wieC1zaG9waWZ5LXdlYmhvb2staWRcIl0gPz8gXCJcIixcblx0XHRcdGV2ZW50VHlwZTogaGVhZGVyc1tcIngtc2hvcGlmeS10b3BpY1wiXSA/PyBcInVua25vd25cIixcblx0XHR9O1xuXHR9LFxufTtcblxuZXhwb3J0IGNvbnN0IHN2aXhWZXJpZmllcjogVmVyaWZpZXIgPSB7XG5cdHZlcmlmeSh7IGhlYWRlcnMsIHJhd0JvZHksIHBhcnNlZEJvZHksIHNlY3JldCwgdG9sZXJhbmNlU2VjIH0pIHtcblx0XHRpZiAoIXNlY3JldCkgcmV0dXJuIHsgb2s6IGZhbHNlLCByZWFzb246IFwibWlzc2luZ19zZWNyZXRcIiwgbWVzc2FnZTogXCJTdml4OiBzZWNyZXQgbm90IGNvbmZpZ3VyZWRcIiB9O1xuXHRcdGNvbnN0IHdlYmhvb2tJZCA9IGhlYWRlcnNbXCJ3ZWJob29rLWlkXCJdO1xuXHRcdGNvbnN0IHdlYmhvb2tUcyA9IGhlYWRlcnNbXCJ3ZWJob29rLXRpbWVzdGFtcFwiXTtcblx0XHRjb25zdCB3ZWJob29rU2lnID0gaGVhZGVyc1tcIndlYmhvb2stc2lnbmF0dXJlXCJdO1xuXHRcdGlmICghd2ViaG9va0lkIHx8ICF3ZWJob29rVHMgfHwgIXdlYmhvb2tTaWcpIHtcblx0XHRcdHJldHVybiB7XG5cdFx0XHRcdG9rOiBmYWxzZSxcblx0XHRcdFx0cmVhc29uOiBcIm1pc3Npbmdfc2lnbmF0dXJlXCIsXG5cdFx0XHRcdG1lc3NhZ2U6IFwiU3ZpeC9TdGFuZGFyZCBXZWJob29rczogbWlzc2luZyB3ZWJob29rLWlkLCB3ZWJob29rLXRpbWVzdGFtcCwgb3Igd2ViaG9vay1zaWduYXR1cmUgaGVhZGVyXCIsXG5cdFx0XHR9O1xuXHRcdH1cblx0XHRjb25zdCB0c051bSA9IE51bWJlci5wYXJzZUludCh3ZWJob29rVHMsIDEwKTtcblx0XHRpZiAoIU51bWJlci5pc0Zpbml0ZSh0c051bSkpIHtcblx0XHRcdHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiBcImJhZF9mb3JtYXRcIiwgbWVzc2FnZTogXCJTdml4OiB3ZWJob29rLXRpbWVzdGFtcCBpcyBub3QgbnVtZXJpY1wiIH07XG5cdFx0fVxuXHRcdGlmICghaXNXaXRoaW5Ub2xlcmFuY2UodHNOdW0sIHRvbGVyYW5jZVNlYyB8fCBERUZBVUxUX1RPTEVSQU5DRV9TRUMpKSB7XG5cdFx0XHRyZXR1cm4ge1xuXHRcdFx0XHRvazogZmFsc2UsXG5cdFx0XHRcdHJlYXNvbjogXCJ0aW1lc3RhbXBfZHJpZnRcIixcblx0XHRcdFx0bWVzc2FnZTogYFN2aXg6IHRpbWVzdGFtcCBkcmlmdCBleGNlZWRzICR7dG9sZXJhbmNlU2VjIHx8IERFRkFVTFRfVE9MRVJBTkNFX1NFQ31zIHRvbGVyYW5jZWAsXG5cdFx0XHR9O1xuXHRcdH1cblx0XHRjb25zdCBzaWduZWQgPSBgJHt3ZWJob29rSWR9LiR7d2ViaG9va1RzfS4ke3Jhd0JvZHl9YDtcblx0XHQvLyBTdHJpcCBvcHRpb25hbCBgd2hzZWNfYCBwcmVmaXggU3ZpeCByZWNvbW1lbmRzIGZvciBzZWNyZXRzLlxuXHRcdGNvbnN0IHJhd1NlY3JldCA9IHNlY3JldC5zdGFydHNXaXRoKFwid2hzZWNfXCIpID8gc2VjcmV0LnNsaWNlKFwid2hzZWNfXCIubGVuZ3RoKSA6IHNlY3JldDtcblx0XHQvLyBTdml4IGVuY29kZXMgdGhlIHNlY3JldCBhcyBiYXNlNjQg4oCUIGRlY29kZSBiZWZvcmUgSE1BQy5cblx0XHRjb25zdCBzZWNyZXRCdWYgPSBCdWZmZXIuZnJvbShyYXdTZWNyZXQsIFwiYmFzZTY0XCIpO1xuXHRcdGNvbnN0IGV4cGVjdGVkID0gY3JlYXRlSG1hYyhcInNoYTI1NlwiLCBzZWNyZXRCdWYpLnVwZGF0ZShzaWduZWQpLmRpZ2VzdChcImJhc2U2NFwiKTtcblx0XHQvLyB3ZWJob29rLXNpZ25hdHVyZSBtYXkgaW5jbHVkZSBtdWx0aXBsZSB2ZXJzaW9uczogYHYxLGJhc2U2NCB2MSxiYXNlNjQgLi4uYFxuXHRcdGNvbnN0IHNpZ3MgPSB3ZWJob29rU2lnLnNwbGl0KFwiIFwiKS5tYXAoKHMpID0+IHtcblx0XHRcdGNvbnN0IGlkeCA9IHMuaW5kZXhPZihcIixcIik7XG5cdFx0XHRyZXR1cm4gaWR4ID09PSAtMSA/IHMgOiBzLnNsaWNlKGlkeCArIDEpO1xuXHRcdH0pO1xuXHRcdGNvbnN0IG1hdGNoZWQgPSBzaWdzLnNvbWUoKHMpID0+IHNhZmVFcXVhbFN0cmluZyhzLCBleHBlY3RlZCkpO1xuXHRcdGlmICghbWF0Y2hlZCkge1xuXHRcdFx0cmV0dXJuIHsgb2s6IGZhbHNlLCByZWFzb246IFwic2lnbmF0dXJlX21pc21hdGNoXCIsIG1lc3NhZ2U6IFwiU3ZpeDogc2lnbmF0dXJlIG1pc21hdGNoXCIgfTtcblx0XHR9XG5cdFx0cmV0dXJuIHtcblx0XHRcdG9rOiB0cnVlLFxuXHRcdFx0ZXZlbnRJZDogd2ViaG9va0lkLFxuXHRcdFx0ZXZlbnRUeXBlOiBnZXRFdmVudFR5cGVGcm9tQm9keShwYXJzZWRCb2R5KSA/PyBcInVua25vd25cIixcblx0XHR9O1xuXHR9LFxufTtcblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIEN1c3RvbSB2ZXJpZmllciDigJQgYnVpbHQgZnJvbSB0aGUgd29ya2Zsb3cncyBgc2lnbmF0dXJlOiB7IC4uLiB9YCBjb25maWdcbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG5cbmV4cG9ydCBpbnRlcmZhY2UgQ3VzdG9tU2lnbmF0dXJlQ29uZmlnIHtcblx0c2NoZW1lOiBcImhtYWMtc2hhMjU2XCIgfCBcImhtYWMtc2hhMVwiIHwgXCJobWFjLXNoYTUxMlwiO1xuXHRoZWFkZXI6IHN0cmluZztcblx0Zm9ybWF0OiBzdHJpbmc7XG5cdHNlY3JldEVudjogc3RyaW5nO1xuXHR0b2xlcmFuY2U6IG51bWJlcjtcblx0dGltZXN0YW1wSGVhZGVyPzogc3RyaW5nO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRDdXN0b21WZXJpZmllcihjb25maWc6IEN1c3RvbVNpZ25hdHVyZUNvbmZpZyk6IFZlcmlmaWVyIHtcblx0Y29uc3QgYWxnbzogXCJzaGEyNTZcIiB8IFwic2hhMVwiIHwgXCJzaGE1MTJcIiA9XG5cdFx0Y29uZmlnLnNjaGVtZSA9PT0gXCJobWFjLXNoYTFcIiA/IFwic2hhMVwiIDogY29uZmlnLnNjaGVtZSA9PT0gXCJobWFjLXNoYTUxMlwiID8gXCJzaGE1MTJcIiA6IFwic2hhMjU2XCI7XG5cdGNvbnN0IGhlYWRlckxvd2VyID0gY29uZmlnLmhlYWRlci50b0xvd2VyQ2FzZSgpO1xuXHRjb25zdCB0c0hlYWRlckxvd2VyID0gY29uZmlnLnRpbWVzdGFtcEhlYWRlcj8udG9Mb3dlckNhc2UoKTtcblxuXHRyZXR1cm4ge1xuXHRcdHZlcmlmeSh7IGhlYWRlcnMsIHJhd0JvZHksIHBhcnNlZEJvZHksIHNlY3JldCwgdG9sZXJhbmNlU2VjIH0pIHtcblx0XHRcdGlmICghc2VjcmV0KSByZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJtaXNzaW5nX3NlY3JldFwiLCBtZXNzYWdlOiBgJHtjb25maWcuaGVhZGVyfTogc2VjcmV0IG5vdCBjb25maWd1cmVkYCB9O1xuXHRcdFx0Y29uc3Qgc2lnID0gaGVhZGVyc1toZWFkZXJMb3dlcl07XG5cdFx0XHRpZiAoIXNpZykge1xuXHRcdFx0XHRyZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogXCJtaXNzaW5nX3NpZ25hdHVyZVwiLCBtZXNzYWdlOiBgJHtjb25maWcuaGVhZGVyfTogaGVhZGVyIG1pc3NpbmdgIH07XG5cdFx0XHR9XG5cblx0XHRcdGxldCBzaWduZWRTdHJpbmcgPSByYXdCb2R5O1xuXHRcdFx0aWYgKHRzSGVhZGVyTG93ZXIpIHtcblx0XHRcdFx0Y29uc3QgdHMgPSBoZWFkZXJzW3RzSGVhZGVyTG93ZXJdO1xuXHRcdFx0XHRpZiAoIXRzKSB7XG5cdFx0XHRcdFx0cmV0dXJuIHtcblx0XHRcdFx0XHRcdG9rOiBmYWxzZSxcblx0XHRcdFx0XHRcdHJlYXNvbjogXCJtaXNzaW5nX3RpbWVzdGFtcFwiLFxuXHRcdFx0XHRcdFx0bWVzc2FnZTogYCR7Y29uZmlnLnRpbWVzdGFtcEhlYWRlcn06IGhlYWRlciBtaXNzaW5nYCxcblx0XHRcdFx0XHR9O1xuXHRcdFx0XHR9XG5cdFx0XHRcdGNvbnN0IHRzTnVtID0gTnVtYmVyLnBhcnNlSW50KHRzLCAxMCk7XG5cdFx0XHRcdGlmICghTnVtYmVyLmlzRmluaXRlKHRzTnVtKSkge1xuXHRcdFx0XHRcdHJldHVybiB7XG5cdFx0XHRcdFx0XHRvazogZmFsc2UsXG5cdFx0XHRcdFx0XHRyZWFzb246IFwiYmFkX2Zvcm1hdFwiLFxuXHRcdFx0XHRcdFx0bWVzc2FnZTogYCR7Y29uZmlnLnRpbWVzdGFtcEhlYWRlcn06IG5vdCBudW1lcmljYCxcblx0XHRcdFx0XHR9O1xuXHRcdFx0XHR9XG5cdFx0XHRcdGlmICghaXNXaXRoaW5Ub2xlcmFuY2UodHNOdW0sIHRvbGVyYW5jZVNlYyB8fCBjb25maWcudG9sZXJhbmNlKSkge1xuXHRcdFx0XHRcdHJldHVybiB7XG5cdFx0XHRcdFx0XHRvazogZmFsc2UsXG5cdFx0XHRcdFx0XHRyZWFzb246IFwidGltZXN0YW1wX2RyaWZ0XCIsXG5cdFx0XHRcdFx0XHRtZXNzYWdlOiBgVGltZXN0YW1wIGRyaWZ0IGV4Y2VlZHMgJHt0b2xlcmFuY2VTZWMgfHwgY29uZmlnLnRvbGVyYW5jZX1zIHRvbGVyYW5jZWAsXG5cdFx0XHRcdFx0fTtcblx0XHRcdFx0fVxuXHRcdFx0XHRzaWduZWRTdHJpbmcgPSBgJHt0c30uJHtyYXdCb2R5fWA7XG5cdFx0XHR9XG5cblx0XHRcdGNvbnN0IGhleCA9IGhtYWNIZXgoYWxnbywgc2VjcmV0LCBzaWduZWRTdHJpbmcpO1xuXHRcdFx0Y29uc3QgYmFzZTY0ID0gaG1hY0Jhc2U2NChhbGdvLCBzZWNyZXQsIHNpZ25lZFN0cmluZyk7XG5cdFx0XHRjb25zdCBleHBlY3RlZCA9IGNvbmZpZy5mb3JtYXQucmVwbGFjZShcIntoZXh9XCIsIGhleCkucmVwbGFjZShcIntiYXNlNjR9XCIsIGJhc2U2NCk7XG5cblx0XHRcdGlmICghc2FmZUVxdWFsU3RyaW5nKHNpZywgZXhwZWN0ZWQpKSB7XG5cdFx0XHRcdHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiBcInNpZ25hdHVyZV9taXNtYXRjaFwiLCBtZXNzYWdlOiBgJHtjb25maWcuaGVhZGVyfTogc2lnbmF0dXJlIG1pc21hdGNoYCB9O1xuXHRcdFx0fVxuXHRcdFx0cmV0dXJuIHtcblx0XHRcdFx0b2s6IHRydWUsXG5cdFx0XHRcdGV2ZW50SWQ6IFwiXCIsXG5cdFx0XHRcdGV2ZW50VHlwZTogZ2V0RXZlbnRUeXBlRnJvbUJvZHkocGFyc2VkQm9keSkgPz8gXCJ1bmtub3duXCIsXG5cdFx0XHR9O1xuXHRcdH0sXG5cdH07XG59XG5cbmV4cG9ydCBjb25zdCBCVUlMVElOX1ZFUklGSUVSUzogUmVjb3JkPHN0cmluZywgVmVyaWZpZXI+ID0ge1xuXHRnaXRodWI6IGdpdGh1YlZlcmlmaWVyLFxuXHRzdHJpcGU6IHN0cmlwZVZlcmlmaWVyLFxuXHRzbGFjazogc2xhY2tWZXJpZmllcixcblx0c2hvcGlmeTogc2hvcGlmeVZlcmlmaWVyLFxuXHRzdml4OiBzdml4VmVyaWZpZXIsXG59O1xuIl19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blokjs/trigger-webhook",
3
- "version": "0.2.1",
3
+ "version": "0.6.1",
4
4
  "description": "Webhook trigger for Blok workflows - supports GitHub, Stripe, Shopify, and custom webhooks",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,13 +14,15 @@
14
14
  "author": "Deskree Technologies Inc.",
15
15
  "license": "Apache-2.0",
16
16
  "dependencies": {
17
- "@blokjs/helper": "^0.2.0",
18
- "@blokjs/runner": "^0.2.0",
19
- "@blokjs/shared": "^0.2.0",
17
+ "@blokjs/helper": "^0.6.1",
18
+ "@blokjs/runner": "^0.6.1",
19
+ "@blokjs/shared": "^0.6.1",
20
20
  "@opentelemetry/api": "^1.9.0",
21
+ "hono": "^4.11.7",
21
22
  "uuid": "^11.1.0"
22
23
  },
23
24
  "devDependencies": {
25
+ "@hono/node-server": "^1.19.9",
24
26
  "@types/node": "^22.15.21",
25
27
  "@types/uuid": "^11.0.0",
26
28
  "typescript": "^5.8.3",
@@ -0,0 +1,162 @@
1
+ /**
2
+ * v0.7 PR 4 — full end-to-end webhook trigger integration test.
3
+ *
4
+ * Spins up a real Hono app + `@hono/node-server` with a real
5
+ * WebhookTrigger configured for a GitHub-style provider. Sends a
6
+ * signed POST via native `fetch` and asserts the workflow ran. A
7
+ * second POST with the same delivery id exercises the replay-cache
8
+ * dedup path and expects `{ status: "duplicate" }`.
9
+ *
10
+ * Complements `WebhookTrigger.test.ts` + `verifiers.test.ts` (unit
11
+ * coverage of the public surface).
12
+ */
13
+
14
+ import { createHmac } from "node:crypto";
15
+ import type { Server } from "node:http";
16
+ import { NodeMap, RunTracker, WorkflowRegistry, defineNode } from "@blokjs/runner";
17
+ import { serve } from "@hono/node-server";
18
+ import { Hono } from "hono";
19
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
20
+ import { z } from "zod";
21
+
22
+ vi.mock("@opentelemetry/api", () => ({
23
+ trace: {
24
+ getTracer: () => ({
25
+ startActiveSpan: (_name: string, fn: (span: unknown) => unknown) =>
26
+ fn({ setAttribute: vi.fn(), setStatus: vi.fn(), recordException: vi.fn(), end: vi.fn() }),
27
+ }),
28
+ },
29
+ metrics: {
30
+ getMeter: () => ({
31
+ createCounter: () => ({ add: vi.fn() }),
32
+ createHistogram: () => ({ record: vi.fn() }),
33
+ createGauge: () => ({ record: vi.fn() }),
34
+ createObservableGauge: () => ({ addCallback: vi.fn() }),
35
+ }),
36
+ },
37
+ SpanStatusCode: { OK: 0, ERROR: 1 },
38
+ }));
39
+
40
+ import WebhookTriggerClass, { _setActiveWebhookTrigger } from "./WebhookTrigger";
41
+
42
+ const TEST_PORT = 4903;
43
+ const SECRET = "shhh-its-a-secret-1234567890";
44
+
45
+ function hmacHex(body: string): string {
46
+ return createHmac("sha256", SECRET).update(body).digest("hex");
47
+ }
48
+
49
+ const handleNode = defineNode({
50
+ name: "handle-event",
51
+ description: "test fixture — record the event id + type",
52
+ input: z.object({}).passthrough(),
53
+ output: z.object({ handled: z.boolean(), eventId: z.string() }),
54
+ async execute(ctx) {
55
+ const body = (ctx.request?.body as { delivery_id?: string } | undefined) ?? {};
56
+ return { handled: true, eventId: body.delivery_id ?? "" };
57
+ },
58
+ });
59
+
60
+ describe("WebhookTrigger — v0.7 PR 4 integration (real HTTP)", () => {
61
+ let app: Hono;
62
+ let trigger: InstanceType<typeof WebhookTriggerClass>;
63
+ let httpServer: Server | null = null;
64
+
65
+ beforeEach(() => {
66
+ WorkflowRegistry.resetInstance();
67
+ _setActiveWebhookTrigger(null);
68
+ process.env.GH_SECRET = SECRET;
69
+ app = new Hono();
70
+ });
71
+
72
+ afterEach(
73
+ () =>
74
+ new Promise<void>((resolve) => {
75
+ if (trigger) void trigger.stop();
76
+ if (httpServer) {
77
+ httpServer.close(() => {
78
+ httpServer = null;
79
+ WorkflowRegistry.resetInstance();
80
+ _setActiveWebhookTrigger(null);
81
+ process.env.GH_SECRET = undefined;
82
+ resolve();
83
+ });
84
+ } else {
85
+ WorkflowRegistry.resetInstance();
86
+ _setActiveWebhookTrigger(null);
87
+ process.env.GH_SECRET = undefined;
88
+ resolve();
89
+ }
90
+ }),
91
+ );
92
+
93
+ it("verifies a signed GitHub-style POST, runs the workflow, and dedups replays", async () => {
94
+ const nodes = new NodeMap();
95
+ nodes.addNode("handle-event", handleNode);
96
+
97
+ WorkflowRegistry.getInstance().register({
98
+ name: "gh-events",
99
+ source: "/test/gh.json",
100
+ workflow: {
101
+ name: "gh-events",
102
+ version: "1.0.0",
103
+ trigger: {
104
+ webhook: {
105
+ provider: "github",
106
+ path: "/webhooks/github",
107
+ secretEnv: "GH_SECRET",
108
+ idempotencyKey: "js/ctx.request.headers['x-github-delivery']",
109
+ },
110
+ },
111
+ steps: [{ id: "handle", node: "handle-event", type: "module", inputs: {} }],
112
+ nodes: { handle: { inputs: {} } },
113
+ },
114
+ });
115
+
116
+ trigger = new WebhookTriggerClass(app);
117
+ trigger.setNodeMap({ nodes });
118
+ await trigger.listen();
119
+
120
+ await new Promise<void>((resolve) => {
121
+ httpServer = serve({ fetch: app.fetch, port: TEST_PORT }, () => resolve()) as Server;
122
+ });
123
+
124
+ const body = JSON.stringify({ ref: "refs/heads/main", delivery_id: "delivery-uuid-9" });
125
+ const sig = `sha256=${hmacHex(body)}`;
126
+ const reqInit = {
127
+ method: "POST",
128
+ headers: {
129
+ "content-type": "application/json",
130
+ "x-hub-signature-256": sig,
131
+ "x-github-event": "push",
132
+ "x-github-delivery": "delivery-uuid-9",
133
+ },
134
+ body,
135
+ };
136
+
137
+ // First delivery — should run the workflow.
138
+ const first = await fetch(`http://localhost:${TEST_PORT}/webhooks/github`, reqInit);
139
+ expect(first.status).toBe(200);
140
+ const firstJson = (await first.json()) as { status?: string; eventId?: string };
141
+ expect(firstJson.status).toBe("ok");
142
+ expect(firstJson.eventId).toBe("delivery-uuid-9");
143
+
144
+ // Second delivery with the same delivery id — replay cache should
145
+ // short-circuit with `duplicate` and NOT run the workflow.
146
+ const second = await fetch(`http://localhost:${TEST_PORT}/webhooks/github`, reqInit);
147
+ expect(second.status).toBe(200);
148
+ const secondJson = (await second.json()) as { status?: string; eventId?: string };
149
+ expect(secondJson.status).toBe("duplicate");
150
+ expect(secondJson.eventId).toBe("delivery-uuid-9");
151
+ }, 15_000);
152
+ });
153
+
154
+ // Drain the per-process RunTracker after the suite to keep singletons
155
+ // from leaking into other tests in the same project.
156
+ afterEach(() => {
157
+ try {
158
+ RunTracker.resetInstance();
159
+ } catch {
160
+ /* ignore — older test orderings */
161
+ }
162
+ });