hono-webhooks 0.1.1 → 0.3.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/dist/cli.js CHANGED
@@ -1,35 +1,463 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync } from "node:fs";
4
6
  import { resolve } from "node:path";
5
7
  import { pathToFileURL } from "node:url";
6
8
  import { parseArgs } from "node:util";
9
+
10
+ // src/index.ts
11
+ import { Hono } from "hono";
12
+ import { HTTPException } from "hono/http-exception";
13
+ import { basePath } from "hono/route";
14
+
15
+ class WebhookValidationError extends HTTPException {
16
+ target;
17
+ issues;
18
+ constructor(target, issues) {
19
+ super(target === "signature" ? 401 : 400, {
20
+ message: `Invalid ${target}`
21
+ });
22
+ this.target = target;
23
+ this.issues = issues;
24
+ this.name = "WebhookValidationError";
25
+ }
26
+ }
27
+ function makeDefineWebhook() {
28
+ return (config, handler) => ({
29
+ config,
30
+ handler
31
+ });
32
+ }
33
+ function detectBodyType(contentType = "") {
34
+ if (/[/+]json\b/i.test(contentType))
35
+ return "json";
36
+ if (/x-www-form-urlencoded/i.test(contentType))
37
+ return "form";
38
+ return "text";
39
+ }
40
+ function parseBody(raw, type, contentType) {
41
+ if (!raw)
42
+ return;
43
+ const resolved = type === "auto" ? detectBodyType(contentType) : type;
44
+ if (resolved === "text")
45
+ return raw;
46
+ if (resolved === "form")
47
+ return Object.fromEntries(new URLSearchParams(raw));
48
+ try {
49
+ return JSON.parse(raw);
50
+ } catch {
51
+ throw new WebhookValidationError("body", [{ message: "Malformed JSON" }]);
52
+ }
53
+ }
54
+ async function parse(schema, value, target) {
55
+ if (!schema)
56
+ return value;
57
+ const result = await schema["~standard"].validate(value);
58
+ if (result.issues)
59
+ throw new WebhookValidationError(target, result.issues);
60
+ return result.value;
61
+ }
62
+ function defaultErrorResponse(error, c, log) {
63
+ if (error instanceof WebhookValidationError) {
64
+ return c.json({ error: error.message, issues: error.issues }, error.status);
65
+ }
66
+ if (error instanceof HTTPException) {
67
+ return error.res ?? c.json({ error: error.message }, error.status);
68
+ }
69
+ if (log)
70
+ console.error(error);
71
+ return c.json({ error: "Internal Server Error" }, 500);
72
+ }
73
+ function report(c, pending) {
74
+ if (!(pending instanceof Promise))
75
+ return;
76
+ const handled = pending.catch((error) => console.error("hono-webhooks: onEvent failed", error));
77
+ try {
78
+ c.executionCtx.waitUntil(handled);
79
+ } catch {}
80
+ }
81
+ function isDynamic(path) {
82
+ return path.includes(":") || path.includes("*");
83
+ }
84
+ function makeCreateWebhooks() {
85
+ return (webhooks, options = {}) => {
86
+ const app = new Hono;
87
+ const methodsByPath = new Map;
88
+ for (const { config } of webhooks) {
89
+ const methods = methodsByPath.get(config.path) ?? [];
90
+ if (methods.includes(config.method)) {
91
+ throw new Error(`hono-webhooks: ${config.method} ${config.path} is defined twice`);
92
+ }
93
+ methods.push(config.method);
94
+ methodsByPath.set(config.path, methods);
95
+ }
96
+ const handle = (route, run) => {
97
+ const execute = async (c) => {
98
+ const start = performance.now();
99
+ const base = basePath(c);
100
+ const event = {
101
+ c,
102
+ route,
103
+ method: c.req.method,
104
+ path: base === "/" ? c.req.path : c.req.path.slice(base.length) || "/",
105
+ status: 0,
106
+ durationMs: 0
107
+ };
108
+ let res;
109
+ try {
110
+ res = await run(c, event);
111
+ } catch (error) {
112
+ event.error = error;
113
+ if (error instanceof WebhookValidationError) {
114
+ event.validation = { target: error.target, issues: error.issues };
115
+ }
116
+ res = await options.onError?.(error, c) ?? defaultErrorResponse(error, c, options.logErrors ?? true);
117
+ }
118
+ event.status = res.status;
119
+ event.durationMs = performance.now() - start;
120
+ if (options.onEvent)
121
+ report(c, options.onEvent(event));
122
+ return res;
123
+ };
124
+ const { around } = options;
125
+ return (c) => around ? around({ c, route, method: c.req.method }, () => execute(c)) : execute(c);
126
+ };
127
+ const ordered = [...webhooks].sort((a, b) => Number(isDynamic(a.config.path)) - Number(isDynamic(b.config.path)));
128
+ for (const { config, handler } of ordered) {
129
+ app.on(config.method, config.path, handle(config.path, async (c, event) => {
130
+ event.rawBody = await c.req.text();
131
+ if (config.verify && !await config.verify({
132
+ c,
133
+ rawBody: event.rawBody,
134
+ headers: c.req.header()
135
+ })) {
136
+ throw new WebhookValidationError("signature", [
137
+ { message: "Signature does not match" }
138
+ ]);
139
+ }
140
+ event.params = await parse(config.params, c.req.param(), "params");
141
+ event.query = await parse(config.query, c.req.query(), "query");
142
+ event.headers = await parse(config.headers, c.req.header(), "headers");
143
+ event.body = await parse(config.body, parseBody(event.rawBody, config.bodyType ?? "auto", c.req.header("content-type")), "body");
144
+ const result = await handler({
145
+ c,
146
+ params: event.params,
147
+ query: event.query,
148
+ headers: event.headers,
149
+ body: event.body,
150
+ rawBody: event.rawBody
151
+ });
152
+ if (result instanceof Response)
153
+ return result;
154
+ event.response = result;
155
+ if (config.response) {
156
+ const checked = await config.response["~standard"].validate(result);
157
+ if (checked.issues) {
158
+ event.validation = { target: "response", issues: checked.issues };
159
+ }
160
+ }
161
+ if (result === undefined)
162
+ return c.body(null, 204);
163
+ if (typeof result === "string") {
164
+ return c.text(result, 200, {
165
+ "content-type": "text/plain; charset=UTF-8"
166
+ });
167
+ }
168
+ return c.json(result);
169
+ }));
170
+ }
171
+ for (const [path, methods] of methodsByPath) {
172
+ app.all(path, handle(path, async (c) => {
173
+ c.header("Allow", methods.join(", "));
174
+ throw new HTTPException(405, {
175
+ message: `Method ${c.req.method} not allowed`
176
+ });
177
+ }));
178
+ }
179
+ app.all("*", handle(undefined, async () => {
180
+ throw new HTTPException(404, { message: "Route not found" });
181
+ }));
182
+ return Object.assign(app, { webhooks });
183
+ };
184
+ }
185
+ var defineWebhook = makeDefineWebhook();
186
+ var createWebhooks = makeCreateWebhooks();
187
+ function createWebhookFactory() {
188
+ return {
189
+ defineWebhook: makeDefineWebhook(),
190
+ createWebhooks: makeCreateWebhooks()
191
+ };
192
+ }
193
+ function formatIssues(issues) {
194
+ return issues.map((issue) => {
195
+ const path = issuePath(issue);
196
+ return path ? `${path}: ${issue.message}` : issue.message;
197
+ }).join("; ");
198
+ }
199
+ function issuePath(issue) {
200
+ const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
201
+ return path || null;
202
+ }
203
+ function summarizeEvent(event) {
204
+ const { c, error, validation } = event;
205
+ return {
206
+ route: event.route ?? null,
207
+ method: event.method,
208
+ path: event.path,
209
+ status: event.status,
210
+ durationMs: Math.round(event.durationMs * 100) / 100,
211
+ ip: c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || c.req.header("cf-connecting-ip") || null,
212
+ userAgent: c.req.header("user-agent") ?? null,
213
+ params: event.params ?? null,
214
+ query: event.query ?? null,
215
+ body: event.body ?? null,
216
+ response: event.response ?? null,
217
+ error: error === undefined || error instanceof WebhookValidationError ? null : error instanceof Error ? {
218
+ name: error.name,
219
+ message: error.message,
220
+ stack: error.stack ?? null
221
+ } : { name: "Error", message: String(error), stack: null },
222
+ validation: validation ? {
223
+ target: validation.target,
224
+ message: formatIssues(validation.issues),
225
+ issues: validation.issues.map((issue) => ({
226
+ message: issue.message,
227
+ path: issuePath(issue)
228
+ }))
229
+ } : null
230
+ };
231
+ }
232
+ function toJsonSchema(schema, side, target) {
233
+ const props = schema?.["~standard"];
234
+ try {
235
+ return props?.jsonSchema?.[side]({ target }) ?? null;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+ function describeWebhooks(source, { target = "draft-2020-12" } = {}) {
241
+ const webhooks = "webhooks" in source ? source.webhooks : source;
242
+ return webhooks.map(({ config }) => ({
243
+ method: config.method,
244
+ path: config.path,
245
+ description: config.description ?? null,
246
+ verified: Boolean(config.verify),
247
+ bodyType: config.bodyType ?? "auto",
248
+ jsonSchema: {
249
+ params: toJsonSchema(config.params, "input", target),
250
+ query: toJsonSchema(config.query, "input", target),
251
+ headers: toJsonSchema(config.headers, "input", target),
252
+ body: toJsonSchema(config.body, "input", target),
253
+ response: toJsonSchema(config.response, "output", target)
254
+ }
255
+ }));
256
+ }
257
+ function fillPath(pattern, params) {
258
+ return pattern.replace(/\/:(\w+)(?:\{[^}]*\})?\??/g, (_, name) => {
259
+ const value = params[name];
260
+ return value === undefined ? "" : `/${encodeURIComponent(String(value))}`;
261
+ });
262
+ }
263
+ async function testWebhook(webhook, input = {}) {
264
+ const { config } = webhook;
265
+ const { body, sign, env, options } = input;
266
+ const headers = new Headers(input.headers);
267
+ let rawBody;
268
+ if (typeof body === "string") {
269
+ rawBody = body;
270
+ if (!headers.has("content-type"))
271
+ headers.set("content-type", "text/plain");
272
+ } else if (body !== undefined && config.bodyType === "form") {
273
+ rawBody = new URLSearchParams(body).toString();
274
+ if (!headers.has("content-type")) {
275
+ headers.set("content-type", "application/x-www-form-urlencoded");
276
+ }
277
+ } else if (body !== undefined) {
278
+ rawBody = JSON.stringify(body);
279
+ if (!headers.has("content-type")) {
280
+ headers.set("content-type", "application/json");
281
+ }
282
+ }
283
+ for (const [name, value] of Object.entries(await sign?.(rawBody ?? "") ?? {})) {
284
+ headers.set(name, value);
285
+ }
286
+ const path = input.path ?? fillPath(config.path, input.params ?? {});
287
+ const query = new URLSearchParams(input.query).toString();
288
+ return createWebhooks([webhook], options).request(`${path || "/"}${query ? `?${query}` : ""}`, { method: config.method, headers, body: rawBody }, env);
289
+ }
290
+
291
+ // src/signatures.ts
292
+ var encoder = new TextEncoder;
293
+ function hmacKey(secret, hash) {
294
+ if (!secret.length) {
295
+ throw new Error("hono-webhooks: the signing secret is empty");
296
+ }
297
+ return crypto.subtle.importKey("raw", typeof secret === "string" ? encoder.encode(secret) : secret, { name: "HMAC", hash }, false, ["sign", "verify"]);
298
+ }
299
+ async function hmacSign(secret, payload, hash = "SHA-256") {
300
+ const key = await hmacKey(secret, hash);
301
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(payload)));
302
+ }
303
+ async function hmacVerify(secret, payload, signature, hash = "SHA-256") {
304
+ const key = await hmacKey(secret, hash);
305
+ return crypto.subtle.verify("HMAC", key, signature, encoder.encode(payload));
306
+ }
307
+ var toHex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
308
+ var toBase64 = (bytes) => btoa(String.fromCharCode(...bytes));
309
+ function fromHex(hex) {
310
+ if (hex.length % 2 || /[^0-9a-f]/i.test(hex))
311
+ return;
312
+ return Uint8Array.from(hex.match(/../g) ?? [], (h) => Number.parseInt(h, 16));
313
+ }
314
+ function fromBase64(base64) {
315
+ try {
316
+ return Uint8Array.from(atob(base64), (ch) => ch.charCodeAt(0));
317
+ } catch {
318
+ return;
319
+ }
320
+ }
321
+ var resolveSecret = async (secret, c) => typeof secret === "function" ? secret(c) : secret;
322
+ function hmac(options) {
323
+ const { header, encoding, prefix = "", hash = "SHA-256" } = options;
324
+ const name = header.toLowerCase();
325
+ return {
326
+ verify: (secret) => async ({ c, rawBody, headers }) => {
327
+ const value = headers[name];
328
+ if (!value?.startsWith(prefix))
329
+ return false;
330
+ const encoded = value.slice(prefix.length);
331
+ const signature = encoding === "hex" ? fromHex(encoded) : fromBase64(encoded);
332
+ if (!signature)
333
+ return false;
334
+ return hmacVerify(await resolveSecret(secret, c), rawBody, signature, hash);
335
+ },
336
+ async sign(rawBody, secret) {
337
+ const signature = await hmacSign(secret, rawBody, hash);
338
+ return {
339
+ [name]: `${prefix}${encoding === "hex" ? toHex(signature) : toBase64(signature)}`
340
+ };
341
+ }
342
+ };
343
+ }
344
+ var github = hmac({
345
+ header: "x-hub-signature-256",
346
+ encoding: "hex",
347
+ prefix: "sha256="
348
+ });
349
+ var shopify = hmac({
350
+ header: "x-shopify-hmac-sha256",
351
+ encoding: "base64"
352
+ });
353
+ var stripe = {
354
+ verify: (secret, { toleranceSeconds = 300 } = {}) => async ({ c, rawBody, headers }) => {
355
+ const parts = (headers["stripe-signature"] ?? "").split(",").map((part) => part.split("="));
356
+ const timestamp = parts.find(([k]) => k === "t")?.[1] ?? "";
357
+ if (!/^\d+$/.test(timestamp))
358
+ return false;
359
+ if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
360
+ return false;
361
+ }
362
+ const key = await resolveSecret(secret, c);
363
+ for (const [k, v] of parts) {
364
+ const signature = k === "v1" && v ? fromHex(v) : undefined;
365
+ if (signature && await hmacVerify(key, `${timestamp}.${rawBody}`, signature)) {
366
+ return true;
367
+ }
368
+ }
369
+ return false;
370
+ },
371
+ async sign(rawBody, secret, { timestamp } = {}) {
372
+ const t = timestamp ?? Math.floor(Date.now() / 1000);
373
+ const signature = await hmacSign(secret, `${t}.${rawBody}`);
374
+ return { "stripe-signature": `t=${t},v1=${toHex(signature)}` };
375
+ }
376
+ };
377
+ function svixKey(secret) {
378
+ const key = fromBase64(secret.replace(/^whsec_/, ""));
379
+ if (!key?.length) {
380
+ throw new Error("hono-webhooks: the Svix secret must be whsec_<base64>");
381
+ }
382
+ return key;
383
+ }
384
+ var svix = {
385
+ verify: (secret, { toleranceSeconds = 300 } = {}) => async ({ c, rawBody, headers }) => {
386
+ const prefix = headers["svix-id"] ? "svix" : "webhook";
387
+ const id = headers[`${prefix}-id`];
388
+ const timestamp = headers[`${prefix}-timestamp`] ?? "";
389
+ const signatures = headers[`${prefix}-signature`];
390
+ if (!id || !signatures || !/^\d+$/.test(timestamp))
391
+ return false;
392
+ if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
393
+ return false;
394
+ }
395
+ const key = svixKey(await resolveSecret(secret, c));
396
+ for (const entry of signatures.split(" ")) {
397
+ const [version, encoded] = entry.split(",");
398
+ const signature = version === "v1" && encoded ? fromBase64(encoded) : undefined;
399
+ if (signature && await hmacVerify(key, `${id}.${timestamp}.${rawBody}`, signature)) {
400
+ return true;
401
+ }
402
+ }
403
+ return false;
404
+ },
405
+ async sign(rawBody, secret, { id, timestamp } = {}) {
406
+ const msgId = id ?? `msg_${crypto.randomUUID().replaceAll("-", "")}`;
407
+ const t = timestamp ?? Math.floor(Date.now() / 1000);
408
+ const signature = await hmacSign(svixKey(secret), `${msgId}.${t}.${rawBody}`);
409
+ return {
410
+ "svix-id": msgId,
411
+ "svix-timestamp": String(t),
412
+ "svix-signature": `v1,${toBase64(signature)}`
413
+ };
414
+ }
415
+ };
416
+
417
+ // src/cli.ts
418
+ var schemes = {
419
+ github,
420
+ shopify,
421
+ stripe,
422
+ svix
423
+ };
7
424
  var HELP = `Usage: hono-webhooks <module> <command> [options]
8
425
 
9
- <module> exports the result of createWebhooks(), as default or named export.
426
+ <module> exports a createWebhooks() app or an array of webhooks, as default
427
+ or named export.
10
428
 
11
429
  Commands:
12
430
  list List the webhooks
13
431
  schema <path> Print the JSON Schemas of the webhooks at <path>
14
432
  test <method> <path> Send a request through the webhooks app
15
433
 
434
+ Options:
435
+ -r, --preload <module> Import a module first (e.g. to load secrets), repeatable
436
+
16
437
  Options for test:
17
438
  -b, --body <text> Request body (JSON sets content-type: application/json)
18
439
  -q, --query <params> Query string, e.g. "a=1&b=2"
19
440
  -H, --header <k: v> Request header, repeatable
441
+ -s, --sign <scheme> Sign the body: ${Object.keys(schemes).join(", ")}
442
+ -e, --secret-env <name> Environment variable holding the signing secret
20
443
 
21
444
  Examples:
22
445
  hono-webhooks ./src/webhooks.ts list
23
446
  hono-webhooks ./src/webhooks.ts schema /orders/:id
24
447
  hono-webhooks ./src/webhooks.ts test POST /orders/42 -b '{"total":10}'
448
+ hono-webhooks ./src/webhooks.ts test POST /shopify -b '{"id":1}' \\
449
+ -r ./src/secrets.ts --sign shopify --secret-env SHOPIFY_WEBHOOK_SECRET
25
450
  `;
