@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.
@@ -1,134 +1,99 @@
1
1
  ---
2
2
  name: framework-webhook
3
3
  description: >-
4
- Guides correct use of @t4h.framework/webhook in workflows: WebhookActivity,
5
- the <<callbackUrl>> placeholder, callback auth (hmac/basic/header/bearer/jwt),
6
- body schema, timeout, the partner's dispatch answer, and how hosts implement
7
- the core GetActivityCallbackUrlClaim and the callbacks route. Use when a
8
- workflow must wait for a third-party HTTP callback, when working in
9
- packages/webhook, or when wiring a host (runtime or dev) for external advances.
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
- Suspend a workflow until a third party calls back. Package path:
15
- `framework/packages/webhook`. Peer dependency: `@t4h.framework/core`.
15
+ ## Wait for a partner callback
16
16
 
17
- ## Import surface
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, WebhookActivity } from '@t4h.framework/webhook'
21
- import type { AsyncActivityCallbackAuth } from '@t4h.framework/core'
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: { type: 'hmac', secret: environments.WEBHOOK_SECRET, header: 'x-signature' },
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: 86_400_000,
44
+ timeoutInMs: 7 * 24 * 60 * 60 * 1000,
37
45
  })
38
46
  ```
39
47
 
40
- The credential fields of `auth` are typed `Secret` (core
41
- `AsyncActivityCallbackAuth.ts`). The host reads the value when it verifies a
42
- callback; the pending activity records the name, never the value.
43
-
44
- ```typescript
45
- callback.dispatch?.body // the partner's answer to the request
46
- callback.body.status // the callback body, typed by schema
47
- ```
48
-
49
- - `request` is optional (inbound only). Put `<<callbackUrl>>` anywhere in it.
50
- It is sent during `bootstrap` through `HttpClientRequestClaim`; a network
51
- error or status ≥ 400 rejects the activity at once.
52
- - `auth` is optional; the URL token alone is the credential without it.
53
- - `schema` is the schema of the callback **body**; the host validates it and
54
- it types `callback.body`.
55
- - Rejections surface as errors from `await webhook()`: `WebhookDispatchError`
56
- (`status?`, `message`) or `WebhookTimeoutError`. The stored output is
57
- `{ reason: 'dispatch_failed', status?, message }` or `{ reason: 'timeout' }`.
58
- - `webhook(options, { wait: false })` sends the request and returns a
59
- `WebhookHandle` instead of suspending; do other work, then
60
- `await handle.wait()`.
61
- - The pending payload is only the partner's answer to the request. A workflow
62
- that needs its own callback URL puts `<<callbackUrl>>` in the request body
63
- and reads it back from `callback.dispatch`.
64
-
65
- ## Auth cheat sheet
66
-
67
- - GitHub: `{ type: 'hmac', header: 'x-hub-signature-256', signaturePrefix: 'sha256=' }`
68
- - Shopify: `{ type: 'hmac', header: 'x-shopify-hmac-sha256', signatureFormat: 'base64' }`
69
- - Slack: `{ type: 'hmac', header: 'x-slack-signature', signaturePrefix: 'v0=', timestampHeader: 'x-slack-request-timestamp', toleranceInSeconds: 300, signedPayload: 'v0:{timestamp}:{body}' }`
70
- - Fixed credentials: `basic`, `header`, `bearer`
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 { mockAsync, fulfilled } from '@t4h.framework/core/testing'
115
-
116
- mockAsync().on(
117
- WebhookActivity,
118
- fulfilled({ method: 'POST', headers: {}, body }),
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
- Unit-test the activity with fake claims under `Claim.run`:
92
+ See the **framework-workflow-testing** skill for the harness.
123
93
 
124
- ```typescript
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
- See the **framework-workflow-testing** skill for the full harness API.
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
- ## Usage
6
-
7
- Declare the callback secret with `type: Secret` in the `EnvDesign` in `src/constants/environments.ts`:
6
+ ## Install
8
7
 
9
- ```typescript
10
- // src/constants/environments.ts
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: { type: 'hmac', secret: environments.PARTNER_SECRET, header: 'x-signature' },
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.dispatch?.body // what the partner answered to the request
35
- callback.body.status // typed by the schema
34
+ callback.body.status
36
35
  ```
37
36
 
38
- Every pending activity already has a callback URL; `webhook()` sends
39
- `request` with the placeholder replaced by that URL and resumes the workflow
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
- Pass `{ wait: false }` as the second argument to send the request without
52
- suspending; you get a handle and call `handle.wait()` later.
40
+ ## Documentation
53
41
 
54
- Hosts implement the core `GetActivityCallbackUrlClaim` and the callbacks
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.8",
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.20.0",
40
- "@t4h.framework/http": "^0.8.0",
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
  },