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: `slack_webhook:${channelId}`, // [!code highlight]
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 sendSlackResponse(request: RequestWithResponse, message: string) {
130
+ async function sendAck(request: RequestWithResponse, message: string) {
171
131
  "use step";
172
132
  await request.respondWith(
173
- new Response(
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 sendStopResponse(request: RequestWithResponse) {
137
+ async function processEvent(data: any) {
184
138
  "use step";
185
- await request.respondWith(
186
- new Response("Stopping workflow...")
187
- );
139
+ console.log("Processing event:", data);
188
140
  }
189
141
 
190
- export async function slackCommandWorkflow(channelId: string) {
142
+ export async function eventCollectorWorkflow() {
191
143
  "use workflow";
192
144
 
193
- using webhook = createWebhook({
194
- token: `slack_command:${channelId}`,
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 formData = await request.formData();
199
- const command = formData.get("command");
200
- const text = formData.get("text");
149
+ const data = await request.json();
201
150
 
202
- if (command === "/status") {
203
- // Respond immediately to Slack
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
- async function postToSlack(channelId: string, message: string) {
224
- "use step";
225
- // Post message to Slack
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
- ## Examples
59
+ ## Usage Note
60
60
 
61
- ### Basic API Route
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
- Forward incoming HTTP requests to a webhook by token:
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 respondToSlack(request: RequestWithResponse, text: string) {
342
+ async function sendAck(request: RequestWithResponse, message: string) {
343
343
  "use step";
344
344
 
345
345
  await request.respondWith(
346
- new Response(
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
- export async function slackCommandWorkflow(channelId: string) {
354
- "use workflow";
350
+ async function processEvent(data: any) {
351
+ "use step";
352
+ console.log("Processing event:", data);
353
+ }
355
354
 
356
- using webhook = createWebhook({
357
- token: `slack_command:${channelId}`,
358
- respondWith: "manual"
359
- });
355
+ export async function eventCollectorWorkflow() {
356
+ "use workflow";
360
357
 
361
- console.log("Configure Slack command webhook:", webhook.url);
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 formData = await request.formData();
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 (text === "stop") {
375
- await respondToSlack(request, "Stopping workflow...");
364
+ if (data.type === "done") {
365
+ await sendAck(request, "Workflow complete");
376
366
  break;
377
367
  }
378
- }
379
- }
380
368
 
381
- async function checkSystemStatus() {
382
- "use step";
383
- return "All systems operational";
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
- When using custom tokens:
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.1.0-beta.63",
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.37",
56
- "@workflow/cli": "4.1.0-beta.63",
57
- "@workflow/core": "4.1.0-beta.63",
58
- "@workflow/errors": "4.1.0-beta.17",
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.59",
61
- "@workflow/nest": "0.0.0-beta.12",
62
- "@workflow/nitro": "4.0.1-beta.58",
63
- "@workflow/nuxt": "4.0.1-beta.47",
64
- "@workflow/sveltekit": "4.0.0-beta.52",
65
- "@workflow/rollup": "4.0.0-beta.20"
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",