queuebear 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1313 @@
1
+ // src/api/base.ts
2
+ var BaseClient = class {
3
+ baseUrl;
4
+ apiKey;
5
+ projectId;
6
+ constructor(options) {
7
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
8
+ this.apiKey = options.apiKey;
9
+ this.projectId = options.projectId;
10
+ }
11
+ /**
12
+ * Build the full URL for a project-scoped endpoint
13
+ */
14
+ buildUrl(path, queryParams) {
15
+ const url = new URL(`${this.baseUrl}/v1/projects/${this.projectId}${path}`);
16
+ if (queryParams) {
17
+ for (const [key, value] of Object.entries(queryParams)) {
18
+ if (value !== void 0) {
19
+ url.searchParams.set(key, String(value));
20
+ }
21
+ }
22
+ }
23
+ return url.toString();
24
+ }
25
+ /**
26
+ * Make an authenticated request
27
+ */
28
+ async request(method, path, options) {
29
+ const url = this.buildUrl(path, options?.queryParams);
30
+ const headers = {
31
+ Authorization: `Bearer ${this.apiKey}`,
32
+ ...options?.headers
33
+ };
34
+ if (options?.body !== void 0) {
35
+ headers["Content-Type"] = "application/json";
36
+ }
37
+ const response = await fetch(url, {
38
+ method,
39
+ headers,
40
+ body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
41
+ });
42
+ if (!response.ok) {
43
+ const errorData = await response.json().catch(() => ({}));
44
+ throw new QueueBearError(
45
+ errorData.error || errorData.message || `Request failed with status ${response.status}`,
46
+ response.status
47
+ );
48
+ }
49
+ return response.json();
50
+ }
51
+ };
52
+ var QueueBearError = class extends Error {
53
+ constructor(message, statusCode) {
54
+ super(message);
55
+ this.statusCode = statusCode;
56
+ this.name = "QueueBearError";
57
+ }
58
+ };
59
+
60
+ // src/api/messages.ts
61
+ var MessagesAPI = class extends BaseClient {
62
+ constructor(options) {
63
+ super(options);
64
+ }
65
+ /**
66
+ * Publish a message to be delivered to a destination URL
67
+ *
68
+ * @param destination - The URL to deliver the message to
69
+ * @param body - The message body (will be JSON-stringified)
70
+ * @param options - Optional configuration for delay, retries, callbacks, etc.
71
+ * @returns The message ID and deduplication status
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const { messageId } = await qb.messages.publish(
76
+ * "https://api.example.com/webhook",
77
+ * { event: "user.created", userId: "123" },
78
+ * { delay: "30s", retries: 5 }
79
+ * );
80
+ * ```
81
+ */
82
+ async publish(destination, body, options) {
83
+ const headers = {
84
+ "Content-Type": "application/json"
85
+ };
86
+ if (options?.delay) {
87
+ headers["queuebear-delay"] = options.delay;
88
+ }
89
+ if (options?.retries !== void 0) {
90
+ headers["queuebear-retries"] = String(options.retries);
91
+ }
92
+ if (options?.method) {
93
+ headers["queuebear-method"] = options.method;
94
+ }
95
+ if (options?.callbackUrl) {
96
+ headers["queuebear-callback"] = options.callbackUrl;
97
+ }
98
+ if (options?.failureCallbackUrl) {
99
+ headers["queuebear-failure-callback"] = options.failureCallbackUrl;
100
+ }
101
+ if (options?.deduplicationId) {
102
+ headers["queuebear-deduplication-id"] = options.deduplicationId;
103
+ }
104
+ if (options?.headers) {
105
+ for (const [key, value] of Object.entries(options.headers)) {
106
+ headers[`queuebear-forward-${key}`] = value;
107
+ }
108
+ }
109
+ const encodedDestination = encodeURIComponent(destination);
110
+ return this.request("POST", `/publish/${encodedDestination}`, {
111
+ body,
112
+ headers
113
+ });
114
+ }
115
+ /**
116
+ * Get message details and delivery history
117
+ *
118
+ * @param messageId - The message ID (e.g., "msg_abc123...")
119
+ * @returns Full message details including delivery logs
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const message = await qb.messages.get("msg_abc123");
124
+ * console.log(message.status); // "completed" | "pending" | "failed"
125
+ * console.log(message.deliveryLogs); // Delivery attempt history
126
+ * ```
127
+ */
128
+ async get(messageId) {
129
+ return this.request("GET", `/messages/${messageId}`);
130
+ }
131
+ /**
132
+ * List messages with optional filtering
133
+ *
134
+ * @param options - Filter by status and pagination
135
+ * @returns Paginated list of messages
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * const { messages, pagination } = await qb.messages.list({
140
+ * status: "pending",
141
+ * limit: 20
142
+ * });
143
+ * ```
144
+ */
145
+ async list(options) {
146
+ return this.request("GET", "/messages", {
147
+ queryParams: {
148
+ status: options?.status,
149
+ limit: options?.limit,
150
+ offset: options?.offset
151
+ }
152
+ });
153
+ }
154
+ /**
155
+ * Cancel a pending or active message
156
+ *
157
+ * @param messageId - The message ID to cancel
158
+ * @returns Cancellation result
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const result = await qb.messages.cancel("msg_abc123");
163
+ * console.log(result.cancelled); // true
164
+ * ```
165
+ */
166
+ async cancel(messageId) {
167
+ return this.request("DELETE", `/messages/${messageId}`);
168
+ }
169
+ /**
170
+ * Publish a message and wait for delivery completion
171
+ * Polls the message status until it reaches a terminal state
172
+ *
173
+ * @param destination - The URL to deliver the message to
174
+ * @param body - The message body
175
+ * @param options - Publish options plus polling configuration
176
+ * @returns The final message status after delivery
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const message = await qb.messages.publishAndWait(
181
+ * "https://api.example.com/webhook",
182
+ * { event: "user.created" },
183
+ * { timeoutMs: 30000 }
184
+ * );
185
+ * console.log(message.status); // "completed"
186
+ * ```
187
+ */
188
+ async publishAndWait(destination, body, options) {
189
+ const { pollIntervalMs = 1e3, timeoutMs = 6e4, ...publishOptions } = options || {};
190
+ const { messageId } = await this.publish(destination, body, publishOptions);
191
+ const terminalStates = ["completed", "failed", "cancelled", "dlq"];
192
+ const startTime = Date.now();
193
+ while (Date.now() - startTime < timeoutMs) {
194
+ const message = await this.get(messageId);
195
+ if (terminalStates.includes(message.status)) {
196
+ return message;
197
+ }
198
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
199
+ }
200
+ throw new Error(`Message ${messageId} did not complete within ${timeoutMs}ms`);
201
+ }
202
+ };
203
+
204
+ // src/api/schedules.ts
205
+ var SchedulesAPI = class extends BaseClient {
206
+ constructor(options) {
207
+ super(options);
208
+ }
209
+ /**
210
+ * Create a cron-based recurring schedule
211
+ *
212
+ * @param options - Schedule configuration including destination, cron, and optional settings
213
+ * @returns The created schedule details
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const schedule = await qb.schedules.create({
218
+ * destination: "https://api.example.com/cron-job",
219
+ * cron: "0 9 * * *", // Daily at 9 AM
220
+ * timezone: "America/New_York",
221
+ * method: "POST",
222
+ * body: JSON.stringify({ type: "daily-report" }),
223
+ * retries: 3
224
+ * });
225
+ * console.log(schedule.scheduleId);
226
+ * ```
227
+ *
228
+ * @example Common cron expressions
229
+ * - `"* * * * *"` - Every minute
230
+ * - `"0 * * * *"` - Every hour
231
+ * - `"0 9 * * *"` - Daily at 9:00 AM
232
+ * - `"0 9 * * 1-5"` - Weekdays at 9:00 AM
233
+ * - `"0 0 1 * *"` - First day of each month
234
+ * - `"0 *​/6 * * *"` - Every 6 hours
235
+ */
236
+ async create(options) {
237
+ return this.request("POST", "/schedules", {
238
+ body: options
239
+ });
240
+ }
241
+ /**
242
+ * Get details of a specific schedule
243
+ *
244
+ * @param scheduleId - The schedule ID (e.g., "sched_abc123...")
245
+ * @returns Full schedule details
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const schedule = await qb.schedules.get("sched_abc123");
250
+ * console.log(schedule.nextExecutionAt);
251
+ * console.log(schedule.executionCount);
252
+ * ```
253
+ */
254
+ async get(scheduleId) {
255
+ return this.request("GET", `/schedules/${scheduleId}`);
256
+ }
257
+ /**
258
+ * List all schedules for the current project
259
+ *
260
+ * @param options - Pagination options
261
+ * @returns Paginated list of schedules
262
+ *
263
+ * @example
264
+ * ```typescript
265
+ * const { schedules, pagination } = await qb.schedules.list({ limit: 20 });
266
+ * for (const schedule of schedules) {
267
+ * console.log(`${schedule.scheduleId}: ${schedule.cron}`);
268
+ * }
269
+ * ```
270
+ */
271
+ async list(options) {
272
+ return this.request("GET", "/schedules", {
273
+ queryParams: {
274
+ limit: options?.limit,
275
+ offset: options?.offset
276
+ }
277
+ });
278
+ }
279
+ /**
280
+ * Delete (soft-delete) a schedule
281
+ *
282
+ * @param scheduleId - The schedule ID to delete
283
+ * @returns Deletion confirmation
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * const result = await qb.schedules.delete("sched_abc123");
288
+ * console.log(result.deleted); // true
289
+ * ```
290
+ */
291
+ async delete(scheduleId) {
292
+ return this.request("DELETE", `/schedules/${scheduleId}`);
293
+ }
294
+ /**
295
+ * Pause a schedule to temporarily stop executions
296
+ *
297
+ * @param scheduleId - The schedule ID to pause
298
+ * @returns Pause confirmation
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * await qb.schedules.pause("sched_abc123");
303
+ * // Schedule will not execute until resumed
304
+ * ```
305
+ */
306
+ async pause(scheduleId) {
307
+ return this.request("PATCH", `/schedules/${scheduleId}/pause`);
308
+ }
309
+ /**
310
+ * Resume a paused schedule
311
+ *
312
+ * @param scheduleId - The schedule ID to resume
313
+ * @returns Resume confirmation with next execution time
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * const result = await qb.schedules.resume("sched_abc123");
318
+ * console.log(result.nextExecutionAt); // Next scheduled run
319
+ * ```
320
+ */
321
+ async resume(scheduleId) {
322
+ return this.request("PATCH", `/schedules/${scheduleId}/resume`);
323
+ }
324
+ };
325
+
326
+ // src/api/dlq.ts
327
+ var DLQAPI = class extends BaseClient {
328
+ constructor(options) {
329
+ super(options);
330
+ }
331
+ /**
332
+ * List all DLQ entries for the current project
333
+ *
334
+ * @param options - Pagination options
335
+ * @returns Paginated list of DLQ entries
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * const { entries, pagination } = await qb.dlq.list({ limit: 20 });
340
+ * for (const entry of entries) {
341
+ * console.log(`${entry.id}: ${entry.failureReason}`);
342
+ * }
343
+ * ```
344
+ */
345
+ async list(options) {
346
+ return this.request("GET", "/dlq", {
347
+ queryParams: {
348
+ limit: options?.limit,
349
+ offset: options?.offset
350
+ }
351
+ });
352
+ }
353
+ /**
354
+ * Get details of a specific DLQ entry
355
+ *
356
+ * @param dlqId - The DLQ entry UUID
357
+ * @returns Full DLQ entry details including original message data
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * const entry = await qb.dlq.get("uuid-123...");
362
+ * console.log(entry.failureReason);
363
+ * console.log(entry.totalAttempts);
364
+ * console.log(entry.body); // Original message body
365
+ * ```
366
+ */
367
+ async get(dlqId) {
368
+ return this.request("GET", `/dlq/${dlqId}`);
369
+ }
370
+ /**
371
+ * Retry a DLQ entry by creating a new message
372
+ *
373
+ * Creates a new message from the failed DLQ entry for redelivery.
374
+ * The DLQ entry is marked as recovered and linked to the new message.
375
+ *
376
+ * @param dlqId - The DLQ entry UUID to retry
377
+ * @returns The new message ID and recovery confirmation
378
+ * @throws Error if the entry was already recovered
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * const result = await qb.dlq.retry("uuid-123...");
383
+ * console.log(result.newMessageId); // New message created
384
+ * console.log(result.recovered); // true
385
+ * ```
386
+ */
387
+ async retry(dlqId) {
388
+ return this.request("POST", `/dlq/${dlqId}/retry`);
389
+ }
390
+ /**
391
+ * Delete a DLQ entry permanently without retry
392
+ *
393
+ * @param dlqId - The DLQ entry UUID to delete
394
+ * @returns Deletion confirmation
395
+ *
396
+ * @example
397
+ * ```typescript
398
+ * await qb.dlq.delete("uuid-123...");
399
+ * ```
400
+ */
401
+ async delete(dlqId) {
402
+ return this.request("DELETE", `/dlq/${dlqId}`);
403
+ }
404
+ /**
405
+ * Purge all DLQ entries for the current project
406
+ *
407
+ * Permanently deletes all DLQ entries without retry.
408
+ * Use with caution as this action cannot be undone.
409
+ *
410
+ * @returns The number of entries purged
411
+ *
412
+ * @example
413
+ * ```typescript
414
+ * const result = await qb.dlq.purge();
415
+ * console.log(`Purged ${result.count} entries`);
416
+ * ```
417
+ */
418
+ async purge() {
419
+ return this.request("POST", "/dlq/purge");
420
+ }
421
+ /**
422
+ * Retry all DLQ entries for the current project
423
+ *
424
+ * Creates new messages from all failed DLQ entries.
425
+ *
426
+ * @returns Array of retry results
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const results = await qb.dlq.retryAll();
431
+ * console.log(`Retried ${results.length} entries`);
432
+ * ```
433
+ */
434
+ async retryAll() {
435
+ const results = [];
436
+ let hasMore = true;
437
+ let offset = 0;
438
+ const limit = 100;
439
+ while (hasMore) {
440
+ const { entries, pagination } = await this.list({ limit, offset });
441
+ for (const entry of entries) {
442
+ try {
443
+ const result = await this.retry(entry.id);
444
+ results.push(result);
445
+ } catch {
446
+ }
447
+ }
448
+ hasMore = pagination.hasMore;
449
+ offset += limit;
450
+ }
451
+ return results;
452
+ }
453
+ };
454
+
455
+ // src/api/workflows.ts
456
+ var WorkflowsAPI = class extends BaseClient {
457
+ constructor(options) {
458
+ super(options);
459
+ }
460
+ /**
461
+ * Trigger a new workflow run
462
+ *
463
+ * @param workflowId - Unique identifier for this workflow type
464
+ * @param workflowUrl - URL of the workflow endpoint
465
+ * @param input - Input data for the workflow
466
+ * @param options - Optional settings (idempotencyKey, maxDuration, metadata)
467
+ * @returns The run ID
468
+ *
469
+ * @example
470
+ * ```typescript
471
+ * const { runId } = await qb.workflows.trigger(
472
+ * "user-onboarding",
473
+ * "https://your-app.com/api/workflows/onboarding",
474
+ * { userId: "123", email: "user@example.com" },
475
+ * { idempotencyKey: "onboarding-user-123" }
476
+ * );
477
+ * ```
478
+ */
479
+ async trigger(workflowId, workflowUrl, input, options) {
480
+ return this.request("POST", `/workflows/${workflowId}/trigger`, {
481
+ body: {
482
+ workflowUrl,
483
+ input,
484
+ idempotencyKey: options?.idempotencyKey,
485
+ maxDuration: options?.maxDuration,
486
+ metadata: options?.metadata
487
+ }
488
+ });
489
+ }
490
+ /**
491
+ * Get workflow run status with all steps
492
+ *
493
+ * @param runId - The workflow run ID
494
+ * @returns Full run details including steps
495
+ *
496
+ * @example
497
+ * ```typescript
498
+ * const status = await qb.workflows.getStatus(runId);
499
+ * console.log(status.status); // "running" | "sleeping" | "completed"
500
+ * console.log(status.steps); // Array of step details
501
+ * ```
502
+ */
503
+ async getStatus(runId) {
504
+ return this.request("GET", `/workflows/runs/${runId}`);
505
+ }
506
+ /**
507
+ * Cancel a running workflow
508
+ *
509
+ * @param runId - The workflow run ID to cancel
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * await qb.workflows.cancel(runId);
514
+ * ```
515
+ */
516
+ async cancel(runId) {
517
+ await this.request("DELETE", `/workflows/runs/${runId}`);
518
+ }
519
+ /**
520
+ * Retry a failed or timed out workflow
521
+ * Resumes from the last completed step
522
+ *
523
+ * @param runId - The workflow run ID to retry
524
+ * @returns The new run ID if a new run was created
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * const result = await qb.workflows.retry(runId);
529
+ * console.log(result.runId);
530
+ * console.log(result.resumed); // true
531
+ * ```
532
+ */
533
+ async retry(runId) {
534
+ return this.request(
535
+ "POST",
536
+ `/workflows/runs/${runId}/retry`
537
+ );
538
+ }
539
+ /**
540
+ * List runs for a specific workflow
541
+ *
542
+ * @param workflowId - The workflow type identifier
543
+ * @param options - Filter by status and pagination
544
+ * @returns Paginated list of workflow runs
545
+ *
546
+ * @example
547
+ * ```typescript
548
+ * const { runs, pagination } = await qb.workflows.listRuns("user-onboarding", {
549
+ * status: "completed",
550
+ * limit: 20
551
+ * });
552
+ * ```
553
+ */
554
+ async listRuns(workflowId, options) {
555
+ return this.request("GET", `/workflows/${workflowId}/runs`, {
556
+ queryParams: {
557
+ status: options?.status,
558
+ limit: options?.limit,
559
+ offset: options?.offset
560
+ }
561
+ });
562
+ }
563
+ /**
564
+ * Send an event to workflows waiting with waitForEvent()
565
+ *
566
+ * @param eventName - Name of the event to send
567
+ * @param options - Optional event key and payload
568
+ * @returns The number of workflows that received the event
569
+ *
570
+ * @example
571
+ * ```typescript
572
+ * const result = await qb.workflows.sendEvent("order.approved", {
573
+ * eventKey: "order-123",
574
+ * payload: { status: "approved" }
575
+ * });
576
+ * console.log(`Notified ${result.matchedRuns} workflows`);
577
+ * ```
578
+ */
579
+ async sendEvent(eventName, options) {
580
+ return this.request(
581
+ "POST",
582
+ `/workflows/events/${encodeURIComponent(eventName)}`,
583
+ {
584
+ body: {
585
+ eventKey: options?.eventKey,
586
+ payload: options?.payload
587
+ }
588
+ }
589
+ );
590
+ }
591
+ /**
592
+ * Wait for a workflow to complete
593
+ * Polls the status until the workflow reaches a terminal state
594
+ *
595
+ * @param runId - The workflow run ID
596
+ * @param options - Polling options
597
+ * @returns The final workflow status
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * const result = await qb.workflows.waitForCompletion(runId, {
602
+ * pollIntervalMs: 2000,
603
+ * timeoutMs: 60000
604
+ * });
605
+ * console.log(result.status); // "completed" | "failed" | "cancelled"
606
+ * ```
607
+ */
608
+ async waitForCompletion(runId, options) {
609
+ const pollInterval = options?.pollIntervalMs || 1e3;
610
+ const timeout = options?.timeoutMs || 3e5;
611
+ const startTime = Date.now();
612
+ const terminalStates = [
613
+ "completed",
614
+ "failed",
615
+ "cancelled",
616
+ "timed_out"
617
+ ];
618
+ while (Date.now() - startTime < timeout) {
619
+ const status = await this.getStatus(runId);
620
+ if (terminalStates.includes(status.status)) {
621
+ return status;
622
+ }
623
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
624
+ }
625
+ throw new Error(`Workflow did not complete within ${timeout}ms`);
626
+ }
627
+ /**
628
+ * Trigger a workflow and wait for completion
629
+ *
630
+ * @param workflowId - Unique identifier for this workflow type
631
+ * @param workflowUrl - URL of the workflow endpoint
632
+ * @param input - Input data for the workflow
633
+ * @param options - Trigger and polling options
634
+ * @returns The final workflow status
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * const result = await qb.workflows.triggerAndWait(
639
+ * "user-onboarding",
640
+ * "https://your-app.com/api/workflows/onboarding",
641
+ * { userId: "123" },
642
+ * { timeoutMs: 120000 }
643
+ * );
644
+ * console.log(result.result); // Workflow output
645
+ * ```
646
+ */
647
+ async triggerAndWait(workflowId, workflowUrl, input, options) {
648
+ const { pollIntervalMs, timeoutMs, ...triggerOptions } = options || {};
649
+ const { runId } = await this.trigger(workflowId, workflowUrl, input, triggerOptions);
650
+ return this.waitForCompletion(runId, { pollIntervalMs, timeoutMs });
651
+ }
652
+ };
653
+
654
+ // src/queuebear.ts
655
+ var QueueBear = class {
656
+ options;
657
+ /**
658
+ * Messages API for publishing and managing webhook messages
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * // Publish a message
663
+ * const { messageId } = await qb.messages.publish(
664
+ * "https://api.example.com/webhook",
665
+ * { event: "user.created", userId: "123" },
666
+ * { delay: "30s", retries: 5 }
667
+ * );
668
+ *
669
+ * // Get message status
670
+ * const message = await qb.messages.get(messageId);
671
+ *
672
+ * // List messages
673
+ * const { messages } = await qb.messages.list({ status: "pending" });
674
+ *
675
+ * // Cancel a message
676
+ * await qb.messages.cancel(messageId);
677
+ * ```
678
+ */
679
+ messages;
680
+ /**
681
+ * Schedules API for managing cron-based recurring jobs
682
+ *
683
+ * @example
684
+ * ```typescript
685
+ * // Create a schedule
686
+ * const schedule = await qb.schedules.create({
687
+ * destination: "https://api.example.com/cron-job",
688
+ * cron: "0 9 * * *", // Daily at 9 AM
689
+ * timezone: "America/New_York",
690
+ * });
691
+ *
692
+ * // Pause a schedule
693
+ * await qb.schedules.pause(schedule.scheduleId);
694
+ *
695
+ * // Resume a schedule
696
+ * await qb.schedules.resume(schedule.scheduleId);
697
+ *
698
+ * // Delete a schedule
699
+ * await qb.schedules.delete(schedule.scheduleId);
700
+ * ```
701
+ */
702
+ schedules;
703
+ /**
704
+ * DLQ API for managing failed messages in the dead letter queue
705
+ *
706
+ * @example
707
+ * ```typescript
708
+ * // List failed messages
709
+ * const { entries } = await qb.dlq.list();
710
+ *
711
+ * // Retry a failed message
712
+ * const result = await qb.dlq.retry(entries[0].id);
713
+ *
714
+ * // Delete a DLQ entry
715
+ * await qb.dlq.delete(entries[0].id);
716
+ *
717
+ * // Purge all DLQ entries
718
+ * await qb.dlq.purge();
719
+ * ```
720
+ */
721
+ dlq;
722
+ /**
723
+ * Workflows API for triggering and managing durable workflows
724
+ *
725
+ * @example
726
+ * ```typescript
727
+ * // Trigger a workflow
728
+ * const { runId } = await qb.workflows.trigger(
729
+ * "user-onboarding",
730
+ * "https://your-app.com/api/workflows/onboarding",
731
+ * { userId: "123", email: "user@example.com" },
732
+ * { idempotencyKey: "onboarding-user-123" }
733
+ * );
734
+ *
735
+ * // Check status
736
+ * const status = await qb.workflows.getStatus(runId);
737
+ *
738
+ * // Wait for completion
739
+ * const result = await qb.workflows.waitForCompletion(runId);
740
+ *
741
+ * // Send event to waiting workflows
742
+ * await qb.workflows.sendEvent("order.approved", {
743
+ * eventKey: "order-123",
744
+ * payload: { status: "approved" }
745
+ * });
746
+ * ```
747
+ */
748
+ workflows;
749
+ constructor(options) {
750
+ this.options = {
751
+ baseUrl: options.baseUrl.replace(/\/$/, ""),
752
+ apiKey: options.apiKey,
753
+ projectId: options.projectId
754
+ };
755
+ this.messages = new MessagesAPI(this.options);
756
+ this.schedules = new SchedulesAPI(this.options);
757
+ this.dlq = new DLQAPI(this.options);
758
+ this.workflows = new WorkflowsAPI(this.options);
759
+ }
760
+ /**
761
+ * Publish a message and wait for delivery completion
762
+ *
763
+ * Convenience method that combines publish + polling in one call.
764
+ *
765
+ * @param destination - The URL to deliver the message to
766
+ * @param body - The message body
767
+ * @param options - Publish options plus polling configuration
768
+ * @returns The final message status after delivery
769
+ *
770
+ * @example
771
+ * ```typescript
772
+ * const message = await qb.publishAndWait(
773
+ * "https://api.example.com/webhook",
774
+ * { event: "user.created" },
775
+ * { timeoutMs: 30000 }
776
+ * );
777
+ * console.log(message.status); // "completed"
778
+ * ```
779
+ */
780
+ async publishAndWait(destination, body, options) {
781
+ return this.messages.publishAndWait(destination, body, options);
782
+ }
783
+ /**
784
+ * Trigger a workflow and wait for completion
785
+ *
786
+ * Convenience method that combines trigger + polling in one call.
787
+ *
788
+ * @param workflowId - Unique identifier for this workflow type
789
+ * @param workflowUrl - URL of the workflow endpoint
790
+ * @param input - Input data for the workflow
791
+ * @param options - Trigger and polling options
792
+ * @returns The final workflow status
793
+ *
794
+ * @example
795
+ * ```typescript
796
+ * const result = await qb.triggerAndWait(
797
+ * "user-onboarding",
798
+ * "https://your-app.com/api/workflows/onboarding",
799
+ * { userId: "123" },
800
+ * { timeoutMs: 120000 }
801
+ * );
802
+ * console.log(result.result); // Workflow output
803
+ * ```
804
+ */
805
+ async triggerAndWait(workflowId, workflowUrl, input, options) {
806
+ return this.workflows.triggerAndWait(workflowId, workflowUrl, input, options);
807
+ }
808
+ };
809
+
810
+ // src/context.ts
811
+ var WorkflowPausedError = class extends Error {
812
+ constructor(reason) {
813
+ super(reason);
814
+ this.reason = reason;
815
+ this.name = "WorkflowPausedError";
816
+ }
817
+ };
818
+ var WorkflowContext = class {
819
+ runId;
820
+ input;
821
+ runToken;
822
+ baseUrl;
823
+ authHeader;
824
+ stepIndex = 0;
825
+ constructor(options) {
826
+ this.runId = options.runId;
827
+ this.runToken = options.runToken;
828
+ this.input = options.input;
829
+ this.baseUrl = options.baseUrl;
830
+ this.authHeader = options.authHeader;
831
+ }
832
+ /**
833
+ * Execute a step with caching
834
+ * Step names must be unique within a workflow run
835
+ * @param stepName - Unique name for this step
836
+ * @param fn - Function to execute
837
+ * @param options - Optional retry configuration
838
+ */
839
+ async run(stepName, fn, options) {
840
+ const checkResponse = await fetch(
841
+ `${this.baseUrl}/v1/workflows/internal/step/check`,
842
+ {
843
+ method: "POST",
844
+ headers: {
845
+ "Content-Type": "application/json",
846
+ Authorization: this.authHeader,
847
+ "X-QueueBear-Run-Token": this.runToken
848
+ },
849
+ body: JSON.stringify({
850
+ runId: this.runId,
851
+ stepName
852
+ })
853
+ }
854
+ );
855
+ const checkData = await checkResponse.json();
856
+ if (checkData.cached) {
857
+ this.stepIndex++;
858
+ return checkData.result;
859
+ }
860
+ const startResponse = await fetch(
861
+ `${this.baseUrl}/v1/workflows/internal/step/start`,
862
+ {
863
+ method: "POST",
864
+ headers: {
865
+ "Content-Type": "application/json",
866
+ Authorization: this.authHeader,
867
+ "X-QueueBear-Run-Token": this.runToken
868
+ },
869
+ body: JSON.stringify({
870
+ runId: this.runId,
871
+ stepName,
872
+ stepIndex: this.stepIndex,
873
+ stepType: "run",
874
+ retryOptions: options
875
+ })
876
+ }
877
+ );
878
+ if (!startResponse.ok) {
879
+ const errorData = await startResponse.json();
880
+ throw new Error(errorData.error || "Failed to start step");
881
+ }
882
+ try {
883
+ const result = await fn();
884
+ await fetch(`${this.baseUrl}/v1/workflows/internal/step/complete`, {
885
+ method: "POST",
886
+ headers: {
887
+ "Content-Type": "application/json",
888
+ Authorization: this.authHeader,
889
+ "X-QueueBear-Run-Token": this.runToken
890
+ },
891
+ body: JSON.stringify({
892
+ runId: this.runId,
893
+ stepName,
894
+ result
895
+ })
896
+ });
897
+ this.stepIndex++;
898
+ return result;
899
+ } catch (error) {
900
+ await fetch(`${this.baseUrl}/v1/workflows/internal/step/fail`, {
901
+ method: "POST",
902
+ headers: {
903
+ "Content-Type": "application/json",
904
+ Authorization: this.authHeader,
905
+ "X-QueueBear-Run-Token": this.runToken
906
+ },
907
+ body: JSON.stringify({
908
+ runId: this.runId,
909
+ stepName,
910
+ error: error instanceof Error ? error.message : String(error)
911
+ })
912
+ });
913
+ throw error;
914
+ }
915
+ }
916
+ /**
917
+ * Sleep for specified seconds
918
+ * Step names must be unique within a workflow run
919
+ */
920
+ async sleep(stepName, seconds) {
921
+ const checkResponse = await fetch(
922
+ `${this.baseUrl}/v1/workflows/internal/step/check`,
923
+ {
924
+ method: "POST",
925
+ headers: {
926
+ "Content-Type": "application/json",
927
+ Authorization: this.authHeader,
928
+ "X-QueueBear-Run-Token": this.runToken
929
+ },
930
+ body: JSON.stringify({
931
+ runId: this.runId,
932
+ stepName
933
+ })
934
+ }
935
+ );
936
+ const checkData = await checkResponse.json();
937
+ if (checkData.cached) {
938
+ this.stepIndex++;
939
+ return;
940
+ }
941
+ const sleepResponse = await fetch(
942
+ `${this.baseUrl}/v1/workflows/internal/sleep`,
943
+ {
944
+ method: "POST",
945
+ headers: {
946
+ "Content-Type": "application/json",
947
+ Authorization: this.authHeader,
948
+ "X-QueueBear-Run-Token": this.runToken
949
+ },
950
+ body: JSON.stringify({
951
+ runId: this.runId,
952
+ stepName,
953
+ stepIndex: this.stepIndex,
954
+ seconds
955
+ })
956
+ }
957
+ );
958
+ if (!sleepResponse.ok) {
959
+ const errorData = await sleepResponse.json();
960
+ throw new Error(errorData.error || "Failed to schedule sleep");
961
+ }
962
+ throw new WorkflowPausedError(`Sleeping for ${seconds}s`);
963
+ }
964
+ /**
965
+ * Sleep until specific date
966
+ */
967
+ async sleepUntil(stepName, until) {
968
+ const seconds = Math.max(
969
+ 0,
970
+ Math.ceil((until.getTime() - Date.now()) / 1e3)
971
+ );
972
+ return this.sleep(stepName, seconds);
973
+ }
974
+ /**
975
+ * Get all completed steps for the current run
976
+ * Useful for inspecting progress or debugging
977
+ */
978
+ async getCompletedSteps() {
979
+ const response = await fetch(
980
+ `${this.baseUrl}/v1/workflows/internal/steps/${this.runId}`,
981
+ {
982
+ headers: {
983
+ Authorization: this.authHeader,
984
+ "X-QueueBear-Run-Token": this.runToken
985
+ }
986
+ }
987
+ );
988
+ if (!response.ok) {
989
+ throw new Error("Failed to fetch completed steps");
990
+ }
991
+ const data = await response.json();
992
+ return data.steps;
993
+ }
994
+ /**
995
+ * Make HTTP call (executed as a step)
996
+ */
997
+ async call(stepName, config) {
998
+ return this.run(stepName, async () => {
999
+ const response = await fetch(config.url, {
1000
+ method: config.method || "GET",
1001
+ headers: {
1002
+ "Content-Type": "application/json",
1003
+ ...config.headers
1004
+ },
1005
+ body: config.body ? JSON.stringify(config.body) : void 0
1006
+ });
1007
+ if (!response.ok) {
1008
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1009
+ }
1010
+ const contentType = response.headers.get("content-type");
1011
+ if (contentType?.includes("application/json")) {
1012
+ return response.json();
1013
+ }
1014
+ return response.text();
1015
+ });
1016
+ }
1017
+ /**
1018
+ * Wait for an external event
1019
+ * @param stepName - Unique step name
1020
+ * @param eventName - Name of the event to wait for
1021
+ * @param options - Optional event key and timeout
1022
+ */
1023
+ async waitForEvent(stepName, eventName, options) {
1024
+ const checkResponse = await fetch(
1025
+ `${this.baseUrl}/v1/workflows/internal/event/check`,
1026
+ {
1027
+ method: "POST",
1028
+ headers: {
1029
+ "Content-Type": "application/json",
1030
+ Authorization: this.authHeader,
1031
+ "X-QueueBear-Run-Token": this.runToken
1032
+ },
1033
+ body: JSON.stringify({
1034
+ runId: this.runId,
1035
+ stepName
1036
+ })
1037
+ }
1038
+ );
1039
+ const checkData = await checkResponse.json();
1040
+ if (checkData.received) {
1041
+ this.stepIndex++;
1042
+ return checkData.payload;
1043
+ }
1044
+ if (checkData.expired) {
1045
+ throw new Error(`Event "${eventName}" timed out`);
1046
+ }
1047
+ const waitResponse = await fetch(
1048
+ `${this.baseUrl}/v1/workflows/internal/event/wait`,
1049
+ {
1050
+ method: "POST",
1051
+ headers: {
1052
+ "Content-Type": "application/json",
1053
+ Authorization: this.authHeader,
1054
+ "X-QueueBear-Run-Token": this.runToken
1055
+ },
1056
+ body: JSON.stringify({
1057
+ runId: this.runId,
1058
+ stepName,
1059
+ stepIndex: this.stepIndex,
1060
+ eventName,
1061
+ eventKey: options?.eventKey,
1062
+ timeoutSeconds: options?.timeoutSeconds
1063
+ })
1064
+ }
1065
+ );
1066
+ if (!waitResponse.ok) {
1067
+ const errorData = await waitResponse.json();
1068
+ throw new Error(errorData.error || "Failed to register event wait");
1069
+ }
1070
+ throw new WorkflowPausedError(`Waiting for event: ${eventName}`);
1071
+ }
1072
+ /**
1073
+ * Send a fire-and-forget notification event
1074
+ * @param eventName - Name of the event to send
1075
+ * @param payload - Optional payload to include
1076
+ */
1077
+ async notify(eventName, payload) {
1078
+ const response = await fetch(
1079
+ `${this.baseUrl}/v1/workflows/events/${encodeURIComponent(eventName)}`,
1080
+ {
1081
+ method: "POST",
1082
+ headers: {
1083
+ "Content-Type": "application/json",
1084
+ Authorization: this.authHeader
1085
+ },
1086
+ body: JSON.stringify({ payload })
1087
+ }
1088
+ );
1089
+ if (!response.ok) {
1090
+ const errorData = await response.json();
1091
+ throw new Error(errorData.error || `Failed to send event: ${eventName}`);
1092
+ }
1093
+ }
1094
+ /**
1095
+ * Execute multiple steps in parallel
1096
+ * @param steps - Array of step definitions with name and function
1097
+ */
1098
+ async parallel(steps) {
1099
+ const stepNames = steps.map((s) => s.name);
1100
+ const batchResponse = await fetch(
1101
+ `${this.baseUrl}/v1/workflows/internal/steps/batch-check`,
1102
+ {
1103
+ method: "POST",
1104
+ headers: {
1105
+ "Content-Type": "application/json",
1106
+ Authorization: this.authHeader,
1107
+ "X-QueueBear-Run-Token": this.runToken
1108
+ },
1109
+ body: JSON.stringify({
1110
+ runId: this.runId,
1111
+ stepNames
1112
+ })
1113
+ }
1114
+ );
1115
+ if (!batchResponse.ok) {
1116
+ throw new Error("Failed to batch check steps");
1117
+ }
1118
+ const batchData = await batchResponse.json();
1119
+ const results = new Array(steps.length);
1120
+ const pendingPromises = [];
1121
+ const errors = [];
1122
+ for (let i = 0; i < steps.length; i++) {
1123
+ const step = steps[i];
1124
+ const cached = batchData.steps[step.name];
1125
+ if (cached?.cached) {
1126
+ results[i] = cached.result;
1127
+ } else {
1128
+ pendingPromises.push(
1129
+ this.run(step.name, step.fn).then((result) => {
1130
+ results[i] = result;
1131
+ }).catch((error) => {
1132
+ errors.push({
1133
+ index: i,
1134
+ stepName: step.name,
1135
+ error: error instanceof Error ? error : new Error(String(error))
1136
+ });
1137
+ })
1138
+ );
1139
+ }
1140
+ }
1141
+ await Promise.all(pendingPromises);
1142
+ if (errors.length > 0) {
1143
+ const errorMessages = errors.map((e) => `Step "${e.stepName}": ${e.error.message}`).join("; ");
1144
+ throw new ParallelExecutionError(
1145
+ `${errors.length} of ${steps.length} parallel steps failed: ${errorMessages}`,
1146
+ errors
1147
+ );
1148
+ }
1149
+ return results;
1150
+ }
1151
+ };
1152
+ var ParallelExecutionError = class extends Error {
1153
+ constructor(message, errors) {
1154
+ super(message);
1155
+ this.errors = errors;
1156
+ this.name = "ParallelExecutionError";
1157
+ }
1158
+ get failedStepCount() {
1159
+ return this.errors.length;
1160
+ }
1161
+ };
1162
+
1163
+ // src/serve.ts
1164
+ import { createHmac, timingSafeEqual } from "crypto";
1165
+ function serve(handler, options) {
1166
+ return async (request) => {
1167
+ let runId;
1168
+ let runToken;
1169
+ let baseUrl;
1170
+ try {
1171
+ if (options?.signingSecret) {
1172
+ const signature = request.headers.get("X-QueueBear-Signature");
1173
+ const bodyText = await request.clone().text();
1174
+ if (!verifySignature(bodyText, signature, options.signingSecret)) {
1175
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
1176
+ status: 401,
1177
+ headers: { "Content-Type": "application/json" }
1178
+ });
1179
+ }
1180
+ }
1181
+ const body = await request.json();
1182
+ runId = body.runId || request.headers.get("X-QueueBear-Workflow-Run") || "";
1183
+ runToken = request.headers.get("X-QueueBear-Run-Token") || "";
1184
+ if (!runId) {
1185
+ return new Response(JSON.stringify({ error: "Missing runId" }), {
1186
+ status: 400,
1187
+ headers: { "Content-Type": "application/json" }
1188
+ });
1189
+ }
1190
+ baseUrl = options?.baseUrl || getBaseUrl(request);
1191
+ const runResponse = await fetch(
1192
+ `${baseUrl}/v1/workflows/runs/${runId}`,
1193
+ {
1194
+ headers: {
1195
+ Authorization: request.headers.get("Authorization") || ""
1196
+ }
1197
+ }
1198
+ );
1199
+ if (!runResponse.ok) {
1200
+ return new Response(
1201
+ JSON.stringify({ error: "Failed to fetch workflow run" }),
1202
+ {
1203
+ status: 500,
1204
+ headers: { "Content-Type": "application/json" }
1205
+ }
1206
+ );
1207
+ }
1208
+ const runData = await runResponse.json();
1209
+ const context = new WorkflowContext({
1210
+ runId,
1211
+ runToken,
1212
+ input: runData.input,
1213
+ baseUrl,
1214
+ authHeader: request.headers.get("Authorization") || ""
1215
+ });
1216
+ const result = await handler(context);
1217
+ await fetch(`${baseUrl}/v1/workflows/internal/complete`, {
1218
+ method: "POST",
1219
+ headers: {
1220
+ "Content-Type": "application/json",
1221
+ Authorization: request.headers.get("Authorization") || "",
1222
+ "X-QueueBear-Run-Token": runToken
1223
+ },
1224
+ body: JSON.stringify({ runId, result })
1225
+ });
1226
+ return new Response(JSON.stringify({ completed: true, result }), {
1227
+ status: 200,
1228
+ headers: { "Content-Type": "application/json" }
1229
+ });
1230
+ } catch (error) {
1231
+ if (error instanceof WorkflowPausedError) {
1232
+ return new Response(
1233
+ JSON.stringify({
1234
+ completed: false,
1235
+ paused: true,
1236
+ reason: error.reason
1237
+ }),
1238
+ {
1239
+ status: 200,
1240
+ headers: { "Content-Type": "application/json" }
1241
+ }
1242
+ );
1243
+ }
1244
+ console.error("[Workflow] Unhandled error - failing run:", error);
1245
+ if (runId && runToken && baseUrl) {
1246
+ try {
1247
+ await fetch(`${baseUrl}/v1/workflows/internal/fail`, {
1248
+ method: "POST",
1249
+ headers: {
1250
+ "Content-Type": "application/json",
1251
+ Authorization: request.headers.get("Authorization") || "",
1252
+ "X-QueueBear-Run-Token": runToken
1253
+ },
1254
+ body: JSON.stringify({
1255
+ runId,
1256
+ error: error instanceof Error ? error.message : String(error)
1257
+ })
1258
+ });
1259
+ } catch (failError) {
1260
+ console.error("[Workflow] Failed to mark run as failed:", failError);
1261
+ }
1262
+ }
1263
+ return new Response(
1264
+ JSON.stringify({
1265
+ error: error instanceof Error ? error.message : "Unknown error",
1266
+ failed: true
1267
+ }),
1268
+ {
1269
+ status: 500,
1270
+ headers: { "Content-Type": "application/json" }
1271
+ }
1272
+ );
1273
+ }
1274
+ };
1275
+ }
1276
+ function verifySignature(body, signature, secret) {
1277
+ if (!signature) return false;
1278
+ const parts = signature.split(",").reduce(
1279
+ (acc, part) => {
1280
+ const [key, value] = part.split("=");
1281
+ acc[key] = value;
1282
+ return acc;
1283
+ },
1284
+ {}
1285
+ );
1286
+ if (!parts.t || !parts.v1) return false;
1287
+ const timestamp = parseInt(parts.t, 10);
1288
+ const now = Math.floor(Date.now() / 1e3);
1289
+ if (Math.abs(now - timestamp) > 300) return false;
1290
+ const payload = `${parts.t}.${body}`;
1291
+ const expected = createHmac("sha256", secret).update(payload).digest("hex");
1292
+ try {
1293
+ return timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
1294
+ } catch {
1295
+ return false;
1296
+ }
1297
+ }
1298
+ function getBaseUrl(request) {
1299
+ const url = new URL(request.url);
1300
+ return `${url.protocol}//${url.host}`;
1301
+ }
1302
+ export {
1303
+ DLQAPI,
1304
+ MessagesAPI,
1305
+ ParallelExecutionError,
1306
+ QueueBear,
1307
+ QueueBearError,
1308
+ SchedulesAPI,
1309
+ WorkflowContext,
1310
+ WorkflowPausedError,
1311
+ WorkflowsAPI,
1312
+ serve
1313
+ };