workflow 4.1.0-beta.63 → 4.2.0-beta.64
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.
|
@@ -100,7 +100,7 @@ export async function slackBotWorkflow(channelId: string) {
|
|
|
100
100
|
|
|
101
101
|
// Token constructed from channel ID
|
|
102
102
|
using hook = createHook<SlackMessage>({ // [!code highlight]
|
|
103
|
-
token: `
|
|
103
|
+
token: `slack_messages:${channelId}`, // [!code highlight]
|
|
104
104
|
}); // [!code highlight]
|
|
105
105
|
|
|
106
106
|
for await (const message of hook) {
|
|
@@ -120,46 +120,6 @@ async function processData(data: any) {
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
### Customizing Tokens
|
|
124
|
-
|
|
125
|
-
Tokens are used to identify a specific webhook. You can customize the token to be more specific to a use case.
|
|
126
|
-
|
|
127
|
-
```typescript lineNumbers
|
|
128
|
-
import { createWebhook, type RequestWithResponse } from "workflow"
|
|
129
|
-
|
|
130
|
-
async function sendAck(request: RequestWithResponse) {
|
|
131
|
-
"use step";
|
|
132
|
-
await request.respondWith(
|
|
133
|
-
new Response(JSON.stringify({ received: true }), {
|
|
134
|
-
headers: { "Content-Type": "application/json" }
|
|
135
|
-
})
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export async function githubWebhookWorkflow(repoName: string) {
|
|
140
|
-
"use workflow";
|
|
141
|
-
|
|
142
|
-
// Use a deterministic token based on the repository
|
|
143
|
-
using webhook = createWebhook({ // [!code highlight]
|
|
144
|
-
token: `github_webhook:${repoName}`, // [!code highlight]
|
|
145
|
-
}); // [!code highlight]
|
|
146
|
-
|
|
147
|
-
console.log("Configure GitHub webhook:", webhook.url);
|
|
148
|
-
|
|
149
|
-
const request = await webhook;
|
|
150
|
-
const event = await request.json();
|
|
151
|
-
|
|
152
|
-
await sendAck(request);
|
|
153
|
-
|
|
154
|
-
await deployCommit(event);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function deployCommit(event: any) {
|
|
158
|
-
"use step";
|
|
159
|
-
// Deploy logic here
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
123
|
### Waiting for Multiple Requests
|
|
164
124
|
|
|
165
125
|
You can also wait for multiple requests by using the `for await...of` syntax.
|
|
@@ -167,62 +127,35 @@ You can also wait for multiple requests by using the `for await...of` syntax.
|
|
|
167
127
|
```typescript lineNumbers
|
|
168
128
|
import { createWebhook, type RequestWithResponse } from "workflow"
|
|
169
129
|
|
|
170
|
-
async function
|
|
130
|
+
async function sendAck(request: RequestWithResponse, message: string) {
|
|
171
131
|
"use step";
|
|
172
132
|
await request.respondWith(
|
|
173
|
-
|
|
174
|
-
JSON.stringify({
|
|
175
|
-
response_type: "in_channel",
|
|
176
|
-
text: message
|
|
177
|
-
}),
|
|
178
|
-
{ headers: { "Content-Type": "application/json" } }
|
|
179
|
-
)
|
|
133
|
+
Response.json({ received: true, message })
|
|
180
134
|
);
|
|
181
135
|
}
|
|
182
136
|
|
|
183
|
-
async function
|
|
137
|
+
async function processEvent(data: any) {
|
|
184
138
|
"use step";
|
|
185
|
-
|
|
186
|
-
new Response("Stopping workflow...")
|
|
187
|
-
);
|
|
139
|
+
console.log("Processing event:", data);
|
|
188
140
|
}
|
|
189
141
|
|
|
190
|
-
export async function
|
|
142
|
+
export async function eventCollectorWorkflow() {
|
|
191
143
|
"use workflow";
|
|
192
144
|
|
|
193
|
-
using webhook = createWebhook({
|
|
194
|
-
|
|
195
|
-
});
|
|
145
|
+
using webhook = createWebhook({ respondWith: "manual" });
|
|
146
|
+
console.log("Send events to:", webhook.url);
|
|
196
147
|
|
|
197
148
|
for await (const request of webhook) { // [!code highlight]
|
|
198
|
-
const
|
|
199
|
-
const command = formData.get("command");
|
|
200
|
-
const text = formData.get("text");
|
|
149
|
+
const data = await request.json();
|
|
201
150
|
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
await sendSlackResponse(request, "Checking status...");
|
|
205
|
-
|
|
206
|
-
// Process the command
|
|
207
|
-
const status = await checkSystemStatus();
|
|
208
|
-
await postToSlack(channelId, `Status: ${status}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (text === "stop") {
|
|
212
|
-
await sendStopResponse(request);
|
|
151
|
+
if (data.type === "done") {
|
|
152
|
+
await sendAck(request, "Workflow complete");
|
|
213
153
|
break;
|
|
214
154
|
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function checkSystemStatus() {
|
|
219
|
-
"use step";
|
|
220
|
-
return "All systems operational";
|
|
221
|
-
}
|
|
222
155
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
156
|
+
await sendAck(request, "Event received");
|
|
157
|
+
await processEvent(data);
|
|
158
|
+
}
|
|
226
159
|
}
|
|
227
160
|
```
|
|
228
161
|
|
|
@@ -56,11 +56,17 @@ Returns a `Promise<Response>` that resolves to:
|
|
|
56
56
|
|
|
57
57
|
Throws an error if the webhook token is not found or invalid.
|
|
58
58
|
|
|
59
|
-
##
|
|
59
|
+
## Usage Note
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
<Callout type="warn">
|
|
62
|
+
In most cases, you should not need to call `resumeWebhook()` directly. When you use `createWebhook()`, the framework automatically generates a random webhook token and provides a public URL at `/.well-known/workflow/v1/webhook/:token`. External systems can send HTTP requests directly to that URL.
|
|
63
|
+
|
|
64
|
+
For server-side hook resumption with deterministic tokens, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) with [`createHook()`](/docs/api-reference/workflow/create-hook) instead.
|
|
65
|
+
</Callout>
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
## Example
|
|
68
|
+
|
|
69
|
+
Forward an incoming HTTP request to a webhook by token:
|
|
64
70
|
|
|
65
71
|
```typescript lineNumbers
|
|
66
72
|
import { resumeWebhook } from "workflow/api";
|
|
@@ -82,159 +88,6 @@ export async function POST(request: Request) {
|
|
|
82
88
|
}
|
|
83
89
|
```
|
|
84
90
|
|
|
85
|
-
### GitHub Webhook Handler
|
|
86
|
-
|
|
87
|
-
Handle GitHub webhook events and forward them to workflows:
|
|
88
|
-
|
|
89
|
-
```typescript lineNumbers
|
|
90
|
-
import { resumeWebhook } from "workflow/api";
|
|
91
|
-
import { verifyGitHubSignature } from "@/lib/github";
|
|
92
|
-
|
|
93
|
-
export async function POST(request: Request) {
|
|
94
|
-
// Extract repository name from URL
|
|
95
|
-
const url = new URL(request.url);
|
|
96
|
-
const repo = url.pathname.split("/").pop();
|
|
97
|
-
|
|
98
|
-
// Verify GitHub signature
|
|
99
|
-
const signature = request.headers.get("x-hub-signature-256");
|
|
100
|
-
const isValid = await verifyGitHubSignature(request, signature);
|
|
101
|
-
|
|
102
|
-
if (!isValid) {
|
|
103
|
-
return new Response("Invalid signature", { status: 401 });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Construct deterministic token
|
|
107
|
-
const token = `github_webhook:${repo}`;
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const response = await resumeWebhook(token, request); // [!code highlight]
|
|
111
|
-
return response;
|
|
112
|
-
} catch (error) {
|
|
113
|
-
return new Response("Workflow not found", { status: 404 });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Slack Slash Command Handler
|
|
119
|
-
|
|
120
|
-
Process Slack slash commands and route them to workflow webhooks:
|
|
121
|
-
|
|
122
|
-
```typescript lineNumbers
|
|
123
|
-
import { resumeWebhook } from "workflow/api";
|
|
124
|
-
|
|
125
|
-
export async function POST(request: Request) {
|
|
126
|
-
const formData = await request.formData();
|
|
127
|
-
const channelId = formData.get("channel_id") as string;
|
|
128
|
-
const command = formData.get("command") as string;
|
|
129
|
-
|
|
130
|
-
// Verify Slack request signature
|
|
131
|
-
const slackSignature = request.headers.get("x-slack-signature");
|
|
132
|
-
if (!slackSignature) {
|
|
133
|
-
return new Response("Unauthorized", { status: 401 });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Construct token from channel ID
|
|
137
|
-
const token = `slack_command:${channelId}`;
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const response = await resumeWebhook(token, request); // [!code highlight]
|
|
141
|
-
return response;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
// If no workflow is listening, return a default response
|
|
144
|
-
return new Response(
|
|
145
|
-
JSON.stringify({
|
|
146
|
-
response_type: "ephemeral",
|
|
147
|
-
text: "No active workflow for this channel"
|
|
148
|
-
}),
|
|
149
|
-
{
|
|
150
|
-
headers: { "Content-Type": "application/json" }
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Multi-Tenant Webhook Router
|
|
158
|
-
|
|
159
|
-
Route webhooks to different workflows based on tenant/organization:
|
|
160
|
-
|
|
161
|
-
```typescript lineNumbers
|
|
162
|
-
import { resumeWebhook } from "workflow/api";
|
|
163
|
-
|
|
164
|
-
export async function POST(request: Request) {
|
|
165
|
-
const url = new URL(request.url);
|
|
166
|
-
|
|
167
|
-
// Extract tenant and webhook ID from path
|
|
168
|
-
// e.g., /api/webhooks/tenant-123/webhook-abc
|
|
169
|
-
const [, , , tenantId, webhookId] = url.pathname.split("/");
|
|
170
|
-
|
|
171
|
-
if (!tenantId || !webhookId) {
|
|
172
|
-
return new Response("Invalid webhook URL", { status: 400 });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Verify API key for tenant
|
|
176
|
-
const apiKey = request.headers.get("authorization");
|
|
177
|
-
const isAuthorized = await verifyTenantApiKey(tenantId, apiKey);
|
|
178
|
-
|
|
179
|
-
if (!isAuthorized) {
|
|
180
|
-
return new Response("Unauthorized", { status: 401 });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Construct namespaced token
|
|
184
|
-
const token = `tenant:${tenantId}:webhook:${webhookId}`;
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const response = await resumeWebhook(token, request); // [!code highlight]
|
|
188
|
-
return response;
|
|
189
|
-
} catch (error) {
|
|
190
|
-
return new Response("Webhook not found or expired", { status: 404 });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function verifyTenantApiKey(tenantId: string, apiKey: string | null) {
|
|
195
|
-
// Verify API key logic
|
|
196
|
-
return apiKey === process.env[`TENANT_${tenantId}_API_KEY`];
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Server Action (Next.js)
|
|
201
|
-
|
|
202
|
-
Use `resumeWebhook` in a Next.js server action:
|
|
203
|
-
|
|
204
|
-
```typescript lineNumbers
|
|
205
|
-
"use server";
|
|
206
|
-
|
|
207
|
-
import { resumeWebhook } from "workflow/api";
|
|
208
|
-
|
|
209
|
-
export async function triggerWebhook(
|
|
210
|
-
token: string,
|
|
211
|
-
payload: Record<string, any>
|
|
212
|
-
) {
|
|
213
|
-
// Create a Request object from the payload
|
|
214
|
-
const request = new Request("http://localhost/webhook", {
|
|
215
|
-
method: "POST",
|
|
216
|
-
headers: {
|
|
217
|
-
"Content-Type": "application/json",
|
|
218
|
-
},
|
|
219
|
-
body: JSON.stringify(payload),
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const response = await resumeWebhook(token, request);
|
|
224
|
-
|
|
225
|
-
// Parse and return the response
|
|
226
|
-
const contentType = response.headers.get("content-type");
|
|
227
|
-
if (contentType?.includes("application/json")) {
|
|
228
|
-
return await response.json();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return await response.text();
|
|
232
|
-
} catch (error) {
|
|
233
|
-
throw new Error("Webhook not found");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
91
|
## Related Functions
|
|
239
92
|
|
|
240
93
|
- [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Create a webhook in a workflow
|
|
@@ -339,53 +339,36 @@ Like hooks, webhooks support iteration:
|
|
|
339
339
|
```typescript lineNumbers
|
|
340
340
|
import { createWebhook, type RequestWithResponse } from "workflow";
|
|
341
341
|
|
|
342
|
-
async function
|
|
342
|
+
async function sendAck(request: RequestWithResponse, message: string) {
|
|
343
343
|
"use step";
|
|
344
344
|
|
|
345
345
|
await request.respondWith(
|
|
346
|
-
|
|
347
|
-
JSON.stringify({ response_type: "in_channel", text }),
|
|
348
|
-
{ headers: { "Content-Type": "application/json" } }
|
|
349
|
-
)
|
|
346
|
+
Response.json({ received: true, message })
|
|
350
347
|
);
|
|
351
348
|
}
|
|
352
349
|
|
|
353
|
-
|
|
354
|
-
"use
|
|
350
|
+
async function processEvent(data: any) {
|
|
351
|
+
"use step";
|
|
352
|
+
console.log("Processing event:", data);
|
|
353
|
+
}
|
|
355
354
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
respondWith: "manual"
|
|
359
|
-
});
|
|
355
|
+
export async function eventCollectorWorkflow() {
|
|
356
|
+
"use workflow";
|
|
360
357
|
|
|
361
|
-
|
|
358
|
+
using webhook = createWebhook({ respondWith: "manual" });
|
|
359
|
+
console.log("Send events to:", webhook.url);
|
|
362
360
|
|
|
363
361
|
for await (const request of webhook) {
|
|
364
|
-
const
|
|
365
|
-
const command = formData.get("command");
|
|
366
|
-
const text = formData.get("text");
|
|
367
|
-
|
|
368
|
-
if (command === "/status") {
|
|
369
|
-
await respondToSlack(request, "Checking status...");
|
|
370
|
-
const status = await checkSystemStatus();
|
|
371
|
-
await postToSlack(channelId, `Status: ${status}`);
|
|
372
|
-
}
|
|
362
|
+
const data = await request.json();
|
|
373
363
|
|
|
374
|
-
if (
|
|
375
|
-
await
|
|
364
|
+
if (data.type === "done") {
|
|
365
|
+
await sendAck(request, "Workflow complete");
|
|
376
366
|
break;
|
|
377
367
|
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
368
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async function postToSlack(channelId: string, message: string) {
|
|
387
|
-
"use step";
|
|
388
|
-
// Post message to Slack
|
|
369
|
+
await sendAck(request, "Event received");
|
|
370
|
+
await processEvent(data);
|
|
371
|
+
}
|
|
389
372
|
}
|
|
390
373
|
```
|
|
391
374
|
|
|
@@ -464,7 +447,9 @@ This pattern is especially valuable in larger applications where the workflow an
|
|
|
464
447
|
|
|
465
448
|
### Token Design
|
|
466
449
|
|
|
467
|
-
|
|
450
|
+
Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always use randomly generated tokens to prevent unauthorized access to public webhook endpoints.
|
|
451
|
+
|
|
452
|
+
When using custom tokens with `createHook()`:
|
|
468
453
|
|
|
469
454
|
- **Make them deterministic**: Base them on data the external system can reconstruct (like channel IDs, user IDs, etc.)
|
|
470
455
|
- **Use namespacing**: Prefix tokens to avoid conflicts (e.g., `slack:${channelId}`, `github:${repoId}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0-beta.64",
|
|
4
4
|
"description": "Workflow DevKit - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"ms": "2.1.3",
|
|
55
|
-
"@workflow/astro": "4.0.0-beta.
|
|
56
|
-
"@workflow/cli": "4.
|
|
57
|
-
"@workflow/core": "4.
|
|
58
|
-
"@workflow/errors": "4.1.0-beta.
|
|
55
|
+
"@workflow/astro": "4.0.0-beta.38",
|
|
56
|
+
"@workflow/cli": "4.2.0-beta.64",
|
|
57
|
+
"@workflow/core": "4.2.0-beta.64",
|
|
58
|
+
"@workflow/errors": "4.1.0-beta.18",
|
|
59
59
|
"@workflow/typescript-plugin": "4.0.1-beta.5",
|
|
60
|
-
"@workflow/next": "4.0.1-beta.
|
|
61
|
-
"@workflow/nest": "0.0.0-beta.
|
|
62
|
-
"@workflow/nitro": "4.0.1-beta.
|
|
63
|
-
"@workflow/nuxt": "4.0.1-beta.
|
|
64
|
-
"@workflow/sveltekit": "4.0.0-beta.
|
|
65
|
-
"@workflow/rollup": "4.0.0-beta.
|
|
60
|
+
"@workflow/next": "4.0.1-beta.60",
|
|
61
|
+
"@workflow/nest": "0.0.0-beta.13",
|
|
62
|
+
"@workflow/nitro": "4.0.1-beta.59",
|
|
63
|
+
"@workflow/nuxt": "4.0.1-beta.48",
|
|
64
|
+
"@workflow/sveltekit": "4.0.0-beta.53",
|
|
65
|
+
"@workflow/rollup": "4.0.0-beta.21"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/ms": "2.1.0",
|