@nebutra/queue 0.1.0 → 0.1.1

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.
Files changed (44) hide show
  1. package/README.md +69 -2
  2. package/dist/factory.d.ts +42 -0
  3. package/dist/factory.d.ts.map +1 -0
  4. package/dist/factory.js +156 -0
  5. package/dist/index.d.ts +12 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +27 -0
  8. package/dist/middleware/qstash-verify.d.ts +16 -0
  9. package/dist/middleware/qstash-verify.d.ts.map +1 -0
  10. package/dist/middleware/qstash-verify.js +87 -0
  11. package/dist/providers/bullmq.d.ts +32 -0
  12. package/dist/providers/bullmq.d.ts.map +1 -0
  13. package/dist/providers/bullmq.js +258 -0
  14. package/dist/providers/memory.d.ts +23 -0
  15. package/dist/providers/memory.d.ts.map +1 -0
  16. package/dist/providers/memory.js +220 -0
  17. package/dist/providers/qstash.d.ts +26 -0
  18. package/dist/providers/qstash.d.ts.map +1 -0
  19. package/dist/providers/qstash.js +226 -0
  20. package/dist/providers/sqs.d.ts +18 -0
  21. package/dist/providers/sqs.d.ts.map +1 -0
  22. package/dist/providers/sqs.js +197 -0
  23. package/dist/queuebase-webhook.d.ts +45 -0
  24. package/dist/queuebase-webhook.d.ts.map +1 -0
  25. package/dist/queuebase-webhook.js +113 -0
  26. package/dist/queuebase.d.ts +27 -0
  27. package/dist/queuebase.d.ts.map +1 -0
  28. package/dist/queuebase.js +48 -0
  29. package/dist/scheduled/index.d.ts +15 -0
  30. package/dist/scheduled/index.d.ts.map +1 -0
  31. package/dist/scheduled/index.js +25 -0
  32. package/dist/scheduled/jobs/invitation-cleanup.d.ts +40 -0
  33. package/dist/scheduled/jobs/invitation-cleanup.d.ts.map +1 -0
  34. package/dist/scheduled/jobs/invitation-cleanup.js +44 -0
  35. package/dist/scheduled/jobs/session-cleanup.d.ts +24 -0
  36. package/dist/scheduled/jobs/session-cleanup.d.ts.map +1 -0
  37. package/dist/scheduled/jobs/session-cleanup.js +32 -0
  38. package/dist/scheduled/scheduler.d.ts +44 -0
  39. package/dist/scheduled/scheduler.d.ts.map +1 -0
  40. package/dist/scheduled/scheduler.js +52 -0
  41. package/dist/types.d.ts +202 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +32 -0
  44. package/package.json +17 -4
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- > **Status: Foundation** — Type definitions, factory pattern, and provider stubs are complete. The in-memory provider exposes a test-only dead-letter queue, and BullMQ exposes retry-exhausted failed jobs through the shared dead-letter contract. QStash provider-side dead-letter retrieval is still not production-ready.
1
+ > **Status: Foundation** — Type definitions, factory pattern, and provider stubs are complete. The in-memory provider exposes a test-only dead-letter queue, BullMQ exposes retry-exhausted failed jobs, and QStash can map records returned by an injected DLQ fetcher into the shared dead-letter contract.
2
2
 
3
3
  # @nebutra/queue
4
4
 
@@ -23,6 +23,71 @@ queue.registerHandler("email", "send", async (job) => {
23
23
  });