26
451
  var { values, positionals } = parseArgs({
27
452
  allowPositionals: true,
28
453
  options: {
29
454
  help: { type: "boolean", short: "h" },
455
+ preload: { type: "string", short: "r", multiple: true },
30
456
  body: { type: "string", short: "b" },
31
457
  query: { type: "string", short: "q" },
32
- header: { type: "string", short: "H", multiple: true }
458
+ header: { type: "string", short: "H", multiple: true },
459
+ sign: { type: "string", short: "s" },
460
+ "secret-env": { type: "string", short: "e" }
33
461
  }
34
462
  });
35
463
  var [modulePath, command, ...args] = positionals;
@@ -37,31 +465,69 @@ function fail(message) {
37
465
  console.error(message);
38
466
  process.exit(1);
39
467
  }
468
+ var importModule = (specifier) => existsSync(resolve(specifier)) ? import(pathToFileURL(resolve(specifier)).href) : import(specifier);
40
469
  function isWebhooksApp(value) {
41
470
  return typeof value === "object" && value !== null && Array.isArray(value.webhooks) && typeof value.request === "function";
42
471
  }
472
+ function isWebhookArray(value) {
473
+ return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item?.config?.path === "string" && typeof item?.handler === "function");
474
+ }
43
475
  async function loadApp(path) {
44
- const mod = await import(pathToFileURL(resolve(path)).href);
45
- const app = [mod.default, ...Object.values(mod)].find(isWebhooksApp);
46
- return app ?? fail(`${path} does not export a createWebhooks() app`);
476
+ const mod = await importModule(path);
477
+ const candidates = [mod.default, ...Object.values(mod)];
478
+ const app = candidates.find(isWebhooksApp);
479
+ if (app)
480
+ return app;
481
+ const webhooks = candidates.find(isWebhookArray);
482
+ if (webhooks)
483
+ return createWebhooks(webhooks);
484
+ return fail(`${path} exports neither a createWebhooks() app nor an array of webhooks`);
47
485
  }
