hono-webhooks 0.1.1 → 0.2.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 +315 -91
- package/dist/cli.js +426 -20
- package/dist/index.d.ts +171 -18
- package/dist/index.js +122 -6
- package/dist/signatures.d.ts +40 -0
- package/dist/signatures.js +91 -0
- package/package.json +9 -3
- package/src/cli.ts +90 -25
- package/src/index.ts +361 -24
- package/src/signatures.ts +171 -0
package/src/cli.ts
CHANGED
|
@@ -1,37 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
2
3
|
import { resolve } from 'node:path'
|
|
3
4
|
import { pathToFileURL } from 'node:url'
|
|
4
5
|
import { parseArgs } from 'node:util'
|
|
5
|
-
import
|
|
6
|
-
|
|
6
|
+
import {
|
|
7
|
+
createWebhooks,
|
|
8
|
+
describeWebhooks,
|
|
9
|
+
type Webhook,
|
|
10
|
+
type WebhooksApp
|
|
11
|
+
} from './index'
|
|
12
|
+
import { github, type SignatureScheme, shopify, stripe } from './signatures'
|
|
13
|
+
|
|
14
|
+
const schemes: Record<string, SignatureScheme<any, any>> = {
|
|
15
|
+
github,
|
|
16
|
+
shopify,
|
|
17
|
+
stripe
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
const HELP = `Usage: hono-webhooks <module> <command> [options]
|
|
9
21
|
|
|
10
|
-
<module> exports
|
|
22
|
+
<module> exports a createWebhooks() app or an array of webhooks, as default
|
|
23
|
+
or named export.
|
|
11
24
|
|
|
12
25
|
Commands:
|
|
13
26
|
list List the webhooks
|
|
14
27
|
schema <path> Print the JSON Schemas of the webhooks at <path>
|
|
15
28
|
test <method> <path> Send a request through the webhooks app
|
|
16
29
|
|
|
30
|
+
Options:
|
|
31
|
+
-r, --preload <module> Import a module first (e.g. to load secrets), repeatable
|
|
32
|
+
|
|
17
33
|
Options for test:
|
|
18
34
|
-b, --body <text> Request body (JSON sets content-type: application/json)
|
|
19
35
|
-q, --query <params> Query string, e.g. "a=1&b=2"
|
|
20
36
|
-H, --header <k: v> Request header, repeatable
|
|
37
|
+
-s, --sign <scheme> Sign the body: ${Object.keys(schemes).join(', ')}
|
|
38
|
+
-e, --secret-env <name> Environment variable holding the signing secret
|
|
21
39
|
|
|
22
40
|
Examples:
|
|
23
41
|
hono-webhooks ./src/webhooks.ts list
|
|
24
42
|
hono-webhooks ./src/webhooks.ts schema /orders/:id
|
|
25
43
|
hono-webhooks ./src/webhooks.ts test POST /orders/42 -b '{"total":10}'
|
|
44
|
+
hono-webhooks ./src/webhooks.ts test POST /shopify -b '{"id":1}' \\
|
|
45
|
+
-r ./src/secrets.ts --sign shopify --secret-env SHOPIFY_WEBHOOK_SECRET
|
|
26
46
|
`
|
|
27
47
|
|
|
28
48
|
const { values, positionals } = parseArgs({
|
|
29
49
|
allowPositionals: true,
|
|
30
50
|
options: {
|
|
31
51
|
help: { type: 'boolean', short: 'h' },
|
|
52
|
+
preload: { type: 'string', short: 'r', multiple: true },
|
|
32
53
|
body: { type: 'string', short: 'b' },
|
|
33
54
|
query: { type: 'string', short: 'q' },
|
|
34
|
-
header: { type: 'string', short: 'H', multiple: true }
|
|
55
|
+
header: { type: 'string', short: 'H', multiple: true },
|
|
56
|
+
sign: { type: 'string', short: 's' },
|
|
57
|
+
'secret-env': { type: 'string', short: 'e' }
|
|
35
58
|
}
|
|
36
59
|
})
|
|
37
60
|
|
|
@@ -42,6 +65,14 @@ function fail(message: string): never {
|
|
|
42
65
|
process.exit(1)
|
|
43
66
|
}
|
|
44
67
|
|
|
68
|
+
/** A file path relative to the cwd, or a package name. */
|
|
69
|
+
const importModule = (specifier: string) =>
|
|
70
|
+
import(
|
|
71
|
+
existsSync(resolve(specifier))
|
|
72
|
+
? pathToFileURL(resolve(specifier)).href
|
|
73
|
+
: specifier
|
|
74
|
+
)
|
|
75
|
+
|
|
45
76
|
function isWebhooksApp(value: unknown): value is WebhooksApp {
|
|
46
77
|
return (
|
|
47
78
|
typeof value === 'object' &&
|
|
@@ -51,19 +82,40 @@ function isWebhooksApp(value: unknown): value is WebhooksApp {
|
|
|
51
82
|
)
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
function isWebhookArray(value: unknown): value is Webhook[] {
|
|
86
|
+
return (
|
|
87
|
+
Array.isArray(value) &&
|
|
88
|
+
value.length > 0 &&
|
|
89
|
+
value.every(
|
|
90
|
+
(item) =>
|
|
91
|
+
typeof item?.config?.path === 'string' &&
|
|
92
|
+
typeof item?.handler === 'function'
|
|
93
|
+
)
|
|
94
|
+
)
|
|
58
95
|
}
|
|
59
96
|
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
async function loadApp(path: string): Promise<WebhooksApp> {
|
|
98
|
+
const mod = await importModule(path)
|
|
99
|
+
const candidates = [mod.default, ...Object.values(mod)]
|
|
100
|
+
const app = candidates.find(isWebhooksApp)
|
|
101
|
+
if (app) return app
|
|
102
|
+
const webhooks = candidates.find(isWebhookArray)
|
|
103
|
+
if (webhooks) return createWebhooks(webhooks)
|
|
104
|
+
return fail(
|
|
105
|
+
`${path} exports neither a createWebhooks() app nor an array of webhooks`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function signatureHeaders(rawBody: string) {
|
|
110
|
+
if (!values.sign) return {}
|
|
111
|
+
const scheme =
|
|
112
|
+
schemes[values.sign] ??
|
|
113
|
+
fail(
|
|
114
|
+
`Unknown scheme "${values.sign}", use one of: ${Object.keys(schemes).join(', ')}`
|
|
115
|
+
)
|
|
116
|
+
const name = values['secret-env'] ?? fail('--sign needs --secret-env <name>')
|
|
117
|
+
const secret = process.env[name] ?? fail(`${name} is not set`)
|
|
118
|
+
return scheme.sign(rawBody, secret)
|
|
67
119
|
}
|
|
68
120
|
|
|
69
121
|
async function main() {
|
|
@@ -71,13 +123,17 @@ async function main() {
|
|
|
71
123
|
console.log(HELP)
|
|
72
124
|
return
|
|
73
125
|
}
|
|
126
|
+
for (const preload of values.preload ?? []) await importModule(preload)
|
|
74
127
|
const app = await loadApp(modulePath)
|
|
75
128
|
|
|
76
129
|
if (command === 'list') {
|
|
77
130
|
for (const { config } of app.webhooks) {
|
|
78
|
-
const validates =
|
|
79
|
-
|
|
80
|
-
|
|
131
|
+
const validates = [
|
|
132
|
+
...(['params', 'query', 'headers', 'body'] as const).filter(
|
|
133
|
+
(key) => config[key]
|
|
134
|
+
),
|
|
135
|
+
...(config.verify ? ['signature'] : [])
|
|
136
|
+
].join(', ')
|
|
81
137
|
console.log(
|
|
82
138
|
`${config.method.padEnd(7)} ${config.path}${config.description ? ` ${config.description}` : ''}`
|
|
83
139
|
)
|
|
@@ -88,21 +144,25 @@ async function main() {
|
|
|
88
144
|
|
|
89
145
|
if (command === 'schema') {
|
|
90
146
|
const [path] = args
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
147
|
+
const descriptions = describeWebhooks(app)
|
|
148
|
+
const indexes = app.webhooks.flatMap((w, i) =>
|
|
149
|
+
w.config.path === path ? [i] : []
|
|
150
|
+
)
|
|
151
|
+
if (!indexes.length) fail(`No webhook at ${path}`)
|
|
152
|
+
for (const i of indexes) {
|
|
153
|
+
const { config } = app.webhooks[i]!
|
|
94
154
|
console.log(`${config.method} ${config.path}`)
|
|
95
|
-
for (const
|
|
155
|
+
for (const part of [
|
|
96
156
|
'params',
|
|
97
157
|
'query',
|
|
98
158
|
'headers',
|
|
99
159
|
'body',
|
|
100
160
|
'response'
|
|
101
161
|
] as const) {
|
|
102
|
-
if (!config[
|
|
103
|
-
const
|
|
162
|
+
if (!config[part]) continue
|
|
163
|
+
const schema = descriptions[i]!.jsonSchema[part]
|
|
104
164
|
console.log(
|
|
105
|
-
` ${
|
|
165
|
+
` ${part}: ${schema ? JSON.stringify(schema, null, 2).replaceAll('\n', '\n ') : '(no JSON Schema support)'}`
|
|
106
166
|
)
|
|
107
167
|
}
|
|
108
168
|
}
|
|
@@ -126,6 +186,11 @@ async function main() {
|
|
|
126
186
|
headers.set('content-type', 'text/plain')
|
|
127
187
|
}
|
|
128
188
|
}
|
|
189
|
+
for (const [name, value] of Object.entries(
|
|
190
|
+
await signatureHeaders(values.body ?? '')
|
|
191
|
+
)) {
|
|
192
|
+
headers.set(name, value)
|
|
193
|
+
}
|
|
129
194
|
|
|
130
195
|
const url = `${path.startsWith('/') ? path : `/${path}`}${values.query ? `?${values.query}` : ''}`
|
|
131
196
|
const start = performance.now()
|