queuebear 0.1.7 → 0.1.9

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/README.md CHANGED
@@ -493,33 +493,34 @@ The signing secret is available in your QueueBear project settings. When configu
493
493
 
494
494
  ## Local Development
495
495
 
496
- When developing locally, your webhook endpoints run on `localhost` which isn't accessible from QueueBear's servers. Use a tunnel service to expose your local server.
496
+ When developing locally, your webhook endpoints run on `localhost` which isn't accessible from QueueBear's servers. Use [Tunnelmole](https://tunnelmole.com) to expose your local server - it's free and requires no signup.
497
497
 
498
- ### Using ngrok (Recommended)
498
+ ### Installing Tunnelmole
499
499
 
500
- [ngrok](https://ngrok.com) provides stable URLs and a built-in request inspector.
501
-
502
- **1. Install and authenticate:**
500
+ **Linux, macOS, Windows WSL:**
503
501
 
504
502
  ```bash
505
- # Install
506
- brew install ngrok # or download from ngrok.com/download
503
+ curl -O https://install.tunnelmole.com/t357g/install && sudo bash install
504
+ ```
507
505
 
508
- # Authenticate (free account required)
509
- ngrok config add-authtoken YOUR_AUTH_TOKEN
506
+ **Node.js (all platforms, requires Node 16+):**
507
+
508
+ ```bash
509
+ npm install -g tunnelmole
510
510
  ```
511
511
 
512
- **2. Start the tunnel:**
512
+ ### Starting a Tunnel
513
513
 
514
514
  ```bash
515
- ngrok http 3000
515
+ tmole 3000
516
+ # Output: https://xxxx.tunnelmole.com is forwarding to localhost:3000
516
517
  ```
517
518
 
518
- **3. Use the forwarding URL:**
519
+ ### Using the Tunnel URL
519
520
 
520
521
  ```typescript
521
- // Use ngrok URL instead of localhost
522
- await qb.messages.publish("https://abc123.ngrok.io/api/webhooks", {
522
+ // Use tunnelmole URL instead of localhost
523
+ await qb.messages.publish("https://xxxx.tunnelmole.com/api/webhooks", {
523
524
  event: "user.created",
524
525
  userId: "123"
525
526
  });
@@ -527,37 +528,16 @@ await qb.messages.publish("https://abc123.ngrok.io/api/webhooks", {
527
528
  // Works for workflows too
528
529
  await qb.workflows.trigger(
529
530
  "onboarding",
530
- "https://abc123.ngrok.io/api/workflows/onboarding",
531
+ "https://xxxx.tunnelmole.com/api/workflows/onboarding",
531
532
  { userId: "123" }
532
533
  );
533
534
  ```
534
535
 
535
- **Debugging:** ngrok provides a web inspector at `http://localhost:4040` to view requests, responses, and replay failed deliveries.
536
-
537
- ### Using localtunnel
538
-
539
- [localtunnel](https://localtunnel.me) is free and requires no signup.
540
-
541
- ```bash
542
- npx localtunnel --port 3000
543
- # Output: your url is: https://good-months-leave.loca.lt
544
- ```
545
-
546
- **Note:** localtunnel shows a reminder page on first visit. Bypass it by adding a header:
547
-
548
- ```typescript
549
- await qb.messages.publish(
550
- "https://good-months-leave.loca.lt/api/webhooks",
551
- { event: "test" },
552
- { headers: { "bypass-tunnel-reminder": "true" } }
553
- );
554
- ```
555
-
556
536
  ### Tips
557
537
 
558
538
  - Store your tunnel URL in `.env` for easy switching between local and production
559
539
  - Both `callbackUrl` and `failureCallbackUrl` need public URLs for local testing
560
- - ngrok URLs change on restart (stable URLs require a paid plan)
540
+ - Tunnel URLs change on restart
561
541
 
562
542
  ---
563
543
 
package/dist/index.d.ts CHANGED
@@ -468,6 +468,26 @@ interface PurgeDLQResponse {
468
468
  purged: true;
469
469
  count: number;
470
470
  }
471
+ /**
472
+ * Options for serveMessage() function
473
+ */
474
+ interface ServeMessageOptions {
475
+ /** Signing secret for verifying message authenticity */
476
+ signingSecret?: string;
477
+ }
478
+ /**
479
+ * Context provided to message handlers by serveMessage()
480
+ */
481
+ interface MessageContext<T> {
482
+ /** The parsed request body */
483
+ body: T;
484
+ /** QueueBear message ID (from X-QueueBear-Message-Id header) */
485
+ messageId?: string;
486
+ /** Current attempt number (0-indexed) */
487
+ attemptNumber: number;
488
+ /** All request headers */
489
+ headers: Record<string, string>;
490
+ }
471
491
 
472
492
  /**
473
493
  * Messages API client for publishing and managing webhook messages
@@ -495,12 +515,12 @@ declare class MessagesAPI extends BaseClient {
495
515
  /**
496
516
  * Get message details and delivery history
497
517
  *
498
- * @param messageId - The message ID (e.g., "msg_abc123...")
518
+ * @param messageId - The message ID (e.g., "abc123...")
499
519
  * @returns Full message details including delivery logs
500
520
  *
501
521
  * @example
502
522
  * ```typescript
503
- * const message = await qb.messages.get("msg_abc123");
523
+ * const message = await qb.messages.get("abc123");
504
524
  * console.log(message.status); // "completed" | "pending" | "failed"
505
525
  * console.log(message.deliveryLogs); // Delivery attempt history
506
526
  * ```
@@ -1189,4 +1209,40 @@ type WorkflowHandler<TInput = unknown, TOutput = unknown> = (context: WorkflowCo
1189
1209
  */
1190
1210
  declare function serve<TInput = unknown, TOutput = unknown>(handler: WorkflowHandler<TInput, TOutput>, options?: ServeOptions): (request: Request) => Promise<Response>;
1191
1211
 
1192
- export { type CallConfig, type CancelMessageResponse, type CompletedStep, type CreateScheduleOptions, type CreateScheduleResponse, DLQAPI, type DLQEntry, type DLQEntrySummary, type DeleteDLQResponse, type DeleteScheduleResponse, type DeliveryLog, type LastResponse, type ListDLQOptions, type ListDLQResponse, type ListMessagesOptions, type ListMessagesResponse, type ListRunsOptions, type ListRunsResponse, type ListSchedulesOptions, type ListSchedulesResponse, type Message, type MessageMethod, type MessageStatus, type MessageSummary, MessagesAPI, type Pagination, ParallelExecutionError, type ParallelStepDefinition, type PauseScheduleResponse, type PublishOptions, type PublishResponse, type PurgeDLQResponse, QueueBear, QueueBearError, type QueueBearOptions, type ResumeScheduleResponse, type RetryDLQResponse, type Schedule, type ScheduleSummary, SchedulesAPI, type SendEventOptions, type SendEventResponse, type ServeOptions, type StatusResponse, type StepRetryOptions, type StepType, type TriggerOptions, type TriggerResponse, type WaitForEventOptions, type WorkflowContext, type WorkflowContextOptions, type WorkflowHandler, WorkflowPausedError, type WorkflowRun, type WorkflowRunStatus, type WorkflowStep, WorkflowsAPI, createWorkflowContext, serve };
1212
+ /**
1213
+ * Message handler function type for serveMessage()
1214
+ */
1215
+ type MessageHandler<TBody = unknown, TResult = unknown> = (context: MessageContext<TBody>) => Promise<TResult>;
1216
+ /**
1217
+ * Create a message webhook handler for simple message delivery.
1218
+ *
1219
+ * Unlike serve() which is designed for durable workflows with steps and state,
1220
+ * serveMessage() is for simple message webhooks published via messages.publish().
1221
+ *
1222
+ * @param handler - The handler function to process incoming messages
1223
+ * @param options - Optional configuration (e.g., signing secret for verification)
1224
+ * @returns A Request handler function for use with any HTTP framework
1225
+ *
1226
+ * @example
1227
+ * ```typescript
1228
+ * import { serveMessage } from 'queuebear';
1229
+ *
1230
+ * const handler = serveMessage<{ userId: string }>(
1231
+ * async (context) => {
1232
+ * console.log(`Processing message ${context.messageId} for user ${context.body.userId}`);
1233
+ * await processUser(context.body.userId);
1234
+ * return { success: true };
1235
+ * },
1236
+ * { signingSecret: process.env.QB_SIGNING_SECRET }
1237
+ * );
1238
+ *
1239
+ * // With Hono
1240
+ * app.post('/webhook', (c) => handler(c.req.raw));
1241
+ *
1242
+ * // With Express (using raw Request)
1243
+ * app.post('/webhook', (req, res) => handler(req).then(r => res.send(r)));
1244
+ * ```
1245
+ */
1246
+ declare function serveMessage<TBody = unknown, TResult = unknown>(handler: MessageHandler<TBody, TResult>, options?: ServeMessageOptions): (request: Request) => Promise<Response>;
1247
+
1248
+ export { type CallConfig, type CancelMessageResponse, type CompletedStep, type CreateScheduleOptions, type CreateScheduleResponse, DLQAPI, type DLQEntry, type DLQEntrySummary, type DeleteDLQResponse, type DeleteScheduleResponse, type DeliveryLog, type LastResponse, type ListDLQOptions, type ListDLQResponse, type ListMessagesOptions, type ListMessagesResponse, type ListRunsOptions, type ListRunsResponse, type ListSchedulesOptions, type ListSchedulesResponse, type Message, type MessageContext, type MessageHandler, type MessageMethod, type MessageStatus, type MessageSummary, MessagesAPI, type Pagination, ParallelExecutionError, type ParallelStepDefinition, type PauseScheduleResponse, type PublishOptions, type PublishResponse, type PurgeDLQResponse, QueueBear, QueueBearError, type QueueBearOptions, type ResumeScheduleResponse, type RetryDLQResponse, type Schedule, type ScheduleSummary, SchedulesAPI, type SendEventOptions, type SendEventResponse, type ServeMessageOptions, type ServeOptions, type StatusResponse, type StepRetryOptions, type StepType, type TriggerOptions, type TriggerResponse, type WaitForEventOptions, type WorkflowContext, type WorkflowContextOptions, type WorkflowHandler, WorkflowPausedError, type WorkflowRun, type WorkflowRunStatus, type WorkflowStep, WorkflowsAPI, createWorkflowContext, serve, serveMessage };
package/dist/index.js CHANGED
@@ -115,12 +115,12 @@ var MessagesAPI = class extends BaseClient {
115
115
  /**
116
116
  * Get message details and delivery history
117
117
  *
118
- * @param messageId - The message ID (e.g., "msg_abc123...")
118
+ * @param messageId - The message ID (e.g., "abc123...")
119
119
  * @returns Full message details including delivery logs
120
120
  *
121
121
  * @example
122
122
  * ```typescript
123
- * const message = await qb.messages.get("msg_abc123");
123
+ * const message = await qb.messages.get("abc123");
124
124
  * console.log(message.status); // "completed" | "pending" | "failed"
125
125
  * console.log(message.deliveryLogs); // Delivery attempt history
126
126
  * ```
@@ -164,7 +164,10 @@ var MessagesAPI = class extends BaseClient {
164
164
  * ```
165
165
  */
166
166
  async cancel(messageId) {
167
- return this.request("DELETE", `/messages/${messageId}`);
167
+ return this.request(
168
+ "DELETE",
169
+ `/messages/${messageId}`
170
+ );
168
171
  }
169
172
  /**
170
173
  * Publish a message and wait for delivery completion
@@ -186,9 +189,18 @@ var MessagesAPI = class extends BaseClient {
186
189
  * ```
187
190
  */
188
191
  async publishAndWait(destination, body, options) {
189
- const { pollIntervalMs = 1e3, timeoutMs = 6e4, ...publishOptions } = options || {};
192
+ const {
193
+ pollIntervalMs = 1e3,
194
+ timeoutMs = 6e4,
195
+ ...publishOptions
196
+ } = options || {};
190
197
  const { messageId } = await this.publish(destination, body, publishOptions);
191
- const terminalStates = ["completed", "failed", "cancelled", "dlq"];
198
+ const terminalStates = [
199
+ "completed",
200
+ "failed",
201
+ "cancelled",
202
+ "dlq"
203
+ ];
192
204
  const startTime = Date.now();
193
205
  while (Date.now() - startTime < timeoutMs) {
194
206
  const message = await this.get(messageId);
@@ -197,7 +209,9 @@ var MessagesAPI = class extends BaseClient {
197
209
  }
198
210
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
199
211
  }
200
- throw new Error(`Message ${messageId} did not complete within ${timeoutMs}ms`);
212
+ throw new Error(
213
+ `Message ${messageId} did not complete within ${timeoutMs}ms`
214
+ );
201
215
  }
202
216
  };
203
217
 
@@ -1257,6 +1271,75 @@ function verifySignature(body, signature, secret) {
1257
1271
  return false;
1258
1272
  }
1259
1273
  }
1274
+
1275
+ // src/serve-message.ts
1276
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1277
+ function serveMessage(handler, options) {
1278
+ return async (request) => {
1279
+ try {
1280
+ if (options?.signingSecret) {
1281
+ const signature = request.headers.get("X-QueueBear-Signature");
1282
+ const bodyText = await request.clone().text();
1283
+ if (!verifySignature2(bodyText, signature, options.signingSecret)) {
1284
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
1285
+ status: 401,
1286
+ headers: { "Content-Type": "application/json" }
1287
+ });
1288
+ }
1289
+ }
1290
+ const messageId = request.headers.get("X-QueueBear-Message-Id") || void 0;
1291
+ const attemptNumber = parseInt(
1292
+ request.headers.get("X-QueueBear-Retry-Count") || "0",
1293
+ 10
1294
+ );
1295
+ const body = await request.json();
1296
+ const context = {
1297
+ body,
1298
+ messageId,
1299
+ attemptNumber,
1300
+ headers: Object.fromEntries(request.headers.entries())
1301
+ };
1302
+ const result = await handler(context);
1303
+ return new Response(JSON.stringify({ success: true, result }), {
1304
+ status: 200,
1305
+ headers: { "Content-Type": "application/json" }
1306
+ });
1307
+ } catch (error) {
1308
+ console.error("[serveMessage] Error:", error);
1309
+ return new Response(
1310
+ JSON.stringify({
1311
+ error: error instanceof Error ? error.message : "Unknown error"
1312
+ }),
1313
+ {
1314
+ status: 500,
1315
+ headers: { "Content-Type": "application/json" }
1316
+ }
1317
+ );
1318
+ }
1319
+ };
1320
+ }
1321
+ function verifySignature2(body, signature, secret) {
1322
+ if (!signature) return false;
1323
+ const parts = signature.split(",").reduce(
1324
+ (acc, part) => {
1325
+ const [key, value] = part.split("=");
1326
+ acc[key] = value;
1327
+ return acc;
1328
+ },
1329
+ {}
1330
+ );
1331
+ if (!parts.t || !parts.v1) return false;
1332
+ const timestamp = parseInt(parts.t, 10);
1333
+ const now = Math.floor(Date.now() / 1e3);
1334
+ if (Math.abs(now - timestamp) > 300) return false;
1335
+ const payload = `${parts.t}.${body}`;
1336
+ const expected = createHmac2("sha256", secret).update(payload).digest("hex");
1337
+ try {
1338
+ return timingSafeEqual2(Buffer.from(parts.v1), Buffer.from(expected));
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
1260
1343
  export {
1261
1344
  DLQAPI,
1262
1345
  MessagesAPI,
@@ -1267,5 +1350,6 @@ export {
1267
1350
  WorkflowPausedError,
1268
1351
  WorkflowsAPI,
1269
1352
  createWorkflowContext,
1270
- serve
1353
+ serve,
1354
+ serveMessage
1271
1355
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "queuebear",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "QueueBear SDK for message queues, scheduled jobs, and durable workflows",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,13 +18,12 @@
18
18
  "type-check": "tsc --noEmit",
19
19
  "clean": "rm -rf dist"
20
20
  },
21
- "dependencies": {},
22
21
  "devDependencies": {
23
22
  "@types/node": "^22.10.2",
24
23
  "tsup": "^8.3.5",
25
- "typescript": "^5.7.2"
24
+ "typescript": "^5.7.2",
25
+ "vitest": "^4.0.16"
26
26
  },
27
- "peerDependencies": {},
28
27
  "files": [
29
28
  "dist"
30
29
  ],