@mantajs/adapter-queue-qstash 0.2.0-beta.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.
@@ -0,0 +1,26 @@
1
+ import type { IQueuePort, QueueMessage } from '@mantajs/core';
2
+ export interface QStashQueueAdapterOptions {
3
+ /** Upstash QStash REST endpoint, e.g. `https://qstash-eu-central-1.upstash.io`. */
4
+ url: string;
5
+ /** Upstash QStash publish token (`QSTASH_TOKEN`). */
6
+ token: string;
7
+ /** Request timeout in ms (default 5s). */
8
+ timeoutMs?: number;
9
+ /** fetch override, for tests. */
10
+ fetch?: typeof globalThis.fetch;
11
+ /** Optional logger for delivery errors. */
12
+ logger?: {
13
+ warn: (msg: string) => void;
14
+ error?: (msg: string, err?: unknown) => void;
15
+ };
16
+ }
17
+ export declare class QStashQueueAdapter implements IQueuePort {
18
+ private _url;
19
+ private _token;
20
+ private _timeoutMs;
21
+ private _fetch;
22
+ private _logger;
23
+ constructor(opts: QStashQueueAdapterOptions);
24
+ enqueue(message: QueueMessage): Promise<void>;
25
+ }
26
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE7D,MAAM,WAAW,yBAAyB;IACxC,mFAAmF;IACnF,GAAG,EAAE,MAAM,CAAA;IACX,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iCAAiC;IACjC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,2CAA2C;IAC3C,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAA;CACvF;AAED,qBAAa,kBAAmB,YAAW,UAAU;IACnD,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAqC;gBAExC,IAAI,EAAE,yBAAyB;IAQrC,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAmCpD"}
@@ -0,0 +1,63 @@
1
+ // QStashQueueAdapter — IQueuePort backed by Upstash QStash (HTTP scheduler).
2
+ //
3
+ // QStash accepts a publish request (`POST {QSTASH_URL}/v2/publish/{dest}`)
4
+ // and calls `dest` later with our payload as the POST body. It handles
5
+ // retries (up to 3 by default), signed headers (HMAC-SHA256 over the body),
6
+ // and delivery to internet-reachable URLs.
7
+ //
8
+ // Auth: `Authorization: Bearer $QSTASH_TOKEN` on the publish request.
9
+ // Delivery headers on the receiving endpoint include
10
+ // `Upstash-Signature` — verify with `@upstash/qstash` SDK or the
11
+ // receiver-side `verifyQStashSignature` helper in this package.
12
+ //
13
+ // Free tier: 500 msg/day, enough for a rebuild button.
14
+ export class QStashQueueAdapter {
15
+ _url;
16
+ _token;
17
+ _timeoutMs;
18
+ _fetch;
19
+ _logger;
20
+ constructor(opts) {
21
+ this._url = opts.url.replace(/\/+$/, '');
22
+ this._token = opts.token;
23
+ this._timeoutMs = opts.timeoutMs ?? 5_000;
24
+ this._fetch = opts.fetch ?? globalThis.fetch;
25
+ this._logger = opts.logger;
26
+ }
27
+ async enqueue(message) {
28
+ const target = `${this._url}/v2/publish/${encodeURIComponent(message.url)}`;
29
+ const headers = {
30
+ Authorization: `Bearer ${this._token}`,
31
+ 'Content-Type': 'application/json',
32
+ };
33
+ if (message.delayMs && message.delayMs > 0) {
34
+ headers['Upstash-Delay'] = `${Math.ceil(message.delayMs / 1000)}s`;
35
+ }
36
+ if (message.idempotencyKey) {
37
+ // QStash doesn't have native idempotency on publish but accepts a
38
+ // custom `Upstash-Deduplication-Id` header on some plans; always include
39
+ // it — backend ignores it if unsupported.
40
+ headers['Upstash-Deduplication-Id'] = message.idempotencyKey;
41
+ }
42
+ const controller = new AbortController();
43
+ const timer = setTimeout(() => controller.abort(), this._timeoutMs);
44
+ try {
45
+ const res = await this._fetch(target, {
46
+ method: 'POST',
47
+ headers,
48
+ body: JSON.stringify(message.payload ?? {}),
49
+ signal: controller.signal,
50
+ });
51
+ if (!res.ok) {
52
+ const body = await res.text().catch(() => '');
53
+ const msg = `[QStashQueueAdapter] publish ${res.status} for ${message.url}: ${body.slice(0, 200)}`;
54
+ this._logger?.warn?.(msg);
55
+ throw new Error(msg);
56
+ }
57
+ }
58
+ finally {
59
+ clearTimeout(timer);
60
+ }
61
+ }
62
+ }
63
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,2EAA2E;AAC3E,uEAAuE;AACvE,4EAA4E;AAC5E,2CAA2C;AAC3C,EAAE;AACF,sEAAsE;AACtE,qDAAqD;AACrD,iEAAiE;AACjE,gEAAgE;AAChE,EAAE;AACF,uDAAuD;AAiBvD,MAAM,OAAO,kBAAkB;IACrB,IAAI,CAAQ;IACZ,MAAM,CAAQ;IACd,UAAU,CAAQ;IAClB,MAAM,CAAyB;IAC/B,OAAO,CAAqC;IAEpD,YAAY,IAA+B;QACzC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QACxC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAA;QACxB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAA;QACzC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAA;QAC5C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,MAAM,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAqB;QACjC,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,eAAe,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAA;QAC3E,MAAM,OAAO,GAA2B;YACtC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;YACtC,cAAc,EAAE,kBAAkB;SACnC,CAAA;QACD,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;YAC3C,OAAO,CAAC,eAAe,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAA;QACpE,CAAC;QACD,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC3B,kEAAkE;YAClE,yEAAyE;YACzE,0CAA0C;YAC1C,OAAO,CAAC,0BAA0B,CAAC,GAAG,OAAO,CAAC,cAAc,CAAA;QAC9D,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACnE,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;gBACpC,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;gBAC3C,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAA;YACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;gBAC7C,MAAM,GAAG,GAAG,gCAAgC,GAAG,CAAC,MAAM,QAAQ,OAAO,CAAC,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;gBAClG,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;gBACzB,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,CAAA;YACtB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ export { QStashQueueAdapter, type QStashQueueAdapterOptions } from './adapter';
2
+ export { type VerifyQStashSignatureOptions, verifyQStashSignature } from './verify';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,WAAW,CAAA;AAC9E,OAAO,EAAE,KAAK,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { QStashQueueAdapter } from './adapter';
2
+ export { verifyQStashSignature } from './verify';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAkC,MAAM,WAAW,CAAA;AAC9E,OAAO,EAAqC,qBAAqB,EAAE,MAAM,UAAU,CAAA"}
@@ -0,0 +1,6 @@
1
+ export interface VerifyQStashSignatureOptions {
2
+ currentSigningKey: string;
3
+ nextSigningKey?: string;
4
+ }
5
+ export declare function verifyQStashSignature(body: string, signatureHeader: string | null | undefined, opts: VerifyQStashSignatureOptions): boolean;
6
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,4BAA4B;IAC3C,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC1C,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAeT"}
package/dist/verify.js ADDED
@@ -0,0 +1,43 @@
1
+ // verifyQStashSignature — HMAC-SHA256 verification for incoming QStash
2
+ // deliveries. QStash signs the body with `QSTASH_CURRENT_SIGNING_KEY` (and
3
+ // rotates to `QSTASH_NEXT_SIGNING_KEY` periodically — we accept both).
4
+ //
5
+ // Protocol (Upstash docs):
6
+ // - Request header `Upstash-Signature`: "v1,<base64url(HMAC-SHA256(body, key))>"
7
+ // - We recompute both signatures (current + next), accept the message if
8
+ // EITHER matches. This covers the ~5-minute key rotation window.
9
+ //
10
+ // Use in your resume endpoint before calling `manager.resume(runId)` so end
11
+ // users can't directly POST /_workflow/:id/resume and replay old messages.
12
+ import { createHmac, timingSafeEqual } from 'node:crypto';
13
+ export function verifyQStashSignature(body, signatureHeader, opts) {
14
+ if (!signatureHeader)
15
+ return false;
16
+ const match = /^v1,(.+)$/.exec(signatureHeader.trim());
17
+ if (!match)
18
+ return false;
19
+ const signature = match[1];
20
+ const expectedCurrent = sign(body, opts.currentSigningKey);
21
+ if (safeEqual(signature, expectedCurrent))
22
+ return true;
23
+ if (opts.nextSigningKey) {
24
+ const expectedNext = sign(body, opts.nextSigningKey);
25
+ if (safeEqual(signature, expectedNext))
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ function sign(body, key) {
31
+ return createHmac('sha256', key).update(body).digest('base64url');
32
+ }
33
+ function safeEqual(a, b) {
34
+ if (a.length !== b.length)
35
+ return false;
36
+ try {
37
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b));
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,2EAA2E;AAC3E,uEAAuE;AACvE,EAAE;AACF,2BAA2B;AAC3B,mFAAmF;AACnF,2EAA2E;AAC3E,qEAAqE;AACrE,EAAE;AACF,4EAA4E;AAC5E,2EAA2E;AAE3E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAOzD,MAAM,UAAU,qBAAqB,CACnC,IAAY,EACZ,eAA0C,EAC1C,IAAkC;IAElC,IAAI,CAAC,eAAe;QAAE,OAAO,KAAK,CAAA;IAClC,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAA;IACtD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IAE1B,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAA;IAC1D,IAAI,SAAS,CAAC,SAAS,EAAE,eAAe,CAAC;QAAE,OAAO,IAAI,CAAA;IAEtD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;QACpD,IAAI,SAAS,CAAC,SAAS,EAAE,YAAY,CAAC;YAAE,OAAO,IAAI,CAAA;IACrD,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,IAAI,CAAC,IAAY,EAAE,GAAW;IACrC,OAAO,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,SAAS,CAAC,CAAS,EAAE,CAAS;IACrC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@mantajs/adapter-queue-qstash",
3
+ "version": "0.2.0-beta.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "peerDependencies": {
15
+ "@mantajs/core": "0.2.0-beta.0"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ]
20
+ }