UNPKG

@upstash/qstash

Version:

Official Typescript client for QStash

1,557 lines (1,532 loc) 130 kB
"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 })` * *