hono-webhooks 0.5.0 → 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 +262 -37
- package/dist/cli.js +5632 -5338
- 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 +198 -169
- package/src/cli.ts +1 -3
- package/src/index.ts +141 -31
- package/src/otel.ts +17 -9
- package/src/signatures.ts +277 -33
package/dist/index.d.ts
CHANGED
|
@@ -35,8 +35,17 @@ export interface VerifyInput<E extends Env = any> {
|
|
|
35
35
|
rawBody: string;
|
|
36
36
|
/** All request headers, lower-case names, not schema-validated. */
|
|
37
37
|
headers: Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* Records why the check failed and returns `false`:
|
|
40
|
+
* `return fail('timestamp-out-of-tolerance')`. The reason reaches
|
|
41
|
+
* `onEvent` (`validation.issues[0].message`), never the response.
|
|
42
|
+
*/
|
|
43
|
+
fail(reason: string): false;
|
|
38
44
|
}
|
|
39
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Checks a request's signature. `hono-webhooks/signatures` has ready-made
|
|
47
|
+
* ones. Only `true` lets the request through.
|
|
48
|
+
*/
|
|
40
49
|
export type Verify<E extends Env = any> = (input: VerifyInput<E>) => MaybePromise<boolean>;
|
|
41
50
|
export interface WebhookConfig<P extends string = string, TParams extends Schema | undefined = Schema | undefined, TQuery extends Schema | undefined = Schema | undefined, THeaders extends Schema | undefined = Schema | undefined, TBody extends Schema | undefined = Schema | undefined, TResponse extends Schema | undefined = Schema | undefined, E extends Env = any> {
|
|
42
51
|
method: WebhookMethod;
|
|
@@ -51,9 +60,14 @@ export interface WebhookConfig<P extends string = string, TParams extends Schema
|
|
|
51
60
|
* Checks the signature right after routing, before anything is validated
|
|
52
61
|
* or parsed. Returning `false` answers `401 { error: 'Invalid signature' }`.
|
|
53
62
|
*
|
|
63
|
+
* Required for every method but `GET`: pass `false` for a webhook that is
|
|
64
|
+
* public on purpose, or `createWebhooks` throws.
|
|
65
|
+
*
|
|
54
66
|
* @example verify: shopify.verify((c) => c.env.SHOPIFY_SECRET)
|
|
55
67
|
*/
|
|
56
|
-
verify?: Verify<E
|
|
68
|
+
verify?: Verify<E> | false;
|
|
69
|
+
/** Overrides `WebhooksOptions.maxBodyBytes` for this webhook. */
|
|
70
|
+
maxBodyBytes?: number;
|
|
57
71
|
/**
|
|
58
72
|
* Checks what the handler returns. A mismatch is reported on
|
|
59
73
|
* `WebhookEvent.validation` and the response is still sent.
|
|
@@ -152,6 +166,12 @@ export interface WebhooksOptions<E extends Env = any> {
|
|
|
152
166
|
* @default true
|
|
153
167
|
*/
|
|
154
168
|
logErrors?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Larger bodies answer `413 { error: 'Payload too large' }` before they
|
|
171
|
+
* are read. `Infinity` turns the limit off. Each webhook can override it.
|
|
172
|
+
* @default 1_048_576 (1 MiB)
|
|
173
|
+
*/
|
|
174
|
+
maxBodyBytes?: number;
|
|
155
175
|
}
|
|
156
176
|
export type WebhooksApp<E extends Env = any> = Hono<E> & {
|
|
157
177
|
/** The definitions, in the order they were passed. */
|
|
@@ -172,7 +192,12 @@ export declare class WebhookValidationError extends HTTPException {
|
|
|
172
192
|
*
|
|
173
193
|
* @example
|
|
174
194
|
* defineWebhook(
|
|
175
|
-
* {
|
|
195
|
+
* {
|
|
196
|
+
* method: 'POST',
|
|
197
|
+
* path: '/orders/:id',
|
|
198
|
+
* verify: shopify.verify((c) => c.env.SHOPIFY_SECRET),
|
|
199
|
+
* body: z.object({ total: z.number() })
|
|
200
|
+
* },
|
|
176
201
|
* async ({ params, body }) => ({ id: params.id, total: body.total })
|
|
177
202
|
* )
|
|
178
203
|
*/
|
|
@@ -180,7 +205,8 @@ export declare const defineWebhook: <const P extends string, TParams extends Sch
|
|
|
180
205
|
/**
|
|
181
206
|
* Builds a Hono app that serves the webhooks. Mount it under a prefix:
|
|
182
207
|
* `app.route('/webhooks', createWebhooks([...]))`. Unknown paths below the
|
|
183
|
-
* prefix answer 404, known paths with the wrong method answer 405.
|
|
208
|
+
* prefix answer 404, known paths with the wrong method answer 405. Throws
|
|
209
|
+
* for a duplicate route and for a non-GET webhook without `verify`.
|
|
184
210
|
*/
|
|
185
211
|
export declare const createWebhooks: (webhooks: readonly Webhook<any, string, any, any, any, any, any>[], options?: WebhooksOptions<any>) => WebhooksApp<any>;
|
|
186
212
|
/**
|
|
@@ -213,6 +239,8 @@ export interface WebhookEventSummary {
|
|
|
213
239
|
userAgent: string | null;
|
|
214
240
|
params: Record<string, unknown> | null;
|
|
215
241
|
query: Record<string, unknown> | null;
|
|
242
|
+
/** Only the headers named in `SummaryOptions.headers`; absent without that option. */
|
|
243
|
+
headers?: Record<string, string>;
|
|
216
244
|
body: unknown;
|
|
217
245
|
response: unknown;
|
|
218
246
|
/** `null` for validation failures, which `validation` describes. */
|
|
@@ -231,19 +259,31 @@ export interface WebhookEventSummary {
|
|
|
231
259
|
}[];
|
|
232
260
|
} | null;
|
|
233
261
|
}
|
|
262
|
+
/** What `summarizeEvent` keeps. Defaults keep everything but headers. */
|
|
263
|
+
export interface SummaryOptions {
|
|
264
|
+
/** `false` sets `body` and `response` to `null`. @default true */
|
|
265
|
+
body?: boolean;
|
|
266
|
+
/** `false` sets `query` to `null`. @default true */
|
|
267
|
+
query?: boolean;
|
|
268
|
+
/**
|
|
269
|
+
* Request headers to keep, lower-case, e.g. a delivery id such as
|
|
270
|
+
* `x-github-delivery` or `svix-id`. @default []
|
|
271
|
+
*/
|
|
272
|
+
headers?: readonly string[];
|
|
273
|
+
}
|
|
234
274
|
/**
|
|
235
|
-
* Flattens an event for logging or storage. Request headers are left out
|
|
236
|
-
*
|
|
275
|
+
* Flattens an event for logging or storage. Request headers are left out
|
|
276
|
+
* unless named in `options.headers`: they carry signatures and credentials.
|
|
237
277
|
*
|
|
238
278
|
* @example
|
|
239
|
-
* onEvent: (e) => db.insert(webhookCalls).values(summarizeEvent(e))
|
|
279
|
+
* onEvent: (e) => db.insert(webhookCalls).values(summarizeEvent(e, { body: false }))
|
|
240
280
|
*/
|
|
241
|
-
export declare function summarizeEvent(event: WebhookEvent): WebhookEventSummary;
|
|
281
|
+
export declare function summarizeEvent(event: WebhookEvent, options?: SummaryOptions): WebhookEventSummary;
|
|
242
282
|
export interface WebhookDescription {
|
|
243
283
|
method: WebhookMethod;
|
|
244
284
|
path: string;
|
|
245
285
|
description: string | null;
|
|
246
|
-
/** Whether the webhook has a `verify` function
|
|
286
|
+
/** Whether the webhook has a `verify` function; `false` for `verify: false`. */
|
|
247
287
|
verified: boolean;
|
|
248
288
|
bodyType: BodyType;
|
|
249
289
|
/**
|
|
@@ -275,8 +315,20 @@ export type TestWebhookInput<W> = W extends Webhook<infer E, infer P, infer TPar
|
|
|
275
315
|
* webhook's `bodyType` is `'form'`.
|
|
276
316
|
*/
|
|
277
317
|
body?: In<TBody, unknown> | string;
|
|
278
|
-
/**
|
|
279
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Headers computed from the raw body and the request, e.g.
|
|
320
|
+
* `(raw) => shopify.sign(raw, secret)` or
|
|
321
|
+
* `(raw, { url }) => twilio.sign(raw, token, { url })`.
|
|
322
|
+
*/
|
|
323
|
+
sign?: (rawBody: string, request: {
|
|
324
|
+
url: string;
|
|
325
|
+
method: string;
|
|
326
|
+
}) => MaybePromise<Record<string, string>>;
|
|
327
|
+
/**
|
|
328
|
+
* Origin the request is sent to, for schemes that sign the URL.
|
|
329
|
+
* @default 'http://localhost'
|
|
330
|
+
*/
|
|
331
|
+
baseUrl?: string;
|
|
280
332
|
/** Bindings, available as `c.env`. */
|
|
281
333
|
env?: E extends {
|
|
282
334
|
Bindings: infer B;
|
|
@@ -295,3 +347,9 @@ export type TestWebhookInput<W> = W extends Webhook<infer E, infer P, infer TPar
|
|
|
295
347
|
* })
|
|
296
348
|
*/
|
|
297
349
|
export declare function testWebhook<W extends Webhook>(webhook: W, input?: TestWebhookInput<W>): Promise<Response>;
|
|
350
|
+
/**
|
|
351
|
+
* The webhook's path with `params` filled in, relative to the mount point:
|
|
352
|
+
* `webhookPath(oauthCallback, { provider: 'gmail' })` gives
|
|
353
|
+
* `/channels/gmail/callback`. Prepend your origin and mount prefix for a URL.
|
|
354
|
+
*/
|
|
355
|
+
export declare function webhookPath<P extends string>(webhook: Webhook<any, P>, ...[params]: [ParamKeys<P>] extends [never] ? [] : [PathParams<P>]): string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
+
import { bodyLimit } from "hono/body-limit";
|
|
3
4
|
import { HTTPException } from "hono/http-exception";
|
|
4
5
|
import { basePath } from "hono/route";
|
|
6
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5
7
|
|
|
6
8
|
class WebhookValidationError extends HTTPException {
|
|
7
9
|
target;
|
|
@@ -52,7 +54,7 @@ async function parse(schema, value, target) {
|
|
|
52
54
|
}
|
|
53
55
|
function defaultErrorResponse(error, c, log) {
|
|
54
56
|
if (error instanceof WebhookValidationError) {
|
|
55
|
-
return c.json({ error: error.message, issues: error.issues }, error.status);
|
|
57
|
+
return error.target === "signature" ? c.json({ error: error.message }, error.status) : c.json({ error: error.message, issues: error.issues }, error.status);
|
|
56
58
|
}
|
|
57
59
|
if (error instanceof HTTPException) {
|
|
58
60
|
return error.res ?? c.json({ error: error.message }, error.status);
|
|
@@ -77,6 +79,9 @@ function makeCreateWebhooks() {
|
|
|
77
79
|
const app = new Hono;
|
|
78
80
|
const methodsByPath = new Map;
|
|
79
81
|
for (const { config } of webhooks) {
|
|
82
|
+
if (config.method !== "GET" && config.verify === undefined) {
|
|
83
|
+
throw new Error(`hono-webhooks: ${config.method} ${config.path} has no verify. Pass a verify function, or verify: false if it is public on purpose.`);
|
|
84
|
+
}
|
|
80
85
|
const methods = methodsByPath.get(config.path) ?? [];
|
|
81
86
|
if (methods.includes(config.method)) {
|
|
82
87
|
throw new Error(`hono-webhooks: ${config.method} ${config.path} is defined twice`);
|
|
@@ -118,15 +123,32 @@ function makeCreateWebhooks() {
|
|
|
118
123
|
const ordered = [...webhooks].sort((a, b) => Number(isDynamic(a.config.path)) - Number(isDynamic(b.config.path)));
|
|
119
124
|
for (const { config, handler } of ordered) {
|
|
120
125
|
app.on(config.method, config.path, handle(config.path, async (c, event) => {
|
|
126
|
+
const maxSize = config.maxBodyBytes ?? options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
127
|
+
if (maxSize !== Infinity) {
|
|
128
|
+
await bodyLimit({
|
|
129
|
+
maxSize,
|
|
130
|
+
onError: () => {
|
|
131
|
+
throw new HTTPException(413, { message: "Payload too large" });
|
|
132
|
+
}
|
|
133
|
+
})(c, async () => {});
|
|
134
|
+
}
|
|
121
135
|
event.rawBody = await c.req.text();
|
|
122
|
-
if (config.verify
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
if (config.verify) {
|
|
137
|
+
let reason = "Signature does not match";
|
|
138
|
+
const ok = await config.verify({
|
|
139
|
+
c,
|
|
140
|
+
rawBody: event.rawBody,
|
|
141
|
+
headers: c.req.header(),
|
|
142
|
+
fail: (why) => {
|
|
143
|
+
reason = why;
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (ok !== true) {
|
|
148
|
+
throw new WebhookValidationError("signature", [
|
|
149
|
+
{ message: reason }
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
130
152
|
}
|
|
131
153
|
event.params = await parse(config.params, c.req.param(), "params");
|
|
132
154
|
event.query = await parse(config.query, c.req.query(), "query");
|
|
@@ -191,8 +213,15 @@ function issuePath(issue) {
|
|
|
191
213
|
const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
192
214
|
return path || null;
|
|
193
215
|
}
|
|
194
|
-
function summarizeEvent(event) {
|
|
216
|
+
function summarizeEvent(event, options = {}) {
|
|
195
217
|
const { c, error, validation } = event;
|
|
218
|
+
const keepBody = options.body ?? true;
|
|
219
|
+
const headers = {};
|
|
220
|
+
for (const name of options.headers ?? []) {
|
|
221
|
+
const value = c.req.header(name);
|
|
222
|
+
if (value !== undefined)
|
|
223
|
+
headers[name] = value;
|
|
224
|
+
}
|
|
196
225
|
return {
|
|
197
226
|
route: event.route ?? null,
|
|
198
227
|
method: event.method,
|
|
@@ -202,9 +231,10 @@ function summarizeEvent(event) {
|
|
|
202
231
|
ip: c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || c.req.header("cf-connecting-ip") || null,
|
|
203
232
|
userAgent: c.req.header("user-agent") ?? null,
|
|
204
233
|
params: event.params ?? null,
|
|
205
|
-
query: event.query ?? null,
|
|
206
|
-
|
|
207
|
-
|
|
234
|
+
query: options.query === false ? null : event.query ?? null,
|
|
235
|
+
...options.headers && { headers },
|
|
236
|
+
body: keepBody ? event.body ?? null : null,
|
|
237
|
+
response: keepBody ? event.response ?? null : null,
|
|
208
238
|
error: error === undefined || error instanceof WebhookValidationError ? null : error instanceof Error ? {
|
|
209
239
|
name: error.name,
|
|
210
240
|
message: error.message,
|
|
@@ -253,7 +283,13 @@ function fillPath(pattern, params) {
|
|
|
253
283
|
}
|
|
254
284
|
async function testWebhook(webhook, input = {}) {
|
|
255
285
|
const { config } = webhook;
|
|
256
|
-
const {
|
|
286
|
+
const {
|
|
287
|
+
body,
|
|
288
|
+
sign,
|
|
289
|
+
env,
|
|
290
|
+
options,
|
|
291
|
+
baseUrl = "http://localhost"
|
|
292
|
+
} = input;
|
|
257
293
|
const headers = new Headers(input.headers);
|
|
258
294
|
let rawBody;
|
|
259
295
|
if (typeof body === "string") {
|
|
@@ -271,12 +307,16 @@ async function testWebhook(webhook, input = {}) {
|
|
|
271
307
|
headers.set("content-type", "application/json");
|
|
272
308
|
}
|
|
273
309
|
}
|
|
274
|
-
for (const [name, value] of Object.entries(await sign?.(rawBody ?? "") ?? {})) {
|
|
275
|
-
headers.set(name, value);
|
|
276
|
-
}
|
|
277
310
|
const path = input.path ?? fillPath(config.path, input.params ?? {});
|
|
278
311
|
const query = new URLSearchParams(input.query).toString();
|
|
279
|
-
|
|
312
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path || "/"}${query ? `?${query}` : ""}`;
|
|
313
|
+
for (const [name, value] of Object.entries(await sign?.(rawBody ?? "", { url, method: config.method }) ?? {})) {
|
|
314
|
+
headers.set(name, value);
|
|
315
|
+
}
|
|
316
|
+
return createWebhooks([webhook], options).request(url, { method: config.method, headers, body: rawBody }, env);
|
|
317
|
+
}
|
|
318
|
+
function webhookPath(webhook, ...[params]) {
|
|
319
|
+
return fillPath(webhook.config.path, params ?? {}) || "/";
|
|
280
320
|
}
|
|
281
321
|
export {
|
|
282
322
|
WebhookValidationError,
|
|
@@ -286,5 +326,6 @@ export {
|
|
|
286
326
|
describeWebhooks,
|
|
287
327
|
formatIssues,
|
|
288
328
|
summarizeEvent,
|
|
289
|
-
testWebhook
|
|
329
|
+
testWebhook,
|
|
330
|
+
webhookPath
|
|
290
331
|
};
|
package/dist/otel.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Meter, type Span, type Tracer } from '@opentelemetry/api';
|
|
2
2
|
import type { Env } from 'hono';
|
|
3
|
-
import { type WebhookEvent, type WebhookEventSummary, type WebhooksOptions } from 'hono-webhooks';
|
|
3
|
+
import { type SummaryOptions, type WebhookEvent, type WebhookEventSummary, type WebhooksOptions } from 'hono-webhooks';
|
|
4
4
|
/** The `route` label for 404s, which match no webhook. */
|
|
5
5
|
export declare const UNMATCHED_ROUTE = "__unmatched__";
|
|
6
6
|
/** A metric `otelWebhooks` records, described for a metric catalog. */
|
|
@@ -39,6 +39,8 @@ export interface WebhookCall extends WebhookEventSummary {
|
|
|
39
39
|
/** `error` for 5xx, `warn` for 4xx, else `info`. */
|
|
40
40
|
level: 'info' | 'warn' | 'error';
|
|
41
41
|
outcome: 'success' | 'failed' | 'validation_failed';
|
|
42
|
+
/** The route is in `ignoreRoutes`: no metrics or log were recorded. */
|
|
43
|
+
ignored: boolean;
|
|
42
44
|
}
|
|
43
45
|
/** `console` fits, as does any logger with these three methods. */
|
|
44
46
|
export type WebhookLogger = Record<WebhookCall['level'], (message: string, fields: Record<string, string | number>) => void>;
|
|
@@ -49,16 +51,18 @@ export interface OtelWebhooksOptions<E extends Env = any> {
|
|
|
49
51
|
meter?: Meter;
|
|
50
52
|
/** Routes that get a span but no metrics, e.g. `['/health']`. */
|
|
51
53
|
ignoreRoutes?: readonly string[];
|
|
54
|
+
/** What `call` keeps of the request, as for `summarizeEvent`. */
|
|
55
|
+
summary?: SummaryOptions;
|
|
52
56
|
/**
|
|
53
|
-
* Called after every request, ignored routes included,
|
|
54
|
-
* still active: log it, store it. A returned promise is
|
|
55
|
-
* `onEvent`'s.
|
|
57
|
+
* Called after every request, ignored routes included (`call.ignored`),
|
|
58
|
+
* with the span still active: log it, store it. A returned promise is
|
|
59
|
+
* handled like `onEvent`'s.
|
|
56
60
|
*/
|
|
57
61
|
onWebhookCall?: (call: WebhookCall, event: WebhookEvent<E>) => void | Promise<void>;
|
|
58
62
|
/**
|
|
59
63
|
* Logs one `webhook.completed` event per request at `call.level`, with
|
|
60
64
|
* `route`, `method`, `status_code`, `duration_ms`, `status` (the outcome)
|
|
61
|
-
* and `
|
|
65
|
+
* `error_type` and `error_message`. 404s and `ignoreRoutes` are skipped. Called with the
|
|
62
66
|
* request's span, so the logger can attach to it.
|
|
63
67
|
*
|
|
64
68
|
* @example
|
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
|
+
}>;
|