24
24
  ```
25
25
 
26
+ ## Queuebase-style Jobs
27
+
28
+ `@nebutra/queue` also exposes a Queuebase-compatible job layer for app-owned
29
+ background jobs: define typed jobs with Zod input, enqueue through a typed
30
+ client, and execute callbacks through a single webhook route.
31
+
32
+ ```ts
33
+ import {
34
+ createJobClient,
35
+ createJobRouter,
36
+ createQueuebaseWebhookHandler,
37
+ defineQueueJob,
38
+ } from "@nebutra/queue";
39
+ import { z } from "zod";
40
+
41
+ export const jobs = createJobRouter({
42
+ sendWelcomeEmail: defineQueueJob({
43
+ input: z.object({ to: z.string().email(), name: z.string() }),
44
+ defaults: { retries: 3, backoff: "exponential" },
45
+ handler: async ({ input, jobId, attempt }) => {
46
+ await sendWelcomeEmail(input.to, input.name, { jobId, attempt });
47
+ return { sent: true };
48
+ },
49
+ }),
50
+ dailyCleanup: defineQueueJob({
51
+ input: z.object({}),
52
+ schedule: { cron: "0 2 * * *", timezone: "UTC", overlap: "skip" },
53
+ handler: async () => ({ cleaned: true }),
54
+ }),
55
+ });
56
+
57
+ export const jobClient = createJobClient(jobs, {
58
+ callbackUrl: "https://app.nebutra.com/api/webhooks/queuebase",
59
+ });
60
+
61
+ export const webhookHandler = createQueuebaseWebhookHandler(jobs);
62
+ ```
63
+
64
+ The default Nebutra web app mounts `queuebaseWebhookHandler` at
65
+ `/api/webhooks/queuebase`. `queuebaseJobClient` uses:
66
+
67
+ - `QUEUEBASE_API_URL`, defaulting to `http://localhost:3847`
68
+ - `QUEUEBASE_API_KEY`, required by hosted Queuebase
69
+ - `NEXT_PUBLIC_SITE_URL`, `VERCEL_URL`, or `PORT` to derive the callback URL
70
+
71
+ Local development mirrors Queuebase's callback model:
72
+
73
+ ```bash
74
+ # Terminal 1: Queuebase-compatible dev server / sync process
75
+ npx queuebase dev
76
+
77
+ # Terminal 2: Nebutra app
78
+ pnpm dev:web
79
+ ```
80
+
81
+ For production, set the env vars on the host and run the provider sync step
82
+ after deployment configuration changes:
83
+
84
+ ```bash
85
+ npx queuebase sync
86
+ ```
87
+
88
+ Use `listQueuebaseSchedules(queuebaseJobs)` to inspect schedule metadata for
89
+ sync tooling without executing handlers.
90
+
26
91
  ## Provider Selection
27
92
 
28
93
  The factory auto-detects the provider:
