@upstash/qstash
Version:
Official Typescript client for QStash
1,559 lines (1,532 loc) • 118 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/hono.ts
var hono_exports = {};
__export(hono_exports, {
serve: () => serve2
});
module.exports = __toCommonJS(hono_exports);
// 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 })`
*
* 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.retry({ all: true, count: 100, cursor });
* cursor = result.cursor;
* } while (cursor);
* ```
*/
async retry(request) {
if (typeof request === "string")
request = [request];
if (Array.isArray(request) && request.length === 0)
return { responses: [] };
const filters = Array.isArray(request) ? { dlqIds: request } : request;
return normalizeCursor(
await this.http.request({
method: "POST",
path: ["v2", "dlq", "retry"],
query: buildBulkActionFilterPayload(filters)
})
);
}
};
// src/client/flow-control.ts
var FlowControlApi = class {
http;
constructor(http) {
this.http = http;
}
/**
* Get a single flow control by key.
*/
async get(flowControlKey) {
return await this.http.request({
method: "GET",
path: ["v2", "flowControl", flowControlKey]
});
}
/**
* Get the global parallelism info.
*/
async getGlobalParallelism() {
const response = await this.http.request({
method: "GET",
path: ["v2", "globalParallelism"]
});
return {
parallelismMax: response.parallelismMax ?? 0,
parallelismCount: response.parallelismCount ?? 0
};
}
/**
* Pause message delivery for a flow-control key.
*
* Messages already in the waitlist will remain there.
* New incoming messages will be added directly to the waitlist.
*/
async pause(flowControlKey) {
await this.http.request({
method: "POST",
path: ["v2", "flowControl", flowControlKey, "pause"],
parseResponseAsJson: false
});
}
/**
* Resume message delivery for a flow-control key.
*/
async resume(flowControlKey) {
await this.http.request({
method: "POST",
path: ["v2", "flowControl", flowControlKey, "resume"],
parseResponseAsJson: false
});
}
/**
* Pin a processing configuration for a flow-control key.
*
* While pinned, the system ignores configurations provided by incoming
* messages and uses the pinned configuration instead.
*/
async pin(flowControlKey, options) {
await this.http.request({
method: "POST",
path: ["v2", "flowControl", flowControlKey, "pin"],
query: {
parallelism: options.parallelism,
rate: options.rate,
period: options.period
},
parseResponseAsJson: false
});
}
/**
* Remove the pinned configuration for a flow-control key.
*
* After unpinning, the system resumes updating the configuration
* based on incoming messages.
*/
async unpin(flowControlKey, options) {
await this.http.request({
method: "POST",
path: ["v2", "flowControl", flowControlKey, "unpin"],
query: {
parallelism: options.parallelism,
rate: options.rate
},
parseResponseAsJson: false
});
}
/**
* Reset the rate configuration state for a flow-control key.
*
* Clears the current rate count and immediately ends the current period.
* The current timestamp becomes the start of the new rate period.
*/
async resetRate(flowControlKey) {
await this.http.request({
method: "POST",
path: ["v2", "flowControl", flowControlKey, "resetRate"],
parseResponseAsJson: false
});
}
};
// src/client/http.ts
var HttpClient = class {
baseUrl;
authorization;
options;
devMode;
retry;
headers;
telemetryHeaders;
constructor(config) {
this.baseUrl = config.baseUrl.replace(/\/$/, "");
this.authorization = config.authorization;
this.devMode = config.devMode;
this.retry = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
typeof config.retry === "boolean" && !config.retry ? {
attempts: 1,
backoff: () => 0
} : {
attempts: config.retry?.retries ?? 5,
backoff: config.retry?.backoff ?? ((retryCount) => Math.exp(retryCount) * 50)
};
this.headers = config.headers;
this.telemetryHeaders = config.telemetryHeaders;
}
async request(request) {
await ensureDevelopmentServer(void 0, this.devMode);
const { response } = await this.requestWithBackoff(request);
if (request.parseResponseAsJson === false) {
return void 0;
}
return await response.json();
}
async *requestStream(request) {
await ensureDevelopmentServer(void 0, this.devMode);
const { response } = await this.requestWithBackoff(request);
if (!response.body) {
throw new Error("No response body");
}
const body = response.body;
const reader = body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunkText = decoder.decode(value, { stream: true });
const chunks = chunkText.split("\n").filter(Boolean);
for (const chunk of chunks) {
if (chunk.startsWith("data: ")) {
const data = chunk.slice(6);
if (data === "[DONE]") {
break;
}
yield JSON.parse(data);
}
}
}
} finally {
await reader.cancel();
}
}
requestWithBackoff = async (request) => {
const [url, requestOptions] = this.processRequest(request);
let response = void 0;
let error = void 0;
for (let index = 0; index <= this.retry.attempts; index++) {
try {
response = await fetch(url.toString(), requestOptions);
break;
} catch (error_) {
error = error_;
if (index < this.retry.attempts) {
await new Promise((r) => setTimeout(r, this.retry.backoff(index)));
}
}
}
if (!response) {
throw error ?? new Error("Exhausted all retries");
}
await this.checkResponse(response);
return {
response,
error
};
};
processRequest = (request) => {
const headers = new Headers(request.headers);
if (!headers.has("Authorization")) {
headers.set("Authorization", this.authorization);
}
const requestOptions = {
method: request.method,
headers,
body: request.body,
keepalive: request.keepalive
};
const url = new URL([request.baseUrl ?? this.baseUrl, ...request.path].join("/"));
if (request.query) {
for (const [key, value] of Object.entries(request.query)) {
if (value === void 0)
continue;
if (Array.isArray(value)) {
if (value.length === 0) {
throw new QstashEmptyArrayError(key);
}
for (const item of value) {
url.searchParams.append(key, item);
}
} else if (value instanceof Date) {
url.searchParams.set(key, value.getTime().toString());
} else {
url.searchParams.set(key, value.toString());
}
}
}
return [url.toString(), requestOptions];
};
async checkResponse(response) {
if (response.status === 429) {
if (response.headers.get("x-ratelimit-limit-requests")) {
throw new QstashChatRatelimitError({
"limit-requests": response.headers.get("x-ratelimit-limit-requests"),
"limit-tokens": response.headers.get("x-ratelimit-limit-tokens"),
"remaining-requests": response.headers.get("x-ratelimit-remaining-requests"),
"remaining-tokens": response.headers.get("x-ratelimit-remaining-tokens"),
"reset-requests": response.headers.get("x-ratelimit-reset-requests"),
"reset-tokens": response.headers.get("x-ratelimit-reset-tokens")
});
} else if (response.headers.get("RateLimit-Limit")) {
throw new QstashDailyRatelimitError({
limit: response.headers.get("RateLimit-Limit"),
remaining: response.headers.get("RateLimit-Remaining"),
reset: response.headers.get("RateLimit-Reset")
});
}
throw new QstashRatelimitError({
limit: response.headers.get("Burst-RateLimit-Limit"),
remaining: response.headers.get("Burst-RateLimit-Remaining"),
reset: response.headers.get("Burst-RateLimit-Reset")
});
}
if (response.status < 200 || response.status >= 300) {
const body = await response.text();
throw new QstashError(
body.length > 0 ? body : `Error: status=${response.status}`,
response.status
);
}
}
};
// src/client/llm/providers.ts
var setupAnalytics = (analytics, providerApiKey, providerBaseUrl, provider) => {
if (!analytics)
return {};
switch (analytics.name) {
case "helicone": {
switch (provider) {
case "upstash": {
return {
baseURL: "https://qstash.helicone.ai/llm/v1/chat/completions",
defaultHeaders: {
"Helicone-Auth": `Bearer ${analytics.token}`,
Authorization: `Bearer ${providerApiKey}`
}
};
}
default: {
return {
baseURL: "https://gateway.helicone.ai/v1/chat/completions",
defaultHeaders: {
"Helicone-Auth": `Bearer ${analytics.token}`,
"Helicone-Target-Url": providerBaseUrl,
Authorization: `Bearer ${providerApiKey}`
}
};
}
}
}
default: {
throw new Error("Unknown analytics provider");
}
}
};
// src/client/llm/chat.ts
var Chat = class _Chat {
http;
token;
constructor(http, token) {
this.http = http;
this.token = token;
}
static toChatRequest(request) {
const messages = [];
messages.push(
{ role: "s