48
- function jsonSchemaOf(schema) {
49
- const props = schema["~standard"];
50
- try {
51
- return props?.jsonSchema?.input({ target: "draft-2020-12" });
52
- } catch {
53
- return;
486
+ var BUN_ONLY_ERRORS = new Set([
487
+ "ERR_MODULE_NOT_FOUND",
488
+ "ERR_UNKNOWN_FILE_EXTENSION",
489
+ "ERR_UNSUPPORTED_DIR_IMPORT",
490
+ "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX",
491
+ "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING"
492
+ ]);
493
+ function rerunUnderBun(error) {
494
+ const code = error.code;
495
+ if (process.versions.bun || !code || !BUN_ONLY_ERRORS.has(code))
496
+ throw error;
497
+ console.error(`Node can't load the module (${code}), trying Bun`);
498
+ const run = spawnSync("bun", process.argv.slice(1), { stdio: "inherit" });
499
+ if (run.error) {
500
+ fail("Bun is not installed either. Run the CLI with a runtime that can load your code, e.g. bunx --bun hono-webhooks ...");
54
501
  }
502
+ process.exit(run.status ?? 1);
503
+ }
504
+ async function signatureHeaders(rawBody) {
505
+ if (!values.sign)
506
+ return {};
507
+ const scheme = schemes[values.sign] ?? fail(`Unknown scheme "${values.sign}", use one of: ${Object.keys(schemes).join(", ")}`);
508
+ const name = values["secret-env"] ?? fail("--sign needs --secret-env <name>");
509
+ const secret = process.env[name] ?? fail(`${name} is not set`);
510
+ return scheme.sign(rawBody, secret);
55
511
  }
56
512
  async function main() {
57
513
  if (values.help || !modulePath || !command) {
58
514
  console.log(HELP);
59
515
  return;
60
516
  }
61
- const app = await loadApp(modulePath);
517
+ let app;
518
+ try {
519
+ for (const preload of values.preload ?? [])
520
+ await importModule(preload);
521
+ app = await loadApp(modulePath);
522
+ } catch (error) {
523
+ rerunUnderBun(error);
524
+ }
62
525
  if (command === "list") {
63
526
  for (const { config } of app.webhooks) {
64
- const validates = ["params", "query", "headers", "body"].filter((key) => config[key]).join(", ");
527
+ const validates = [
528
+ ...["params", "query", "headers", "body"].filter((key) => config[key]),
529
+ ...config.verify ? ["signature"] : []
530
+ ].join(", ");
65
531
  console.log(`${config.method.padEnd(7)} ${config.path}${config.description ? ` ${config.description}` : ""}`);
66
532
  if (validates)
67
533
  console.log(` validates ${validates}`);
@@ -70,22 +536,24 @@ async function main() {
70
536
  }
71
537
  if (command === "schema") {
72
538
  const [path] = args;
73
- const matches = app.webhooks.filter((w) => w.config.path === path);
74
- if (!matches.length)
539
+ const descriptions = describeWebhooks(app);
540
+ const indexes = app.webhooks.flatMap((w, i) => w.config.path === path ? [i] : []);
541
+ if (!indexes.length)
75
542
  fail(`No webhook at ${path}`);
76
- for (const { config } of matches) {
543
+ for (const i of indexes) {
544
+ const { config } = app.webhooks[i];
77
545
  console.log(`${config.method} ${config.path}`);
78
- for (const key of [
546
+ for (const part of [
79
547
  "params",
80
548
  "query",
81
549
  "headers",
82
550
  "body",
83
551
  "response"
84
552
  ]) {
85
- if (!config[key])
553
+ if (!config[part])
86
554
  continue;
87
- const json = jsonSchemaOf(config[key]);
88
- console.log(` ${key}: ${json ? JSON.stringify(json, null, 2).replaceAll(`
555
+ const schema = descriptions[i].jsonSchema[part];
556
+ console.log(` ${part}: ${schema ? JSON.stringify(schema, null, 2).replaceAll(`
89
557
  `, `
90
558
  `) : "(no JSON Schema support)"}`);
91
559
  }
@@ -111,6 +579,9 @@ async function main() {
111
579
  headers.set("content-type", "text/plain");
112
580
  }
113
581
  }
582
+ for (const [name, value] of Object.entries(await signatureHeaders(values.body ?? ""))) {
583
+ headers.set(name, value);
584
+ }
114
585
  const url = `${path.startsWith("/") ? path : `/${path}`}${values.query ? `?${values.query}` : ""}`;
115
586
  const start = performance.now();
116
587
  const res = await app.request(url, {