@t4h.framework/webhook 0.2.8 → 0.2.11
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/.ai/skills/framework-webhook/SKILL.md +71 -106
- package/CHANGELOG.md +10 -0
- package/README.md +17 -33
- package/package.json +3 -3
|
@@ -1,134 +1,99 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: framework-webhook
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
Suspends a T4H Framework workflow until a third party calls back over HTTP,
|
|
5
|
+
with webhook() and WebhookActivity from @t4h.framework/webhook. Use when a
|
|
6
|
+
workflow must send a request to a partner and wait for its callback (payment
|
|
7
|
+
provider, document signing, long partner job), or when the user mentions the
|
|
8
|
+
<<callbackUrl>> placeholder, callback auth (hmac, basic, header, bearer, jwt,
|
|
9
|
+
jwks), a callback body schema, timeoutInMs, callback.dispatch,
|
|
10
|
+
{ wait: false }, WebhookDispatchError or WebhookTimeoutError.
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# @t4h.framework/webhook
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
`framework/packages/webhook`. Peer dependency: `@t4h.framework/core`.
|
|
15
|
+
## Wait for a partner callback
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
1. Declare the callback credential with `type: Secret` in the `EnvDesign` in
|
|
18
|
+
`src/constants/environments.ts`. A token for the outbound
|
|
19
|
+
`request.headers` is `type: String`, because `headers` takes strings.
|
|
20
|
+
2. Call `webhook()` in the workflow body. Put `<<callbackUrl>>` where the
|
|
21
|
+
partner expects the URL to call back.
|
|
22
|
+
3. Read `callback.body` (typed by `schema`) and `callback.dispatch` (the
|
|
23
|
+
partner's answer to the request).
|
|
18
24
|
|
|
19
25
|
```typescript
|
|
20
|
-
import { webhook
|
|
21
|
-
import
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Workflow usage
|
|
26
|
+
import { webhook } from '@t4h.framework/webhook'
|
|
27
|
+
import Type from 'typebox'
|
|
25
28
|
|
|
26
|
-
```typescript
|
|
27
29
|
import { environments } from '../constants/environments.js'
|
|
28
30
|
|
|
29
31
|
const callback = await webhook({
|
|
30
32
|
request: {
|
|
31
33
|
url: 'https://partner.example.com/jobs',
|
|
34
|
+
headers: { authorization: `Bearer ${environments.PARTNER_TOKEN}` },
|
|
32
35
|
body: { json: { notifyUrl: '<<callbackUrl>>' } },
|
|
33
36
|
},
|
|
34
|
-
auth: {
|
|
37
|
+
auth: {
|
|
38
|
+
type: 'hmac',
|
|
39
|
+
secret: environments.PARTNER_WEBHOOK_SECRET,
|
|
40
|
+
header: 'x-hub-signature-256',
|
|
41
|
+
signaturePrefix: 'sha256=',
|
|
42
|
+
},
|
|
35
43
|
schema: Type.Object({ status: Type.String() }),
|
|
36
|
-
timeoutInMs:
|
|
44
|
+
timeoutInMs: 7 * 24 * 60 * 60 * 1000,
|
|
37
45
|
})
|
|
38
46
|
```
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- `
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
(`status
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- Google Pub/Sub push style: `{ type: 'jwt', jwksUri, issuer, audience }`
|
|
72
|
-
|
|
73
|
-
## How it fits the core
|
|
74
|
-
|
|
75
|
-
Every pending async activity can be advanced from outside through
|
|
76
|
-
`POST .../activities/:activityId/callbacks?token=…`. `WebhookActivity` only
|
|
77
|
-
adds the outbound request: `bootstrap` reads the URL from the core
|
|
78
|
-
`GetActivityCallbackUrlClaim`, sends `request` with `HttpClientRequestClaim`,
|
|
79
|
-
and returns `pending({ payload: dispatch, schema, auth, timeoutAt })`.
|
|
80
|
-
`advance` receives the parsed body as payload, the bootstrap pending (for
|
|
81
|
-
`dispatch`) and `{ method, headers }` as the third argument
|
|
82
|
-
(`AsyncActivityAdvanceContext`).
|
|
83
|
-
|
|
84
|
-
## Host contract
|
|
85
|
-
|
|
86
|
-
The callbacks route belongs to the core and serves every async activity; the
|
|
87
|
-
helpers below live in the `AsyncActivityCallback` namespace of `@t4h.framework/core`, not
|
|
88
|
-
in this package.
|
|
89
|
-
|
|
90
|
-
1. `GetActivityCallbackUrlClaim.getActivityCallbackUrl()`: absolute `http(s)`
|
|
91
|
-
URL of the current activity's callbacks route with
|
|
92
|
-
`?token=AsyncActivityCallback.deriveToken(secret, activityId)`. Nothing stored.
|
|
93
|
-
2. Callback route: `AsyncActivityCallback.isTokenValid` **first** (404 if the token was
|
|
94
|
-
never minted) → activity/pending lookup (503 + `Retry-After` if the pending
|
|
95
|
-
is not visible yet: the partner called back before the tick committed) →
|
|
96
|
-
`AsyncActivityCallback.authorize(pending.auth, { method, headers, rawBody }, authorizers)`
|
|
97
|
-
(401) → advance with `AsyncActivityCallback.parseBody(contentType, rawBody)` and
|
|
98
|
-
`{ method, headers: AsyncActivityCallback.flattenHeaders(headers) }`. 410 when no longer pending,
|
|
99
|
-
202 otherwise (repeats included).
|
|
100
|
-
3. `authorizers` is `{ hmac, jwt }` with the default exports of
|
|
101
|
-
`@t4h.framework/hmac/callback` and `@t4h.framework/jwks/callback`; `basic`,
|
|
102
|
-
`header` and `bearer` are built into the core. A declared type without an
|
|
103
|
-
authorizer throws `AsyncActivityCallbackAuthorizerMissingException`.
|
|
104
|
-
4. Verification needs the **raw body**; parse JSON only after it.
|
|
105
|
-
|
|
106
|
-
The dev server implements this in `packages/dev` (`DevGetActivityCallbackUrlClaim`,
|
|
107
|
-
`CallbackTokenService`, `CallbackAuthorizersService`,
|
|
108
|
-
`CreateWorkflowRunAsyncActivityPendingCallback`, route
|
|
109
|
-
`POST /projects/:project/workflows/:workflow/runs/:run/activities/:activity/callbacks`).
|
|
110
|
-
|
|
111
|
-
## Testing
|
|
48
|
+
- Omit `request` when the partner already knows the URL (inbound only).
|
|
49
|
+
- Pass `{ wait: false }` as the second argument to get a handle, do other
|
|
50
|
+
work, then `await handle.wait()`.
|
|
51
|
+
- The fields of `auth` by type: `hmac` in **framework-hmac**, `jwt` and `jwks`
|
|
52
|
+
in **framework-jwks**. `basic`, `header` and `bearer` are built into the
|
|
53
|
+
core.
|
|
54
|
+
|
|
55
|
+
## Traps
|
|
56
|
+
|
|
57
|
+
- `auth` credentials (`secret`, `password`, `value`, `token`) are `Secret`s:
|
|
58
|
+
pass the `environments` entry whole. Do not read the value.
|
|
59
|
+
- A value in `request.headers` is a plain string and goes into the history.
|
|
60
|
+
- `schema` is the schema of the callback BODY, not of the dispatch answer. The
|
|
61
|
+
host refuses a body that does not match; your code never sees it.
|
|
62
|
+
- `timeoutInMs` is milliseconds. Without it, the activity sets no timeout.
|
|
63
|
+
- A dispatch with a network error or status 400 or more makes `webhook()`
|
|
64
|
+
throw `WebhookDispatchError` (`status`, `message`). No callback before the
|
|
65
|
+
timeout throws `WebhookTimeoutError`. Catch them in the workflow if the
|
|
66
|
+
workflow must continue.
|
|
67
|
+
- The request body is `{ json }` or `{ text }`, not a raw object.
|
|
68
|
+
`method` defaults to `POST`.
|
|
69
|
+
- `callback.headers` keys are lower case.
|
|
70
|
+
- The host must provide `GetActivityCallbackUrlClaim` (core) and
|
|
71
|
+
`HttpClientRequestClaim` (`@t4h.framework/http`). An invalid callback URL
|
|
72
|
+
throws `WebhookCallbackUrlError`.
|
|
73
|
+
- Partners cannot reach `localhost`. In development, use a local stub that
|
|
74
|
+
calls the `<<callbackUrl>>` it receives.
|
|
75
|
+
|
|
76
|
+
## Test
|
|
77
|
+
|
|
78
|
+
With `TestWorkflowEnvironment`, mock the activity like any async activity:
|
|
112
79
|
|
|
113
80
|
```typescript
|
|
114
|
-
import {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
81
|
+
import { fulfilled, mockAsync } from '@t4h.framework/core/testing'
|
|
82
|
+
import { WebhookActivity } from '@t4h.framework/webhook'
|
|
83
|
+
|
|
84
|
+
await env.execute(workflow, input, {
|
|
85
|
+
mockAsyncActivity: mockAsync().on(
|
|
86
|
+
WebhookActivity,
|
|
87
|
+
fulfilled({ method: 'POST', headers: {}, body: { status: 'signed' } }),
|
|
88
|
+
),
|
|
89
|
+
})
|
|
120
90
|
```
|
|
121
91
|
|
|
122
|
-
|
|
92
|
+
See the **framework-workflow-testing** skill for the harness.
|
|
123
93
|
|
|
124
|
-
|
|
125
|
-
Claim.run(
|
|
126
|
-
[
|
|
127
|
-
{ provide: HttpClientRequestClaim, value: fakeHttpClaim },
|
|
128
|
-
{ provide: GetActivityCallbackUrlClaim, value: fakeCallbackUrlClaim },
|
|
129
|
-
],
|
|
130
|
-
() => activity.bootstrap(input),
|
|
131
|
-
)
|
|
132
|
-
```
|
|
94
|
+
## Reference
|
|
133
95
|
|
|
134
|
-
|
|
96
|
+
- Guide: <https://tech4humans-brasil.github.io/framework/webhook/>
|
|
97
|
+
- Types: `node_modules/@t4h.framework/webhook/dist/webhook.d.ts`
|
|
98
|
+
- Callback `auth` types: `AsyncActivityCallbackAuth` in
|
|
99
|
+
`node_modules/@t4h.framework/core/dist/`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @t4h.framework/webhook
|
|
2
2
|
|
|
3
|
+
## 0.2.11
|
|
4
|
+
|
|
5
|
+
## 0.2.10
|
|
6
|
+
|
|
7
|
+
## 0.2.9
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#257](https://github.com/tech4humans-brasil/framework/pull/257) [`586bfb4`](https://github.com/tech4humans-brasil/framework/commit/586bfb4663f4a478527ee0c8db360603f034d615) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The webhook README is now short and links to the site. The site page now describes the outbound request body and the claims that the host must provide, and gives a 410 answer for a callback after the activity was advanced.
|
|
12
|
+
|
|
3
13
|
## 0.2.8
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
# @t4h.framework/webhook
|
|
2
2
|
|
|
3
|
-
Suspend a workflow until a third party calls back over HTTP.
|
|
3
|
+
Suspend a T4H Framework workflow until a third party calls back over HTTP. The
|
|
4
|
+
host verifies the callback before the workflow sees it.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
Declare the callback secret with `type: Secret` in the `EnvDesign` in `src/constants/environments.ts`:
|
|
6
|
+
## Install
|
|
8
7
|
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
import { EnvDesign, Secret } from '@t4h.framework/core'
|
|
12
|
-
|
|
13
|
-
export const environments = EnvDesign({
|
|
14
|
-
PARTNER_SECRET: { type: Secret },
|
|
15
|
-
})
|
|
8
|
+
```bash
|
|
9
|
+
npm install @t4h.framework/webhook @t4h.framework/core @t4h.framework/http
|
|
16
10
|
```
|
|
17
11
|
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
18
14
|
```typescript
|
|
19
15
|
import { webhook } from '@t4h.framework/webhook'
|
|
20
16
|
import Type from 'typebox'
|
|
@@ -26,33 +22,21 @@ const callback = await webhook({
|
|
|
26
22
|
url: 'https://partner.example.com/jobs',
|
|
27
23
|
body: { json: { callback: '<<callbackUrl>>' } },
|
|
28
24
|
},
|
|
29
|
-
auth: {
|
|
25
|
+
auth: {
|
|
26
|
+
type: 'hmac',
|
|
27
|
+
secret: environments.PARTNER_SECRET,
|
|
28
|
+
header: 'x-signature',
|
|
29
|
+
},
|
|
30
30
|
schema: Type.Object({ status: Type.String() }),
|
|
31
31
|
timeoutInMs: 24 * 60 * 60 * 1000,
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
callback.
|
|
35
|
-
callback.body.status // typed by the schema
|
|
34
|
+
callback.body.status
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
`
|
|
40
|
-
when the partner posts to it. The callback must carry the token in the URL and
|
|
41
|
-
pass the `auth` you declared; the body must match `schema`.
|
|
42
|
-
|
|
43
|
-
A failed request throws `WebhookDispatchError` and a callback that never
|
|
44
|
-
arrives before `timeoutInMs` throws `WebhookTimeoutError`, so the workflow
|
|
45
|
-
never sees a callback that did not happen.
|
|
46
|
-
|
|
47
|
-
Supported `auth`: `hmac` (GitHub, Shopify, Slack style), `basic`, `header`,
|
|
48
|
-
`bearer`, `jwt` (bearer JWT verified with a JWKS) and `jwks` (detached
|
|
49
|
-
signature verified with a JWKS key).
|
|
37
|
+
Declare `PARTNER_SECRET` with `type: Secret` in the `EnvDesign` in
|
|
38
|
+
`src/constants/environments.ts`.
|
|
50
39
|
|
|
51
|
-
|
|
52
|
-
suspending; you get a handle and call `handle.wait()` later.
|
|
40
|
+
## Documentation
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
route, both defined by `@t4h.framework/core` and shared by every async
|
|
56
|
-
activity; the outbound request goes through `HttpClientRequestClaim` from
|
|
57
|
-
`@t4h.framework/http`. Peer dependencies: `@t4h.framework/core` and
|
|
58
|
-
`@t4h.framework/http`.
|
|
42
|
+
<https://tech4humans-brasil.github.io/framework/webhook/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t4h.framework/webhook",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Webhook module for the T4H Framework",
|
|
5
5
|
"homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/webhook",
|
|
6
6
|
"bugs": "https://github.com/tech4humans-brasil/framework/issues",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"typebox": "^1.1.6"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@t4h.framework/core": "^0.
|
|
40
|
-
"@t4h.framework/http": "^0.8.
|
|
39
|
+
"@t4h.framework/core": "^0.21.2",
|
|
40
|
+
"@t4h.framework/http": "^0.8.3",
|
|
41
41
|
"typescript": "^5.9.3",
|
|
42
42
|
"vitest": "^4.0.18"
|
|
43
43
|
},
|