@@ -69,4 +134,6 @@ app.post("/api/queue/:queue/:type", async (c) => {
69
134
 
70
135
  ## Failure Observability
71
136
 
72
- Providers may expose `getDeadLetteredJobs(queue?)` for jobs that exhausted retries and need operator attention. The memory provider implements this for deterministic tests and local harnesses. The BullMQ provider maps durable failed jobs whose attempts are exhausted into the same contract, including the original payload, attempt count, configured retry limit, failure reason, and `failedAt` timestamp. QStash provider-side dead-letter retrieval is still a known gap, so package metadata keeps `productionReady: false`.
137
+ Providers may expose `getDeadLetteredJobs(queue?)` for jobs that exhausted retries and need operator attention. The memory provider implements this for deterministic tests and local harnesses. The BullMQ provider maps durable failed jobs whose attempts are exhausted into the same contract, including the original payload, attempt count, configured retry limit, failure reason, and `failedAt` timestamp.
138
+
139
+ For QStash, pass an injected `dlqFetcher` and optional `dlqEndpoint`; this package does not assume unstable provider SDK DLQ APIs. The fetcher returns provider-side records, and the provider maps records whose body is the original `JobPayload` into `DeadLetterJob`. Fetcher errors fail closed to `[]` and are logged.
@@ -0,0 +1,42 @@
1
+ import type { QueueConfig, QueueProvider } from "./types";
2
+ /**
3
+ * Create a queue provider instance.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * // Auto-detect from environment
8
+ * const queue = await createQueue();
9
+ *
10
+ * // Explicit QStash
11
+ * const queue = await createQueue({
12
+ * provider: "qstash",
13
+ * callbackBaseUrl: "https://api.nebutra.com",
14
+ * });
15
+ *
16
+ * // Explicit BullMQ
17
+ * const queue = await createQueue({
18
+ * provider: "bullmq",
19
+ * redisUrl: "redis://localhost:6379",
20
+ * });
21
+ * ```
22
+ */
23
+ export declare function createQueue(config?: QueueConfig): Promise<QueueProvider>;
24
+ /**
25
+ * Get or create the default (singleton) queue provider.
26
+ * Uses lazy initialisation so import-time side effects are avoided.
27
+ */
28
+ export declare function getQueue(): Promise<QueueProvider>;
29
+ /**
30
+ * Replace the default queue provider (useful in tests).
31
+ */
32
+ export declare function setQueue(provider: QueueProvider): void;
33
+ /**
34
+ * Gracefully shut down the default queue provider.
35
+ */
36
+ export declare function closeQueue(): Promise<void>;
37
+ import type { JobOptions, JobPayload } from "./types";
38
+ /**
39
+ * Build a `JobPayload` with auto-generated ID and timestamp.
40
+ */
41
+ export declare function createJob(queue: string, type: string, data: Record<string, unknown>, options?: JobOptions): JobPayload;
42
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAqB,MAAM,SAAS,CAAC;AAwC7E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CA0E9E;AAED;;;GAGG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,aAAa,CAAC,CAKvD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAEtD;AAED;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAKhD;AAMD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtD;;GAEG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,UAAU,GACnB,UAAU,CASZ"}
@@ -0,0 +1,156 @@
1
+ import { logger } from "@nebutra/logger";
2
+ import { assertProviderAllowed, envPresent, resolveProviderType } from "@nebutra/provider-factory";
3
+ // =============================================================================
4
+ // Queue Factory — Provider-agnostic queue creation
5
+ // =============================================================================
6
+ // The factory resolves the correct provider at runtime based on:
7
+ // 1. Explicit config passed to `createQueue()`
8
+ // 2. `QUEUE_PROVIDER` environment variable
9
+ // 3. Auto-detection based on available env vars
10
+ //
11
+ // This lets customers switch backends without changing application code.
12
+ // =============================================================================
13
+ let defaultProvider = null;
14
+ // Provider selection is delegated to the shared @nebutra/provider-factory
15
+ // primitive (identical precedence + prod-guard across every provider-agnostic
16
+ // package); only the instantiation switch below stays queue-specific.
17
+ function resolveQueueProvider(explicit) {
18
+ return resolveProviderType({
19
+ explicit,
20
+ envVarName: "QUEUE_PROVIDER",
21
+ detectors: [
22
+ { provider: "qstash", when: envPresent("QSTASH_TOKEN") },
23
+ { provider: "bullmq", when: envPresent("REDIS_URL") },
24
+ { provider: "sqs", when: envPresent("AWS_SQS_QUEUE_URL") },
25
+ ],
26
+ fallback: "memory",
27
+ });
28
+ }
29
+ function assertMemoryProviderAllowed() {
30
+ assertProviderAllowed("memory", {
31
+ disallowedInProd: ["memory"],
32
+ overrideEnv: "ALLOW_MEMORY_QUEUE_IN_PRODUCTION",
33
+ message: "Refusing to use the in-memory queue provider in production. Configure QSTASH_TOKEN or REDIS_URL, or set ALLOW_MEMORY_QUEUE_IN_PRODUCTION=true for an explicit temporary override.",
34
+ });
35
+ }
36
+ /**
37
+ * Create a queue provider instance.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // Auto-detect from environment
42
+ * const queue = await createQueue();
43
+ *
44
+ * // Explicit QStash
45
+ * const queue = await createQueue({
46
+ * provider: "qstash",
47
+ * callbackBaseUrl: "https://api.nebutra.com",
48
+ * });
49
+ *
50
+ * // Explicit BullMQ
51
+ * const queue = await createQueue({
52
+ * provider: "bullmq",
53
+ * redisUrl: "redis://localhost:6379",
54
+ * });
55
+ * ```
56
+ */
57
+ export async function createQueue(config) {
58
+ const providerType = resolveQueueProvider(config?.provider);
59
+ logger.info("[queue] Creating provider", { provider: providerType });
60
+ switch (providerType) {
61
+ case "qstash": {
62
+ const { QStashProvider } = await import("./providers/qstash");
63
+ const qstashConfig = config;
64
+ return new QStashProvider({
65
+ callbackBaseUrl: qstashConfig?.callbackBaseUrl ??
66
+ process.env.QSTASH_CALLBACK_BASE_URL ??
67
+ process.env.API_GATEWAY_URL ??
68
+ "http://localhost:3002",
69
+ ...(qstashConfig?.token !== undefined ? { token: qstashConfig.token } : {}),
70
+ ...(qstashConfig?.currentSigningKey !== undefined
71
+ ? { currentSigningKey: qstashConfig.currentSigningKey }
72
+ : {}),
73
+ ...(qstashConfig?.nextSigningKey !== undefined
74
+ ? { nextSigningKey: qstashConfig.nextSigningKey }
75
+ : {}),
76
+ ...(qstashConfig?.dlqEndpoint !== undefined
77
+ ? { dlqEndpoint: qstashConfig.dlqEndpoint }
78
+ : {}),
79
+ ...(qstashConfig?.dlqFetcher !== undefined ? { dlqFetcher: qstashConfig.dlqFetcher } : {}),
80
+ });
81
+ }
82
+ case "bullmq": {
83
+ const { BullMQProvider } = await import("./providers/bullmq");
84
+ const bullConfig = config;
85
+ return new BullMQProvider({
86
+ ...(bullConfig?.redisUrl !== undefined ? { redisUrl: bullConfig.redisUrl } : {}),
87
+ ...(bullConfig?.concurrency !== undefined ? { concurrency: bullConfig.concurrency } : {}),
88
+ ...(bullConfig?.prefix !== undefined ? { prefix: bullConfig.prefix } : {}),
89
+ });
90
+ }
91
+ case "sqs": {
92
+ const { SQSProvider } = await import("./providers/sqs");
93
+ const sqsConfig = config;
94
+ return new SQSProvider({
95
+ ...(sqsConfig?.region !== undefined ? { region: sqsConfig.region } : {}),
96
+ ...(sqsConfig?.queueUrl !== undefined ? { queueUrl: sqsConfig.queueUrl } : {}),
97
+ ...(sqsConfig?.accessKeyId !== undefined ? { accessKeyId: sqsConfig.accessKeyId } : {}),
98
+ ...(sqsConfig?.secretAccessKey !== undefined
99
+ ? { secretAccessKey: sqsConfig.secretAccessKey }
100
+ : {}),
101
+ ...(sqsConfig?.waitTimeSeconds !== undefined
102
+ ? { waitTimeSeconds: sqsConfig.waitTimeSeconds }
103
+ : {}),
104
+ ...(sqsConfig?.maxMessages !== undefined ? { maxMessages: sqsConfig.maxMessages } : {}),
105
+ ...(sqsConfig?.visibilityTimeoutSeconds !== undefined
106
+ ? { visibilityTimeoutSeconds: sqsConfig.visibilityTimeoutSeconds }
107
+ : {}),
108
+ });
109
+ }
110
+ case "memory": {
111
+ assertMemoryProviderAllowed();
112
+ const { MemoryProvider } = await import("./providers/memory");
113
+ return new MemoryProvider();
114
+ }
115
+ default:
116
+ throw new Error(`Unknown queue provider: ${providerType}`);
117
+ }
118
+ }
119
+ /**
120
+ * Get or create the default (singleton) queue provider.
121
+ * Uses lazy initialisation so import-time side effects are avoided.
122
+ */
123
+ export async function getQueue() {
124
+ if (!defaultProvider) {
125
+ defaultProvider = await createQueue();
126
+ }
127
+ return defaultProvider;
128
+ }
129
+ /**
130
+ * Replace the default queue provider (useful in tests).
131
+ */
132
+ export function setQueue(provider) {
133
+ defaultProvider = provider;
134
+ }
135
+ /**
136
+ * Gracefully shut down the default queue provider.
137
+ */
138
+ export async function closeQueue() {
139
+ if (defaultProvider) {
140
+ await defaultProvider.close();
141
+ defaultProvider = null;
142
+ }
143
+ }
144
+ /**
145
+ * Build a `JobPayload` with auto-generated ID and timestamp.
146
+ */
147
+ export function createJob(queue, type, data, options) {
148
+ return {
149
+ id: crypto.randomUUID(),
150
+ queue,
151
+ type,
152
+ data,
153
+ options,
154
+ createdAt: new Date().toISOString(),
155
+ };
156
+ }
@@ -0,0 +1,12 @@
1
+ export { closeQueue, createJob, createQueue, getQueue, setQueue, } from "./factory";
2
+ export { createQStashWebhookHandler } from "./middleware/qstash-verify";
3
+ export { BullMQProvider } from "./providers/bullmq";
4
+ export { MemoryProvider } from "./providers/memory";
5
+ export { getQStashHandler, getQStashHandlerKeys, QStashProvider } from "./providers/qstash";
6
+ export type { QueuebaseBackoff, QueuebaseClient, QueuebaseClientOptions, QueuebaseEnqueueResult, QueuebaseJobContext, QueuebaseJobDefinition, QueuebaseJobRouter, QueuebaseJobs, QueuebaseSchedule, QueuebaseScheduleConfig, QueuebaseScheduleMetadata, } from "./queuebase";
7
+ export { createJobClient, createJobRouter, createQueuebaseWebhookHandler, defineQueueJob, getQueuebaseCallbackUrl, listQueuebaseSchedules, queuebaseJobClient, queuebaseJobs, queuebaseWebhookHandler, } from "./queuebase";
8
+ export type { ScheduledJob, ScheduledJobResult } from "./scheduled/index";
9
+ export { clearScheduledJobs, getScheduledJob, invitationCleanup, listScheduledJobs, registerDefaultScheduledJobs, registerScheduledJob, runInvitationCleanup, runSessionCleanup, sessionCleanup, } from "./scheduled/index";
10
+ export type { BullMQProviderConfig, DeadLetterJob, JobHandler, JobLifecycleAction, JobLifecycleActionResult, JobLifecycleEvent, JobOptions, JobPayload, JobResult, JobStatus, JobStatusInfo, MemoryProviderConfig, QStashProviderConfig, QueueConfig, QueueProvider, QueueProviderType, } from "./types";
11
+ export { JobOptionsSchema, JobPayloadSchema } from "./types";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,UAAU,EACV,SAAS,EACT,WAAW,EACX,QAAQ,EACR,QAAQ,GACT,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC5F,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,eAAe,EACf,eAAe,EACf,6BAA6B,EAC7B,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,EAClB,aAAa,EACb,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE1E,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,4BAA4B,EAC5B,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,GACf,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACV,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,EACX,aAAa,EACb,iBAAiB,GAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ // =============================================================================
2
+ // @nebutra/queue — Provider-agnostic message queue
3
+ // =============================================================================
4
+ // Supports:
5
+ // - Upstash QStash (serverless, HTTP-based)
6
+ // - BullMQ (self-hosted Redis)
7
+ // - In-memory (dev/test only)
8
+ //
9
+ // Usage:
10
+ // import { getQueue, createJob } from "@nebutra/queue";
11
+ //
12
+ // const queue = await getQueue(); // auto-detects provider
13
+ // await queue.enqueue(createJob("email", "send", { to: "user@example.com" }));
14
+ // =============================================================================
15
+ // ── Factory ─────────────────────────────────────────────────────────────────
16
+ export { closeQueue, createJob, createQueue, getQueue, setQueue, } from "./factory";
17
+ // ── Middleware ───────────────────────────────────────────────────────────────
18
+ export { createQStashWebhookHandler } from "./middleware/qstash-verify";
19
+ export { BullMQProvider } from "./providers/bullmq";
20
+ export { MemoryProvider } from "./providers/memory";
21
+ // ── Providers (tree-shakable direct imports) ────────────────────────────────
22
+ export { getQStashHandler, getQStashHandlerKeys, QStashProvider } from "./providers/qstash";
23
+ // ── Queuebase-style typed jobs ──────────────────────────────────────────────
24
+ export { createJobClient, createJobRouter, createQueuebaseWebhookHandler, defineQueueJob, getQueuebaseCallbackUrl, listQueuebaseSchedules, queuebaseJobClient, queuebaseJobs, queuebaseWebhookHandler, } from "./queuebase";
25
+ // ── Scheduled (cron) jobs ────────────────────────────────────────────────────
26
+ export { clearScheduledJobs, getScheduledJob, invitationCleanup, listScheduledJobs, registerDefaultScheduledJobs, registerScheduledJob, runInvitationCleanup, runSessionCleanup, sessionCleanup, } from "./scheduled/index";
27
+ export { JobOptionsSchema, JobPayloadSchema } from "./types";
@@ -0,0 +1,16 @@
1
+ interface QStashVerifyOptions {
2
+ /** Current signing key (defaults to `process.env.QSTASH_CURRENT_SIGNING_KEY`) */
3
+ currentSigningKey?: string;
4
+ /** Next signing key for key rotation (defaults to `process.env.QSTASH_NEXT_SIGNING_KEY`) */
5
+ nextSigningKey?: string;
6
+ }
7
+ /**
8
+ * Creates a request handler that:
9
+ * 1. Verifies the QStash signature
10
+ * 2. Parses the job payload
11
+ * 3. Routes to the registered handler
12
+ * 4. Returns 200 on success, 500 on failure (triggers QStash retry)
13
+ */
14
+ export declare function createQStashWebhookHandler(options?: QStashVerifyOptions): (request: Request) => Promise<Response>;
15
+ export {};
16
+ //# sourceMappingURL=qstash-verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qstash-verify.d.ts","sourceRoot":"","sources":["../../src/middleware/qstash-verify.ts"],"names":[],"mappings":"AAyBA,UAAU,mBAAmB;IAC3B,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4FAA4F;IAC5F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,CAAC,EAAE,mBAAmB,IAYxD,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA4EnD"}
@@ -0,0 +1,87 @@
1
+ import { logger } from "@nebutra/logger";
2
+ import { Receiver } from "@upstash/qstash";
3
+ import { getQStashHandler } from "../providers/qstash";
4
+ /**
5
+ * Creates a request handler that:
6
+ * 1. Verifies the QStash signature
7
+ * 2. Parses the job payload
8
+ * 3. Routes to the registered handler
9
+ * 4. Returns 200 on success, 500 on failure (triggers QStash retry)
10
+ */
11
+ export function createQStashWebhookHandler(options) {
12
+ const currentSigningKey = options?.currentSigningKey ?? process.env.QSTASH_CURRENT_SIGNING_KEY ?? "";
13
+ const nextSigningKey = options?.nextSigningKey ?? process.env.QSTASH_NEXT_SIGNING_KEY ?? "";
14
+ const requiresSignature = process.env.NODE_ENV === "production" || process.env.QUEUE_PROVIDER === "qstash";
15
+ const receiver = currentSigningKey && nextSigningKey
16
+ ? new Receiver({ currentSigningKey, nextSigningKey })
17
+ : null;
18
+ return async (request) => {
19
+ const body = await request.text();
20
+ // ── Signature verification ──────────────────────────────────────────
21
+ if (!receiver && requiresSignature) {
22
+ logger.error("[queue:qstash-verify] Signing keys required but not configured", {
23
+ nodeEnv: process.env.NODE_ENV,
24
+ queueProvider: process.env.QUEUE_PROVIDER,
25
+ });
26
+ return new Response("QStash signing keys not configured", { status: 401 });
27
+ }
28
+ if (receiver) {
29
+ try {
30
+ const signature = request.headers.get("upstash-signature") ?? "";
31
+ const isValid = await receiver.verify({
32
+ signature,
33
+ body,
34
+ });
35
+ if (!isValid) {
36
+ logger.warn("[queue:qstash-verify] Invalid signature");
37
+ return new Response("Unauthorized", { status: 401 });
38
+ }
39
+ }
40
+ catch (error) {
41
+ logger.error("[queue:qstash-verify] Signature verification failed", {
42
+ error: error instanceof Error ? error.message : String(error),
43
+ });
44
+ return new Response("Unauthorized", { status: 401 });
45
+ }
46
+ }
47
+ else {
48
+ logger.warn("[queue:qstash-verify] No signing keys configured — skipping verification (dev mode)");
49
+ }
50
+ // ── Parse payload ───────────────────────────────────────────────────
51
+ let payload;
52
+ try {
53
+ payload = JSON.parse(body);
54
+ }
55
+ catch {
56
+ logger.error("[queue:qstash-verify] Invalid JSON payload");
57
+ return new Response("Bad Request", { status: 400 });
58
+ }
59
+ // ── Route to handler ────────────────────────────────────────────────
60
+ const handler = getQStashHandler(payload.queue, payload.type);
61
+ if (!handler) {
62
+ logger.warn("[queue:qstash-verify] No handler for job", {
63
+ queue: payload.queue,
64
+ type: payload.type,
65
+ });
66
+ // Return 200 to prevent infinite retries for unregistered handlers
67
+ return new Response("No handler registered", { status: 200 });
68
+ }
69
+ try {
70
+ await handler(payload);
71
+ logger.info("[queue:qstash-verify] Job processed successfully", {
72
+ jobId: payload.id,
73
+ queue: payload.queue,
74
+ type: payload.type,
75
+ });
76
+ return new Response("OK", { status: 200 });
77
+ }
78
+ catch (error) {
79
+ logger.error("[queue:qstash-verify] Job processing failed", {
80
+ jobId: payload.id,
81
+ error: error instanceof Error ? error.message : String(error),
82
+ });
83
+ // Return 500 so QStash retries the delivery
84
+ return new Response("Internal Server Error", { status: 500 });
85
+ }
86
+ };
87
+ }
@@ -0,0 +1,32 @@
1
+ import type { BullMQProviderConfig, DeadLetterJob, JobHandler, JobPayload, JobResult, JobStatusInfo, QueueProvider } from "../types";
2
+ interface BullMQFailedJobLike {
3
+ id?: string;
4
+ data: JobPayload;
5
+ attemptsMade: number;
6
+ opts: {
7
+ attempts?: number;
8
+ };
9
+ failedReason?: string;
10
+ finishedOn?: number;
11
+ }
12
+ export declare function toBullMQDeadLetterJob(queue: string, job: BullMQFailedJobLike): DeadLetterJob | undefined;
13
+ export declare class BullMQProvider implements QueueProvider {
14
+ readonly name: "bullmq";
15
+ private connection;
16
+ private queues;
17
+ private workers;
18
+ private handlers;
19
+ private prefix;
20
+ private concurrency;
21
+ constructor(config: Omit<BullMQProviderConfig, "provider">);
22
+ private getOrCreateQueue;
23
+ enqueue(job: JobPayload): Promise<JobResult>;
24
+ enqueueBatch(jobs: JobPayload[]): Promise<JobResult[]>;
25
+ registerHandler<T extends Record<string, unknown>>(queue: string, type: string, handler: JobHandler<T>): void;
26
+ private ensureWorker;
27
+ getJobStatus(jobId: string, queue: string): Promise<JobStatusInfo | undefined>;
28
+ getDeadLetteredJobs(queue?: string): Promise<DeadLetterJob[]>;
29
+ close(): Promise<void>;
30
+ }
31
+ export {};
32
+ //# sourceMappingURL=bullmq.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq.d.ts","sourceRoot":"","sources":["../../src/providers/bullmq.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACV,UAAU,EACV,SAAS,EAET,aAAa,EACb,aAAa,EACd,MAAM,UAAU,CAAC;AAIlB,UAAU,mBAAmB;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,mBAAmB,GACvB,aAAa,GAAG,SAAS,CAe3B;AAaD,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IA+B1D,OAAO,CAAC,gBAAgB;IAoBlB,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IAgC5C,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA0C5D,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GACrB,IAAI;IAUP,OAAO,CAAC,YAAY;IAyDd,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAiC9E,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAkB7D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA4B7B"}