UNPKG

@upstash/qstash

Version:

Official Typescript client for QStash

1,575 lines (1,550 loc) 116 kB
// src/receiver.ts import * as jose from "jose"; import crypto2 from "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"); }; var openai = ({ token, organization }) => { return new LLMProvider("https://api.openai.com", token, "openai", organization); }; var anthropic = ({ token }) => { return new LLMProvider("https://api.anthropic.com", token, "anthropic"); }; var custom = ({ baseUrl, token }) => { const trimmedBaseUrl = baseUrl.replace(/\/(v1\/)?chat\/completions$/, ""); return new LLMProvider(trimmedBaseUrl, token, "custom"); }; // 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 = crypto2.SHA256(request.body).toString(crypto2.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: "system", content: request.system }, { role: "user", content: request.user } ); const chatRequest = { ...request, messages }; return chatRequest; } /** * Calls the Upstash completions api given a ChatRequest. * * Returns a ChatCompletion or a stream of ChatCompletionChunks * if stream is enabled. * * @param request ChatRequest with messages * @returns Chat completion or stream */ create = async (request) => { if (request.provider.owner != "upstash") return this.createThirdParty(request); const body = JSON.stringify(request); let baseUrl = void 0; let headers = { "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, ..."stream" in request && request.stream ? { Connection: "keep-alive", Accept: "text/event-stream", "Cache-Control": "no-cache" } : {} }; if (request.analytics) { const { baseURL, defaultHeaders } = setupAnalytics( { name: "helicone", token: request.analytics.token }, this.getAuthorizationToken(), request.provider.baseUrl, "upstash" ); headers = { ...he