hono-webhooks 0.5.1 → 0.6.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/README.md +260 -37
- package/dist/cli.js +183 -35
- package/dist/index.d.ts +69 -11
- package/dist/index.js +60 -19
- package/dist/otel.d.ts +9 -5
- package/dist/otel.js +8 -5
- package/dist/signatures.d.ts +64 -0
- package/dist/signatures.js +171 -24
- package/package.json +2 -2
- package/src/cli-commands.ts +19 -4
- package/src/index.ts +141 -31
- package/src/otel.ts +17 -9
- package/src/signatures.ts +277 -33
package/dist/otel.js
CHANGED
|
@@ -49,9 +49,10 @@ function otelWebhooks(options = {}) {
|
|
|
49
49
|
});
|
|
50
50
|
},
|
|
51
51
|
onEvent: (event) => {
|
|
52
|
-
const summary = summarizeEvent(event);
|
|
52
|
+
const summary = summarizeEvent(event, options.summary);
|
|
53
53
|
const success = summary.status < 400;
|
|
54
54
|
const call = {
|
|
55
|
+
ignored: summary.route !== null && ignored.has(summary.route),
|
|
55
56
|
...summary,
|
|
56
57
|
level: summary.status >= 500 ? "error" : success ? "info" : "warn",
|
|
57
58
|
outcome: summary.validation ? "validation_failed" : success ? "success" : "failed"
|
|
@@ -71,8 +72,7 @@ function otelWebhooks(options = {}) {
|
|
|
71
72
|
span?.setStatus({
|
|
72
73
|
code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR
|
|
73
74
|
});
|
|
74
|
-
|
|
75
|
-
if (!isIgnored) {
|
|
75
|
+
if (!call.ignored) {
|
|
76
76
|
instruments ??= createInstruments(options.meter ?? metrics.getMeter("webhooks"));
|
|
77
77
|
const attributes = {
|
|
78
78
|
route: call.route ?? UNMATCHED_ROUTE,
|
|
@@ -81,7 +81,7 @@ function otelWebhooks(options = {}) {
|
|
|
81
81
|
instruments.total.add(1, attributes);
|
|
82
82
|
instruments.duration.record(event.durationMs, attributes);
|
|
83
83
|
}
|
|
84
|
-
if (span && options.logger && call.route !== null && !
|
|
84
|
+
if (span && options.logger && call.route !== null && !call.ignored) {
|
|
85
85
|
options.logger(span)[call.level]("webhook.completed", {
|
|
86
86
|
event: "webhook.completed",
|
|
87
87
|
route: call.route,
|
|
@@ -89,7 +89,10 @@ function otelWebhooks(options = {}) {
|
|
|
89
89
|
status_code: call.status,
|
|
90
90
|
duration_ms: call.durationMs,
|
|
91
91
|
status: call.outcome,
|
|
92
|
-
...call.error && {
|
|
92
|
+
...call.error && {
|
|
93
|
+
error_type: call.error.name,
|
|
94
|
+
error_message: call.error.message
|
|
95
|
+
}
|
|
93
96
|
});
|
|
94
97
|
}
|
|
95
98
|
return options.onWebhookCall?.(call, event);
|
package/dist/signatures.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ export interface HmacOptions {
|
|
|
17
17
|
/** @default 'SHA-256' */
|
|
18
18
|
hash?: 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The URL a provider signed. Behind a proxy `c.req.url` is the internal URL,
|
|
22
|
+
* so return the public one; return several and any match passes.
|
|
23
|
+
* @default (c) => c.req.url
|
|
24
|
+
*/
|
|
25
|
+
export type SignedUrl = (c: Context<any>) => string | readonly string[] | Promise<string | readonly string[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Why a built-in scheme rejected a request, passed to `fail` and so to
|
|
28
|
+
* `onEvent` as `validation.issues[0].message`.
|
|
29
|
+
*/
|
|
30
|
+
export type VerifyFailureReason = 'missing-header' | 'malformed' | 'timestamp-out-of-tolerance' | 'mismatch';
|
|
20
31
|
/**
|
|
21
32
|
* A scheme where one header carries an HMAC of the raw body. Covers most
|
|
22
33
|
* providers; `github` and `shopify` are built with it.
|
|
@@ -27,6 +38,8 @@ export interface HmacOptions {
|
|
|
27
38
|
export declare function hmac(options: HmacOptions): SignatureScheme;
|
|
28
39
|
/** GitHub: `x-hub-signature-256: sha256=<hex>`. */
|
|
29
40
|
export declare const github: SignatureScheme<void, void>;
|
|
41
|
+
/** Meta (WhatsApp, Messenger, Instagram): signs like GitHub, with the app secret. */
|
|
42
|
+
export declare const meta: SignatureScheme<void, void>;
|
|
30
43
|
/** Shopify: `x-shopify-hmac-sha256: <base64>`. */
|
|
31
44
|
export declare const shopify: SignatureScheme<void, void>;
|
|
32
45
|
/**
|
|
@@ -51,3 +64,54 @@ export declare const svix: SignatureScheme<{
|
|
|
51
64
|
id?: string;
|
|
52
65
|
timestamp?: number;
|
|
53
66
|
}>;
|
|
67
|
+
export interface SharedSecretOptions {
|
|
68
|
+
/** Header carrying the secret, e.g. `x-gitlab-token` or `authorization`. */
|
|
69
|
+
header: string;
|
|
70
|
+
/** Text before the secret in the header, e.g. `Bearer `. */
|
|
71
|
+
prefix?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A secret sent as is in a header (GitLab's `x-gitlab-token`, Telegram's
|
|
75
|
+
* `x-telegram-bot-api-secret-token`, `authorization: Bearer <token>`),
|
|
76
|
+
* compared in constant time. An empty secret throws (a 500).
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* verify: sharedSecret({ header: 'x-gitlab-token' }).verify((c) => c.env.GITLAB_TOKEN)
|
|
80
|
+
*/
|
|
81
|
+
export declare function sharedSecret(options: SharedSecretOptions): SignatureScheme;
|
|
82
|
+
/**
|
|
83
|
+
* Infisical: `x-infisical-signature: t=<ms>;<hex>`, an HMAC-SHA256 of the
|
|
84
|
+
* JSON body. The body's own `timestamp` (milliseconds) is what is signed, so
|
|
85
|
+
* that is what must be within `toleranceSeconds` (default 300).
|
|
86
|
+
*/
|
|
87
|
+
export declare const infisical: SignatureScheme<{
|
|
88
|
+
toleranceSeconds?: number;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Twilio: `x-twilio-signature: <base64>`, an HMAC-SHA1 with the auth token
|
|
92
|
+
* over the full public URL and the sorted form fields (`bodyType: 'form'`).
|
|
93
|
+
* Pass `url` when `c.req.url` is not the URL Twilio called.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* verify: twilio.verify((c) => c.env.TWILIO_AUTH_TOKEN, { url: (c) => publicUrl(c) })
|
|
97
|
+
*/
|
|
98
|
+
export declare const twilio: SignatureScheme<{
|
|
99
|
+
url?: SignedUrl;
|
|
100
|
+
}, {
|
|
101
|
+
url: string;
|
|
102
|
+
}>;
|
|
103
|
+
/**
|
|
104
|
+
* HubSpot v3: `x-hubspot-signature-v3: <base64>`, an HMAC-SHA256 with the
|
|
105
|
+
* app's client secret over `<METHOD><url><body><timestamp>`, and
|
|
106
|
+
* `x-hubspot-request-timestamp` in milliseconds, rejected when more than
|
|
107
|
+
* `toleranceSeconds` (default 300) away. Pass `url` when `c.req.url` is not
|
|
108
|
+
* the URL HubSpot called.
|
|
109
|
+
*/
|
|
110
|
+
export declare const hubspot: SignatureScheme<{
|
|
111
|
+
url?: SignedUrl;
|
|
112
|
+
toleranceSeconds?: number;
|
|
113
|
+
}, {
|
|
114
|
+
url: string;
|
|
115
|
+
method?: string;
|
|
116
|
+
timestamp?: number;
|
|
117
|
+
}>;
|
package/dist/signatures.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/signatures.ts
|
|
2
2
|
var encoder = new TextEncoder;
|
|
3
|
+
var reject = (input, reason) => input.fail?.(reason) ?? false;
|
|
3
4
|
function hmacKey(secret, hash) {
|
|
4
5
|
if (!secret.length) {
|
|
5
6
|
throw new Error("hono-webhooks: the signing secret is empty");
|
|
@@ -29,19 +30,23 @@ function fromBase64(base64) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
var resolveSecret = async (secret, c) => typeof secret === "function" ? secret(c) : secret;
|
|
33
|
+
var signedUrls = async (url, c) => [await url?.(c) ?? c.req.url].flat();
|
|
34
|
+
var outsideTolerance = (seconds, toleranceSeconds) => Math.abs(Date.now() / 1000 - seconds) > toleranceSeconds;
|
|
32
35
|
function hmac(options) {
|
|
33
36
|
const { header, encoding, prefix = "", hash = "SHA-256" } = options;
|
|
34
37
|
const name = header.toLowerCase();
|
|
35
38
|
return {
|
|
36
|
-
verify: (secret) => async (
|
|
37
|
-
const value = headers[name];
|
|
38
|
-
if (!value
|
|
39
|
-
return
|
|
39
|
+
verify: (secret) => async (input) => {
|
|
40
|
+
const value = input.headers[name];
|
|
41
|
+
if (!value)
|
|
42
|
+
return reject(input, "missing-header");
|
|
43
|
+
if (!value.startsWith(prefix))
|
|
44
|
+
return reject(input, "malformed");
|
|
40
45
|
const encoded = value.slice(prefix.length);
|
|
41
46
|
const signature = encoding === "hex" ? fromHex(encoded) : fromBase64(encoded);
|
|
42
47
|
if (!signature)
|
|
43
|
-
return
|
|
44
|
-
return hmacVerify(await resolveSecret(secret, c), rawBody, signature, hash);
|
|
48
|
+
return reject(input, "malformed");
|
|
49
|
+
return await hmacVerify(await resolveSecret(secret, input.c), input.rawBody, signature, hash) || reject(input, "mismatch");
|
|
45
50
|
},
|
|
46
51
|
async sign(rawBody, secret) {
|
|
47
52
|
const signature = await hmacSign(secret, rawBody, hash);
|
|
@@ -56,27 +61,31 @@ var github = hmac({
|
|
|
56
61
|
encoding: "hex",
|
|
57
62
|
prefix: "sha256="
|
|
58
63
|
});
|
|
64
|
+
var meta = github;
|
|
59
65
|
var shopify = hmac({
|
|
60
66
|
header: "x-shopify-hmac-sha256",
|
|
61
67
|
encoding: "base64"
|
|
62
68
|
});
|
|
63
69
|
var stripe = {
|
|
64
|
-
verify: (secret, { toleranceSeconds = 300 } = {}) => async (
|
|
65
|
-
const
|
|
70
|
+
verify: (secret, { toleranceSeconds = 300 } = {}) => async (input) => {
|
|
71
|
+
const header = input.headers["stripe-signature"];
|
|
72
|
+
if (!header)
|
|
73
|
+
return reject(input, "missing-header");
|
|
74
|
+
const parts = header.split(",").map((part) => part.split("="));
|
|
66
75
|
const timestamp = parts.find(([k]) => k === "t")?.[1] ?? "";
|
|
67
76
|
if (!/^\d+$/.test(timestamp))
|
|
68
|
-
return
|
|
69
|
-
if (
|
|
70
|
-
return
|
|
77
|
+
return reject(input, "malformed");
|
|
78
|
+
if (outsideTolerance(Number(timestamp), toleranceSeconds)) {
|
|
79
|
+
return reject(input, "timestamp-out-of-tolerance");
|
|
71
80
|
}
|
|
72
|
-
const key = await resolveSecret(secret, c);
|
|
81
|
+
const key = await resolveSecret(secret, input.c);
|
|
73
82
|
for (const [k, v] of parts) {
|
|
74
83
|
const signature = k === "v1" && v ? fromHex(v) : undefined;
|
|
75
|
-
if (signature && await hmacVerify(key, `${timestamp}.${rawBody}`, signature)) {
|
|
84
|
+
if (signature && await hmacVerify(key, `${timestamp}.${input.rawBody}`, signature)) {
|
|
76
85
|
return true;
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
|
-
return
|
|
88
|
+
return reject(input, "mismatch");
|
|
80
89
|
},
|
|
81
90
|
async sign(rawBody, secret, { timestamp } = {}) {
|
|
82
91
|
const t = timestamp ?? Math.floor(Date.now() / 1000);
|
|
@@ -92,25 +101,29 @@ function svixKey(secret) {
|
|
|
92
101
|
return key;
|
|
93
102
|
}
|
|
94
103
|
var svix = {
|
|
95
|
-
verify: (secret, { toleranceSeconds = 300 } = {}) => async (
|
|
104
|
+
verify: (secret, { toleranceSeconds = 300 } = {}) => async (input) => {
|
|
105
|
+
const { headers } = input;
|
|
96
106
|
const prefix = headers["svix-id"] ? "svix" : "webhook";
|
|
97
107
|
const id = headers[`${prefix}-id`];
|
|
98
|
-
const timestamp = headers[`${prefix}-timestamp`]
|
|
108
|
+
const timestamp = headers[`${prefix}-timestamp`];
|
|
99
109
|
const signatures = headers[`${prefix}-signature`];
|
|
100
|
-
if (!id || !signatures ||
|
|
101
|
-
return
|
|
102
|
-
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
|
|
103
|
-
return false;
|
|
110
|
+
if (!id || !signatures || !timestamp) {
|
|
111
|
+
return reject(input, "missing-header");
|
|
104
112
|
}
|
|
105
|
-
|
|
113
|
+
if (!/^\d+$/.test(timestamp))
|
|
114
|
+
return reject(input, "malformed");
|
|
115
|
+
if (outsideTolerance(Number(timestamp), toleranceSeconds)) {
|
|
116
|
+
return reject(input, "timestamp-out-of-tolerance");
|
|
117
|
+
}
|
|
118
|
+
const key = svixKey(await resolveSecret(secret, input.c));
|
|
106
119
|
for (const entry of signatures.split(" ")) {
|
|
107
120
|
const [version, encoded] = entry.split(",");
|
|
108
121
|
const signature = version === "v1" && encoded ? fromBase64(encoded) : undefined;
|
|
109
|
-
if (signature && await hmacVerify(key, `${id}.${timestamp}.${rawBody}`, signature)) {
|
|
122
|
+
if (signature && await hmacVerify(key, `${id}.${timestamp}.${input.rawBody}`, signature)) {
|
|
110
123
|
return true;
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
|
-
return
|
|
126
|
+
return reject(input, "mismatch");
|
|
114
127
|
},
|
|
115
128
|
async sign(rawBody, secret, { id, timestamp } = {}) {
|
|
116
129
|
const msgId = id ?? `msg_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
@@ -123,10 +136,144 @@ var svix = {
|
|
|
123
136
|
};
|
|
124
137
|
}
|
|
125
138
|
};
|
|
139
|
+
var compareKey;
|
|
140
|
+
async function secretsEqual(given, expected) {
|
|
141
|
+
if (!expected)
|
|
142
|
+
throw new Error("hono-webhooks: the shared secret is empty");
|
|
143
|
+
compareKey ??= crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
144
|
+
const key = await compareKey;
|
|
145
|
+
const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(expected));
|
|
146
|
+
return crypto.subtle.verify("HMAC", key, mac, encoder.encode(given));
|
|
147
|
+
}
|
|
148
|
+
function sharedSecret(options) {
|
|
149
|
+
const { prefix = "" } = options;
|
|
150
|
+
const name = options.header.toLowerCase();
|
|
151
|
+
return {
|
|
152
|
+
verify: (secret) => async (input) => {
|
|
153
|
+
const value = input.headers[name];
|
|
154
|
+
if (!value)
|
|
155
|
+
return reject(input, "missing-header");
|
|
156
|
+
if (!value.startsWith(prefix))
|
|
157
|
+
return reject(input, "malformed");
|
|
158
|
+
return await secretsEqual(value.slice(prefix.length), await resolveSecret(secret, input.c)) || reject(input, "mismatch");
|
|
159
|
+
},
|
|
160
|
+
async sign(_rawBody, secret) {
|
|
161
|
+
return { [name]: `${prefix}${secret}` };
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
var infisical = {
|
|
166
|
+
verify: (secret, { toleranceSeconds = 300 } = {}) => async (input) => {
|
|
167
|
+
const header = input.headers["x-infisical-signature"];
|
|
168
|
+
if (!header)
|
|
169
|
+
return reject(input, "missing-header");
|
|
170
|
+
const signature = fromHex(header.split(";")[1] ?? "");
|
|
171
|
+
if (!signature?.length)
|
|
172
|
+
return reject(input, "malformed");
|
|
173
|
+
let timestamp;
|
|
174
|
+
try {
|
|
175
|
+
timestamp = JSON.parse(input.rawBody)?.timestamp;
|
|
176
|
+
} catch {}
|
|
177
|
+
if (typeof timestamp !== "number")
|
|
178
|
+
return reject(input, "malformed");
|
|
179
|
+
if (!await hmacVerify(await resolveSecret(secret, input.c), input.rawBody, signature)) {
|
|
180
|
+
return reject(input, "mismatch");
|
|
181
|
+
}
|
|
182
|
+
return !outsideTolerance(timestamp / 1000, toleranceSeconds) || reject(input, "timestamp-out-of-tolerance");
|
|
183
|
+
},
|
|
184
|
+
async sign(rawBody, secret) {
|
|
185
|
+
let t;
|
|
186
|
+
try {
|
|
187
|
+
t = JSON.parse(rawBody)?.timestamp;
|
|
188
|
+
} catch {}
|
|
189
|
+
const signature = toHex(await hmacSign(secret, rawBody));
|
|
190
|
+
return { "x-infisical-signature": `t=${t ?? Date.now()};${signature}` };
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function twilioPayload(url, rawBody) {
|
|
194
|
+
const fields = [...new URLSearchParams(rawBody)].sort(([a, x], [b, y]) => a === b ? x < y ? -1 : 1 : a < b ? -1 : 1);
|
|
195
|
+
return url + fields.map(([k, v]) => k + v).join("");
|
|
196
|
+
}
|
|
197
|
+
var twilio = {
|
|
198
|
+
verify: (secret, { url } = {}) => async (input) => {
|
|
199
|
+
const signature = input.headers["x-twilio-signature"];
|
|
200
|
+
if (!signature)
|
|
201
|
+
return reject(input, "missing-header");
|
|
202
|
+
const bytes = fromBase64(signature);
|
|
203
|
+
if (!bytes)
|
|
204
|
+
return reject(input, "malformed");
|
|
205
|
+
const key = await resolveSecret(secret, input.c);
|
|
206
|
+
for (const candidate of await signedUrls(url, input.c)) {
|
|
207
|
+
const payload = twilioPayload(candidate, input.rawBody);
|
|
208
|
+
if (await hmacVerify(key, payload, bytes, "SHA-1"))
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
return reject(input, "mismatch");
|
|
212
|
+
},
|
|
213
|
+
async sign(rawBody, secret, options) {
|
|
214
|
+
if (!options?.url)
|
|
215
|
+
throw new Error("hono-webhooks: twilio.sign needs a url");
|
|
216
|
+
const signature = await hmacSign(secret, twilioPayload(options.url, rawBody), "SHA-1");
|
|
217
|
+
return { "x-twilio-signature": toBase64(signature) };
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var HUBSPOT_DECODE = {
|
|
221
|
+
"%3A": ":",
|
|
222
|
+
"%2F": "/",
|
|
223
|
+
"%3F": "?",
|
|
224
|
+
"%40": "@",
|
|
225
|
+
"%21": "!",
|
|
226
|
+
"%24": "$",
|
|
227
|
+
"%27": "'",
|
|
228
|
+
"%28": "(",
|
|
229
|
+
"%29": ")",
|
|
230
|
+
"%2A": "*",
|
|
231
|
+
"%2C": ",",
|
|
232
|
+
"%3B": ";"
|
|
233
|
+
};
|
|
234
|
+
var hubspotUrl = (url) => url.replace(/%(3A|2F|3F|40|21|24|27|28|29|2A|2C|3B)/gi, (match) => HUBSPOT_DECODE[match.toUpperCase()] ?? match);
|
|
235
|
+
var hubspot = {
|
|
236
|
+
verify: (secret, { url, toleranceSeconds = 300 } = {}) => async (input) => {
|
|
237
|
+
const signature = input.headers["x-hubspot-signature-v3"];
|
|
238
|
+
const timestamp = input.headers["x-hubspot-request-timestamp"];
|
|
239
|
+
if (!signature || !timestamp)
|
|
240
|
+
return reject(input, "missing-header");
|
|
241
|
+
const bytes = fromBase64(signature);
|
|
242
|
+
if (!bytes || !/^\d+$/.test(timestamp))
|
|
243
|
+
return reject(input, "malformed");
|
|
244
|
+
if (outsideTolerance(Number(timestamp) / 1000, toleranceSeconds)) {
|
|
245
|
+
return reject(input, "timestamp-out-of-tolerance");
|
|
246
|
+
}
|
|
247
|
+
const key = await resolveSecret(secret, input.c);
|
|
248
|
+
const method = input.c.req.method.toUpperCase();
|
|
249
|
+
for (const candidate of await signedUrls(url, input.c)) {
|
|
250
|
+
const payload = `${method}${hubspotUrl(candidate)}${input.rawBody}${timestamp}`;
|
|
251
|
+
if (await hmacVerify(key, payload, bytes))
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return reject(input, "mismatch");
|
|
255
|
+
},
|
|
256
|
+
async sign(rawBody, secret, options) {
|
|
257
|
+
if (!options?.url)
|
|
258
|
+
throw new Error("hono-webhooks: hubspot.sign needs a url");
|
|
259
|
+
const t = options.timestamp ?? Date.now();
|
|
260
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
261
|
+
const signature = await hmacSign(secret, `${method}${hubspotUrl(options.url)}${rawBody}${t}`);
|
|
262
|
+
return {
|
|
263
|
+
"x-hubspot-signature-v3": toBase64(signature),
|
|
264
|
+
"x-hubspot-request-timestamp": String(t)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
};
|
|
126
268
|
export {
|
|
127
269
|
github,
|
|
128
270
|
hmac,
|
|
271
|
+
hubspot,
|
|
272
|
+
infisical,
|
|
273
|
+
meta,
|
|
274
|
+
sharedSecret,
|
|
129
275
|
shopify,
|
|
130
276
|
stripe,
|
|
131
|
-
svix
|
|
277
|
+
svix,
|
|
278
|
+
twilio
|
|
132
279
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono-webhooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Type-safe webhook routes for Hono, validated with any Standard Schema library (Zod, Valibot, ArkType)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"@opentelemetry/sdk-trace-base": "^2.12.0",
|
|
89
89
|
"@types/bun": "^1.4.2",
|
|
90
90
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
91
|
-
"hanzio": "^3.
|
|
91
|
+
"hanzio": "^3.4.0",
|
|
92
92
|
"hono": "^4.13.10",
|
|
93
93
|
"husky": "^9.1.7",
|
|
94
94
|
"typescript": "^7.0.2",
|
package/src/cli-commands.ts
CHANGED
|
@@ -18,19 +18,30 @@ import {
|
|
|
18
18
|
} from './index'
|
|
19
19
|
import {
|
|
20
20
|
github,
|
|
21
|
+
hubspot,
|
|
22
|
+
infisical,
|
|
23
|
+
meta,
|
|
21
24
|
type SignatureScheme,
|
|
22
25
|
shopify,
|
|
23
26
|
stripe,
|
|
24
|
-
svix
|
|
27
|
+
svix,
|
|
28
|
+
twilio
|
|
25
29
|
} from './signatures'
|
|
26
30
|
|
|
27
31
|
const schemes: Record<string, SignatureScheme<any, any>> = {
|
|
28
32
|
github,
|
|
33
|
+
hubspot,
|
|
34
|
+
infisical,
|
|
35
|
+
meta,
|
|
29
36
|
shopify,
|
|
30
37
|
stripe,
|
|
31
|
-
svix
|
|
38
|
+
svix,
|
|
39
|
+
twilio
|
|
32
40
|
}
|
|
33
41
|
|
|
42
|
+
/** `request()` with a bare path runs at this origin, so URL-signing schemes sign it. */
|
|
43
|
+
const ORIGIN = 'http://localhost'
|
|
44
|
+
|
|
34
45
|
/** A file path relative to the cwd, or a package name. */
|
|
35
46
|
const importModule = (specifier: string) =>
|
|
36
47
|
import(
|
|
@@ -119,6 +130,7 @@ const list = defineCommand({
|
|
|
119
130
|
`${config.method.padEnd(7)} ${config.path}${config.description ? ` ${config.description}` : ''}`
|
|
120
131
|
)
|
|
121
132
|
if (validates) print(ctx, ` validates ${validates}`)
|
|
133
|
+
if (config.verify === false) print(ctx, ' public (verify: false)')
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
})
|
|
@@ -213,6 +225,7 @@ const testCommand = defineCommand({
|
|
|
213
225
|
headers.set('content-type', 'text/plain')
|
|
214
226
|
}
|
|
215
227
|
}
|
|
228
|
+
const url = `${path.startsWith('/') ? path : `/${path}`}${input.query ? `?${input.query}` : ''}`
|
|
216
229
|
if (input.sign) {
|
|
217
230
|
const scheme = schemes[input.sign]
|
|
218
231
|
if (!scheme) {
|
|
@@ -225,13 +238,15 @@ const testCommand = defineCommand({
|
|
|
225
238
|
const secret = ctx.env[name]
|
|
226
239
|
if (secret === undefined) throw new CliError(`${name} is not set`)
|
|
227
240
|
for (const [key, value] of Object.entries(
|
|
228
|
-
await scheme.sign(input.body ?? '', secret
|
|
241
|
+
await scheme.sign(input.body ?? '', secret, {
|
|
242
|
+
url: ORIGIN + url,
|
|
243
|
+
method: method.toUpperCase()
|
|
244
|
+
})
|
|
229
245
|
)) {
|
|
230
246
|
headers.set(key, value)
|
|
231
247
|
}
|
|
232
248
|
}
|
|
233
249
|
|
|
234
|
-
const url = `${path.startsWith('/') ? path : `/${path}`}${input.query ? `?${input.query}` : ''}`
|
|
235
250
|
const start = performance.now()
|
|
236
251
|
const res = await ctx.context.request(url, {
|
|
237
252
|
method: method.toUpperCase(),
|