@upstash/qstash
Version:
Official Typescript client for QStash
1,557 lines (1,532 loc) • 130 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// platforms/h3.ts
var h3_exports = {};
__export(h3_exports, {
serve: () => serve2,
verifySignatureH3: () => verifySignatureH3
});
module.exports = __toCommonJS(h3_exports);
// node_modules/defu/dist/defu.mjs
function isPlainObject(value) {
if (value === null || typeof value !== "object") {
return false;
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {
return false;
}
if (Symbol.iterator in value) {
return false;
}
if (Symbol.toStringTag in value) {
return Object.prototype.toString.call(value) === "[object Module]";
}
return true;
}
function _defu(baseObject, defaults, namespace = ".", merger) {
if (!isPlainObject(defaults)) {
return _defu(baseObject, {}, namespace, merger);
}
const object = Object.assign({}, defaults);
for (const key in baseObject) {
if (key === "__proto__" || key === "constructor") {
continue;
}
const value = baseObject[key];
if (value === null || value === void 0) {
continue;
}
if (merger && merger(object, key, value, namespace)) {
continue;
}
if (Array.isArray(value) && Array.isArray(object[key])) {
object[key] = [...value, ...object[key]];
} else if (isPlainObject(value) && isPlainObject(object[key])) {
object[key] = _defu(
value,
object[key],
(namespace ? `${namespace}.` : "") + key.toString(),
merger
);
} else {
object[key] = value;
}
}
return object;
}
function createDefu(merger) {
return (...arguments_) => (
// eslint-disable-next-line unicorn/no-array-reduce
arguments_.reduce((p, c) => _defu(p, c, "", merger), {})
);
}
var defu = createDefu();
var defuFn = createDefu((object, key, currentValue) => {
if (object[key] !== void 0 && typeof currentValue === "function") {
object[key] = currentValue(object[key]);
return true;
}
});
var defuArrayFn = createDefu((object, key, currentValue) => {
if (Array.isArray(object[key]) && typeof currentValue === "function") {
object[key] = currentValue(object[key]);
return true;
}
});
// node_modules/h3/dist/index.mjs
function hasProp(obj, prop) {
try {
return prop in obj;
} catch {
return false;
}
}
var __defProp$2 = Object.defineProperty;
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$2 = (obj, key, value) => {
__defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
var H3Error = class extends Error {
constructor(message, opts = {}) {
super(message, opts);
__publicField$2(this, "statusCode", 500);
__publicField$2(this, "fatal", false);
__publicField$2(this, "unhandled", false);
__publicField$2(this, "statusMessage");
__publicField$2(this, "data");
__publicField$2(this, "cause");
if (opts.cause && !this.cause) {
this.cause = opts.cause;
}
}
toJSON() {
const obj = {
message: this.message,
statusCode: sanitizeStatusCode(this.statusCode, 500)
};
if (this.statusMessage) {
obj.statusMessage = sanitizeStatusMessage(this.statusMessage);
}
if (this.data !== void 0) {
obj.data = this.data;
}
return obj;
}
};
__publicField$2(H3Error, "__h3_error__", true);
function createError(input) {
if (typeof input === "string") {
return new H3Error(input);
}
if (isError(input)) {
return input;
}
const err4 = new H3Error(input.message ?? input.statusMessage ?? "", {
cause: input.cause || input
});
if (hasProp(input, "stack")) {
try {
Object.defineProperty(err4, "stack", {
get() {
return input.stack;
}
});
} catch {
try {
err4.stack = input.stack;
} catch {
}
}
}
if (input.data) {
err4.data = input.data;
}
if (input.statusCode) {
err4.statusCode = sanitizeStatusCode(input.statusCode, err4.statusCode);
} else if (input.status) {
err4.statusCode = sanitizeStatusCode(input.status, err4.statusCode);
}
if (input.statusMessage) {
err4.statusMessage = input.statusMessage;
} else if (input.statusText) {
err4.statusMessage = input.statusText;
}
if (err4.statusMessage) {
const originalMessage = err4.statusMessage;
const sanitizedMessage = sanitizeStatusMessage(err4.statusMessage);
if (sanitizedMessage !== originalMessage) {
console.warn(
"[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future, `statusMessage` will be sanitized by default."
);
}
}
if (input.fatal !== void 0) {
err4.fatal = input.fatal;
}
if (input.unhandled !== void 0) {
err4.unhandled = input.unhandled;
}
return err4;
}
function isError(input) {
return input?.constructor?.__h3_error__ === true;
}
function isMethod(event, expected, allowHead) {
if (allowHead && event.method === "HEAD") {
return true;
}
if (typeof expected === "string") {
if (event.method === expected) {
return true;
}
} else if (expected.includes(event.method)) {
return true;
}
return false;
}
function assertMethod(event, expected, allowHead) {
if (!isMethod(event, expected, allowHead)) {
throw createError({
statusCode: 405,
statusMessage: "HTTP method is not allowed."
});
}
}
function getRequestHeaders(event) {
const _headers = {};
for (const key in event.node.req.headers) {
const val = event.node.req.headers[key];
_headers[key] = Array.isArray(val) ? val.filter(Boolean).join(", ") : val;
}
return _headers;
}
function getRequestHeader(event, name) {
const headers = getRequestHeaders(event);
const value = headers[name.toLowerCase()];
return value;
}
var getHeader = getRequestHeader;
var RawBodySymbol = Symbol.for("h3RawBody");
var ParsedBodySymbol = Symbol.for("h3ParsedBody");
var PayloadMethods$1 = ["PATCH", "POST", "PUT", "DELETE"];
function readRawBody(event, encoding = "utf8") {
assertMethod(event, PayloadMethods$1);
const _rawBody = event._requestBody || event.web?.request?.body || event.node.req[RawBodySymbol] || event.node.req.rawBody || event.node.req.body;
if (_rawBody) {
const promise2 = Promise.resolve(_rawBody).then((_resolved) => {
if (Buffer.isBuffer(_resolved)) {
return _resolved;
}
if (typeof _resolved.pipeTo === "function") {
return new Promise((resolve, reject) => {
const chunks = [];
_resolved.pipeTo(
new WritableStream({
write(chunk) {
chunks.push(chunk);
},
close() {
resolve(Buffer.concat(chunks));
},
abort(reason) {
reject(reason);
}
})
).catch(reject);
});
} else if (typeof _resolved.pipe === "function") {
return new Promise((resolve, reject) => {
const chunks = [];
_resolved.on("data", (chunk) => {
chunks.push(chunk);
}).on("end", () => {
resolve(Buffer.concat(chunks));
}).on("error", reject);
});
}
if (_resolved.constructor === Object) {
return Buffer.from(JSON.stringify(_resolved));
}
return Buffer.from(_resolved);
});
return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2;
}
if (!Number.parseInt(event.node.req.headers["content-length"] || "") && !String(event.node.req.headers["transfer-encoding"] ?? "").split(",").map((e) => e.trim()).filter(Boolean).includes("chunked")) {
return Promise.resolve(void 0);
}
const promise = event.node.req[RawBodySymbol] = new Promise(
(resolve, reject) => {
const bodyData = [];
event.node.req.on("error", (err4) => {
reject(err4);
}).on("data", (chunk) => {
bodyData.push(chunk);
}).on("end", () => {
resolve(Buffer.concat(bodyData));
});
}
);
const result = encoding ? promise.then((buff) => buff.toString(encoding)) : promise;
return result;
}
var DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g;
function sanitizeStatusMessage(statusMessage = "") {
return statusMessage.replace(DISALLOWED_STATUS_CHARS, "");
}
function sanitizeStatusCode(statusCode, defaultStatusCode = 200) {
if (!statusCode) {
return defaultStatusCode;
}
if (typeof statusCode === "string") {
statusCode = Number.parseInt(statusCode, 10);
}
if (statusCode < 100 || statusCode > 999) {
return defaultStatusCode;
}
return statusCode;
}
var getSessionPromise = Symbol("getSession");
function defineEventHandler(handler) {
if (typeof handler === "function") {
handler.__is_handler__ = true;
return handler;
}
const _hooks = {
onRequest: _normalizeArray(handler.onRequest),
onBeforeResponse: _normalizeArray(handler.onBeforeResponse)
};
const _handler = (event) => {
return _callHandler(event, handler.handler, _hooks);
};
_handler.__is_handler__ = true;
_handler.__resolve__ = handler.handler.__resolve__;
_handler.__websocket__ = handler.websocket;
return _handler;
}
function _normalizeArray(input) {
return input ? Array.isArray(input) ? input : [input] : void 0;
}
async function _callHandler(event, handler, hooks) {
if (hooks.onRequest) {
for (const hook of hooks.onRequest) {
await hook(event);
if (event.handled) {
return;
}
}
}
const body = await handler(event);
const response = { body };
if (hooks.onBeforeResponse) {
for (const hook of hooks.onBeforeResponse) {
await hook(event, response);
}
}
return response.body;
}
var H3Headers = globalThis.Headers;
var H3Response = globalThis.Response;
// src/receiver.ts
var jose = __toESM(require("jose"));
var import_crypto_js = __toESM(require("crypto-js"));
// src/client/api/base.ts
var BaseProvider = class {
baseUrl;
token;
owner;
constructor(baseUrl, token, owner) {
this.baseUrl = baseUrl;
this.token = token;
this.owner = owner;
}
getUrl() {
return `${this.baseUrl}/${this.getRoute().join("/")}`;
}
};
// src/client/api/llm.ts
var LLMProvider = class extends BaseProvider {
apiKind = "llm";
organization;
method = "POST";
constructor(baseUrl, token, owner, organization) {
super(baseUrl, token, owner);
this.organization = organization;
}
getRoute() {
return this.owner === "anthropic" ? ["v1", "messages"] : ["v1", "chat", "completions"];
}
getHeaders(options) {
if (this.owner === "upstash" && !options.analytics) {
return { "content-type": "application/json" };
}
const header = this.owner === "anthropic" ? "x-api-key" : "authorization";
const headerValue = this.owner === "anthropic" ? this.token : `Bearer ${this.token}`;
const headers = {
[header]: headerValue,
"content-type": "application/json"
};
if (this.owner === "openai" && this.organization) {
headers["OpenAI-Organization"] = this.organization;
}
if (this.owner === "anthropic") {
headers["anthropic-version"] = "2023-06-01";
}
return headers;
}
/**
* Checks if callback exists and adds analytics in place if it's set.
*
* @param request
* @param options
*/
onFinish(providerInfo, options) {
if (options.analytics) {
return updateWithAnalytics(providerInfo, options.analytics);
}
return providerInfo;
}
};
var upstash = () => {
return new LLMProvider("https://qstash.upstash.io/llm", "", "upstash");
};
// src/client/api/utils.ts
var getProviderInfo = (api, upstashToken) => {
const { name, provider, ...parameters } = api;
const finalProvider = provider ?? upstash();
if (finalProvider.owner === "upstash" && !finalProvider.token) {
finalProvider.token = upstashToken;
}
if (!finalProvider.baseUrl)
throw new TypeError("baseUrl cannot be empty or undefined!");
if (!finalProvider.token)
throw new TypeError("token cannot be empty or undefined!");
if (finalProvider.apiKind !== name) {
throw new TypeError(
`Unexpected api name. Expected '${finalProvider.apiKind}', received ${name}`
);
}
const providerInfo = {
url: finalProvider.getUrl(),
baseUrl: finalProvider.baseUrl,
route: finalProvider.getRoute(),
appendHeaders: finalProvider.getHeaders(parameters),
owner: finalProvider.owner,
method: finalProvider.method
};
return finalProvider.onFinish(providerInfo, parameters);
};
var safeJoinHeaders = (headers, record) => {
const joinedHeaders = new Headers(record);
for (const [header, value] of headers.entries()) {
joinedHeaders.set(header, value);
}
return joinedHeaders;
};
var processApi = (request, headers, upstashToken) => {
if (!request.api) {
request.headers = headers;
return request;
}
const { url, appendHeaders, owner, method } = getProviderInfo(request.api, upstashToken);
if (request.api.name === "llm") {
const callback = request.callback;
if (!callback) {
throw new TypeError("Callback cannot be undefined when using LLM api.");
}
return {
...request,
method: request.method ?? method,
headers: safeJoinHeaders(headers, appendHeaders),
...owner === "upstash" && !request.api.analytics ? { api: { name: "llm" }, url: void 0, callback } : { url, api: void 0 }
};
} else {
return {
...request,
method: request.method ?? method,
headers: safeJoinHeaders(headers, appendHeaders),
url,
api: void 0
};
}
};
function updateWithAnalytics(providerInfo, analytics) {
switch (analytics.name) {
case "helicone": {
providerInfo.appendHeaders["Helicone-Auth"] = `Bearer ${analytics.token}`;
if (providerInfo.owner === "upstash") {
updateProviderInfo(providerInfo, "https://qstash.helicone.ai", [
"llm",
...providerInfo.route
]);
} else {
providerInfo.appendHeaders["Helicone-Target-Url"] = providerInfo.baseUrl;
updateProviderInfo(providerInfo, "https://gateway.helicone.ai", providerInfo.route);
}
return providerInfo;
}
default: {
throw new Error("Unknown analytics provider");
}
}
}
function updateProviderInfo(providerInfo, baseUrl, route) {
providerInfo.baseUrl = baseUrl;
providerInfo.route = route;
providerInfo.url = `${baseUrl}/${route.join("/")}`;
}
// src/client/error.ts
var RATELIMIT_STATUS = 429;
var QstashError = class extends Error {
status;
constructor(message, status) {
super(message);
this.name = "QstashError";
this.status = status;
}
};
var QstashRatelimitError = class extends QstashError {
limit;
remaining;
reset;
constructor(args) {
super(`Exceeded burst rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
this.name = "QstashRatelimitError";
this.limit = args.limit;
this.remaining = args.remaining;
this.reset = args.reset;
}
};
var QstashChatRatelimitError = class extends QstashError {
limitRequests;
limitTokens;
remainingRequests;
remainingTokens;
resetRequests;
resetTokens;
constructor(args) {
super(`Exceeded chat rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
this.name = "QstashChatRatelimitError";
this.limitRequests = args["limit-requests"];
this.limitTokens = args["limit-tokens"];
this.remainingRequests = args["remaining-requests"];
this.remainingTokens = args["remaining-tokens"];
this.resetRequests = args["reset-requests"];
this.resetTokens = args["reset-tokens"];
}
};
var QstashDailyRatelimitError = class extends QstashError {
limit;
remaining;
reset;
constructor(args) {
super(`Exceeded daily rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS);
this.name = "QstashDailyRatelimitError";
this.limit = args.limit;
this.remaining = args.remaining;
this.reset = args.reset;
}
};
var QstashEmptyArrayError = class extends QstashError {
constructor(parameterName) {
super(
`Empty array provided for query parameter "${parameterName}". This would result in no filter being applied, which could affect all resources.`
);
this.name = "QstashEmptyArrayError";
}
};
var QStashWorkflowError = class extends QstashError {
constructor(message) {
super(message);
this.name = "QStashWorkflowError";
}
};
var QStashWorkflowAbort = class extends Error {
stepInfo;
stepName;
constructor(stepName, stepInfo) {
super(
`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}'.`
);
this.name = "QStashWorkflowAbort";
this.stepName = stepName;
this.stepInfo = stepInfo;
}
};
var formatWorkflowError = (error) => {
return error instanceof Error ? {
error: error.name,
message: error.message
} : {
error: "Error",
message: "An error occured while executing workflow."
};
};
// src/client/utils.ts
var DEFAULT_BULK_COUNT = 100;
var isIgnoredHeader = (header) => {
const lowerCaseHeader = header.toLowerCase();
return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
};
function prefixHeaders(headers) {
const keysToBePrefixed = [...headers.keys()].filter((key) => !isIgnoredHeader(key));
for (const key of keysToBePrefixed) {
const value = headers.get(key);
if (value !== null) {
headers.set(`Upstash-Forward-${key}`, value);
}
headers.delete(key);
}
return headers;
}
function wrapWithGlobalHeaders(headers, globalHeaders, telemetryHeaders) {
if (!globalHeaders) {
return headers;
}
const finalHeaders = new Headers(globalHeaders);
headers.forEach((value, key) => {
finalHeaders.set(key, value);
});
telemetryHeaders?.forEach((value, key) => {
if (!value)
return;
finalHeaders.append(key, value);
});
return finalHeaders;
}
function processHeaders(request) {
const headers = prefixHeaders(new Headers(request.headers));
headers.set("Upstash-Method", request.method ?? "POST");
if (request.delay !== void 0) {
if (typeof request.delay === "string") {
headers.set("Upstash-Delay", request.delay);
} else {
headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`);
}
}
if (request.notBefore !== void 0) {
headers.set("Upstash-Not-Before", request.notBefore.toFixed(0));
}
if (request.deduplicationId !== void 0) {
headers.set("Upstash-Deduplication-Id", request.deduplicationId);
}
if (request.contentBasedDeduplication) {
headers.set("Upstash-Content-Based-Deduplication", "true");
}
if (request.retries !== void 0) {
headers.set("Upstash-Retries", request.retries.toFixed(0));
}
if (request.retryDelay !== void 0) {
headers.set("Upstash-Retry-Delay", request.retryDelay);
}
if (request.callback !== void 0) {
headers.set("Upstash-Callback", request.callback);
}
if (request.failureCallback !== void 0) {
headers.set("Upstash-Failure-Callback", request.failureCallback);
}
if (request.timeout !== void 0) {
if (typeof request.timeout === "string") {
headers.set("Upstash-Timeout", request.timeout);
} else {
headers.set("Upstash-Timeout", `${request.timeout}s`);
}
}
if (request.flowControl?.key) {
const parallelism = request.flowControl.parallelism?.toString();
const rate = (request.flowControl.rate ?? request.flowControl.ratePerSecond)?.toString();
const period = typeof request.flowControl.period === "number" ? `${request.flowControl.period}s` : request.flowControl.period;
const controlValue = [
parallelism ? `parallelism=${parallelism}` : void 0,
rate ? `rate=${rate}` : void 0,
period ? `period=${period}` : void 0
].filter(Boolean);
if (controlValue.length === 0) {
throw new QstashError("Provide at least one of parallelism or ratePerSecond for flowControl");
}
headers.set("Upstash-Flow-Control-Key", request.flowControl.key);
headers.set("Upstash-Flow-Control-Value", controlValue.join(", "));
}
if (request.label !== void 0) {
headers.set("Upstash-Label", request.label);
}
if (request.redact !== void 0) {
const redactParts = [];
if (request.redact.body) {
redactParts.push("body");
}
if (request.redact.header !== void 0) {
if (request.redact.header === true) {
redactParts.push("header");
} else if (Array.isArray(request.redact.header) && request.redact.header.length > 0) {
for (const headerName of request.redact.header) {
redactParts.push(`header[${headerName}]`);
}
}
}
if (redactParts.length > 0) {
headers.set("Upstash-Redact-Fields", redactParts.join(","));
}
}
return headers;
}
function getRequestPath(request) {
const nonApiPath = request.url ?? request.urlGroup ?? request.topic;
if (nonApiPath)
return nonApiPath;
if (request.api?.name === "llm")
return `api/llm`;
if (request.api?.name === "email") {
const providerInfo = getProviderInfo(request.api, "not-needed");
return providerInfo.baseUrl;
}
throw new QstashError(`Failed to infer request path for ${JSON.stringify(request)}`);
}
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
var NANOID_LENGTH = 21;
function nanoid() {
return [...crypto.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
}
function decodeBase64(base64) {
try {
const binString = atob(base64);
const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
return new TextDecoder().decode(intArray);
} catch (error) {
try {
const result = atob(base64);
console.warn(
`Upstash QStash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
);
return result;
} catch (error2) {
console.warn(
`Upstash QStash: Failed to decode base64 "${base64}" with atob. Returning it as it is. ${error2}`
);
return base64;
}
}
}
function buildBulkActionFilterPayload(request) {
const cursor = "cursor" in request ? request.cursor : void 0;
if ("all" in request) {
const count2 = "count" in request ? request.count ?? DEFAULT_BULK_COUNT : DEFAULT_BULK_COUNT;
return { count: count2, cursor };
}
if ("dlqIds" in request) {
const ids = request.dlqIds;
if (Array.isArray(ids) && ids.length === 0) {
throw new QstashError(
"Empty dlqIds array provided. If you intend to target all DLQ messages, use { all: true } explicitly."
);
}
return { dlqIds: ids, cursor };
}
if ("messageIds" in request && request.messageIds) {
if (request.messageIds.length === 0) {
throw new QstashError(
"Empty messageIds array provided. If you intend to target all messages, use { all: true } explicitly."
);
}
return { messageIds: request.messageIds, cursor };
}
const count = "count" in request ? request.count ?? DEFAULT_BULK_COUNT : DEFAULT_BULK_COUNT;
return {
...renameUrlGroup(request.filter),
count,
cursor
};
}
function renameUrlGroup(filter) {
const { urlGroup, api, ...rest } = filter;
return { ...rest, ...urlGroup === void 0 ? {} : { topicName: urlGroup } };
}
function normalizeCursor(response) {
const cursor = response.cursor;
return { ...response, cursor: cursor || void 0 };
}
function _processGlobal() {
const proc = globalThis["process"];
return proc;
}
function getRuntime() {
const proc = _processGlobal();
if (proc?.versions?.bun)
return `bun@${proc.versions.bun}`;
if (typeof EdgeRuntime === "string")
return "edge-light";
if (typeof proc?.version === "string")
return `node@${proc.version}`;
return "";
}
function getSafeEnvironment() {
const proc = _processGlobal();
return proc?.env ?? {};
}
// src/client/multi-region/utils.ts
var VALID_REGIONS = ["EU_CENTRAL_1", "US_EAST_1"];
var DEFAULT_QSTASH_URL = "https://qstash.upstash.io";
var getRegionFromEnvironment = (environment) => {
const region = environment.QSTASH_REGION;
return normalizeRegionHeader(region);
};
function readEnvironmentVariables(environmentVariables, environment, region) {
const result = {};
for (const variable of environmentVariables) {
const key = region ? `${region}_${variable}` : variable;
result[variable] = environment[key];
}
return result;
}
function readClientEnvironmentVariables(environment, region) {
return readEnvironmentVariables(["QSTASH_URL", "QSTASH_TOKEN"], environment, region);
}
function readReceiverEnvironmentVariables(environment, region) {
return readEnvironmentVariables(
["QSTASH_CURRENT_SIGNING_KEY", "QSTASH_NEXT_SIGNING_KEY"],
environment,
region
);
}
function normalizeRegionHeader(region) {
if (!region) {
return void 0;
}
region = region.replaceAll("-", "_").toUpperCase();
if (VALID_REGIONS.includes(region)) {
return region;
}
console.warn(
`[Upstash QStash] Invalid UPSTASH_REGION header value: "${region}". Expected one of: ${VALID_REGIONS.join(
", "
)}.`
);
return void 0;
}
// src/dev-server/constants.ts
var DEFAULT_DEV_PORT = 8080;
var DEV_CREDENTIALS = {
token: "eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=",
currentSigningKey: "sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r",
nextSigningKey: "sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs"
};
var GITHUB_RELEASES_URL = "https://api.github.com/repos/upstash/qstash-cli/releases/latest";
var BINARY_URL_BASE = "https://artifacts.upstash.com/qstash/versions";
var CONSOLE_URL = "https://console.upstash.com/qstash/local-mode-user";
var DEV_PREFIX = "\x1B[2m[QStash Dev]\x1B[0m";
var CLI_PREFIX = "\x1B[2m[QStash CLI]\x1B[0m";
var _n = (m) => `node:${m}`;
var importHttp = () => import(
/* webpackIgnore: true */
_n("http")
);
var importHttps = () => import(
/* webpackIgnore: true */
_n("https")
);
var importFs = () => import(
/* webpackIgnore: true */
_n("fs")
);
var importChildProcess = () => import(
/* webpackIgnore: true */
_n("child_process")
);
var importOs = () => import(
/* webpackIgnore: true */
_n("os")
);
// src/dev-server/http.ts
var HTTP_OK = 200;
var HTTP_MULTI_CHOICE = 300;
var nativeGet = async (url, headers, timeoutMs) => {
const parsedUrl = new URL(url);
const httpModule = parsedUrl.protocol === "https:" ? await importHttps() : await importHttp();
return new Promise((resolve, reject) => {
const request = httpModule.get(url, { headers }, (response) => {
const chunks = [];
response.on("data", (chunk) => chunks.push(chunk));
response.on("end", () => {
const statusCode = response.statusCode ?? 0;
resolve({
ok: statusCode >= HTTP_OK && statusCode < HTTP_MULTI_CHOICE,
statusCode,
body: Buffer.concat(chunks)
});
});
response.on("error", reject);
});
if (timeoutMs) {
request.setTimeout(timeoutMs, () => {
request.destroy(new Error("Request timed out"));
});
}
request.on("error", reject);
});
};
// src/dev-server/health.ts
var HEALTH_CHECK_TIMEOUT_MS = 2e3;
var isDevServerRunning = async (baseUrl) => {
try {
const { ok: ok4, body } = await nativeGet(
`${baseUrl}/v2/keys`,
{ Authorization: `Bearer ${DEV_CREDENTIALS.token}` },
HEALTH_CHECK_TIMEOUT_MS
);
if (!ok4)
return false;
const data = JSON.parse(body.toString());
return data.current === DEV_CREDENTIALS.currentSigningKey && data.next === DEV_CREDENTIALS.nextSigningKey;
} catch {
return false;
}
};
var _didLogUnreachable = false;
var checkDevServerReachable = async (baseUrl, runtime) => {
if (await pingEdge(baseUrl))
return;
if (!_didLogUnreachable) {
console.error(unreachableMessage(baseUrl, runtime));
_didLogUnreachable = true;
}
throw new Error(`${DEV_PREFIX} dev server unreachable at ${baseUrl}`);
};
var pingEdge = async (baseUrl) => {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
const response = await fetch(`${baseUrl}/v2/keys`, {
headers: { Authorization: `Bearer ${DEV_CREDENTIALS.token}` },
signal: controller.signal
});
clearTimeout(timeout);
return response.ok;
} catch {
return false;
}
};
var unreachableMessage = (baseUrl, runtime) => {
const port = new URL(baseUrl).port;
const manualStartCmd = `npx @upstash/qstash-cli dev --port ${port}`;
const header = `
${DEV_PREFIX} The dev server is not running at ${baseUrl}.
`;
if (runtime === "cloudflare-workers") {
return header + `Cloudflare Workers cannot start the dev server automatically.
Start it manually before running wrangler dev:
${manualStartCmd}
`;
}
return header + `Edge runtimes cannot start the dev server automatically.
Either:
1. Add the instrumentation hook to start it with your app:
// instrumentation.ts
import { registerQStashDev } from "@upstash/qstash/nextjs";
export async function register() { await registerQStashDev(); }
2. Or start it manually:
${manualStartCmd}
`;
};
// src/dev-server/binary.ts
var ensureBinary = async () => {
const fs = await importFs();
const os = await importOs();
const cacheDirectory = await findCacheDirectory();
const isWindows = os.platform() === "win32";
const binaryName = isWindows ? "qstash.exe" : "qstash";
const binaryPath = `${cacheDirectory}/${binaryName}`;
const versionFile = `${cacheDirectory}/.version`;
let version;
try {
version = await fetchLatestVersion();
} catch (error) {
if (fs.existsSync(binaryPath)) {
const cachedVersion = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, "utf8").trim() : "unknown";
console.log(`${DEV_PREFIX} Offline, using local v${cachedVersion}`);
return binaryPath;
}
throw error;
}
return downloadBinary(version, cacheDirectory);
};
var fetchLatestVersion = async () => {
const { ok: ok4, statusCode, body } = await nativeGet(GITHUB_RELEASES_URL, {
Accept: "application/vnd.github.v3+json",
"User-Agent": "upstash-qstash-js"
});
if (!ok4) {
throw new Error(`[QStash Dev] Failed to fetch latest version: HTTP ${statusCode}`);
}
const data = JSON.parse(body.toString());
return data.tag_name.replace(/^v/, "");
};
var findCacheDirectory = async () => {
const fs = await importFs();
const os = await importOs();
const home = os.homedir();
const platform = os.platform();
let base;
if (platform === "darwin") {
base = `${home}/Library/Caches/upstash`;
} else if (platform === "win32") {
base = `${process.env.LOCALAPPDATA ?? `${home}/AppData/Local`}/upstash`;
} else {
base = `${home}/.cache/upstash`;
}
const cacheDirectory = `${base}/qstash-dev`;
await fs.promises.mkdir(cacheDirectory, { recursive: true });
return cacheDirectory;
};
var downloadBinary = async (version, cacheDirectory) => {
const fs = await importFs();
const childProcess = await importChildProcess();
const os = await importOs();
const osPlatform = os.platform();
const isWindows = osPlatform === "win32";
const platform = isWindows ? "windows" : osPlatform === "darwin" ? "darwin" : "linux";
const arch = os.arch() === "arm64" ? "arm64" : "amd64";
const archiveName = `qstash-server_${version}_${platform}_${arch}`;
const binaryName = isWindows ? "qstash.exe" : "qstash";
const binaryPath = `${cacheDirectory}/${binaryName}`;
const versionFile = `${cacheDirectory}/.version`;
if (fs.existsSync(binaryPath) && fs.existsSync(versionFile)) {
const cachedVersion = fs.readFileSync(versionFile, "utf8").trim();
if (cachedVersion === version) {
return binaryPath;
}
}
await fs.promises.rm(cacheDirectory, { recursive: true, force: true });
await fs.promises.mkdir(cacheDirectory, { recursive: true });
const extension = isWindows ? "zip" : "tar.gz";
const archiveUrl = `${BINARY_URL_BASE}/${version}/${archiveName}.${extension}`;
console.log(`${DEV_PREFIX} Downloading dev server v${version}...`);
const { ok: ok4, statusCode, body } = await nativeGet(archiveUrl);
if (!ok4) {
throw new Error(`[QStash Dev] Failed to download binary: HTTP ${statusCode}`);
}
const archivePath = `${cacheDirectory}/${archiveName}.${extension}`;
await fs.promises.writeFile(archivePath, new Uint8Array(body));
childProcess.execFileSync("tar", ["-xf", archivePath, "-C", cacheDirectory], {
stdio: "pipe"
});
if (!isWindows) {
const EXECUTABLE_PERMISSION = 493;
await fs.promises.chmod(binaryPath, EXECUTABLE_PERMISSION);
}
await fs.promises.writeFile(versionFile, version);
await fs.promises.unlink(archivePath).catch(() => {
});
return binaryPath;
};
// src/dev-server/process.ts
var STARTUP_TIMEOUT_MS = 3e4;
var _proc = () => {
return globalThis["process"] ?? {};
};
var spawnServer = async (binaryPath, port, onUnexpectedExit) => {
const childProcess = await importChildProcess();
const child = await new Promise((resolve, reject) => {
const child2 = childProcess.spawn(binaryPath, ["dev", "--port", String(port)], {
stdio: ["ignore", "pipe", "pipe"]
});
const timeout = setTimeout(() => {
child2.kill();
reject(new Error("[QStash Dev] Server failed to start within 30 seconds"));
}, STARTUP_TIMEOUT_MS);
let startupOutput = "";
let started = false;
const bufferLine = (line) => {
if (!started)
startupOutput += `${line}
`;
};
forwardWithPrefix(child2.stdout, _proc().stdout, (line) => {
bufferLine(line);
if (!started && /runn+ing( at|\.)/i.test(line)) {
clearTimeout(timeout);
started = true;
resolve(child2);
}
});
forwardWithPrefix(child2.stderr, _proc().stderr, bufferLine);
child2.on("error", (error) => {
clearTimeout(timeout);
reject(new Error(`[QStash Dev] Failed to start server: ${error.message}`));
});
child2.on("close", (code, _signal) => {
if (started) {
onUnexpectedExit?.();
return;
}
clearTimeout(timeout);
reject(new Error(formatStartupError(code, startupOutput)));
});
});
registerCleanup(child);
child.unref?.();
child.stdout?.unref?.();
child.stderr?.unref?.();
};
var formatStartupError = (code, startupOutput) => {
const cleaned = startupOutput.replaceAll(/\u001B\[[\d;]*m/g, "").replaceAll(/^\d{1,2}:\d{2}(AM|PM)\s+\w{3}\s+/gm, "").trim();
if (/address already in use/i.test(cleaned)) {
const match = /:(\d+)\s*$/.exec(cleaned);
const portHint = match ? ` on port ${match[1]}` : "";
return `[QStash Dev] Port already in use${portHint}. Set QSTASH_DEV_PORT to use a different port, or stop the process holding it.`;
}
const codeSuffix = code ? ` with code ${code}` : "";
const detail = cleaned ? `: ${cleaned}` : "";
return `[QStash Dev] Server exited unexpectedly${codeSuffix}${detail}`;
};
var forwardWithPrefix = (source, destination, onLine) => {
if (!source)
return;
let buffer = "";
const flushLine = (line) => {
destination?.write(`${CLI_PREFIX} ${line}
`);
onLine(line);
};
source.on("data", (data) => {
buffer += data.toString();
let newlineIndex = buffer.indexOf("\n");
while (newlineIndex !== -1) {
flushLine(buffer.slice(0, newlineIndex));
buffer = buffer.slice(newlineIndex + 1);
newlineIndex = buffer.indexOf("\n");
}
});
source.on("end", () => {
if (buffer.length > 0) {
flushLine(buffer);
buffer = "";
}
});
source.on("error", () => {
});
};
var currentChild;
var processHandlersRegistered = false;
var killCurrentChild = () => {
if (!currentChild)
return;
try {
currentChild.kill("SIGTERM");
} catch {
}
currentChild = void 0;
};
var registerCleanup = (child) => {
currentChild = child;
if (!processHandlersRegistered) {
processHandlersRegistered = true;
const proc = _proc();
proc.on?.("exit", killCurrentChild);
proc.on?.("SIGINT", () => {
killCurrentChild();
proc.exit?.(0);
});
proc.on?.("SIGTERM", () => {
killCurrentChild();
proc.exit?.(0);
});
}
};
// src/dev-server/index.ts
var _processGlobal2 = () => {
const proc = globalThis["process"];
return proc;
};
var devServerPromise;
var ensureDevelopmentServer = (env, devMode) => {
if (!shouldUseDevelopmentMode(devMode, env))
return Promise.resolve();
const procEnv = _processGlobal2()?.env;
if (procEnv?.NEXT_PHASE === "phase-production-build")
return Promise.resolve();
if (procEnv?.NODE_ENV === "production")
return Promise.resolve();
const runtime = getRuntime2();
if (runtime !== "nodejs") {
return checkDevServerReachable(getDevUrl(env), runtime);
}
if (!devServerPromise) {
devServerPromise = startPipeline(env).catch((error) => {
devServerPromise = void 0;
throw error;
});
}
return devServerPromise;
};
var startPipeline = async (env) => {
const baseUrl = getDevUrl(env);
const port = new URL(baseUrl).port;
const consoleLink = `\x1B[36m${CONSOLE_URL}?port=${port}\x1B[0m`;
if (await isDevServerRunning(baseUrl)) {
console.log(
`${DEV_PREFIX} Server already running at ${baseUrl}
${DEV_PREFIX} Console: ${consoleLink}`
);
return;
}
const binaryPath = await ensureBinary();
await spawnServer(binaryPath, port, () => {
devServerPromise = void 0;
});
};
var shouldUseDevelopmentMode = (devMode, env) => {
if (devMode !== void 0)
return devMode;
const value = env?.QSTASH_DEV ?? getProcessEnvironment("QSTASH_DEV");
if (value === void 0 || value === "" || value === "false" || value === "0")
return false;
if (value === "true" || value === "1")
return true;
throw new Error(`[QStash Dev] Invalid value for QSTASH_DEV in environment: ${value}`);
};
var getDevelopmentCredentials = (env) => {
return {
...DEV_CREDENTIALS,
baseUrl: getDevUrl(env)
};
};
var getDevUrl = (env) => {
const portString = env?.QSTASH_DEV_PORT ?? getProcessEnvironment("QSTASH_DEV_PORT");
let port = DEFAULT_DEV_PORT;
if (portString) {
const parsed = Number.parseInt(portString, 10);
if (!Number.isNaN(parsed) && parsed > 0) {
port = parsed;
}
}
return `http://127.0.0.1:${port}`;
};
var getRuntime2 = () => {
if (typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers") {
return "cloudflare-workers";
}
const proc = _processGlobal2();
if (!proc) {
return "browser";
}
if (!proc.release?.name) {
return "edge";
}
return "nodejs";
};
var getProcessEnvironment = (key) => {
const proc = _processGlobal2();
return proc?.env ? proc.env[key] : void 0;
};
// src/client/multi-region/incoming.ts
var getReceiverSigningKeys = ({
environment,
regionFromHeader,
config,
devMode
}) => {
if (shouldUseDevelopmentMode(devMode, environment)) {
if (config?.currentSigningKey || config?.nextSigningKey) {
console.warn(
`${DEV_PREFIX} Dev mode is active. Ignoring signing keys from config. Set devMode: false to use your own keys.`
);
}
const developmentCreds = getDevelopmentCredentials(environment);
return {
currentSigningKey: developmentCreds.currentSigningKey,
nextSigningKey: developmentCreds.nextSigningKey
};
}
if (config?.currentSigningKey && config.nextSigningKey) {
return {
currentSigningKey: config.currentSigningKey,
nextSigningKey: config.nextSigningKey
};
}
const regionEnvironment = getRegionFromEnvironment(environment);
if (regionEnvironment) {
const regionHeader = normalizeRegionHeader(regionFromHeader);
if (regionHeader) {
const regionCreds = readReceiverEnvironmentVariables(environment, regionHeader);
if (regionCreds.QSTASH_CURRENT_SIGNING_KEY && regionCreds.QSTASH_NEXT_SIGNING_KEY) {
return {
currentSigningKey: regionCreds.QSTASH_CURRENT_SIGNING_KEY,
nextSigningKey: regionCreds.QSTASH_NEXT_SIGNING_KEY,
region: regionHeader
};
} else {
console.warn(
`[Upstash QStash] Signing keys not found for region "${regionHeader}". Falling back to default signing keys.`
);
}
} else {
console.warn(
`[Upstash QStash] Invalid UPSTASH_REGION header value: "${regionFromHeader}". Expected one of: EU-CENTRAL-1, US-EAST-1. Falling back to default signing keys.`
);
}
}
const defaultCreds = readReceiverEnvironmentVariables(environment);
if (defaultCreds.QSTASH_CURRENT_SIGNING_KEY && defaultCreds.QSTASH_NEXT_SIGNING_KEY) {
return {
currentSigningKey: defaultCreds.QSTASH_CURRENT_SIGNING_KEY,
nextSigningKey: defaultCreds.QSTASH_NEXT_SIGNING_KEY
};
}
};
// src/client/multi-region/outgoing.ts
var getClientCredentials = (clientCredentialConfig) => {
const credentials = resolveCredentials(clientCredentialConfig);
return verifyCredentials(credentials);
};
var resolveCredentials = ({
environment,
config,
devMode
}) => {
if (shouldUseDevelopmentMode(devMode, environment)) {
if (config?.baseUrl || config?.token) {
console.warn(
`${DEV_PREFIX} Dev mode is active. Ignoring baseUrl/token from config. Set devMode: false to use your own credentials.`
);
}
const developmentCreds = getDevelopmentCredentials(environment);
return {
baseUrl: developmentCreds.baseUrl,
token: developmentCreds.token
};
}
if (config?.baseUrl && config.token) {
return {
baseUrl: config.baseUrl,
token: config.token
};
}
const region = getRegionFromEnvironment(environment);
if (region) {
const regionCreds = readClientEnvironmentVariables(environment, region);
if (regionCreds.QSTASH_URL && regionCreds.QSTASH_TOKEN) {
return {
baseUrl: regionCreds.QSTASH_URL,
token: regionCreds.QSTASH_TOKEN,
region
};
} else {
console.warn(
`[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.`
);
}
}
const defaultCreds = readClientEnvironmentVariables(environment);
return {
baseUrl: config?.baseUrl ?? defaultCreds.QSTASH_URL ?? DEFAULT_QSTASH_URL,
token: config?.token ?? defaultCreds.QSTASH_TOKEN ?? ""
};
};
var verifyCredentials = (credentials) => {
const token = credentials.token;
let baseUrl = credentials.baseUrl;
baseUrl = baseUrl.replace(/\/$/, "");
if (baseUrl === "https://qstash.upstash.io/v2/publish") {
baseUrl = DEFAULT_QSTASH_URL;
}
if (!token) {
console.warn(
"[Upstash QStash] client token is not set. Either pass a token or set QSTASH_TOKEN env variable."
);
}
return { baseUrl, token };
};
// src/receiver.ts
var SignatureError = class extends Error {
constructor(message) {
super(message);
this.name = "SignatureError";
}
};
var Receiver = class {
currentSigningKey;
nextSigningKey;
devMode;
constructor(config) {
this.currentSigningKey = config?.currentSigningKey;
this.nextSigningKey = config?.nextSigningKey;
this.devMode = config?.devMode;
}
/**
* Verify the signature of a request.
*
* Tries to verify the signature with the current signing key.
* If that fails, maybe because you have rotated the keys recently, it will
* try to verify the signature with the next signing key.
*
* If that fails, the signature is invalid and a `SignatureError` is thrown.
*/
async verify(request) {
const environment = getSafeEnvironment();
const signingKeys = getReceiverSigningKeys({
environment,
regionFromHeader: request.upstashRegion,
config: {
currentSigningKey: this.currentSigningKey,
nextSigningKey: this.nextSigningKey
},
devMode: this.devMode
});
if (!signingKeys) {
throw new Error(
"[Upstash QStash] No signing keys available for verification. See the warning above for more details."
);
}
let payload;
try {
payload = await this.verifyWithKey(signingKeys.currentSigningKey, request);
} catch {
payload = await this.verifyWithKey(signingKeys.nextSigningKey, request);
}
this.verifyBodyAndUrl(payload, request);
return true;
}
/**
* Verify signature with a specific signing key
*/
async verifyWithKey(key, request) {
const jwt = await jose.jwtVerify(request.signature, new TextEncoder().encode(key), {
issuer: "Upstash",
clockTolerance: request.clockTolerance
}).catch((error) => {
throw new SignatureError(error.message);
});
return jwt.payload;
}
verifyBodyAndUrl(payload, request) {
const p = payload;
if (request.url !== void 0 && p.sub !== request.url) {
throw new SignatureError(`invalid subject: ${p.sub}, want: ${request.url}`);
}
const bodyHash = import_crypto_js.default.SHA256(request.body).toString(import_crypto_js.default.enc.Base64url);
const padding = new RegExp(/=+$/);
if (p.body.replace(padding, "") !== bodyHash.replace(padding, "")) {
throw new SignatureError(`body hash does not match, want: ${p.body}, got: ${bodyHash}`);
}
}
};
// src/client/dlq.ts
var DLQ = class {
http;
constructor(http) {
this.http = http;
}
/**
* List messages in the dlq
*
* Can be called with:
* - Filters: `listMessages({ filter: { url: "https://example.com" } })`
* - DLQ IDs: `listMessages({ dlqIds: ["id1", "id2"] })`
* - No filter (list all): `listMessages()`
*/
async listMessages(options = {}) {
const query = {
count: options.count,
..."dlqIds" in options ? { dlqIds: options.dlqIds } : { ...renameUrlGroup(options.filter ?? {}), cursor: options.cursor }
};
const messagesPayload = await this.http.request({
method: "GET",
path: ["v2", "dlq"],
query
});
return {
messages: messagesPayload.messages.map((message) => {
return {
...message,
urlGroup: message.topicName,
ratePerSecond: "rate" in message ? message.rate : void 0
};
}),
cursor: messagesPayload.cursor
};
}
/**
* Remove messages from the dlq.
*
* Can be called with:
* - A single dlqId: `delete("id")`
* - An array of dlqIds: `delete(["id1", "id2"])`
* - An object with dlqIds: `delete({ dlqIds: ["id1", "id2"] })`
* - A filter object: `delete({ filter: { url: "https://example.com", label: "label" } })`
* - All messages: `delete({ all: true })`
*
* Pass `count` to limit the number of messages processed per call (defaults to 100).
* Call in a loop until cursor is undefined:
*
* ```ts
* let cursor: string | undefined;
* do {
* const result = await dlq.delete({ all: true, count: 100, cursor });
* cursor = result.cursor;
* } while (cursor);
* ```
*/
async delete(request) {
if (typeof request === "string") {
await this.http.request({
method: "DELETE",
path: ["v2", "dlq", request],
parseResponseAsJson: false
});
return { deleted: 1 };
}
if (Array.isArray(request) && request.length === 0)
return { deleted: 0 };
const filters = Array.isArray(request) ? { dlqIds: request } : request;
return await this.http.request({
method: "DELETE",
path: ["v2", "dlq"],
query: buildBulkActionFilterPayload(filters)
});
}
/**
* Remove multiple messages from the dlq using their `dlqId`s
*
* @deprecated Use `delete` instead
*/
async deleteMany(request) {
return await this.delete(request);
}
/**
* Retry messages from the dlq.
*
* Can be called with:
* - A single dlqId: `retry("id")`
* - An array of dlqIds: `retry(["id1", "id2"])`
* - An object with dlqIds: `retry({ dlqIds: ["id1", "id2"] })`
* - A filter object: `retry({ filter: { url: "https://example.com", label: "label" } })`
* - All messages: `retry({ all: true })`
*
*