@upstash/qstash 2.8.4 → 2.9.0-rc

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/nextjs.js CHANGED
@@ -41,6 +41,510 @@ module.exports = __toCommonJS(nextjs_exports);
41
41
  // src/receiver.ts
42
42
  var jose = __toESM(require("jose"));
43
43
  var import_crypto_js = __toESM(require("crypto-js"));
44
+
45
+ // src/client/api/base.ts
46
+ var BaseProvider = class {
47
+ baseUrl;
48
+ token;
49
+ owner;
50
+ constructor(baseUrl, token, owner) {
51
+ this.baseUrl = baseUrl;
52
+ this.token = token;
53
+ this.owner = owner;
54
+ }
55
+ getUrl() {
56
+ return `${this.baseUrl}/${this.getRoute().join("/")}`;
57
+ }
58
+ };
59
+
60
+ // src/client/api/llm.ts
61
+ var LLMProvider = class extends BaseProvider {
62
+ apiKind = "llm";
63
+ organization;
64
+ method = "POST";
65
+ constructor(baseUrl, token, owner, organization) {
66
+ super(baseUrl, token, owner);
67
+ this.organization = organization;
68
+ }
69
+ getRoute() {
70
+ return this.owner === "anthropic" ? ["v1", "messages"] : ["v1", "chat", "completions"];
71
+ }
72
+ getHeaders(options) {
73
+ if (this.owner === "upstash" && !options.analytics) {
74
+ return { "content-type": "application/json" };
75
+ }
76
+ const header = this.owner === "anthropic" ? "x-api-key" : "authorization";
77
+ const headerValue = this.owner === "anthropic" ? this.token : `Bearer ${this.token}`;
78
+ const headers = {
79
+ [header]: headerValue,
80
+ "content-type": "application/json"
81
+ };
82
+ if (this.owner === "openai" && this.organization) {
83
+ headers["OpenAI-Organization"] = this.organization;
84
+ }
85
+ if (this.owner === "anthropic") {
86
+ headers["anthropic-version"] = "2023-06-01";
87
+ }
88
+ return headers;
89
+ }
90
+ /**
91
+ * Checks if callback exists and adds analytics in place if it's set.
92
+ *
93
+ * @param request
94
+ * @param options
95
+ */
96
+ onFinish(providerInfo, options) {
97
+ if (options.analytics) {
98
+ return updateWithAnalytics(providerInfo, options.analytics);
99
+ }
100
+ return providerInfo;
101
+ }
102
+ };
103
+ var upstash = () => {
104
+ return new LLMProvider("https://qstash.upstash.io/llm", "", "upstash");
105
+ };
106
+
107
+ // src/client/api/utils.ts
108
+ var getProviderInfo = (api, upstashToken) => {
109
+ const { name, provider, ...parameters } = api;
110
+ const finalProvider = provider ?? upstash();
111
+ if (finalProvider.owner === "upstash" && !finalProvider.token) {
112
+ finalProvider.token = upstashToken;
113
+ }
114
+ if (!finalProvider.baseUrl)
115
+ throw new TypeError("baseUrl cannot be empty or undefined!");
116
+ if (!finalProvider.token)
117
+ throw new TypeError("token cannot be empty or undefined!");
118
+ if (finalProvider.apiKind !== name) {
119
+ throw new TypeError(
120
+ `Unexpected api name. Expected '${finalProvider.apiKind}', received ${name}`
121
+ );
122
+ }
123
+ const providerInfo = {
124
+ url: finalProvider.getUrl(),
125
+ baseUrl: finalProvider.baseUrl,
126
+ route: finalProvider.getRoute(),
127
+ appendHeaders: finalProvider.getHeaders(parameters),
128
+ owner: finalProvider.owner,
129
+ method: finalProvider.method
130
+ };
131
+ return finalProvider.onFinish(providerInfo, parameters);
132
+ };
133
+ var safeJoinHeaders = (headers, record) => {
134
+ const joinedHeaders = new Headers(record);
135
+ for (const [header, value] of headers.entries()) {
136
+ joinedHeaders.set(header, value);
137
+ }
138
+ return joinedHeaders;
139
+ };
140
+ var processApi = (request, headers, upstashToken) => {
141
+ if (!request.api) {
142
+ request.headers = headers;
143
+ return request;
144
+ }
145
+ const { url, appendHeaders, owner, method } = getProviderInfo(request.api, upstashToken);
146
+ if (request.api.name === "llm") {
147
+ const callback = request.callback;
148
+ if (!callback) {
149
+ throw new TypeError("Callback cannot be undefined when using LLM api.");
150
+ }
151
+ return {
152
+ ...request,
153
+ method: request.method ?? method,
154
+ headers: safeJoinHeaders(headers, appendHeaders),
155
+ ...owner === "upstash" && !request.api.analytics ? { api: { name: "llm" }, url: void 0, callback } : { url, api: void 0 }
156
+ };
157
+ } else {
158
+ return {
159
+ ...request,
160
+ method: request.method ?? method,
161
+ headers: safeJoinHeaders(headers, appendHeaders),
162
+ url,
163
+ api: void 0
164
+ };
165
+ }
166
+ };
167
+ function updateWithAnalytics(providerInfo, analytics) {
168
+ switch (analytics.name) {
169
+ case "helicone": {
170
+ providerInfo.appendHeaders["Helicone-Auth"] = `Bearer ${analytics.token}`;
171
+ if (providerInfo.owner === "upstash") {
172
+ updateProviderInfo(providerInfo, "https://qstash.helicone.ai", [
173
+ "llm",
174
+ ...providerInfo.route
175
+ ]);
176
+ } else {
177
+ providerInfo.appendHeaders["Helicone-Target-Url"] = providerInfo.baseUrl;
178
+ updateProviderInfo(providerInfo, "https://gateway.helicone.ai", providerInfo.route);
179
+ }
180
+ return providerInfo;
181
+ }
182
+ default: {
183
+ throw new Error("Unknown analytics provider");
184
+ }
185
+ }
186
+ }
187
+ function updateProviderInfo(providerInfo, baseUrl, route) {
188
+ providerInfo.baseUrl = baseUrl;
189
+ providerInfo.route = route;
190
+ providerInfo.url = `${baseUrl}/${route.join("/")}`;
191
+ }
192
+
193
+ // src/client/error.ts
194
+ var RATELIMIT_STATUS = 429;
195
+ var QstashError = class extends Error {
196
+ status;
197
+ constructor(message, status) {
198
+ super(message);
199
+ this.name = "QstashError";
200
+ this.status = status;
201
+ }
202
+ };
203
+ var QstashRatelimitError = class extends QstashError {
204
+ limit;
205
+ remaining;
206
+ reset;
207
+ constructor(args) {
208
+ super(`Exceeded burst rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
209
+ this.name = "QstashRatelimitError";
210
+ this.limit = args.limit;
211
+ this.remaining = args.remaining;
212
+ this.reset = args.reset;
213
+ }
214
+ };
215
+ var QstashChatRatelimitError = class extends QstashError {
216
+ limitRequests;
217
+ limitTokens;
218
+ remainingRequests;
219
+ remainingTokens;
220
+ resetRequests;
221
+ resetTokens;
222
+ constructor(args) {
223
+ super(`Exceeded chat rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
224
+ this.name = "QstashChatRatelimitError";
225
+ this.limitRequests = args["limit-requests"];
226
+ this.limitTokens = args["limit-tokens"];
227
+ this.remainingRequests = args["remaining-requests"];
228
+ this.remainingTokens = args["remaining-tokens"];
229
+ this.resetRequests = args["reset-requests"];
230
+ this.resetTokens = args["reset-tokens"];
231
+ }
232
+ };
233
+ var QstashDailyRatelimitError = class extends QstashError {
234
+ limit;
235
+ remaining;
236
+ reset;
237
+ constructor(args) {
238
+ super(`Exceeded daily rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
239
+ this.name = "QstashDailyRatelimitError";
240
+ this.limit = args.limit;
241
+ this.remaining = args.remaining;
242
+ this.reset = args.reset;
243
+ }
244
+ };
245
+ var QStashWorkflowError = class extends QstashError {
246
+ constructor(message) {
247
+ super(message);
248
+ this.name = "QStashWorkflowError";
249
+ }
250
+ };
251
+ var QStashWorkflowAbort = class extends Error {
252
+ stepInfo;
253
+ stepName;
254
+ constructor(stepName, stepInfo) {
255
+ super(
256
+ `This is an Upstash Workflow error thrown after a step executes. It is expected to be raised. Make sure that you await for each step. Also, if you are using try/catch blocks, you should not wrap context.run/sleep/sleepUntil/call methods with try/catch. Aborting workflow after executing step '${stepName}'.`
257
+ );
258
+ this.name = "QStashWorkflowAbort";
259
+ this.stepName = stepName;
260
+ this.stepInfo = stepInfo;
261
+ }
262
+ };
263
+ var formatWorkflowError = (error) => {
264
+ return error instanceof Error ? {
265
+ error: error.name,
266
+ message: error.message
267
+ } : {
268
+ error: "Error",
269
+ message: "An error occured while executing workflow."
270
+ };
271
+ };
272
+
273
+ // src/client/utils.ts
274
+ var isIgnoredHeader = (header) => {
275
+ const lowerCaseHeader = header.toLowerCase();
276
+ return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
277
+ };
278
+ function prefixHeaders(headers) {
279
+ const keysToBePrefixed = [...headers.keys()].filter((key) => !isIgnoredHeader(key));
280
+ for (const key of keysToBePrefixed) {
281
+ const value = headers.get(key);
282
+ if (value !== null) {
283
+ headers.set(`Upstash-Forward-${key}`, value);
284
+ }
285
+ headers.delete(key);
286
+ }
287
+ return headers;
288
+ }
289
+ function wrapWithGlobalHeaders(headers, globalHeaders, telemetryHeaders) {
290
+ if (!globalHeaders) {
291
+ return headers;
292
+ }
293
+ const finalHeaders = new Headers(globalHeaders);
294
+ headers.forEach((value, key) => {
295
+ finalHeaders.set(key, value);
296
+ });
297
+ telemetryHeaders?.forEach((value, key) => {
298
+ if (!value)
299
+ return;
300
+ finalHeaders.append(key, value);
301
+ });
302
+ return finalHeaders;
303
+ }
304
+ function processHeaders(request) {
305
+ const headers = prefixHeaders(new Headers(request.headers));
306
+ headers.set("Upstash-Method", request.method ?? "POST");
307
+ if (request.delay !== void 0) {
308
+ if (typeof request.delay === "string") {
309
+ headers.set("Upstash-Delay", request.delay);
310
+ } else {
311
+ headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`);
312
+ }
313
+ }
314
+ if (request.notBefore !== void 0) {
315
+ headers.set("Upstash-Not-Before", request.notBefore.toFixed(0));
316
+ }
317
+ if (request.deduplicationId !== void 0) {
318
+ headers.set("Upstash-Deduplication-Id", request.deduplicationId);
319
+ }
320
+ if (request.contentBasedDeduplication) {
321
+ headers.set("Upstash-Content-Based-Deduplication", "true");
322
+ }
323
+ if (request.retries !== void 0) {
324
+ headers.set("Upstash-Retries", request.retries.toFixed(0));
325
+ }
326
+ if (request.retryDelay !== void 0) {
327
+ headers.set("Upstash-Retry-Delay", request.retryDelay);
328
+ }
329
+ if (request.callback !== void 0) {
330
+ headers.set("Upstash-Callback", request.callback);
331
+ }
332
+ if (request.failureCallback !== void 0) {
333
+ headers.set("Upstash-Failure-Callback", request.failureCallback);
334
+ }
335
+ if (request.timeout !== void 0) {
336
+ if (typeof request.timeout === "string") {
337
+ headers.set("Upstash-Timeout", request.timeout);
338
+ } else {
339
+ headers.set("Upstash-Timeout", `${request.timeout}s`);
340
+ }
341
+ }
342
+ if (request.flowControl?.key) {
343
+ const parallelism = request.flowControl.parallelism?.toString();
344
+ const rate = (request.flowControl.rate ?? request.flowControl.ratePerSecond)?.toString();
345
+ const period = typeof request.flowControl.period === "number" ? `${request.flowControl.period}s` : request.flowControl.period;
346
+ const controlValue = [
347
+ parallelism ? `parallelism=${parallelism}` : void 0,
348
+ rate ? `rate=${rate}` : void 0,
349
+ period ? `period=${period}` : void 0
350
+ ].filter(Boolean);
351
+ if (controlValue.length === 0) {
352
+ throw new QstashError("Provide at least one of parallelism or ratePerSecond for flowControl");
353
+ }
354
+ headers.set("Upstash-Flow-Control-Key", request.flowControl.key);
355
+ headers.set("Upstash-Flow-Control-Value", controlValue.join(", "));
356
+ }
357
+ if (request.label !== void 0) {
358
+ headers.set("Upstash-Label", request.label);
359
+ }
360
+ return headers;
361
+ }
362
+ function getRequestPath(request) {
363
+ const nonApiPath = request.url ?? request.urlGroup ?? request.topic;
364
+ if (nonApiPath)
365
+ return nonApiPath;
366
+ if (request.api?.name === "llm")
367
+ return `api/llm`;
368
+ if (request.api?.name === "email") {
369
+ const providerInfo = getProviderInfo(request.api, "not-needed");
370
+ return providerInfo.baseUrl;
371
+ }
372
+ throw new QstashError(`Failed to infer request path for ${JSON.stringify(request)}`);
373
+ }
374
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
375
+ var NANOID_LENGTH = 21;
376
+ function nanoid() {
377
+ return [...crypto.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
378
+ }
379
+ function decodeBase64(base64) {
380
+ try {
381
+ const binString = atob(base64);
382
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
383
+ return new TextDecoder().decode(intArray);
384
+ } catch (error) {
385
+ try {
386
+ const result = atob(base64);
387
+ console.warn(
388
+ `Upstash QStash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
389
+ );
390
+ return result;
391
+ } catch (error2) {
392
+ console.warn(
393
+ `Upstash QStash: Failed to decode base64 "${base64}" with atob. Returning it as it is. ${error2}`
394
+ );
395
+ return base64;
396
+ }
397
+ }
398
+ }
399
+ function getRuntime() {
400
+ if (typeof process === "object" && typeof process.versions == "object" && process.versions.bun)
401
+ return `bun@${process.versions.bun}`;
402
+ if (typeof EdgeRuntime === "string")
403
+ return "edge-light";
404
+ else if (typeof process === "object" && typeof process.version === "string")
405
+ return `node@${process.version}`;
406
+ return "";
407
+ }
408
+ function getSafeEnvironment() {
409
+ return typeof process === "undefined" ? {} : process.env;
410
+ }
411
+
412
+ // src/client/multi-region/utils.ts
413
+ var VALID_REGIONS = ["EU_CENTRAL_1", "US_EAST_1"];
414
+ var DEFAULT_QSTASH_URL = "https://qstash.upstash.io";
415
+ var getRegionFromEnvironment = (environment) => {
416
+ const region = environment.QSTASH_REGION;
417
+ return normalizeRegionHeader(region);
418
+ };
419
+ function readEnvironmentVariables(environmentVariables, environment, region) {
420
+ const result = {};
421
+ for (const variable of environmentVariables) {
422
+ const key = region ? `${region}_${variable}` : variable;
423
+ result[variable] = environment[key];
424
+ }
425
+ return result;
426
+ }
427
+ function readClientEnvironmentVariables(environment, region) {
428
+ return readEnvironmentVariables(["QSTASH_URL", "QSTASH_TOKEN"], environment, region);
429
+ }
430
+ function readReceiverEnvironmentVariables(environment, region) {
431
+ return readEnvironmentVariables(
432
+ ["QSTASH_CURRENT_SIGNING_KEY", "QSTASH_NEXT_SIGNING_KEY"],
433
+ environment,
434
+ region
435
+ );
436
+ }
437
+ function normalizeRegionHeader(region) {
438
+ if (!region) {
439
+ return void 0;
440
+ }
441
+ region = region.replaceAll("-", "_").toUpperCase();
442
+ if (VALID_REGIONS.includes(region)) {
443
+ return region;
444
+ }
445
+ console.warn(
446
+ `[Upstash QStash] Invalid UPSTASH_REGION header value: "${region}". Expected one of: ${VALID_REGIONS.join(
447
+ ", "
448
+ )}.`
449
+ );
450
+ return void 0;
451
+ }
452
+
453
+ // src/client/multi-region/incoming.ts
454
+ var getReceiverSigningKeys = ({
455
+ environment,
456
+ regionFromHeader,
457
+ config
458
+ }) => {
459
+ if (config?.currentSigningKey && config.nextSigningKey) {
460
+ return {
461
+ currentSigningKey: config.currentSigningKey,
462
+ nextSigningKey: config.nextSigningKey
463
+ };
464
+ }
465
+ const regionEnvironment = getRegionFromEnvironment(environment);
466
+ if (regionEnvironment) {
467
+ const regionHeader = normalizeRegionHeader(regionFromHeader);
468
+ if (regionHeader) {
469
+ const regionCreds = readReceiverEnvironmentVariables(environment, regionHeader);
470
+ if (regionCreds.QSTASH_CURRENT_SIGNING_KEY && regionCreds.QSTASH_NEXT_SIGNING_KEY) {
471
+ return {
472
+ currentSigningKey: regionCreds.QSTASH_CURRENT_SIGNING_KEY,
473
+ nextSigningKey: regionCreds.QSTASH_NEXT_SIGNING_KEY,
474
+ region: regionHeader
475
+ };
476
+ } else {
477
+ console.warn(
478
+ `[Upstash QStash] Signing keys not found for region "${regionHeader}". Falling back to default signing keys.`
479
+ );
480
+ }
481
+ } else {
482
+ console.warn(
483
+ `[Upstash QStash] Invalid UPSTASH_REGION header value: "${regionFromHeader}". Expected one of: EU-CENTRAL-1, US-EAST-1. Falling back to default signing keys.`
484
+ );
485
+ }
486
+ }
487
+ const defaultCreds = readReceiverEnvironmentVariables(environment);
488
+ if (defaultCreds.QSTASH_CURRENT_SIGNING_KEY && defaultCreds.QSTASH_NEXT_SIGNING_KEY) {
489
+ return {
490
+ currentSigningKey: defaultCreds.QSTASH_CURRENT_SIGNING_KEY,
491
+ nextSigningKey: defaultCreds.QSTASH_NEXT_SIGNING_KEY
492
+ };
493
+ }
494
+ };
495
+
496
+ // src/client/multi-region/outgoing.ts
497
+ var getClientCredentials = (clientCredentialConfig) => {
498
+ const credentials = resolveCredentials(clientCredentialConfig);
499
+ return verifyCredentials(credentials);
500
+ };
501
+ var resolveCredentials = ({
502
+ environment,
503
+ config
504
+ }) => {
505
+ if (config?.baseUrl && config.token) {
506
+ return {
507
+ baseUrl: config.baseUrl,
508
+ token: config.token
509
+ };
510
+ }
511
+ const region = getRegionFromEnvironment(environment);
512
+ if (region) {
513
+ const regionCreds = readClientEnvironmentVariables(environment, region);
514
+ if (regionCreds.QSTASH_URL && regionCreds.QSTASH_TOKEN) {
515
+ return {
516
+ baseUrl: regionCreds.QSTASH_URL,
517
+ token: regionCreds.QSTASH_TOKEN,
518
+ region
519
+ };
520
+ } else {
521
+ console.warn(
522
+ `[Upstash QStash] QSTASH_REGION is set to "${region}" but credentials are missing. Expected ${region}_QSTASH_URL and ${region}_QSTASH_TOKEN. Falling back to default credentials.`
523
+ );
524
+ }
525
+ }
526
+ const defaultCreds = readClientEnvironmentVariables(environment);
527
+ return {
528
+ baseUrl: config?.baseUrl ?? defaultCreds.QSTASH_URL ?? DEFAULT_QSTASH_URL,
529
+ token: config?.token ?? defaultCreds.QSTASH_TOKEN ?? ""
530
+ };
531
+ };
532
+ var verifyCredentials = (credentials) => {
533
+ const token = credentials.token;
534
+ let baseUrl = credentials.baseUrl;
535
+ baseUrl = baseUrl.replace(/\/$/, "");
536
+ if (baseUrl === "https://qstash.upstash.io/v2/publish") {
537
+ baseUrl = DEFAULT_QSTASH_URL;
538
+ }
539
+ if (!token) {
540
+ console.warn(
541
+ "[Upstash QStash] client token is not set. Either pass a token or set QSTASH_TOKEN env variable."
542
+ );
543
+ }
544
+ return { baseUrl, token };
545
+ };
546
+
547
+ // src/receiver.ts
44
548
  var SignatureError = class extends Error {
45
549
  constructor(message) {
46
550
  super(message);
@@ -51,8 +555,8 @@ var Receiver = class {
51
555
  currentSigningKey;
52
556
  nextSigningKey;
53
557
  constructor(config) {
54
- this.currentSigningKey = config.currentSigningKey;
55
- this.nextSigningKey = config.nextSigningKey;
558
+ this.currentSigningKey = config?.currentSigningKey;
559
+ this.nextSigningKey = config?.nextSigningKey;
56
560
  }
57
561
  /**
58
562
  * Verify the signature of a request.
@@ -64,11 +568,25 @@ var Receiver = class {
64
568
  * If that fails, the signature is invalid and a `SignatureError` is thrown.
65
569
  */
66
570
  async verify(request) {
571
+ const environment = getSafeEnvironment();
572
+ const signingKeys = getReceiverSigningKeys({
573
+ environment,
574
+ regionFromHeader: request.upstashRegion,
575
+ config: {
576
+ currentSigningKey: this.currentSigningKey,
577
+ nextSigningKey: this.nextSigningKey
578
+ }
579
+ });
580
+ if (!signingKeys) {
581
+ throw new Error(
582
+ "[Upstash QStash] No signing keys available for verification. See the warning above for more details."
583
+ );
584
+ }
67
585
  let payload;
68
586
  try {
69
- payload = await this.verifyWithKey(this.currentSigningKey, request);
587
+ payload = await this.verifyWithKey(signingKeys.currentSigningKey, request);
70
588
  } catch {
71
- payload = await this.verifyWithKey(this.nextSigningKey, request);
589
+ payload = await this.verifyWithKey(signingKeys.nextSigningKey, request);
72
590
  }
73
591
  this.verifyBodyAndUrl(payload, request);
74
592
  return true;
@@ -133,108 +651,28 @@ var DLQ = class {
133
651
  };
134
652
  }
135
653
  /**
136
- * Remove a message from the dlq using it's `dlqId`
137
- */
138
- async delete(dlqMessageId) {
139
- return await this.http.request({
140
- method: "DELETE",
141
- path: ["v2", "dlq", dlqMessageId],
142
- parseResponseAsJson: false
143
- // there is no response
144
- });
145
- }
146
- /**
147
- * Remove multiple messages from the dlq using their `dlqId`s
148
- */
149
- async deleteMany(request) {
150
- return await this.http.request({
151
- method: "DELETE",
152
- path: ["v2", "dlq"],
153
- headers: { "Content-Type": "application/json" },
154
- body: JSON.stringify({ dlqIds: request.dlqIds })
155
- });
156
- }
157
- };
158
-
159
- // src/client/error.ts
160
- var RATELIMIT_STATUS = 429;
161
- var QstashError = class extends Error {
162
- status;
163
- constructor(message, status) {
164
- super(message);
165
- this.name = "QstashError";
166
- this.status = status;
167
- }
168
- };
169
- var QstashRatelimitError = class extends QstashError {
170
- limit;
171
- remaining;
172
- reset;
173
- constructor(args) {
174
- super(`Exceeded burst rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
175
- this.name = "QstashRatelimitError";
176
- this.limit = args.limit;
177
- this.remaining = args.remaining;
178
- this.reset = args.reset;
179
- }
180
- };
181
- var QstashChatRatelimitError = class extends QstashError {
182
- limitRequests;
183
- limitTokens;
184
- remainingRequests;
185
- remainingTokens;
186
- resetRequests;
187
- resetTokens;
188
- constructor(args) {
189
- super(`Exceeded chat rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
190
- this.name = "QstashChatRatelimitError";
191
- this.limitRequests = args["limit-requests"];
192
- this.limitTokens = args["limit-tokens"];
193
- this.remainingRequests = args["remaining-requests"];
194
- this.remainingTokens = args["remaining-tokens"];
195
- this.resetRequests = args["reset-requests"];
196
- this.resetTokens = args["reset-tokens"];
197
- }
198
- };
199
- var QstashDailyRatelimitError = class extends QstashError {
200
- limit;
201
- remaining;
202
- reset;
203
- constructor(args) {
204
- super(`Exceeded daily rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
205
- this.name = "QstashDailyRatelimitError";
206
- this.limit = args.limit;
207
- this.remaining = args.remaining;
208
- this.reset = args.reset;
209
- }
210
- };
211
- var QStashWorkflowError = class extends QstashError {
212
- constructor(message) {
213
- super(message);
214
- this.name = "QStashWorkflowError";
654
+ * Remove a message from the dlq using it's `dlqId`
655
+ */
656
+ async delete(dlqMessageId) {
657
+ return await this.http.request({
658
+ method: "DELETE",
659
+ path: ["v2", "dlq", dlqMessageId],
660
+ parseResponseAsJson: false
661
+ // there is no response
662
+ });
215
663
  }
216
- };
217
- var QStashWorkflowAbort = class extends Error {
218
- stepInfo;
219
- stepName;
220
- constructor(stepName, stepInfo) {
221
- super(
222
- `This is an Upstash Workflow error thrown after a step executes. It is expected to be raised. Make sure that you await for each step. Also, if you are using try/catch blocks, you should not wrap context.run/sleep/sleepUntil/call methods with try/catch. Aborting workflow after executing step '${stepName}'.`
223
- );
224
- this.name = "QStashWorkflowAbort";
225
- this.stepName = stepName;
226
- this.stepInfo = stepInfo;
664
+ /**
665
+ * Remove multiple messages from the dlq using their `dlqId`s
666
+ */
667
+ async deleteMany(request) {
668
+ return await this.http.request({
669
+ method: "DELETE",
670
+ path: ["v2", "dlq"],
671
+ headers: { "Content-Type": "application/json" },
672
+ body: JSON.stringify({ dlqIds: request.dlqIds })
673
+ });
227
674
  }
228
675
  };
229
- var formatWorkflowError = (error) => {
230
- return error instanceof Error ? {
231
- error: error.name,
232
- message: error.message
233
- } : {
234
- error: "Error",
235
- message: "An error occured while executing workflow."
236
- };
237
- };
238
676
 
239
677
  // src/client/http.ts
240
678
  var HttpClient = class {
@@ -493,387 +931,103 @@ var Chat = class _Chat {
493
931
  const body = JSON.stringify(request);
494
932
  const isAnalyticsEnabled = analytics?.name && analytics.token;
495
933
  const analyticsConfig = analytics?.name && analytics.token ? setupAnalytics({ name: analytics.name, token: analytics.token }, token, baseUrl, owner) : { defaultHeaders: void 0, baseURL: baseUrl };
496
- const isStream = "stream" in request && request.stream;
497
- const headers = {
498
- "Content-Type": "application/json",
499
- Authorization: `Bearer ${token}`,
500
- ...organization ? {
501
- "OpenAI-Organization": organization
502
- } : {},
503
- ...isStream ? {
504
- Connection: "keep-alive",
505
- Accept: "text/event-stream",
506
- "Cache-Control": "no-cache"
507
- } : {},
508
- ...analyticsConfig.defaultHeaders
509
- };
510
- const response = await this.http[isStream ? "requestStream" : "request"]({
511
- path: isAnalyticsEnabled ? [] : ["v1", "chat", "completions"],
512
- method: "POST",
513
- headers,
514
- body,
515
- baseUrl: analyticsConfig.baseURL
516
- });
517
- return response;
518
- };
519
- // Helper method to get the authorization token
520
- getAuthorizationToken() {
521
- const authHeader = String(this.http.authorization);
522
- const match = /Bearer (.+)/.exec(authHeader);
523
- if (!match) {
524
- throw new Error("Invalid authorization header format");
525
- }
526
- return match[1];
527
- }
528
- /**
529
- * Calls the Upstash completions api given a PromptRequest.
530
- *
531
- * Returns a ChatCompletion or a stream of ChatCompletionChunks
532
- * if stream is enabled.
533
- *
534
- * @param request PromptRequest with system and user messages.
535
- * Note that system parameter shouldn't be passed in the case of
536
- * mistralai/Mistral-7B-Instruct-v0.2 model.
537
- * @returns Chat completion or stream
538
- */
539
- prompt = async (request) => {
540
- const chatRequest = _Chat.toChatRequest(request);
541
- return this.create(chatRequest);
542
- };
543
- };
544
-
545
- // src/client/messages.ts
546
- var Messages = class {
547
- http;
548
- constructor(http) {
549
- this.http = http;
550
- }
551
- /**
552
- * Get a message
553
- */
554
- async get(messageId) {
555
- const messagePayload = await this.http.request({
556
- method: "GET",
557
- path: ["v2", "messages", messageId]
558
- });
559
- const message = {
560
- ...messagePayload,
561
- urlGroup: messagePayload.topicName,
562
- ratePerSecond: "rate" in messagePayload ? messagePayload.rate : void 0
563
- };
564
- return message;
565
- }
566
- /**
567
- * Cancel a message
568
- */
569
- async delete(messageId) {
570
- return await this.http.request({
571
- method: "DELETE",
572
- path: ["v2", "messages", messageId],
573
- parseResponseAsJson: false
574
- });
575
- }
576
- async deleteMany(messageIds) {
577
- const result = await this.http.request({
578
- method: "DELETE",
579
- path: ["v2", "messages"],
580
- headers: { "Content-Type": "application/json" },
581
- body: JSON.stringify({ messageIds })
582
- });
583
- return result.cancelled;
584
- }
585
- async deleteAll() {
586
- const result = await this.http.request({
587
- method: "DELETE",
588
- path: ["v2", "messages"]
589
- });
590
- return result.cancelled;
591
- }
592
- };
593
-
594
- // src/client/api/base.ts
595
- var BaseProvider = class {
596
- baseUrl;
597
- token;
598
- owner;
599
- constructor(baseUrl, token, owner) {
600
- this.baseUrl = baseUrl;
601
- this.token = token;
602
- this.owner = owner;
603
- }
604
- getUrl() {
605
- return `${this.baseUrl}/${this.getRoute().join("/")}`;
606
- }
607
- };
608
-
609
- // src/client/api/llm.ts
610
- var LLMProvider = class extends BaseProvider {
611
- apiKind = "llm";
612
- organization;
613
- method = "POST";
614
- constructor(baseUrl, token, owner, organization) {
615
- super(baseUrl, token, owner);
616
- this.organization = organization;
617
- }
618
- getRoute() {
619
- return this.owner === "anthropic" ? ["v1", "messages"] : ["v1", "chat", "completions"];
620
- }
621
- getHeaders(options) {
622
- if (this.owner === "upstash" && !options.analytics) {
623
- return { "content-type": "application/json" };
624
- }
625
- const header = this.owner === "anthropic" ? "x-api-key" : "authorization";
626
- const headerValue = this.owner === "anthropic" ? this.token : `Bearer ${this.token}`;
627
- const headers = {
628
- [header]: headerValue,
629
- "content-type": "application/json"
630
- };
631
- if (this.owner === "openai" && this.organization) {
632
- headers["OpenAI-Organization"] = this.organization;
633
- }
634
- if (this.owner === "anthropic") {
635
- headers["anthropic-version"] = "2023-06-01";
636
- }
637
- return headers;
638
- }
639
- /**
640
- * Checks if callback exists and adds analytics in place if it's set.
641
- *
642
- * @param request
643
- * @param options
644
- */
645
- onFinish(providerInfo, options) {
646
- if (options.analytics) {
647
- return updateWithAnalytics(providerInfo, options.analytics);
648
- }
649
- return providerInfo;
650
- }
651
- };
652
- var upstash = () => {
653
- return new LLMProvider("https://qstash.upstash.io/llm", "", "upstash");
654
- };
655
-
656
- // src/client/api/utils.ts
657
- var getProviderInfo = (api, upstashToken) => {
658
- const { name, provider, ...parameters } = api;
659
- const finalProvider = provider ?? upstash();
660
- if (finalProvider.owner === "upstash" && !finalProvider.token) {
661
- finalProvider.token = upstashToken;
662
- }
663
- if (!finalProvider.baseUrl)
664
- throw new TypeError("baseUrl cannot be empty or undefined!");
665
- if (!finalProvider.token)
666
- throw new TypeError("token cannot be empty or undefined!");
667
- if (finalProvider.apiKind !== name) {
668
- throw new TypeError(
669
- `Unexpected api name. Expected '${finalProvider.apiKind}', received ${name}`
670
- );
671
- }
672
- const providerInfo = {
673
- url: finalProvider.getUrl(),
674
- baseUrl: finalProvider.baseUrl,
675
- route: finalProvider.getRoute(),
676
- appendHeaders: finalProvider.getHeaders(parameters),
677
- owner: finalProvider.owner,
678
- method: finalProvider.method
679
- };
680
- return finalProvider.onFinish(providerInfo, parameters);
681
- };
682
- var safeJoinHeaders = (headers, record) => {
683
- const joinedHeaders = new Headers(record);
684
- for (const [header, value] of headers.entries()) {
685
- joinedHeaders.set(header, value);
686
- }
687
- return joinedHeaders;
688
- };
689
- var processApi = (request, headers, upstashToken) => {
690
- if (!request.api) {
691
- request.headers = headers;
692
- return request;
693
- }
694
- const { url, appendHeaders, owner, method } = getProviderInfo(request.api, upstashToken);
695
- if (request.api.name === "llm") {
696
- const callback = request.callback;
697
- if (!callback) {
698
- throw new TypeError("Callback cannot be undefined when using LLM api.");
699
- }
700
- return {
701
- ...request,
702
- method: request.method ?? method,
703
- headers: safeJoinHeaders(headers, appendHeaders),
704
- ...owner === "upstash" && !request.api.analytics ? { api: { name: "llm" }, url: void 0, callback } : { url, api: void 0 }
705
- };
706
- } else {
707
- return {
708
- ...request,
709
- method: request.method ?? method,
710
- headers: safeJoinHeaders(headers, appendHeaders),
711
- url,
712
- api: void 0
934
+ const isStream = "stream" in request && request.stream;
935
+ const headers = {
936
+ "Content-Type": "application/json",
937
+ Authorization: `Bearer ${token}`,
938
+ ...organization ? {
939
+ "OpenAI-Organization": organization
940
+ } : {},
941
+ ...isStream ? {
942
+ Connection: "keep-alive",
943
+ Accept: "text/event-stream",
944
+ "Cache-Control": "no-cache"
945
+ } : {},
946
+ ...analyticsConfig.defaultHeaders
713
947
  };
714
- }
715
- };
716
- function updateWithAnalytics(providerInfo, analytics) {
717
- switch (analytics.name) {
718
- case "helicone": {
719
- providerInfo.appendHeaders["Helicone-Auth"] = `Bearer ${analytics.token}`;
720
- if (providerInfo.owner === "upstash") {
721
- updateProviderInfo(providerInfo, "https://qstash.helicone.ai", [
722
- "llm",
723
- ...providerInfo.route
724
- ]);
725
- } else {
726
- providerInfo.appendHeaders["Helicone-Target-Url"] = providerInfo.baseUrl;
727
- updateProviderInfo(providerInfo, "https://gateway.helicone.ai", providerInfo.route);
728
- }
729
- return providerInfo;
730
- }
731
- default: {
732
- throw new Error("Unknown analytics provider");
948
+ const response = await this.http[isStream ? "requestStream" : "request"]({
949
+ path: isAnalyticsEnabled ? [] : ["v1", "chat", "completions"],
950
+ method: "POST",
951
+ headers,
952
+ body,
953
+ baseUrl: analyticsConfig.baseURL
954
+ });
955
+ return response;
956
+ };
957
+ // Helper method to get the authorization token
958
+ getAuthorizationToken() {
959
+ const authHeader = String(this.http.authorization);
960
+ const match = /Bearer (.+)/.exec(authHeader);
961
+ if (!match) {
962
+ throw new Error("Invalid authorization header format");
733
963
  }
964
+ return match[1];
734
965
  }
735
- }
736
- function updateProviderInfo(providerInfo, baseUrl, route) {
737
- providerInfo.baseUrl = baseUrl;
738
- providerInfo.route = route;
739
- providerInfo.url = `${baseUrl}/${route.join("/")}`;
740
- }
741
-
742
- // src/client/utils.ts
743
- var isIgnoredHeader = (header) => {
744
- const lowerCaseHeader = header.toLowerCase();
745
- return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
966
+ /**
967
+ * Calls the Upstash completions api given a PromptRequest.
968
+ *
969
+ * Returns a ChatCompletion or a stream of ChatCompletionChunks
970
+ * if stream is enabled.
971
+ *
972
+ * @param request PromptRequest with system and user messages.
973
+ * Note that system parameter shouldn't be passed in the case of
974
+ * mistralai/Mistral-7B-Instruct-v0.2 model.
975
+ * @returns Chat completion or stream
976
+ */
977
+ prompt = async (request) => {
978
+ const chatRequest = _Chat.toChatRequest(request);
979
+ return this.create(chatRequest);
980
+ };
746
981
  };
747
- function prefixHeaders(headers) {
748
- const keysToBePrefixed = [...headers.keys()].filter((key) => !isIgnoredHeader(key));
749
- for (const key of keysToBePrefixed) {
750
- const value = headers.get(key);
751
- if (value !== null) {
752
- headers.set(`Upstash-Forward-${key}`, value);
753
- }
754
- headers.delete(key);
755
- }
756
- return headers;
757
- }
758
- function wrapWithGlobalHeaders(headers, globalHeaders, telemetryHeaders) {
759
- if (!globalHeaders) {
760
- return headers;
761
- }
762
- const finalHeaders = new Headers(globalHeaders);
763
- headers.forEach((value, key) => {
764
- finalHeaders.set(key, value);
765
- });
766
- telemetryHeaders?.forEach((value, key) => {
767
- if (!value)
768
- return;
769
- finalHeaders.append(key, value);
770
- });
771
- return finalHeaders;
772
- }
773
- function processHeaders(request) {
774
- const headers = prefixHeaders(new Headers(request.headers));
775
- headers.set("Upstash-Method", request.method ?? "POST");
776
- if (request.delay !== void 0) {
777
- if (typeof request.delay === "string") {
778
- headers.set("Upstash-Delay", request.delay);
779
- } else {
780
- headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`);
781
- }
782
- }
783
- if (request.notBefore !== void 0) {
784
- headers.set("Upstash-Not-Before", request.notBefore.toFixed(0));
785
- }
786
- if (request.deduplicationId !== void 0) {
787
- headers.set("Upstash-Deduplication-Id", request.deduplicationId);
788
- }
789
- if (request.contentBasedDeduplication) {
790
- headers.set("Upstash-Content-Based-Deduplication", "true");
791
- }
792
- if (request.retries !== void 0) {
793
- headers.set("Upstash-Retries", request.retries.toFixed(0));
794
- }
795
- if (request.retryDelay !== void 0) {
796
- headers.set("Upstash-Retry-Delay", request.retryDelay);
797
- }
798
- if (request.callback !== void 0) {
799
- headers.set("Upstash-Callback", request.callback);
800
- }
801
- if (request.failureCallback !== void 0) {
802
- headers.set("Upstash-Failure-Callback", request.failureCallback);
803
- }
804
- if (request.timeout !== void 0) {
805
- if (typeof request.timeout === "string") {
806
- headers.set("Upstash-Timeout", request.timeout);
807
- } else {
808
- headers.set("Upstash-Timeout", `${request.timeout}s`);
809
- }
982
+
983
+ // src/client/messages.ts
984
+ var Messages = class {
985
+ http;
986
+ constructor(http) {
987
+ this.http = http;
810
988
  }
811
- if (request.flowControl?.key) {
812
- const parallelism = request.flowControl.parallelism?.toString();
813
- const rate = (request.flowControl.rate ?? request.flowControl.ratePerSecond)?.toString();
814
- const period = typeof request.flowControl.period === "number" ? `${request.flowControl.period}s` : request.flowControl.period;
815
- const controlValue = [
816
- parallelism ? `parallelism=${parallelism}` : void 0,
817
- rate ? `rate=${rate}` : void 0,
818
- period ? `period=${period}` : void 0
819
- ].filter(Boolean);
820
- if (controlValue.length === 0) {
821
- throw new QstashError("Provide at least one of parallelism or ratePerSecond for flowControl");
822
- }
823
- headers.set("Upstash-Flow-Control-Key", request.flowControl.key);
824
- headers.set("Upstash-Flow-Control-Value", controlValue.join(", "));
989
+ /**
990
+ * Get a message
991
+ */
992
+ async get(messageId) {
993
+ const messagePayload = await this.http.request({
994
+ method: "GET",
995
+ path: ["v2", "messages", messageId]
996
+ });
997
+ const message = {
998
+ ...messagePayload,
999
+ urlGroup: messagePayload.topicName,
1000
+ ratePerSecond: "rate" in messagePayload ? messagePayload.rate : void 0
1001
+ };
1002
+ return message;
825
1003
  }
826
- if (request.label !== void 0) {
827
- headers.set("Upstash-Label", request.label);
1004
+ /**
1005
+ * Cancel a message
1006
+ */
1007
+ async delete(messageId) {
1008
+ return await this.http.request({
1009
+ method: "DELETE",
1010
+ path: ["v2", "messages", messageId],
1011
+ parseResponseAsJson: false
1012
+ });
828
1013
  }
829
- return headers;
830
- }
831
- function getRequestPath(request) {
832
- const nonApiPath = request.url ?? request.urlGroup ?? request.topic;
833
- if (nonApiPath)
834
- return nonApiPath;
835
- if (request.api?.name === "llm")
836
- return `api/llm`;
837
- if (request.api?.name === "email") {
838
- const providerInfo = getProviderInfo(request.api, "not-needed");
839
- return providerInfo.baseUrl;
1014
+ async deleteMany(messageIds) {
1015
+ const result = await this.http.request({
1016
+ method: "DELETE",
1017
+ path: ["v2", "messages"],
1018
+ headers: { "Content-Type": "application/json" },
1019
+ body: JSON.stringify({ messageIds })
1020
+ });
1021
+ return result.cancelled;
840
1022
  }
841
- throw new QstashError(`Failed to infer request path for ${JSON.stringify(request)}`);
842
- }
843
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
844
- var NANOID_LENGTH = 21;
845
- function nanoid() {
846
- return [...crypto.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
847
- }
848
- function decodeBase64(base64) {
849
- try {
850
- const binString = atob(base64);
851
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
852
- return new TextDecoder().decode(intArray);
853
- } catch (error) {
854
- try {
855
- const result = atob(base64);
856
- console.warn(
857
- `Upstash QStash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
858
- );
859
- return result;
860
- } catch (error2) {
861
- console.warn(
862
- `Upstash QStash: Failed to decode base64 "${base64}" with atob. Returning it as it is. ${error2}`
863
- );
864
- return base64;
865
- }
1023
+ async deleteAll() {
1024
+ const result = await this.http.request({
1025
+ method: "DELETE",
1026
+ path: ["v2", "messages"]
1027
+ });
1028
+ return result.cancelled;
866
1029
  }
867
- }
868
- function getRuntime() {
869
- if (typeof process === "object" && typeof process.versions == "object" && process.versions.bun)
870
- return `bun@${process.versions.bun}`;
871
- if (typeof EdgeRuntime === "string")
872
- return "edge-light";
873
- else if (typeof process === "object" && typeof process.version === "string")
874
- return `node@${process.version}`;
875
- return "";
876
- }
1030
+ };
877
1031
 
878
1032
  // src/client/queue.ts
879
1033
  var Queue = class {
@@ -1204,19 +1358,15 @@ var UrlGroups = class {
1204
1358
  };
1205
1359
 
1206
1360
  // version.ts
1207
- var VERSION = "v2.8.4";
1361
+ var VERSION = "v2.9.0-rc";
1208
1362
 
1209
1363
  // src/client/client.ts
1210
1364
  var Client = class {
1211
1365
  http;
1212
1366
  token;
1213
1367
  constructor(config) {
1214
- const environment = typeof process === "undefined" ? {} : process.env;
1215
- let baseUrl = (config?.baseUrl ?? environment.QSTASH_URL ?? "https://qstash.upstash.io").replace(/\/$/, "");
1216
- if (baseUrl === "https://qstash.upstash.io/v2/publish") {
1217
- baseUrl = "https://qstash.upstash.io";
1218
- }
1219
- const token = config?.token ?? environment.QSTASH_TOKEN;
1368
+ const environment = getSafeEnvironment();
1369
+ const { baseUrl, token } = getClientCredentials({ environment, config });
1220
1370
  const enableTelemetry = environment.UPSTASH_DISABLE_TELEMETRY ? false : config?.enableTelemetry ?? true;
1221
1371
  const isCloudflare = typeof caches !== "undefined" && "default" in caches;
1222
1372
  const telemetryHeaders = new Headers(
@@ -1235,11 +1385,6 @@ var Client = class {
1235
1385
  //@ts-expect-error caused by undici and bunjs type overlap
1236
1386
  telemetryHeaders
1237
1387
  });
1238
- if (!token) {
1239
- console.warn(
1240
- "[Upstash QStash] client token is not set. Either pass a token or set QSTASH_TOKEN env variable."
1241
- );
1242
- }
1243
1388
  this.token = token;
1244
1389
  }
1245
1390
  /**
@@ -2858,15 +3003,10 @@ var Workflow = class {
2858
3003
  var BAD_REQUEST = 400;
2859
3004
  function verifySignature(handler, config) {
2860
3005
  const currentSigningKey = config?.currentSigningKey ?? process.env.QSTASH_CURRENT_SIGNING_KEY;
2861
- if (!currentSigningKey) {
2862
- throw new Error(
2863
- "currentSigningKey is required, either in the config or as env variable QSTASH_CURRENT_SIGNING_KEY"
2864
- );
2865
- }
2866
3006
  const nextSigningKey = config?.nextSigningKey ?? process.env.QSTASH_NEXT_SIGNING_KEY;
2867
- if (!nextSigningKey) {
3007
+ if (!currentSigningKey && !nextSigningKey && !process.env.QSTASH_REGION) {
2868
3008
  throw new Error(
2869
- "nextSigningKey is required, either in the config or as env variable QSTASH_NEXT_SIGNING_KEY"
3009
+ "currentSigningKey and nextSigningKey are required, either in the config or as env variables (QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY)"
2870
3010
  );
2871
3011
  }
2872
3012
  const receiver = new Receiver({
@@ -2884,6 +3024,7 @@ function verifySignature(handler, config) {
2884
3024
  if (typeof signature !== "string") {
2885
3025
  throw new TypeError("`Upstash-Signature` header is not a string");
2886
3026
  }
3027
+ const upstashRegion = request.headers["upstash-region"];
2887
3028
  const chunks = [];
2888
3029
  for await (const chunk of request) {
2889
3030
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -2892,7 +3033,8 @@ function verifySignature(handler, config) {
2892
3033
  const isValid = await receiver.verify({
2893
3034
  signature,
2894
3035
  body,
2895
- clockTolerance: config?.clockTolerance
3036
+ clockTolerance: config?.clockTolerance,
3037
+ upstashRegion: typeof upstashRegion === "string" ? upstashRegion : void 0
2896
3038
  });
2897
3039
  if (!isValid) {
2898
3040
  response.status(BAD_REQUEST);
@@ -2910,15 +3052,10 @@ function verifySignature(handler, config) {
2910
3052
  }
2911
3053
  function verifySignatureEdge(handler, config) {
2912
3054
  const currentSigningKey = config?.currentSigningKey ?? process.env.QSTASH_CURRENT_SIGNING_KEY;
2913
- if (!currentSigningKey) {
2914
- throw new Error(
2915
- "currentSigningKey is required, either in the config or as env variable QSTASH_CURRENT_SIGNING_KEY"
2916
- );
2917
- }
2918
3055
  const nextSigningKey = config?.nextSigningKey ?? process.env.QSTASH_NEXT_SIGNING_KEY;
2919
- if (!nextSigningKey) {
3056
+ if (!currentSigningKey && !nextSigningKey && !process.env.QSTASH_REGION) {
2920
3057
  throw new Error(
2921
- "nextSigningKey is required, either in the config or as env variable QSTASH_NEXT_SIGNING_KEY"
3058
+ "currentSigningKey and nextSigningKey are required, either in the config or as env variables (QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY)"
2922
3059
  );
2923
3060
  }
2924
3061
  const receiver = new Receiver({
@@ -2936,11 +3073,13 @@ function verifySignatureEdge(handler, config) {
2936
3073
  if (typeof signature !== "string") {
2937
3074
  throw new TypeError("`Upstash-Signature` header is not a string");
2938
3075
  }
3076
+ const upstashRegion = request.headers.get("upstash-region");
2939
3077
  const body = await requestClone.text();
2940
3078
  const isValid = await receiver.verify({
2941
3079
  signature,
2942
3080
  body,
2943
- clockTolerance: config?.clockTolerance
3081
+ clockTolerance: config?.clockTolerance,
3082
+ upstashRegion: upstashRegion ?? void 0
2944
3083
  });
2945
3084
  if (!isValid) {
2946
3085
  return new Response(new TextEncoder().encode("invalid signature"), { status: 403 });
@@ -2950,15 +3089,10 @@ function verifySignatureEdge(handler, config) {
2950
3089
  }
2951
3090
  function verifySignatureAppRouter(handler, config) {
2952
3091
  const currentSigningKey = config?.currentSigningKey ?? process.env.QSTASH_CURRENT_SIGNING_KEY;
2953
- if (!currentSigningKey) {
2954
- throw new Error(
2955
- "currentSigningKey is required, either in the config or as env variable QSTASH_CURRENT_SIGNING_KEY"
2956
- );
2957
- }
2958
3092
  const nextSigningKey = config?.nextSigningKey ?? process.env.QSTASH_NEXT_SIGNING_KEY;
2959
- if (!nextSigningKey) {
3093
+ if (!currentSigningKey && !nextSigningKey && !process.env.QSTASH_REGION) {
2960
3094
  throw new Error(
2961
- "nextSigningKey is required, either in the config or as env variable QSTASH_NEXT_SIGNING_KEY"
3095
+ "currentSigningKey and nextSigningKey are required, either in the config or as env variables (QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY)"
2962
3096
  );
2963
3097
  }
2964
3098
  const receiver = new Receiver({
@@ -2976,11 +3110,13 @@ function verifySignatureAppRouter(handler, config) {
2976
3110
  if (typeof signature !== "string") {
2977
3111
  throw new TypeError("`Upstash-Signature` header is not a string");
2978
3112
  }
3113
+ const upstashRegion = request.headers.get("upstash-region");
2979
3114
  const body = await requestClone.text();
2980
3115
  const isValid = await receiver.verify({
2981
3116
  signature,
2982
3117
  body,
2983
- clockTolerance: config?.clockTolerance
3118
+ clockTolerance: config?.clockTolerance,
3119
+ upstashRegion: upstashRegion ?? void 0
2984
3120
  });
2985
3121
  if (!isValid) {
2986
3122
  return new Response(new TextEncoder().encode("invalid signature"), { status: 403 });