@simpleq/sdk 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/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/chunk-72DDDNF6.js +138 -0
- package/dist/chunk-72DDDNF6.js.map +1 -0
- package/dist/errors-D7trszMq.d.cts +66 -0
- package/dist/errors-D7trszMq.d.ts +66 -0
- package/dist/express.cjs +126 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +19 -0
- package/dist/express.d.ts +19 -0
- package/dist/express.js +44 -0
- package/dist/express.js.map +1 -0
- package/dist/index.cjs +298 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/dist/nest.cjs +174 -0
- package/dist/nest.cjs.map +1 -0
- package/dist/nest.d.cts +34 -0
- package/dist/nest.d.ts +34 -0
- package/dist/nest.js +87 -0
- package/dist/nest.js.map +1 -0
- package/dist/types-eDJHwexZ.d.cts +91 -0
- package/dist/types-eDJHwexZ.d.ts +91 -0
- package/dist/webhooks.cjs +46 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +20 -0
- package/dist/webhooks.d.ts +20 -0
- package/dist/webhooks.js +3 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +112 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { verifyWebhookSignature, verifyWebhook } from './webhooks.js';
|
|
2
|
+
export { SIGNATURE_HEADER } from './webhooks.js';
|
|
3
|
+
import { S as SimpleQOptions, P as PublishParams, a as PublishJobResponse, J as Job, A as AckResponse, N as NackOptions, D as DeferOptions } from './types-eDJHwexZ.js';
|
|
4
|
+
export { b as JobAttempt, c as JobAttemptStatus, d as JobStatus, e as PublishJobRequest, W as WebhookPayload } from './types-eDJHwexZ.js';
|
|
5
|
+
export { A as ApiError, a as AuthenticationError, B as BackpressureStatus, N as NotFoundError, R as RateLimitError, S as SignatureVerificationError, b as SimpleQBackpressure, c as SimpleQConnectionError, d as SimpleQError, V as ValidationError, r as retryAfterSeconds } from './errors-D7trszMq.js';
|
|
6
|
+
|
|
7
|
+
/** The SimpleQ client: publish jobs, read job status, and run the ack-mode callbacks. */
|
|
8
|
+
declare class SimpleQ {
|
|
9
|
+
private readonly http;
|
|
10
|
+
/** Webhook signature helpers. Usable without an API key (only a `signingSecret` is needed). */
|
|
11
|
+
readonly webhooks: {
|
|
12
|
+
verifyWebhookSignature: typeof verifyWebhookSignature;
|
|
13
|
+
verifyWebhook: typeof verifyWebhook;
|
|
14
|
+
};
|
|
15
|
+
constructor(options?: SimpleQOptions);
|
|
16
|
+
/**
|
|
17
|
+
* Publish a job to a queue. Retries transient failures automatically; the idempotency key
|
|
18
|
+
* (yours, or one generated per call when you omit it) is reused across those retries, so a
|
|
19
|
+
* retry can never create a duplicate job. A 200 (idempotent hit) and a 201 (created) are both
|
|
20
|
+
* returned as success.
|
|
21
|
+
*/
|
|
22
|
+
publish(queueName: string, params: PublishParams): Promise<PublishJobResponse>;
|
|
23
|
+
/** Fetch a job's current status and attempt history. */
|
|
24
|
+
getJob(jobId: string): Promise<Job>;
|
|
25
|
+
/** Ack a job (ack-mode queues): report successful completion. */
|
|
26
|
+
ack(jobId: string): Promise<AckResponse>;
|
|
27
|
+
/** Nack a job (ack-mode queues): report failure. `retryable: false` dead-letters immediately. */
|
|
28
|
+
nack(jobId: string, options?: NackOptions): Promise<AckResponse>;
|
|
29
|
+
/** Defer a job (ack-mode queues): apply backpressure — held and redelivered, no attempt burned. */
|
|
30
|
+
defer(jobId: string, options: DeferOptions): Promise<AckResponse>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { AckResponse, DeferOptions, Job, NackOptions, PublishJobResponse, PublishParams, SimpleQ, SimpleQOptions, SimpleQ as default, verifyWebhook, verifyWebhookSignature };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { verifyWebhook, verifyWebhookSignature, SimpleQError, ValidationError, SimpleQConnectionError, mapApiError, ApiError, RateLimitError } from './chunk-72DDDNF6.js';
|
|
2
|
+
export { ApiError, AuthenticationError, NotFoundError, RateLimitError, SIGNATURE_HEADER, SignatureVerificationError, SimpleQBackpressure, SimpleQConnectionError, SimpleQError, ValidationError, retryAfterSeconds, verifyWebhook, verifyWebhookSignature } from './chunk-72DDDNF6.js';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
// src/http.ts
|
|
6
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
7
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
function backoffMs(attempt) {
|
|
9
|
+
const ceiling = Math.min(250 * 2 ** attempt, 8e3);
|
|
10
|
+
return Math.round(ceiling * (0.5 + Math.random() * 0.5));
|
|
11
|
+
}
|
|
12
|
+
var HttpClient = class {
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.opts = opts;
|
|
15
|
+
}
|
|
16
|
+
async request(req) {
|
|
17
|
+
const maxAttempts = req.retry ?? true ? this.opts.maxRetries + 1 : 1;
|
|
18
|
+
let lastError;
|
|
19
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
return await this.attempt(req);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
lastError = err;
|
|
24
|
+
if (!this.isRetryable(err) || attempt === maxAttempts - 1) throw err;
|
|
25
|
+
await sleep(this.retryAfterMs(err) ?? backoffMs(attempt));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw lastError instanceof Error ? lastError : new SimpleQError(String(lastError));
|
|
29
|
+
}
|
|
30
|
+
async attempt(req) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), this.opts.timeout);
|
|
33
|
+
let res;
|
|
34
|
+
try {
|
|
35
|
+
res = await this.opts.fetchImpl(this.opts.baseUrl + req.path, {
|
|
36
|
+
method: req.method,
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"User-Agent": this.opts.userAgent,
|
|
41
|
+
...req.headers
|
|
42
|
+
},
|
|
43
|
+
body: req.body !== void 0 ? JSON.stringify(req.body) : void 0,
|
|
44
|
+
signal: controller.signal
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new SimpleQConnectionError(
|
|
48
|
+
`Request to ${req.method} ${req.path} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
49
|
+
);
|
|
50
|
+
} finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) throw mapApiError(res.status, await this.parseBody(res), res.headers);
|
|
54
|
+
if (res.status === 204) return void 0;
|
|
55
|
+
return await this.parseBody(res);
|
|
56
|
+
}
|
|
57
|
+
async parseBody(res) {
|
|
58
|
+
const text = await res.text();
|
|
59
|
+
if (!text) return void 0;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(text);
|
|
62
|
+
} catch {
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
isRetryable(err) {
|
|
67
|
+
if (err instanceof SimpleQConnectionError) return true;
|
|
68
|
+
if (err instanceof ApiError) return RETRYABLE_STATUS.has(err.status);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
retryAfterMs(err) {
|
|
72
|
+
return err instanceof RateLimitError && typeof err.retryAfter === "number" ? err.retryAfter * 1e3 : void 0;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/client.ts
|
|
77
|
+
var DEFAULT_BASE_URL = "https://api.simpleq.io";
|
|
78
|
+
var DEFAULT_TIMEOUT_SECONDS = 30;
|
|
79
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
80
|
+
var VERSION = "0.1.0";
|
|
81
|
+
var SimpleQ = class {
|
|
82
|
+
constructor(options = {}) {
|
|
83
|
+
/** Webhook signature helpers. Usable without an API key (only a `signingSecret` is needed). */
|
|
84
|
+
this.webhooks = {
|
|
85
|
+
verifyWebhookSignature,
|
|
86
|
+
verifyWebhook
|
|
87
|
+
};
|
|
88
|
+
const apiKey = options.apiKey ?? process.env.SIMPLEQ_API_KEY;
|
|
89
|
+
if (!apiKey) {
|
|
90
|
+
throw new SimpleQError("A SimpleQ API key is required. Pass { apiKey } or set SIMPLEQ_API_KEY.");
|
|
91
|
+
}
|
|
92
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
93
|
+
if (typeof fetchImpl !== "function") {
|
|
94
|
+
throw new SimpleQError("No fetch implementation found. Use Node 18+ or pass { fetch }.");
|
|
95
|
+
}
|
|
96
|
+
this.http = new HttpClient({
|
|
97
|
+
apiKey,
|
|
98
|
+
baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
99
|
+
timeout: (options.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3,
|
|
100
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
101
|
+
fetchImpl,
|
|
102
|
+
userAgent: `simpleq-node/${VERSION}`
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Publish a job to a queue. Retries transient failures automatically; the idempotency key
|
|
107
|
+
* (yours, or one generated per call when you omit it) is reused across those retries, so a
|
|
108
|
+
* retry can never create a duplicate job. A 200 (idempotent hit) and a 201 (created) are both
|
|
109
|
+
* returned as success.
|
|
110
|
+
*/
|
|
111
|
+
async publish(queueName, params) {
|
|
112
|
+
const idempotencyKey = params.idempotencyKey ?? randomUUID();
|
|
113
|
+
const body = { payload: params.payload };
|
|
114
|
+
if (idempotencyKey !== void 0) body.idempotencyKey = idempotencyKey;
|
|
115
|
+
if (params.delay !== void 0) body.delay = params.delay;
|
|
116
|
+
return this.http.request({
|
|
117
|
+
method: "POST",
|
|
118
|
+
path: `/v1/queues/${encodeURIComponent(queueName)}/jobs`,
|
|
119
|
+
body
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Fetch a job's current status and attempt history. */
|
|
123
|
+
async getJob(jobId) {
|
|
124
|
+
return this.http.request({
|
|
125
|
+
method: "GET",
|
|
126
|
+
path: `/v1/jobs/${encodeURIComponent(jobId)}`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Ack a job (ack-mode queues): report successful completion. */
|
|
130
|
+
async ack(jobId) {
|
|
131
|
+
return this.http.request({
|
|
132
|
+
method: "POST",
|
|
133
|
+
path: `/v1/jobs/${encodeURIComponent(jobId)}/ack`,
|
|
134
|
+
body: {}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Nack a job (ack-mode queues): report failure. `retryable: false` dead-letters immediately. */
|
|
138
|
+
async nack(jobId, options = {}) {
|
|
139
|
+
return this.http.request({
|
|
140
|
+
method: "POST",
|
|
141
|
+
path: `/v1/jobs/${encodeURIComponent(jobId)}/nack`,
|
|
142
|
+
body: options
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Defer a job (ack-mode queues): apply backpressure — held and redelivered, no attempt burned. */
|
|
146
|
+
async defer(jobId, options) {
|
|
147
|
+
const { retryAfter } = options ?? {};
|
|
148
|
+
if (typeof retryAfter !== "number" || !Number.isFinite(retryAfter) || retryAfter < 0 || retryAfter > 3600) {
|
|
149
|
+
throw new ValidationError("defer requires retryAfter to be a number of seconds between 0 and 3600.", 400, void 0);
|
|
150
|
+
}
|
|
151
|
+
return this.http.request({
|
|
152
|
+
method: "POST",
|
|
153
|
+
path: `/v1/jobs/${encodeURIComponent(jobId)}/defer`,
|
|
154
|
+
body: options
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export { SimpleQ, SimpleQ as default };
|
|
160
|
+
//# sourceMappingURL=index.js.map
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/http.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// Internal fetch wrapper + retry engine. Not part of the public API.\nimport { ApiError, RateLimitError, SimpleQConnectionError, SimpleQError, mapApiError } from './errors.js';\n\nexport interface HttpClientOptions {\n apiKey: string;\n baseUrl: string;\n timeout: number;\n maxRetries: number;\n fetchImpl: typeof fetch;\n userAgent: string;\n}\n\nexport interface RequestOptions {\n method: string;\n path: string;\n body?: unknown;\n headers?: Record<string, string>;\n /** Retry transient failures (network / 5xx / 429). Default true. */\n retry?: boolean;\n}\n\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n// Exponential backoff (base 250ms, cap 8s) with full jitter.\nfunction backoffMs(attempt: number): number {\n const ceiling = Math.min(250 * 2 ** attempt, 8000);\n return Math.round(ceiling * (0.5 + Math.random() * 0.5));\n}\n\nexport class HttpClient {\n constructor(private readonly opts: HttpClientOptions) {}\n\n async request<T>(req: RequestOptions): Promise<T> {\n const maxAttempts = (req.retry ?? true) ? this.opts.maxRetries + 1 : 1;\n let lastError: unknown;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await this.attempt<T>(req);\n } catch (err) {\n lastError = err;\n if (!this.isRetryable(err) || attempt === maxAttempts - 1) throw err;\n await sleep(this.retryAfterMs(err) ?? backoffMs(attempt));\n }\n }\n // Unreachable in practice — the loop either returns or throws.\n throw lastError instanceof Error ? lastError : new SimpleQError(String(lastError));\n }\n\n private async attempt<T>(req: RequestOptions): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.opts.timeout);\n let res: Response;\n try {\n res = await this.opts.fetchImpl(this.opts.baseUrl + req.path, {\n method: req.method,\n headers: {\n Authorization: `Bearer ${this.opts.apiKey}`,\n 'Content-Type': 'application/json',\n 'User-Agent': this.opts.userAgent,\n ...req.headers,\n },\n body: req.body !== undefined ? JSON.stringify(req.body) : undefined,\n signal: controller.signal,\n });\n } catch (err) {\n throw new SimpleQConnectionError(\n `Request to ${req.method} ${req.path} failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (!res.ok) throw mapApiError(res.status, await this.parseBody(res), res.headers);\n if (res.status === 204) return undefined as T;\n return (await this.parseBody(res)) as T;\n }\n\n private async parseBody(res: Response): Promise<unknown> {\n const text = await res.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n }\n\n private isRetryable(err: unknown): boolean {\n if (err instanceof SimpleQConnectionError) return true;\n if (err instanceof ApiError) return RETRYABLE_STATUS.has(err.status);\n return false;\n }\n\n private retryAfterMs(err: unknown): number | undefined {\n return err instanceof RateLimitError && typeof err.retryAfter === 'number'\n ? err.retryAfter * 1000\n : undefined;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport { HttpClient } from './http.js';\nimport { SimpleQError, ValidationError } from './errors.js';\nimport { verifyWebhook, verifyWebhookSignature } from './webhooks.js';\nimport type {\n AckResponse,\n DeferOptions,\n Job,\n NackOptions,\n PublishJobResponse,\n PublishParams,\n SimpleQOptions,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://api.simpleq.io';\n// Customer-facing durations are seconds throughout SimpleQ; convert to ms at the fetch boundary.\nconst DEFAULT_TIMEOUT_SECONDS = 30;\nconst DEFAULT_MAX_RETRIES = 2;\n// Mirrors package.json; surfaced in the User-Agent header.\nconst VERSION = '0.1.0';\n\n/** The SimpleQ client: publish jobs, read job status, and run the ack-mode callbacks. */\nexport class SimpleQ {\n private readonly http: HttpClient;\n\n /** Webhook signature helpers. Usable without an API key (only a `signingSecret` is needed). */\n readonly webhooks = {\n verifyWebhookSignature,\n verifyWebhook,\n };\n\n constructor(options: SimpleQOptions = {}) {\n const apiKey = options.apiKey ?? process.env.SIMPLEQ_API_KEY;\n if (!apiKey) {\n throw new SimpleQError('A SimpleQ API key is required. Pass { apiKey } or set SIMPLEQ_API_KEY.');\n }\n\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new SimpleQError('No fetch implementation found. Use Node 18+ or pass { fetch }.');\n }\n\n this.http = new HttpClient({\n apiKey,\n baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, ''),\n timeout: (options.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000,\n maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,\n fetchImpl,\n userAgent: `simpleq-node/${VERSION}`,\n });\n }\n\n /**\n * Publish a job to a queue. Retries transient failures automatically; the idempotency key\n * (yours, or one generated per call when you omit it) is reused across those retries, so a\n * retry can never create a duplicate job. A 200 (idempotent hit) and a 201 (created) are both\n * returned as success.\n */\n async publish(queueName: string, params: PublishParams): Promise<PublishJobResponse> {\n const idempotencyKey = params.idempotencyKey ?? randomUUID();\n\n const body: Record<string, unknown> = { payload: params.payload };\n if (idempotencyKey !== undefined) body.idempotencyKey = idempotencyKey;\n if (params.delay !== undefined) body.delay = params.delay;\n\n return this.http.request<PublishJobResponse>({\n method: 'POST',\n path: `/v1/queues/${encodeURIComponent(queueName)}/jobs`,\n body,\n });\n }\n\n /** Fetch a job's current status and attempt history. */\n async getJob(jobId: string): Promise<Job> {\n return this.http.request<Job>({\n method: 'GET',\n path: `/v1/jobs/${encodeURIComponent(jobId)}`,\n });\n }\n\n /** Ack a job (ack-mode queues): report successful completion. */\n async ack(jobId: string): Promise<AckResponse> {\n return this.http.request<AckResponse>({\n method: 'POST',\n path: `/v1/jobs/${encodeURIComponent(jobId)}/ack`,\n body: {},\n });\n }\n\n /** Nack a job (ack-mode queues): report failure. `retryable: false` dead-letters immediately. */\n async nack(jobId: string, options: NackOptions = {}): Promise<AckResponse> {\n return this.http.request<AckResponse>({\n method: 'POST',\n path: `/v1/jobs/${encodeURIComponent(jobId)}/nack`,\n body: options,\n });\n }\n\n /** Defer a job (ack-mode queues): apply backpressure — held and redelivered, no attempt burned. */\n async defer(jobId: string, options: DeferOptions): Promise<AckResponse> {\n const { retryAfter } = options ?? {};\n if (typeof retryAfter !== 'number' || !Number.isFinite(retryAfter) || retryAfter < 0 || retryAfter > 3600) {\n throw new ValidationError('defer requires retryAfter to be a number of seconds between 0 and 3600.', 400, undefined);\n }\n return this.http.request<AckResponse>({\n method: 'POST',\n path: `/v1/jobs/${encodeURIComponent(jobId)}/defer`,\n body: options,\n });\n }\n}\n"]}
|
package/dist/nest.cjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('reflect-metadata');
|
|
4
|
+
var common = require('@nestjs/common');
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
9
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
10
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
11
|
+
if (decorator = decorators[i])
|
|
12
|
+
result = (decorator(result)) || result;
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
16
|
+
|
|
17
|
+
// src/errors.ts
|
|
18
|
+
var SimpleQError = class extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = this.constructor.name;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var SignatureVerificationError = class extends SimpleQError {
|
|
25
|
+
};
|
|
26
|
+
var SimpleQBackpressure = class _SimpleQBackpressure extends SimpleQError {
|
|
27
|
+
constructor(retryAfter, options) {
|
|
28
|
+
const detail = retryAfter != null ? ` (retry after ${retryAfter}s)` : "";
|
|
29
|
+
super(options?.reason ?? `SimpleQ backpressure${detail}`);
|
|
30
|
+
this.retryAfter = retryAfter;
|
|
31
|
+
this.status = options?.status ?? 503;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a backpressure signal directly from a provider error (Anthropic, OpenAI, any
|
|
35
|
+
* HTTP-shaped error). Reads `err.status` (429/503/529 pass through; anything else maps
|
|
36
|
+
* to 503) and the `Retry-After` header in seconds from `err.headers` or
|
|
37
|
+
* `err.response.headers` (plain object or Headers). When no header is present,
|
|
38
|
+
* `options.fallback` seconds is used; with neither, SimpleQ applies its own 60s hold.
|
|
39
|
+
*/
|
|
40
|
+
static from(err, options) {
|
|
41
|
+
const e = err;
|
|
42
|
+
const status = e?.status === 429 || e?.status === 503 || e?.status === 529 ? e.status : 503;
|
|
43
|
+
const retryAfter = retryAfterSeconds(err) ?? options?.fallback;
|
|
44
|
+
const reason = options?.reason ?? (typeof e?.message === "string" && e.message ? e.message : void 0);
|
|
45
|
+
return new _SimpleQBackpressure(retryAfter, { status, reason });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function readHeader(headers, name) {
|
|
49
|
+
if (!headers || typeof headers !== "object") return void 0;
|
|
50
|
+
if (typeof headers.get === "function") {
|
|
51
|
+
return headers.get(name) ?? void 0;
|
|
52
|
+
}
|
|
53
|
+
const record = headers;
|
|
54
|
+
const value = record[name.toLowerCase()] ?? record["Retry-After"];
|
|
55
|
+
return typeof value === "string" ? value : void 0;
|
|
56
|
+
}
|
|
57
|
+
function retryAfterSeconds(err) {
|
|
58
|
+
const e = err;
|
|
59
|
+
const raw = readHeader(e?.headers, "retry-after") ?? readHeader(e?.response?.headers, "retry-after");
|
|
60
|
+
if (raw === void 0) return void 0;
|
|
61
|
+
const seconds = Number(raw);
|
|
62
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/webhooks.ts
|
|
66
|
+
var SIGNATURE_HEADER = "x-simpleq-signature";
|
|
67
|
+
function toBuffer(rawBody) {
|
|
68
|
+
if (typeof rawBody === "string") return Buffer.from(rawBody, "utf8");
|
|
69
|
+
if (Buffer.isBuffer(rawBody)) return rawBody;
|
|
70
|
+
return Buffer.from(rawBody);
|
|
71
|
+
}
|
|
72
|
+
function expectedSignature(rawBody, signingSecret) {
|
|
73
|
+
return "sha256=" + crypto.createHmac("sha256", signingSecret).update(toBuffer(rawBody)).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
function verifyWebhookSignature(rawBody, signatureHeader, signingSecret) {
|
|
76
|
+
if (!signatureHeader) return false;
|
|
77
|
+
const expected = Buffer.from(expectedSignature(rawBody, signingSecret));
|
|
78
|
+
const received = Buffer.from(signatureHeader);
|
|
79
|
+
if (expected.length !== received.length) return false;
|
|
80
|
+
return crypto.timingSafeEqual(expected, received);
|
|
81
|
+
}
|
|
82
|
+
function verifyWebhook(rawBody, signatureHeader, signingSecret) {
|
|
83
|
+
if (!verifyWebhookSignature(rawBody, signatureHeader, signingSecret)) {
|
|
84
|
+
throw new SignatureVerificationError("SimpleQ webhook signature verification failed");
|
|
85
|
+
}
|
|
86
|
+
const text = typeof rawBody === "string" ? rawBody : toBuffer(rawBody).toString("utf8");
|
|
87
|
+
return JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/nest.ts
|
|
91
|
+
var SIMPLEQ_WEBHOOK_OPTIONS = "SIMPLEQ_WEBHOOK_OPTIONS";
|
|
92
|
+
function firstHeader(value) {
|
|
93
|
+
return Array.isArray(value) ? value[0] : value;
|
|
94
|
+
}
|
|
95
|
+
exports.SimpleQSignatureGuard = class SimpleQSignatureGuard {
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.options = options;
|
|
98
|
+
}
|
|
99
|
+
canActivate(context) {
|
|
100
|
+
const req = context.switchToHttp().getRequest();
|
|
101
|
+
if (!req.rawBody) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"SimpleQ: request.rawBody is undefined \u2014 create the app with NestFactory.create(App, { rawBody: true })."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
req.simpleqJob = verifyWebhook(
|
|
108
|
+
req.rawBody,
|
|
109
|
+
firstHeader(req.headers[SIGNATURE_HEADER]),
|
|
110
|
+
this.options.signingSecret
|
|
111
|
+
);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new common.UnauthorizedException("Invalid SimpleQ webhook signature");
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
exports.SimpleQSignatureGuard = __decorateClass([
|
|
119
|
+
common.Injectable(),
|
|
120
|
+
__decorateParam(0, common.Inject(SIMPLEQ_WEBHOOK_OPTIONS))
|
|
121
|
+
], exports.SimpleQSignatureGuard);
|
|
122
|
+
var SimpleQJob = common.createParamDecorator(
|
|
123
|
+
(_data, context) => {
|
|
124
|
+
const req = context.switchToHttp().getRequest();
|
|
125
|
+
if (!req.simpleqJob) {
|
|
126
|
+
throw new Error("SimpleQ: @SimpleQJob() requires SimpleQSignatureGuard \u2014 apply @UseGuards(SimpleQSignatureGuard).");
|
|
127
|
+
}
|
|
128
|
+
return req.simpleqJob;
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
exports.SimpleQBackpressureFilter = class SimpleQBackpressureFilter {
|
|
132
|
+
catch(exception, host) {
|
|
133
|
+
const res = host.switchToHttp().getResponse();
|
|
134
|
+
if (exception.retryAfter != null) res.setHeader("Retry-After", String(exception.retryAfter));
|
|
135
|
+
res.status(exception.status).end();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
exports.SimpleQBackpressureFilter = __decorateClass([
|
|
139
|
+
common.Catch(SimpleQBackpressure)
|
|
140
|
+
], exports.SimpleQBackpressureFilter);
|
|
141
|
+
var PROVIDERS = [exports.SimpleQSignatureGuard, exports.SimpleQBackpressureFilter];
|
|
142
|
+
var EXPORTS = [SIMPLEQ_WEBHOOK_OPTIONS, exports.SimpleQSignatureGuard, exports.SimpleQBackpressureFilter];
|
|
143
|
+
exports.SimpleQModule = class SimpleQModule {
|
|
144
|
+
static forRoot(options) {
|
|
145
|
+
return {
|
|
146
|
+
module: exports.SimpleQModule,
|
|
147
|
+
providers: [{ provide: SIMPLEQ_WEBHOOK_OPTIONS, useValue: options }, ...PROVIDERS],
|
|
148
|
+
exports: EXPORTS
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
static forRootAsync(options) {
|
|
152
|
+
return {
|
|
153
|
+
module: exports.SimpleQModule,
|
|
154
|
+
imports: options.imports ?? [],
|
|
155
|
+
providers: [
|
|
156
|
+
{
|
|
157
|
+
provide: SIMPLEQ_WEBHOOK_OPTIONS,
|
|
158
|
+
useFactory: options.useFactory,
|
|
159
|
+
inject: options.inject ?? []
|
|
160
|
+
},
|
|
161
|
+
...PROVIDERS
|
|
162
|
+
],
|
|
163
|
+
exports: EXPORTS
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
exports.SimpleQModule = __decorateClass([
|
|
168
|
+
common.Module({})
|
|
169
|
+
], exports.SimpleQModule);
|
|
170
|
+
|
|
171
|
+
exports.SIMPLEQ_WEBHOOK_OPTIONS = SIMPLEQ_WEBHOOK_OPTIONS;
|
|
172
|
+
exports.SimpleQJob = SimpleQJob;
|
|
173
|
+
//# sourceMappingURL=nest.cjs.map
|
|
174
|
+
//# sourceMappingURL=nest.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/webhooks.ts","../src/nest.ts"],"names":["createHmac","timingSafeEqual","SimpleQSignatureGuard","UnauthorizedException","Injectable","createParamDecorator","SimpleQBackpressureFilter","Catch","SimpleQModule","Module"],"mappings":"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","file":"nest.cjs","sourcesContent":["// Error and signal types for @simpleq/sdk.\n\n/** Base class for everything thrown by the SDK. Catch this to handle any SimpleQ error. */\nexport class SimpleQError extends Error {\n constructor(message: string) {\n super(message);\n this.name = this.constructor.name;\n }\n}\n\n/** A network failure, timeout, or aborted request — retryable. */\nexport class SimpleQConnectionError extends SimpleQError {}\n\n/** Thrown by `verifyWebhook` when a webhook signature does not verify. */\nexport class SignatureVerificationError extends SimpleQError {}\n\nexport type BackpressureStatus = 429 | 503 | 529;\n\n/**\n * A backpressure signal — not a failure. Throw this from a standard-mode webhook handler to\n * tell the adapter to respond with `429`/`503`/`529` and a `Retry-After`. SimpleQ then holds\n * the job and redelivers it without burning a delivery attempt.\n */\nexport class SimpleQBackpressure extends SimpleQError {\n /** Seconds to hold the job before redelivery. Omit to let SimpleQ pick its fallback. */\n readonly retryAfter?: number;\n /** HTTP status the adapter responds with. Defaults to `503`. */\n readonly status: BackpressureStatus;\n\n constructor(retryAfter?: number, options?: { status?: BackpressureStatus; reason?: string }) {\n const detail = retryAfter != null ? ` (retry after ${retryAfter}s)` : '';\n super(options?.reason ?? `SimpleQ backpressure${detail}`);\n this.retryAfter = retryAfter;\n this.status = options?.status ?? 503;\n }\n\n /**\n * Build a backpressure signal directly from a provider error (Anthropic, OpenAI, any\n * HTTP-shaped error). Reads `err.status` (429/503/529 pass through; anything else maps\n * to 503) and the `Retry-After` header in seconds from `err.headers` or\n * `err.response.headers` (plain object or Headers). When no header is present,\n * `options.fallback` seconds is used; with neither, SimpleQ applies its own 60s hold.\n */\n static from(err: unknown, options?: { fallback?: number; reason?: string }): SimpleQBackpressure {\n const e = err as { status?: unknown; message?: unknown } | null | undefined;\n const status: BackpressureStatus =\n e?.status === 429 || e?.status === 503 || e?.status === 529 ? e.status : 503;\n const retryAfter = retryAfterSeconds(err) ?? options?.fallback;\n const reason =\n options?.reason ?? (typeof e?.message === 'string' && e.message ? e.message : undefined);\n return new SimpleQBackpressure(retryAfter, { status, reason });\n }\n}\n\nfunction readHeader(headers: unknown, name: string): string | undefined {\n if (!headers || typeof headers !== 'object') return undefined;\n if (typeof (headers as Headers).get === 'function') {\n return (headers as Headers).get(name) ?? undefined;\n }\n const record = headers as Record<string, unknown>;\n const value = record[name.toLowerCase()] ?? record['Retry-After'];\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Read the `Retry-After` value, in **seconds**, from a provider error (Anthropic, OpenAI, any\n * HTTP-shaped error). Looks at `err.headers` and `err.response.headers` (plain object or a\n * `Headers` instance). Returns `undefined` when the header is absent or non-numeric (e.g. an\n * HTTP-date). Pair with `simpleq.defer` in ack mode: `defer(id, { retryAfter: retryAfterSeconds(err) ?? 10 })`.\n */\nexport function retryAfterSeconds(err: unknown): number | undefined {\n const e = err as { headers?: unknown; response?: { headers?: unknown } } | null | undefined;\n const raw =\n readHeader(e?.headers, 'retry-after') ?? readHeader(e?.response?.headers, 'retry-after');\n if (raw === undefined) return undefined;\n const seconds = Number(raw);\n return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;\n}\n\n/** Any non-2xx response from the SimpleQ API. */\nexport class ApiError extends SimpleQError {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.status = status;\n this.body = body;\n }\n}\n\n/** `401`/`403` — the API key is missing, invalid, or revoked. */\nexport class AuthenticationError extends ApiError {}\n\n/** `400` — request validation failed. `body.error` carries the field-level details. */\nexport class ValidationError extends ApiError {}\n\n/** `404` — the queue or job was not found. */\nexport class NotFoundError extends ApiError {}\n\n/** `429` — rate limited. `retryAfter` is the `Retry-After` header in seconds, if present. */\nexport class RateLimitError extends ApiError {\n readonly retryAfter?: number;\n\n constructor(message: string, status: number, body: unknown, retryAfter?: number) {\n super(message, status, body);\n this.retryAfter = retryAfter;\n }\n}\n\nfunction extractMessage(status: number, body: unknown): string {\n if (body && typeof body === 'object' && 'error' in body) {\n const err = (body as { error: unknown }).error;\n if (typeof err === 'string') return err;\n if (err && typeof err === 'object') return `Validation failed: ${JSON.stringify(err)}`;\n }\n return `SimpleQ API error (HTTP ${status})`;\n}\n\n/** Map an HTTP status + parsed body to the right ApiError subclass. */\nexport function mapApiError(status: number, body: unknown, headers?: Headers): ApiError {\n const message = extractMessage(status, body);\n switch (status) {\n case 400:\n return new ValidationError(message, status, body);\n case 401:\n case 403:\n return new AuthenticationError(message, status, body);\n case 404:\n return new NotFoundError(message, status, body);\n case 429: {\n const raw = headers?.get('retry-after');\n const retryAfter = raw != null ? Number(raw) : NaN;\n return new RateLimitError(message, status, body, Number.isFinite(retryAfter) ? retryAfter : undefined);\n }\n default:\n return new ApiError(message, status, body);\n }\n}\n","// Standalone webhook verification — no API key or client required. Importable as\n// `@simpleq/sdk/webhooks` with only `node:crypto` pulled in.\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport { SignatureVerificationError } from './errors.js';\nimport type { WebhookPayload } from './types.js';\n\n/** The header SimpleQ sends with every webhook delivery. */\nexport const SIGNATURE_HEADER = 'x-simpleq-signature';\n\nfunction toBuffer(rawBody: string | Buffer | Uint8Array): Buffer {\n if (typeof rawBody === 'string') return Buffer.from(rawBody, 'utf8');\n if (Buffer.isBuffer(rawBody)) return rawBody;\n return Buffer.from(rawBody);\n}\n\nfunction expectedSignature(rawBody: string | Buffer | Uint8Array, signingSecret: string): string {\n return 'sha256=' + createHmac('sha256', signingSecret).update(toBuffer(rawBody)).digest('hex');\n}\n\n/**\n * Verify the `x-simpleq-signature` header against the raw request body, in constant time.\n * Returns a boolean and never throws — a missing or malformed header simply returns `false`.\n *\n * Always pass the *raw* body bytes (a string or Buffer), never a re-serialized parsed object.\n */\nexport function verifyWebhookSignature(\n rawBody: string | Buffer | Uint8Array,\n signatureHeader: string | null | undefined,\n signingSecret: string,\n): boolean {\n if (!signatureHeader) return false;\n const expected = Buffer.from(expectedSignature(rawBody, signingSecret));\n const received = Buffer.from(signatureHeader);\n // timingSafeEqual throws on differing lengths — guard before the constant-time compare.\n if (expected.length !== received.length) return false;\n return timingSafeEqual(expected, received);\n}\n\n/**\n * Verify the signature and return the parsed, typed webhook envelope (the equivalent of\n * Stripe's `constructEvent`). Throws `SignatureVerificationError` if the signature does\n * not match — the body is only parsed after verification passes, so a tampered payload\n * never reaches `JSON.parse`.\n */\nexport function verifyWebhook(\n rawBody: string | Buffer | Uint8Array,\n signatureHeader: string | null | undefined,\n signingSecret: string,\n): WebhookPayload {\n if (!verifyWebhookSignature(rawBody, signatureHeader, signingSecret)) {\n throw new SignatureVerificationError('SimpleQ webhook signature verification failed');\n }\n const text = typeof rawBody === 'string' ? rawBody : toBuffer(rawBody).toString('utf8');\n return JSON.parse(text) as WebhookPayload;\n}\n","// Nest.js adapter — `@simpleq/sdk/nest`. Requires the optional `@nestjs/common` peer and an\n// app created with `NestFactory.create(App, { rawBody: true })` so `request.rawBody` is set.\nimport 'reflect-metadata';\nimport {\n Catch,\n Inject,\n Injectable,\n Module,\n UnauthorizedException,\n createParamDecorator,\n} from '@nestjs/common';\nimport type {\n ArgumentsHost,\n CanActivate,\n DynamicModule,\n ExceptionFilter,\n ExecutionContext,\n InjectionToken,\n ModuleMetadata,\n OptionalFactoryDependency,\n Provider,\n} from '@nestjs/common';\nimport { verifyWebhook, SIGNATURE_HEADER } from './webhooks.js';\nimport { SimpleQBackpressure } from './errors.js';\nimport type { WebhookPayload } from './types.js';\n\n/** Injection token holding the resolved `SimpleQModule` options. */\nexport const SIMPLEQ_WEBHOOK_OPTIONS = 'SIMPLEQ_WEBHOOK_OPTIONS';\n\nexport interface SimpleQNestOptions {\n signingSecret: string;\n}\n\nexport interface SimpleQNestAsyncOptions extends Pick<ModuleMetadata, 'imports'> {\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useFactory: (...args: any[]) => SimpleQNestOptions | Promise<SimpleQNestOptions>;\n}\n\ninterface RawRequest {\n headers: Record<string, string | string[] | undefined>;\n rawBody?: Buffer;\n}\n\ninterface RequestWithJob extends RawRequest {\n simpleqJob?: WebhookPayload;\n}\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\n/**\n * Guard that verifies the `x-simpleq-signature` header against `request.rawBody`. On success it\n * stashes the parsed job for `@SimpleQJob()`; on failure it throws `UnauthorizedException` (401).\n */\n@Injectable()\nexport class SimpleQSignatureGuard implements CanActivate {\n constructor(@Inject(SIMPLEQ_WEBHOOK_OPTIONS) private readonly options: SimpleQNestOptions) {}\n\n canActivate(context: ExecutionContext): boolean {\n const req = context.switchToHttp().getRequest<RequestWithJob>();\n if (!req.rawBody) {\n throw new Error(\n 'SimpleQ: request.rawBody is undefined — create the app with NestFactory.create(App, { rawBody: true }).',\n );\n }\n try {\n req.simpleqJob = verifyWebhook(\n req.rawBody,\n firstHeader(req.headers[SIGNATURE_HEADER]),\n this.options.signingSecret,\n );\n } catch {\n throw new UnauthorizedException('Invalid SimpleQ webhook signature');\n }\n return true;\n }\n}\n\n/** Parameter decorator that injects the verified, typed webhook payload into a route handler. */\nexport const SimpleQJob = createParamDecorator(\n (_data: unknown, context: ExecutionContext): WebhookPayload => {\n const req = context.switchToHttp().getRequest<RequestWithJob>();\n if (!req.simpleqJob) {\n throw new Error('SimpleQ: @SimpleQJob() requires SimpleQSignatureGuard — apply @UseGuards(SimpleQSignatureGuard).');\n }\n return req.simpleqJob;\n },\n);\n\ninterface BackpressureResponse {\n setHeader(name: string, value: string): unknown;\n status(code: number): { end(): unknown };\n}\n\n/** Exception filter mapping a thrown `SimpleQBackpressure` to its status + `Retry-After` header. */\n@Catch(SimpleQBackpressure)\nexport class SimpleQBackpressureFilter implements ExceptionFilter {\n catch(exception: SimpleQBackpressure, host: ArgumentsHost): void {\n const res = host.switchToHttp().getResponse<BackpressureResponse>();\n if (exception.retryAfter != null) res.setHeader('Retry-After', String(exception.retryAfter));\n res.status(exception.status).end();\n }\n}\n\nconst PROVIDERS: Provider[] = [SimpleQSignatureGuard, SimpleQBackpressureFilter];\nconst EXPORTS = [SIMPLEQ_WEBHOOK_OPTIONS, SimpleQSignatureGuard, SimpleQBackpressureFilter];\n\n/** Provides the signing secret plus the guard and backpressure filter to your Nest app. */\n@Module({})\nexport class SimpleQModule {\n static forRoot(options: SimpleQNestOptions): DynamicModule {\n return {\n module: SimpleQModule,\n providers: [{ provide: SIMPLEQ_WEBHOOK_OPTIONS, useValue: options }, ...PROVIDERS],\n exports: EXPORTS,\n };\n }\n\n static forRootAsync(options: SimpleQNestAsyncOptions): DynamicModule {\n return {\n module: SimpleQModule,\n imports: options.imports ?? [],\n providers: [\n {\n provide: SIMPLEQ_WEBHOOK_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n ...PROVIDERS,\n ],\n exports: EXPORTS,\n };\n }\n}\n"]}
|
package/dist/nest.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ExceptionFilter, ArgumentsHost, DynamicModule, ModuleMetadata, InjectionToken, OptionalFactoryDependency, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { b as SimpleQBackpressure } from './errors-D7trszMq.cjs';
|
|
3
|
+
|
|
4
|
+
/** Injection token holding the resolved `SimpleQModule` options. */
|
|
5
|
+
declare const SIMPLEQ_WEBHOOK_OPTIONS = "SIMPLEQ_WEBHOOK_OPTIONS";
|
|
6
|
+
interface SimpleQNestOptions {
|
|
7
|
+
signingSecret: string;
|
|
8
|
+
}
|
|
9
|
+
interface SimpleQNestAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
10
|
+
inject?: Array<InjectionToken | OptionalFactoryDependency>;
|
|
11
|
+
useFactory: (...args: any[]) => SimpleQNestOptions | Promise<SimpleQNestOptions>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Guard that verifies the `x-simpleq-signature` header against `request.rawBody`. On success it
|
|
15
|
+
* stashes the parsed job for `@SimpleQJob()`; on failure it throws `UnauthorizedException` (401).
|
|
16
|
+
*/
|
|
17
|
+
declare class SimpleQSignatureGuard implements CanActivate {
|
|
18
|
+
private readonly options;
|
|
19
|
+
constructor(options: SimpleQNestOptions);
|
|
20
|
+
canActivate(context: ExecutionContext): boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Parameter decorator that injects the verified, typed webhook payload into a route handler. */
|
|
23
|
+
declare const SimpleQJob: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
24
|
+
/** Exception filter mapping a thrown `SimpleQBackpressure` to its status + `Retry-After` header. */
|
|
25
|
+
declare class SimpleQBackpressureFilter implements ExceptionFilter {
|
|
26
|
+
catch(exception: SimpleQBackpressure, host: ArgumentsHost): void;
|
|
27
|
+
}
|
|
28
|
+
/** Provides the signing secret plus the guard and backpressure filter to your Nest app. */
|
|
29
|
+
declare class SimpleQModule {
|
|
30
|
+
static forRoot(options: SimpleQNestOptions): DynamicModule;
|
|
31
|
+
static forRootAsync(options: SimpleQNestAsyncOptions): DynamicModule;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { SIMPLEQ_WEBHOOK_OPTIONS, SimpleQBackpressureFilter, SimpleQJob, SimpleQModule, type SimpleQNestAsyncOptions, type SimpleQNestOptions, SimpleQSignatureGuard };
|
package/dist/nest.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ExceptionFilter, ArgumentsHost, DynamicModule, ModuleMetadata, InjectionToken, OptionalFactoryDependency, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { b as SimpleQBackpressure } from './errors-D7trszMq.js';
|
|
3
|
+
|
|
4
|
+
/** Injection token holding the resolved `SimpleQModule` options. */
|
|
5
|
+
declare const SIMPLEQ_WEBHOOK_OPTIONS = "SIMPLEQ_WEBHOOK_OPTIONS";
|
|
6
|
+
interface SimpleQNestOptions {
|
|
7
|
+
signingSecret: string;
|
|
8
|
+
}
|
|
9
|
+
interface SimpleQNestAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
10
|
+
inject?: Array<InjectionToken | OptionalFactoryDependency>;
|
|
11
|
+
useFactory: (...args: any[]) => SimpleQNestOptions | Promise<SimpleQNestOptions>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Guard that verifies the `x-simpleq-signature` header against `request.rawBody`. On success it
|
|
15
|
+
* stashes the parsed job for `@SimpleQJob()`; on failure it throws `UnauthorizedException` (401).
|
|
16
|
+
*/
|
|
17
|
+
declare class SimpleQSignatureGuard implements CanActivate {
|
|
18
|
+
private readonly options;
|
|
19
|
+
constructor(options: SimpleQNestOptions);
|
|
20
|
+
canActivate(context: ExecutionContext): boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Parameter decorator that injects the verified, typed webhook payload into a route handler. */
|
|
23
|
+
declare const SimpleQJob: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
24
|
+
/** Exception filter mapping a thrown `SimpleQBackpressure` to its status + `Retry-After` header. */
|
|
25
|
+
declare class SimpleQBackpressureFilter implements ExceptionFilter {
|
|
26
|
+
catch(exception: SimpleQBackpressure, host: ArgumentsHost): void;
|
|
27
|
+
}
|
|
28
|
+
/** Provides the signing secret plus the guard and backpressure filter to your Nest app. */
|
|
29
|
+
declare class SimpleQModule {
|
|
30
|
+
static forRoot(options: SimpleQNestOptions): DynamicModule;
|
|
31
|
+
static forRootAsync(options: SimpleQNestAsyncOptions): DynamicModule;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { SIMPLEQ_WEBHOOK_OPTIONS, SimpleQBackpressureFilter, SimpleQJob, SimpleQModule, type SimpleQNestAsyncOptions, type SimpleQNestOptions, SimpleQSignatureGuard };
|
package/dist/nest.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { __decorateClass, __decorateParam, SimpleQBackpressure, verifyWebhook, SIGNATURE_HEADER } from './chunk-72DDDNF6.js';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { Injectable, Inject, createParamDecorator, Catch, Module, UnauthorizedException } from '@nestjs/common';
|
|
4
|
+
|
|
5
|
+
var SIMPLEQ_WEBHOOK_OPTIONS = "SIMPLEQ_WEBHOOK_OPTIONS";
|
|
6
|
+
function firstHeader(value) {
|
|
7
|
+
return Array.isArray(value) ? value[0] : value;
|
|
8
|
+
}
|
|
9
|
+
var SimpleQSignatureGuard = class {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
canActivate(context) {
|
|
14
|
+
const req = context.switchToHttp().getRequest();
|
|
15
|
+
if (!req.rawBody) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"SimpleQ: request.rawBody is undefined \u2014 create the app with NestFactory.create(App, { rawBody: true })."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
req.simpleqJob = verifyWebhook(
|
|
22
|
+
req.rawBody,
|
|
23
|
+
firstHeader(req.headers[SIGNATURE_HEADER]),
|
|
24
|
+
this.options.signingSecret
|
|
25
|
+
);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new UnauthorizedException("Invalid SimpleQ webhook signature");
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
SimpleQSignatureGuard = __decorateClass([
|
|
33
|
+
Injectable(),
|
|
34
|
+
__decorateParam(0, Inject(SIMPLEQ_WEBHOOK_OPTIONS))
|
|
35
|
+
], SimpleQSignatureGuard);
|
|
36
|
+
var SimpleQJob = createParamDecorator(
|
|
37
|
+
(_data, context) => {
|
|
38
|
+
const req = context.switchToHttp().getRequest();
|
|
39
|
+
if (!req.simpleqJob) {
|
|
40
|
+
throw new Error("SimpleQ: @SimpleQJob() requires SimpleQSignatureGuard \u2014 apply @UseGuards(SimpleQSignatureGuard).");
|
|
41
|
+
}
|
|
42
|
+
return req.simpleqJob;
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
var SimpleQBackpressureFilter = class {
|
|
46
|
+
catch(exception, host) {
|
|
47
|
+
const res = host.switchToHttp().getResponse();
|
|
48
|
+
if (exception.retryAfter != null) res.setHeader("Retry-After", String(exception.retryAfter));
|
|
49
|
+
res.status(exception.status).end();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
SimpleQBackpressureFilter = __decorateClass([
|
|
53
|
+
Catch(SimpleQBackpressure)
|
|
54
|
+
], SimpleQBackpressureFilter);
|
|
55
|
+
var PROVIDERS = [SimpleQSignatureGuard, SimpleQBackpressureFilter];
|
|
56
|
+
var EXPORTS = [SIMPLEQ_WEBHOOK_OPTIONS, SimpleQSignatureGuard, SimpleQBackpressureFilter];
|
|
57
|
+
var SimpleQModule = class {
|
|
58
|
+
static forRoot(options) {
|
|
59
|
+
return {
|
|
60
|
+
module: SimpleQModule,
|
|
61
|
+
providers: [{ provide: SIMPLEQ_WEBHOOK_OPTIONS, useValue: options }, ...PROVIDERS],
|
|
62
|
+
exports: EXPORTS
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
static forRootAsync(options) {
|
|
66
|
+
return {
|
|
67
|
+
module: SimpleQModule,
|
|
68
|
+
imports: options.imports ?? [],
|
|
69
|
+
providers: [
|
|
70
|
+
{
|
|
71
|
+
provide: SIMPLEQ_WEBHOOK_OPTIONS,
|
|
72
|
+
useFactory: options.useFactory,
|
|
73
|
+
inject: options.inject ?? []
|
|
74
|
+
},
|
|
75
|
+
...PROVIDERS
|
|
76
|
+
],
|
|
77
|
+
exports: EXPORTS
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
SimpleQModule = __decorateClass([
|
|
82
|
+
Module({})
|
|
83
|
+
], SimpleQModule);
|
|
84
|
+
|
|
85
|
+
export { SIMPLEQ_WEBHOOK_OPTIONS, SimpleQBackpressureFilter, SimpleQJob, SimpleQModule, SimpleQSignatureGuard };
|
|
86
|
+
//# sourceMappingURL=nest.js.map
|
|
87
|
+
//# sourceMappingURL=nest.js.map
|
package/dist/nest.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/nest.ts"],"names":[],"mappings":"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","file":"nest.js","sourcesContent":["// Nest.js adapter — `@simpleq/sdk/nest`. Requires the optional `@nestjs/common` peer and an\n// app created with `NestFactory.create(App, { rawBody: true })` so `request.rawBody` is set.\nimport 'reflect-metadata';\nimport {\n Catch,\n Inject,\n Injectable,\n Module,\n UnauthorizedException,\n createParamDecorator,\n} from '@nestjs/common';\nimport type {\n ArgumentsHost,\n CanActivate,\n DynamicModule,\n ExceptionFilter,\n ExecutionContext,\n InjectionToken,\n ModuleMetadata,\n OptionalFactoryDependency,\n Provider,\n} from '@nestjs/common';\nimport { verifyWebhook, SIGNATURE_HEADER } from './webhooks.js';\nimport { SimpleQBackpressure } from './errors.js';\nimport type { WebhookPayload } from './types.js';\n\n/** Injection token holding the resolved `SimpleQModule` options. */\nexport const SIMPLEQ_WEBHOOK_OPTIONS = 'SIMPLEQ_WEBHOOK_OPTIONS';\n\nexport interface SimpleQNestOptions {\n signingSecret: string;\n}\n\nexport interface SimpleQNestAsyncOptions extends Pick<ModuleMetadata, 'imports'> {\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useFactory: (...args: any[]) => SimpleQNestOptions | Promise<SimpleQNestOptions>;\n}\n\ninterface RawRequest {\n headers: Record<string, string | string[] | undefined>;\n rawBody?: Buffer;\n}\n\ninterface RequestWithJob extends RawRequest {\n simpleqJob?: WebhookPayload;\n}\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\n/**\n * Guard that verifies the `x-simpleq-signature` header against `request.rawBody`. On success it\n * stashes the parsed job for `@SimpleQJob()`; on failure it throws `UnauthorizedException` (401).\n */\n@Injectable()\nexport class SimpleQSignatureGuard implements CanActivate {\n constructor(@Inject(SIMPLEQ_WEBHOOK_OPTIONS) private readonly options: SimpleQNestOptions) {}\n\n canActivate(context: ExecutionContext): boolean {\n const req = context.switchToHttp().getRequest<RequestWithJob>();\n if (!req.rawBody) {\n throw new Error(\n 'SimpleQ: request.rawBody is undefined — create the app with NestFactory.create(App, { rawBody: true }).',\n );\n }\n try {\n req.simpleqJob = verifyWebhook(\n req.rawBody,\n firstHeader(req.headers[SIGNATURE_HEADER]),\n this.options.signingSecret,\n );\n } catch {\n throw new UnauthorizedException('Invalid SimpleQ webhook signature');\n }\n return true;\n }\n}\n\n/** Parameter decorator that injects the verified, typed webhook payload into a route handler. */\nexport const SimpleQJob = createParamDecorator(\n (_data: unknown, context: ExecutionContext): WebhookPayload => {\n const req = context.switchToHttp().getRequest<RequestWithJob>();\n if (!req.simpleqJob) {\n throw new Error('SimpleQ: @SimpleQJob() requires SimpleQSignatureGuard — apply @UseGuards(SimpleQSignatureGuard).');\n }\n return req.simpleqJob;\n },\n);\n\ninterface BackpressureResponse {\n setHeader(name: string, value: string): unknown;\n status(code: number): { end(): unknown };\n}\n\n/** Exception filter mapping a thrown `SimpleQBackpressure` to its status + `Retry-After` header. */\n@Catch(SimpleQBackpressure)\nexport class SimpleQBackpressureFilter implements ExceptionFilter {\n catch(exception: SimpleQBackpressure, host: ArgumentsHost): void {\n const res = host.switchToHttp().getResponse<BackpressureResponse>();\n if (exception.retryAfter != null) res.setHeader('Retry-After', String(exception.retryAfter));\n res.status(exception.status).end();\n }\n}\n\nconst PROVIDERS: Provider[] = [SimpleQSignatureGuard, SimpleQBackpressureFilter];\nconst EXPORTS = [SIMPLEQ_WEBHOOK_OPTIONS, SimpleQSignatureGuard, SimpleQBackpressureFilter];\n\n/** Provides the signing secret plus the guard and backpressure filter to your Nest app. */\n@Module({})\nexport class SimpleQModule {\n static forRoot(options: SimpleQNestOptions): DynamicModule {\n return {\n module: SimpleQModule,\n providers: [{ provide: SIMPLEQ_WEBHOOK_OPTIONS, useValue: options }, ...PROVIDERS],\n exports: EXPORTS,\n };\n }\n\n static forRootAsync(options: SimpleQNestAsyncOptions): DynamicModule {\n return {\n module: SimpleQModule,\n imports: options.imports ?? [],\n providers: [\n {\n provide: SIMPLEQ_WEBHOOK_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n ...PROVIDERS,\n ],\n exports: EXPORTS,\n };\n }\n}\n"]}
|