inngest
Version:
Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.
1,378 lines (1,377 loc) • 52.3 kB
JavaScript
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
const require_consts = require('../helpers/consts.cjs');
const require_enum = require('../helpers/enum.cjs');
const require_version = require('../version.cjs');
const require_env = require('../helpers/env.cjs');
const require_errors = require('../helpers/errors.cjs');
const require_types = require('../types.cjs');
const require_InngestExecution = require('./execution/InngestExecution.cjs');
const require_types$1 = require('../helpers/types.cjs');
const require_log = require('../helpers/log.cjs');
const require_functions = require('../helpers/functions.cjs');
const require_net = require('../helpers/net.cjs');
const require_promises = require('../helpers/promises.cjs');
const require_ServerTiming = require('../helpers/ServerTiming.cjs');
const require_strings = require('../helpers/strings.cjs');
const require_stream = require('../helpers/stream.cjs');
const require_als = require('./execution/als.cjs');
const require_InngestFunction = require('./InngestFunction.cjs');
const require_utils = require('./middleware/utils.cjs');
const require_Inngest = require('./Inngest.cjs');
const require_engine = require('./execution/engine.cjs');
let zod_v3 = require("zod/v3");
//#region src/components/InngestCommHandler.ts
const internalServerErrorResponse = {
body: require_strings.stringify({ code: "internal_server_error" }),
headers: { "Content-Type": "application/json" },
status: 500,
version: void 0
};
/**
* A schema for the response from Inngest when registering.
*/
const registerResSchema = zod_v3.z.object({
status: zod_v3.z.number().default(200),
skipped: zod_v3.z.boolean().optional().default(false),
modified: zod_v3.z.boolean().optional().default(false),
error: zod_v3.z.string().default("Successfully registered")
});
/**
* `InngestCommHandler` is a class for handling incoming requests from Inngest (or
* Inngest's tooling such as the dev server or CLI) and taking appropriate
* action for any served functions.
*
* All handlers (Next.js, RedwoodJS, Remix, Deno Fresh, etc.) are created using
* this class; the exposed `serve` function will - most commonly - create an
* instance of `InngestCommHandler` and then return `instance.createHandler()`.
*
* See individual parameter details for more information, or see the
* source code for an existing handler, e.g.
* {@link https://github.com/inngest/inngest-js/blob/main/src/next.ts}
*
* @example
* ```
* // my-custom-handler.ts
* import {
* InngestCommHandler,
* type ServeHandlerOptions,
* } from "./components/InngestCommHandler";
*
* export const serve = (options: ServeHandlerOptions) => {
* const handler = new InngestCommHandler({
* frameworkName: "my-custom-handler",
* ...options,
* handler: (req: Request) => {
* return {
* body: () => req.json(),
* headers: (key) => req.headers.get(key),
* method: () => req.method,
* url: () => new URL(req.url, `https://${req.headers.get("host") || ""}`),
* transformResponse: ({ body, status, headers }) => {
* return new Response(body, { status, headers });
* },
* };
* },
* });
*
* return handler.createHandler();
* };
* ```
*
* @public
*/
var InngestCommHandler = class {
/**
* The handler specified during instantiation of the class.
*/
handler;
/**
* The URL of the Inngest function registration endpoint.
*/
inngestRegisterUrl;
/**
* The name of the framework this handler is designed for. Should be
* lowercase, alphanumeric characters inclusive of `-` and `/`.
*/
frameworkName;
/**
* The origin used to access the Inngest serve endpoint, e.g.:
*
* "https://myapp.com" or "https://myapp.com:1234"
*
* By default, the library will try to infer this using request details such
* as the "Host" header and request path, but sometimes this isn't possible
* (e.g. when running in a more controlled environments such as AWS Lambda or
* when dealing with proxies/redirects).
*
* Provide the custom origin here to ensure that the path is reported
* correctly when registering functions with Inngest.
*
* To also provide a custom path, use `servePath`.
*/
_serveOrigin;
/**
* The path to the Inngest serve endpoint. e.g.:
*
* "/some/long/path/to/inngest/endpoint"
*
* By default, the library will try to infer this using request details such
* as the "Host" header and request path, but sometimes this isn't possible
* (e.g. when running in a more controlled environments such as AWS Lambda or
* when dealing with proxies/redirects).
*
* Provide the custom path (excluding the hostname) here to ensure that the
* path is reported correctly when registering functions with Inngest.
*
* To also provide a custom hostname, use `serveOrigin`.
*/
_servePath;
streaming;
/**
* A private collection of just Inngest functions, as they have been passed
* when instantiating the class.
*/
rawFns;
client;
/**
* A private collection of functions that are being served. This map is used
* to find and register functions when interacting with Inngest Cloud.
*/
fns = {};
env = require_env.allProcessEnv();
allowExpiredSignatures;
_options;
skipSignatureValidation;
constructor(options) {
this._options = options;
/**
* v2 -> v3 migration error.
* TODO: do we need to handle people going from v2->v4?
*
* If a serve handler is passed a client as the first argument, it'll be
* spread in to these options. We should be able to detect this by picking
* up a unique property on the object.
*/
if (Object.hasOwn(options, "eventKey")) throw new Error(`${require_consts.logPrefix} You've passed an Inngest client as the first argument to your serve handler. This is no longer supported in v3; please pass the Inngest client as the \`client\` property of an options object instead. See https://www.inngest.com/docs/sdk/migration`);
this.frameworkName = options.frameworkName;
this.client = options.client;
this.handler = options.handler;
/**
* Provide a hidden option to allow expired signatures to be accepted during
* testing.
*/
this.allowExpiredSignatures = Boolean(arguments["0"]?.__testingAllowExpiredSignatures);
this.rawFns = options.functions?.filter(Boolean) ?? [];
if (this.rawFns.length !== (options.functions ?? []).length) this.client[require_Inngest.internalLoggerSymbol].warn(`Some functions passed to serve() are undefined and misconfigured. Please check your imports.`);
this.fns = this.rawFns.reduce((acc, fn) => {
const configs = fn["getConfig"]({
baseUrl: new URL("https://example.com"),
appPrefix: this.client.id
});
const fns = configs.reduce((acc$1, { id }, index) => {
return {
...acc$1,
[id]: {
fn,
onFailure: Boolean(index)
}
};
}, {});
configs.forEach(({ id }) => {
if (acc[id]) throw new Error(`Duplicate function ID "${id}"; please change a function's name or provide an explicit ID to avoid conflicts.`);
});
return {
...acc,
...fns
};
}, {});
this.inngestRegisterUrl = new URL("/fn/register", this.client.apiBaseUrl);
this._serveOrigin = options.serveOrigin || this.env[require_consts.envKeys.InngestServeOrigin];
this._servePath = options.servePath || this.env[require_consts.envKeys.InngestServePath];
this.skipSignatureValidation = options.skipSignatureValidation || false;
const defaultStreamingOption = false;
this.streaming = zod_v3.z.boolean().default(defaultStreamingOption).catch((ctx) => {
this.client[require_Inngest.internalLoggerSymbol].warn({
input: ctx.input,
default: defaultStreamingOption
}, "Unknown streaming option; using default");
return defaultStreamingOption;
}).parse(options.streaming || require_env.parseAsBoolean(this.env[require_consts.envKeys.InngestStreaming]));
this.client.setEnvVars(this.env);
}
/**
* The origin used to access the Inngest serve endpoint, e.g.:
*
* "https://myapp.com"
*
* By default, the library will try to infer this using request details such
* as the "Host" header and request path, but sometimes this isn't possible
* (e.g. when running in a more controlled environments such as AWS Lambda or
* when dealing with proxies/redirects).
*
* Provide the custom origin here to ensure that the path is reported
* correctly when registering functions with Inngest.
*
* To also provide a custom path, use `servePath`.
*/
get serveOrigin() {
if (this._serveOrigin) return this._serveOrigin;
const envOrigin = this.env[require_consts.envKeys.InngestServeOrigin];
if (envOrigin) return envOrigin;
const envHost = this.env[require_consts.envKeys.InngestServeHost];
if (envHost) {
require_log.warnOnce(this.client[require_Inngest.internalLoggerSymbol], "serve-host-deprecated", "INNGEST_SERVE_HOST is deprecated; use INNGEST_SERVE_ORIGIN instead");
return envHost;
}
}
/**
* The path to the Inngest serve endpoint. e.g.:
*
* "/some/long/path/to/inngest/endpoint"
*
* By default, the library will try to infer this using request details such
* as the "Host" header and request path, but sometimes this isn't possible
* (e.g. when running in a more controlled environments such as AWS Lambda or
* when dealing with proxies/redirects).
*
* Provide the custom path (excluding the hostname) here to ensure that the
* path is reported correctly when registering functions with Inngest.
*
* To also provide a custom hostname, use `serveOrigin`.
*
* This is a getter to encourage checking the environment for the serve path
* each time it's accessed, as it may change during execution.
*/
get servePath() {
return this._servePath || this.env[require_consts.envKeys.InngestServePath];
}
get hashedEventKey() {
if (!this.client.eventKey) return;
return require_strings.hashEventKey(this.client.eventKey);
}
get hashedSigningKey() {
if (!this.client.signingKey) return;
return require_strings.hashSigningKey(this.client.signingKey);
}
get hashedSigningKeyFallback() {
if (!this.client.signingKeyFallback) return;
return require_strings.hashSigningKey(this.client.signingKeyFallback);
}
/**
* Returns a `boolean` representing whether this handler will stream responses
* or not. Takes into account the user's preference and the platform's
* capabilities.
*/
async shouldStream(actions) {
if (await actions.queryStringWithDefaults("testing for probe", require_consts.queryKeys.Probe) !== void 0) return false;
const envStreaming = this.env[require_consts.envKeys.InngestStreaming];
if (envStreaming === "allow" || envStreaming === "force") require_log.warnOnce(this.client[require_Inngest.internalLoggerSymbol], "streaming-allow-force-deprecated", { value: envStreaming }, `INNGEST_STREAMING="${envStreaming}" is deprecated; set INNGEST_STREAMING=true instead`);
const streamingRequested = this.streaming === true || require_env.parseAsBoolean(this.env[require_consts.envKeys.InngestStreaming]) === true || envStreaming === "allow" || envStreaming === "force";
if (!actions.transformStreamingResponse) {
if (streamingRequested) throw new Error(`${require_consts.logPrefix} Streaming has been forced but the serve handler does not support streaming. Please either remove the streaming option or use a serve handler that supports streaming.`);
return false;
}
return streamingRequested;
}
async isInngestReq(actions) {
const reqMessage = `checking if this is an Inngest request`;
const [runId, signature] = await Promise.all([actions.headers(reqMessage, require_consts.headerKeys.InngestRunId), actions.headers(reqMessage, require_consts.headerKeys.Signature)]);
return Boolean(runId && typeof signature === "string");
}
/**
* Start handling a request, setting up environments, modes, and returning
* some helpers.
*/
async initRequest(...args) {
const timer = new require_ServerTiming.ServerTiming(this.client[require_Inngest.internalLoggerSymbol]);
const actions = await this.getActions(timer, ...args);
const [env, expectedServerKind] = await Promise.all([actions.env?.("starting to handle request"), actions.headers("checking expected server kind", require_consts.headerKeys.InngestServerKind)]);
this.env = {
...require_env.allProcessEnv(),
...env
};
this.client.setEnvVars(this.env);
const headerPromises = require_consts.forwardedHeaders.map(async (header) => {
return {
header,
value: await actions.headers(`fetching ${header} for forwarding`, header)
};
});
const headersToForwardP = Promise.all(headerPromises).then((fetchedHeaders) => {
return fetchedHeaders.reduce((acc, { header, value }) => {
if (value) acc[header] = value;
return acc;
}, {});
});
const getHeaders = async () => ({
...require_env.inngestHeaders({
env: this.env,
framework: this.frameworkName,
client: this.client,
expectedServerKind: expectedServerKind || void 0,
extras: { "Server-Timing": timer.getHeader() }
}),
...await headersToForwardP
});
return {
timer,
actions,
getHeaders
};
}
/**
* `createSyncHandler` should be used to return a type-equivalent version of
* the `handler` specified during instantiation.
*/
createSyncHandler() {
return (handler) => {
return this.wrapHandler((async (...args) => {
const reqInit = await this.initRequest(...args);
const fn = new require_InngestFunction.InngestFunction(this.client, {
id: this._options.syncOptions?.functionId ?? "",
retries: this._options.syncOptions?.retries ?? require_consts.defaultMaxRetries
}, () => handler(...args));
if (await this.isInngestReq(reqInit.actions)) return this.handleAsyncRequest({
...reqInit,
forceExecution: true,
args,
fns: [fn]
});
return this.handleSyncRequest({
...reqInit,
args,
asyncMode: this._options.syncOptions?.asyncResponse ?? require_types.AsyncResponseType.Redirect,
asyncRedirectUrl: this._options.syncOptions?.asyncRedirectUrl,
fn
});
}));
};
}
/**
* `createHandler` should be used to return a type-equivalent version of the
* `handler` specified during instantiation.
*
* @example
* ```
* // my-custom-handler.ts
* import {
* InngestCommHandler,
* type ServeHandlerOptions,
* } from "./components/InngestCommHandler";
*
* export const serve = (options: ServeHandlerOptions) => {
* const handler = new InngestCommHandler({
* frameworkName: "my-custom-handler",
* ...options,
* handler: (req: Request) => {
* return {
* body: () => req.json(),
* headers: (key) => req.headers.get(key),
* method: () => req.method,
* url: () => new URL(req.url, `https://${req.headers.get("host") || ""}`),
* transformResponse: ({ body, status, headers }) => {
* return new Response(body, { status, headers });
* },
* };
* },
* });
*
* return handler.createHandler();
* };
* ```
*/
createHandler() {
return this.wrapHandler((async (...args) => {
return this.handleAsyncRequest({
...await this.initRequest(...args),
args
});
}));
}
/**
* Given a set of actions that let us access the incoming request, create an
* event that repesents a run starting from an HTTP request.
*/
async createHttpEvent(actions, fn) {
const reason = "creating sync event";
const contentTypePromise = actions.headers(reason, require_consts.headerKeys.ContentType).then((v) => v ?? "");
const ipPromise = actions.headers(reason, require_consts.headerKeys.ForwardedFor).then((v) => {
if (v) return v;
return actions.headers(reason, require_consts.headerKeys.RealIp).then((v$1) => v$1 ?? "");
});
const methodPromise = actions.method(reason);
const urlPromise = actions.url(reason).then((v) => this.reqUrl(v));
const domainPromise = urlPromise.then((url) => `${url.protocol}//${url.host}`);
const pathPromise = urlPromise.then((url) => url.pathname);
const queryParamsPromise = urlPromise.then((url) => url.searchParams.toString());
const bodyPromise = actions.body(reason).then((body$1) => {
return typeof body$1 === "string" ? body$1 : require_strings.stringify(body$1);
});
const [contentType, domain, ip, method, path, queryParams, body] = await Promise.all([
contentTypePromise,
domainPromise,
ipPromise,
methodPromise,
pathPromise,
queryParamsPromise,
bodyPromise
]);
return {
name: require_consts.internalEvents.HttpRequest,
data: {
content_type: contentType,
domain,
ip,
method,
path,
query_params: queryParams,
body,
fn: fn.id()
}
};
}
async handleSyncRequest({ timer, actions, fn, asyncMode, asyncRedirectUrl, args }) {
if (!actions.experimentalTransformSyncResponse) throw new Error("This platform does not support synchronous Inngest function executions.");
if (await require_als.getAsyncCtx()) throw new Error("We already seem to be in the context of an Inngest execution, but didn't expect to be. Did you already wrap this handler?");
const { ulid } = await import("ulid");
const runId = ulid();
const event = await this.createHttpEvent(actions, fn);
const exeVersion = require_consts.ExecutionVersion.V2;
const result = await fn["createExecution"]({ partialOptions: {
client: this.client,
data: {
runId,
event,
attempt: 0,
events: [event],
maxAttempts: fn.opts.retries ?? require_consts.defaultMaxRetries
},
runId,
headers: {},
reqArgs: args,
stepCompletionOrder: [],
stepState: {},
disableImmediateExecution: false,
isFailureHandler: false,
timer,
createResponse: (data) => actions.experimentalTransformSyncResponse("creating sync execution", data).then((res) => ({
...res,
version: exeVersion
})),
stepMode: require_types.StepMode.Sync
} }).start();
const resultHandler = {
"step-not-found": () => {
throw new Error("We should not get the result 'step-not-found' when checkpointing. This is a bug in the `inngest` SDK");
},
"steps-found": () => {
throw new Error("We should not get the result 'steps-found' when checkpointing. This is a bug in the `inngest` SDK");
},
"step-ran": () => {
throw new Error("We should not get the result 'step-ran' when checkpointing. This is a bug in the `inngest` SDK");
},
"function-rejected": (result$1) => {
return actions.transformResponse("creating sync error response", {
status: result$1.retriable ? 500 : 400,
headers: {
"Content-Type": "application/json",
[require_consts.headerKeys.NoRetry]: result$1.retriable ? "false" : "true",
...typeof result$1.retriable === "string" ? { [require_consts.headerKeys.RetryAfter]: result$1.retriable } : {}
},
version: exeVersion,
body: require_strings.stringify(require_functions.undefinedToNull(result$1.error))
});
},
"function-resolved": ({ data }) => {
return data;
},
"change-mode": async ({ token }) => {
switch (asyncMode) {
case require_types.AsyncResponseType.Redirect: {
let redirectUrl;
if (asyncRedirectUrl) if (typeof asyncRedirectUrl === "function") redirectUrl = await asyncRedirectUrl({
runId,
token
});
else {
const baseUrl = await actions.url("getting request origin");
const url = new URL(asyncRedirectUrl, baseUrl.origin);
url.searchParams.set("runId", runId);
url.searchParams.set("token", token);
redirectUrl = url.toString();
}
else redirectUrl = await this.client["inngestApi"]["getTargetUrl"](`/v1/http/runs/${runId}/output?token=${token}`).then((url) => url.toString());
return actions.transformResponse("creating sync->async redirect response", {
status: 302,
headers: { [require_consts.headerKeys.Location]: redirectUrl },
version: exeVersion,
body: ""
});
}
case require_types.AsyncResponseType.Token: return actions.transformResponse("creating sync->async token response", {
status: 200,
headers: {},
version: exeVersion,
body: require_strings.stringify({
run_id: runId,
token
})
});
default: break;
}
throw new Error("Not implemented: change-mode");
}
}[result.type];
if (!resultHandler) throw new Error(`No handler for execution result type: ${result.type}. This is a bug in the \`inngest\` SDK`);
return resultHandler(result);
}
async handleAsyncRequest({ timer, actions, args, getHeaders, forceExecution, fns }) {
if (forceExecution && !actions.experimentalTransformSyncResponse) throw new Error("This platform does not support async executions in Inngest for APIs.");
const methodP = actions.method("starting to handle request");
const [signature, method, body] = await Promise.all([
actions.headers("checking signature for request", require_consts.headerKeys.Signature).then((headerSignature) => {
return headerSignature ?? void 0;
}),
methodP,
methodP.then(async (method$1) => {
if (method$1 === "POST" || method$1 === "PUT") {
const body$1 = await actions.body(`checking body for request signing as method is ${method$1}`);
if (!body$1) return "";
if (typeof body$1 === "string") return JSON.parse(body$1);
return body$1;
}
return "";
})
]);
const signatureValidation = this.validateSignature(signature, body);
const mwInstances = this.client.middleware.map((Cls) => new Cls({ client: this.client }));
/**
* Prepares an action response by merging returned data to provide
* trailing information such as `Server-Timing` headers.
*
* It should always prioritize the headers returned by the action, as they
* may contain important information such as `Content-Type`.
*/
const prepareActionRes = async (res) => {
const headers = {
...await getHeaders(),
...res.headers,
...res.version === null ? {} : { [require_consts.headerKeys.RequestVersion]: (res.version ?? require_InngestExecution.PREFERRED_ASYNC_EXECUTION_VERSION).toString() }
};
let signature$1;
try {
signature$1 = await signatureValidation.then(async (result) => {
if (!result.success || !result.keyUsed) return;
return await this.getResponseSignature(result.keyUsed, res.body);
});
} catch (err) {
return {
...res,
headers,
body: require_strings.stringify(require_errors.serializeError(err)),
status: 500
};
}
if (signature$1) headers[require_consts.headerKeys.Signature] = signature$1;
return {
...res,
headers
};
};
let actionResponseVersion;
const handleAndPrepare = async () => {
const rawRes = await timer.wrap("action", () => this.handleAction({
actions,
timer,
getHeaders,
reqArgs: args,
signatureValidation,
body,
method,
forceExecution: Boolean(forceExecution),
fns,
mwInstances
}));
actionResponseVersion = rawRes.version;
return prepareActionRes(rawRes);
};
let chainResult;
if (method === "POST") {
const url = await actions.url("building requestInfo for middleware");
const fnId = url.searchParams.get(require_consts.queryKeys.FnId);
const matchedFn = fnId ? this.fns[fnId] : void 0;
const fnMw = matchedFn?.fn?.opts?.middleware ?? [];
mwInstances.push(...fnMw.map((Cls) => {
return new Cls({ client: this.client });
}));
const fn = matchedFn?.fn ?? null;
const requestInfo = {
headers: Object.freeze({ ...await getHeaders() }),
method,
url,
body: () => Promise.resolve(body)
};
let runId = "";
if (require_types$1.isRecord(body) && require_types$1.isRecord(body.ctx) && body.ctx.run_id && typeof body.ctx.run_id === "string") runId = body.ctx.run_id;
const innerHandler = async () => {
const prepared = await handleAndPrepare();
return {
status: prepared.status,
headers: prepared.headers,
body: prepared.body
};
};
chainResult = require_utils.buildWrapRequestChain({
fn,
handler: innerHandler,
middleware: mwInstances,
requestArgs: args,
requestInfo,
runId
})();
} else chainResult = handleAndPrepare().then((prepared) => ({
status: prepared.status,
headers: prepared.headers,
body: prepared.body
}));
const safeChainResult = chainResult.catch((err) => ({
status: 500,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify({
type: "internal",
...require_errors.serializeError(err)
})
}));
let shouldStream;
try {
shouldStream = await this.shouldStream(actions);
} catch (err) {
return actions.transformResponse("sending back response", {
status: 500,
headers: {
...await getHeaders(),
"Content-Type": "application/json"
},
body: require_strings.stringify(require_errors.serializeError(err)),
version: void 0
});
}
if (shouldStream) {
if (await actions.method("starting streaming response") === "POST") {
const { stream, finalize } = await require_stream.createStream();
/**
* Errors are handled by `handleAction` here to ensure that an
* appropriate response is always given.
*/
safeChainResult.then((res) => {
return finalize(Promise.resolve({
...res,
version: actionResponseVersion
}));
});
return timer.wrap("res", async () => {
return actions.transformStreamingResponse?.("starting streaming response", {
status: 201,
headers: await getHeaders(),
body: stream,
version: null
});
});
}
}
return timer.wrap("res", async () => {
return safeChainResult.then((res) => {
return actions.transformResponse("sending back response", {
...res,
version: actionResponseVersion
});
});
});
}
async getActions(timer, ...args) {
/**
* Used for testing, allow setting action overrides externally when
* calling the handler. Always search the final argument.
*/
const lastArg = args[args.length - 1];
const actionOverrides = typeof lastArg === "object" && lastArg !== null && "actionOverrides" in lastArg && typeof lastArg["actionOverrides"] === "object" && lastArg["actionOverrides"] !== null ? lastArg["actionOverrides"] : {};
/**
* We purposefully `await` the handler, as it could be either sync or
* async.
*/
const rawActions = {
...await timer.wrap("handler", () => this.handler(...args)).catch(require_errors.rethrowError("Serve handler failed to run")),
...actionOverrides
};
/**
* Mapped promisified handlers from userland `serve()` function mixed in
* with some helpers.
*/
const actions = {
...Object.entries(rawActions).reduce((acc, [key, value]) => {
if (typeof value !== "function") return acc;
return {
...acc,
[key]: (reason, ...args$1) => {
const errMessage = [`Failed calling \`${key}\` from serve handler`, reason].filter(Boolean).join(" when ");
const fn = () => value(...args$1);
return require_promises.runAsPromise(fn).catch(require_errors.rethrowError(errMessage)).catch((err) => {
this.client[require_Inngest.internalLoggerSymbol].error({ err }, errMessage);
throw err;
});
}
};
}, {}),
queryStringWithDefaults: async (reason, key) => {
const url = await actions.url(reason);
return await actions.queryString?.(reason, key, url) || url.searchParams.get(key) || void 0;
},
...actionOverrides
};
return actions;
}
wrapHandler(handler) {
/**
* Some platforms check (at runtime) the length of the function being used
* to handle an endpoint. If this is a variadic function, it will fail that
* check.
*
* Therefore, we expect the arguments accepted to be the same length as the
* `handler` function passed internally.
*
* We also set a name to avoid a common useless name in tracing such as
* `"anonymous"` or `"bound function"`.
*
* https://github.com/getsentry/sentry-javascript/issues/3284
*/
Object.defineProperties(handler, {
name: { value: "InngestHandler" },
length: { value: this.handler.length }
});
return handler;
}
/**
* Given a set of functions to check if an action is available from the
* instance's handler, enact any action that is found.
*
* This method can fetch varying payloads of data, but ultimately is the place
* where _decisions_ are made regarding functionality.
*
* For example, if we find that we should be viewing the UI, this function
* will decide whether the UI should be visible based on the payload it has
* found (e.g. env vars, options, etc).
*/
async handleAction({ actions, timer, getHeaders, reqArgs, signatureValidation, body: rawBody, method, forceExecution, fns, mwInstances }) {
if (!this.checkModeConfiguration()) return internalServerErrorResponse;
const isMissingBody = !rawBody;
let body = rawBody;
try {
let url = await actions.url("starting to handle request");
if (method === "POST" || forceExecution) {
if (!forceExecution && isMissingBody) {
this.client[require_Inngest.internalLoggerSymbol].error("Missing body when executing, possibly due to missing request body middleware");
return {
status: 500,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify(require_errors.serializeError(/* @__PURE__ */ new Error("Missing request body when executing, possibly due to missing request body middleware"))),
version: void 0
};
}
const validationResult = await signatureValidation;
if (!validationResult.success) return {
status: 401,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify(require_errors.serializeError(validationResult.err)),
version: void 0
};
let fn;
let fnId;
if (forceExecution) {
fn = fns?.length && fns[0] ? {
fn: fns[0],
onFailure: false
} : Object.values(this.fns)[0];
fnId = fn?.fn.id();
let die = false;
const dieHeader = await actions.headers("getting step plan force control for forced execution", require_consts.headerKeys.InngestForceStepPlan);
if (dieHeader) {
const parsed = require_env.parseAsBoolean(dieHeader);
if (typeof parsed === "boolean") die = parsed;
else this.client[require_Inngest.internalLoggerSymbol].warn({
header: require_consts.headerKeys.InngestForceStepPlan,
value: dieHeader
}, "Invalid boolean header value; defaulting to false");
}
body = {
event: {},
events: [],
steps: {},
version: require_InngestExecution.PREFERRED_ASYNC_EXECUTION_VERSION,
sdkDecided: true,
ctx: {
attempt: 0,
disable_immediate_execution: die,
use_api: true,
max_attempts: Infinity,
run_id: await actions.headers("getting run ID for forced execution", require_consts.headerKeys.InngestRunId),
stack: {
stack: [],
current: 0
}
}
};
} else {
const rawProbe = await actions.queryStringWithDefaults("testing for probe", require_consts.queryKeys.Probe);
if (rawProbe) {
const probe$1 = require_enum.enumFromValue(require_consts.probe, rawProbe);
if (!probe$1) return {
status: 400,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify(require_errors.serializeError(/* @__PURE__ */ new Error(`Unknown probe "${rawProbe}"`))),
version: void 0
};
return { [require_consts.probe.Trust]: () => ({
status: 200,
headers: { "Content-Type": "application/json" },
body: "",
version: void 0
}) }[probe$1]();
}
fnId = await actions.queryStringWithDefaults("processing run request", require_consts.queryKeys.FnId);
if (!fnId) throw new Error("No function ID found in async request");
fn = this.fns[fnId];
}
if (typeof fnId === "undefined" || !fn) throw new Error("No function ID found in request");
const stepId = await actions.queryStringWithDefaults("processing run request", require_consts.queryKeys.StepId) || await actions.headers("processing run request", require_consts.headerKeys.InngestStepId) || null;
let headerReqVersion;
try {
const rawVersionHeader = await actions.headers("processing run request", require_consts.headerKeys.RequestVersion);
if (rawVersionHeader && Number.isFinite(Number(rawVersionHeader))) {
const res = require_functions.createVersionSchema(this.client[require_Inngest.internalLoggerSymbol]).parse(Number(rawVersionHeader));
if (!res.sdkDecided) headerReqVersion = res.version;
}
} catch {}
const resolvedHeaders = await getHeaders();
const { version: version$1, result } = this.runStep({
functionId: fnId,
data: body,
stepId,
timer,
reqArgs,
headers: resolvedHeaders,
fn,
forceExecution,
actions,
headerReqVersion,
requestInfo: {
headers: Object.freeze({ ...resolvedHeaders }),
method,
url,
body: () => Promise.resolve(body)
},
mwInstances
});
const stepOutput = await result;
/**
* Functions can return `undefined`, but we'll always convert this to
* `null`, as this is appropriately serializable by JSON.
*/
const opDataUndefinedToNull = (op) => {
op.data = require_functions.undefinedToNull(op.data);
return op;
};
const handler = {
"function-rejected": (result$1) => {
return {
status: result$1.retriable ? 500 : 400,
headers: {
"Content-Type": "application/json",
[require_consts.headerKeys.NoRetry]: result$1.retriable ? "false" : "true",
...typeof result$1.retriable === "string" ? { [require_consts.headerKeys.RetryAfter]: result$1.retriable } : {}
},
body: require_strings.stringify(require_functions.undefinedToNull(result$1.error)),
version: version$1
};
},
"function-resolved": (result$1) => {
if (forceExecution) {
const runCompleteOp = {
id: require_engine._internals.hashId("complete"),
op: require_types.StepOpCode.RunComplete,
data: require_functions.undefinedToNull(result$1.data)
};
return {
status: 206,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify(runCompleteOp),
version: version$1
};
}
return {
status: 200,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify(require_functions.undefinedToNull(result$1.data)),
version: version$1
};
},
"step-not-found": (result$1) => {
let error = `Could not find step "${result$1.step.displayName || result$1.step.id}" to run; timed out.`;
if (result$1.foundSteps.length > 0) {
const foundStepsSummary = result$1.foundSteps.map((step) => {
return `${step.displayName || step.id} (${step.id})`;
}).join("\n");
error = `${error} Found new steps: \n${foundStepsSummary}.`;
}
if (result$1.totalFoundSteps > result$1.foundSteps.length) error = `${error} (showing ${result$1.foundSteps.length} of ${result$1.totalFoundSteps})`;
return {
status: 500,
headers: {
"Content-Type": "application/json",
[require_consts.headerKeys.NoRetry]: "false"
},
body: require_strings.stringify({
error,
requestedStep: result$1.step.id,
foundSteps: result$1.foundSteps,
totalFoundSteps: result$1.totalFoundSteps
}),
version: version$1
};
},
"step-ran": (result$1) => {
const step = opDataUndefinedToNull(result$1.step);
return {
status: 206,
headers: {
"Content-Type": "application/json",
...typeof result$1.retriable !== "undefined" ? {
[require_consts.headerKeys.NoRetry]: result$1.retriable ? "false" : "true",
...typeof result$1.retriable === "string" ? { [require_consts.headerKeys.RetryAfter]: result$1.retriable } : {}
} : {}
},
body: require_strings.stringify([step]),
version: version$1
};
},
"steps-found": (result$1) => {
const steps = result$1.steps.map(opDataUndefinedToNull);
return {
status: 206,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify(steps),
version: version$1
};
},
"change-mode": (result$1) => {
return {
status: 500,
headers: {
"Content-Type": "application/json",
[require_consts.headerKeys.NoRetry]: "true"
},
body: require_strings.stringify({ error: `We wanted to change mode to "${result$1.to}", but this is not supported within the InngestCommHandler. This is a bug in the Inngest SDK.` }),
version: version$1
};
}
}[stepOutput.type];
try {
return await handler(stepOutput);
} catch (err) {
this.client[require_Inngest.internalLoggerSymbol].error({ err }, "Error handling execution result");
throw err;
}
}
const env = (await getHeaders())[require_consts.headerKeys.Environment] ?? null;
if (method === "GET") return {
status: 200,
body: require_strings.stringify(await this.introspectionBody({
actions,
env,
signatureValidation,
url
})),
headers: { "Content-Type": "application/json" },
version: void 0
};
if (method === "PUT") {
const [deployId, inBandSyncRequested] = await Promise.all([actions.queryStringWithDefaults("processing deployment request", require_consts.queryKeys.DeployId).then((deployId$1) => {
return deployId$1 === "undefined" ? void 0 : deployId$1;
}), Promise.resolve(require_env.parseAsBoolean(this.env[require_consts.envKeys.InngestAllowInBandSync])).then((allowInBandSync) => {
if (allowInBandSync !== void 0 && !allowInBandSync) return require_consts.syncKind.OutOfBand;
return actions.headers("processing deployment request", require_consts.headerKeys.InngestSyncKind);
}).then((kind) => {
return kind === require_consts.syncKind.InBand;
})]);
if (inBandSyncRequested) {
if (isMissingBody) {
this.client[require_Inngest.internalLoggerSymbol].error("Missing body when syncing, possibly due to missing request body middleware");
return {
status: 500,
headers: { "Content-Type": "application/json" },
body: require_strings.stringify(require_errors.serializeError(/* @__PURE__ */ new Error("Missing request body when syncing, possibly due to missing request body middleware"))),
version: void 0
};
}
if (!(await signatureValidation).success) return {
status: 401,
body: require_strings.stringify({ code: "sig_verification_failed" }),
headers: { "Content-Type": "application/json" },
version: void 0
};
const res = require_types.inBandSyncRequestBodySchema.safeParse(body);
if (!res.success) return {
status: 400,
body: require_strings.stringify({
code: "invalid_request",
message: res.error.message
}),
headers: { "Content-Type": "application/json" },
version: void 0
};
url = this.reqUrl(new URL(res.data.url));
return {
status: 200,
body: require_strings.stringify(await this.inBandRegisterBody({
actions,
deployId,
env,
signatureValidation,
url
})),
headers: {
"Content-Type": "application/json",
[require_consts.headerKeys.InngestSyncKind]: require_consts.syncKind.InBand
},
version: void 0
};
}
const { status, message, modified } = await this.register(this.reqUrl(url), deployId, getHeaders);
return {
status,
body: require_strings.stringify({
message,
modified
}),
headers: {
"Content-Type": "application/json",
[require_consts.headerKeys.InngestSyncKind]: require_consts.syncKind.OutOfBand
},
version: void 0
};
}
} catch (err) {
return {
status: 500,
body: require_strings.stringify({
type: "internal",
...require_errors.serializeError(err)
}),
headers: { "Content-Type": "application/json" },
version: void 0
};
}
this.client[require_Inngest.internalLoggerSymbol].error({ method }, "Received unhandled HTTP method; expected POST, PUT, or GET");
return {
status: 405,
body: JSON.stringify({
message: `No action found; expected POST, PUT, or GET but received "${method}"`,
mode: this.client.mode
}),
headers: {},
version: void 0
};
}
runStep({ actions, functionId, stepId, data, timer, reqArgs, headers, fn, forceExecution, headerReqVersion, requestInfo, mwInstances }) {
if (!fn) throw new Error(`Could not find function with ID "${functionId}"`);
const immediateFnData = require_functions.parseFnData(data, headerReqVersion, this.client[require_Inngest.internalLoggerSymbol]);
const { sdkDecided } = immediateFnData;
let version$1 = require_consts.ExecutionVersion.V2;
if (version$1 === require_consts.ExecutionVersion.V2 && sdkDecided && fn.fn["shouldOptimizeParallelism"]?.() === false) version$1 = require_consts.ExecutionVersion.V1;
const result = require_promises.runAsPromise(async () => {
const anyFnData = await require_functions.fetchAllFnData({
data: immediateFnData,
api: this.client["inngestApi"],
logger: this.client[require_Inngest.internalLoggerSymbol]
});
if (!anyFnData.ok) throw new Error(anyFnData.error);
const createResponse = forceExecution && actions.experimentalTransformSyncResponse ? (data$1) => actions.experimentalTransformSyncResponse("created sync->async response", data$1).then((res) => ({
...res,
version: version$1
})) : void 0;
const { event, events, steps, ctx } = anyFnData.value;
const stepState = Object.entries(steps ?? {}).reduce((acc, [id, result$1]) => {
return {
...acc,
[id]: result$1.type === "data" ? {
id,
data: result$1.data
} : result$1.type === "input" ? {
id,
input: result$1.input
} : {
id,
error: result$1.error
}
};
}, {});
const requestedRunStep = stepId === "step" ? void 0 : stepId || void 0;
const checkpointingConfig = fn.fn["shouldAsyncCheckpoint"](requestedRunStep, ctx?.fn_id, Boolean(ctx?.disable_immediate_execution));
const executionOptions = { partialOptions: {
client: this.client,
runId: ctx?.run_id || "",
stepMode: checkpointingConfig ? require_types.StepMode.AsyncCheckpointing : require_types.StepMode.Async,
checkpointingConfig,
data: {
event,
events,
runId: ctx?.run_id || "",
attempt: ctx?.attempt ?? 0,
maxAttempts: ctx?.max_attempts
},
internalFnId: ctx?.fn_id,
queueItemId: ctx?.qi_id,
stepState,
requestedRunStep,
timer,
isFailureHandler: fn.onFailure,
disableImmediateExecution: ctx?.disable_immediate_execution,
stepCompletionOrder: ctx?.stack?.stack ?? [],
reqArgs,
headers,
createResponse,
requestInfo,
middlewareInstances: mwInstances
} };
return fn.fn["createExecution"](executionOptions).start();
});
return {
version: version$1,
result
};
}
configs(url) {
const configs = Object.values(this.rawFns).reduce((acc, fn) => [...acc, ...fn["getConfig"]({
baseUrl: url,
appPrefix: this.client.id
})], []);
for (const config of configs) {
const check = require_types.functionConfigSchema.safeParse(config);
if (!check.success) {
const errors = check.error.errors.map((err) => err.message).join("; ");
this.client[require_Inngest.internalLoggerSymbol].warn({
functionId: config.id,
errors
}, "Invalid function config");
}
}
return configs;
}
/**
* Return an Inngest serve endpoint URL given a potential `path` and `host`.
*
* Will automatically use the `serveOrigin` and `servePath` if they have been
* set when registering.
*/
reqUrl(url) {
let ret = new URL(url);
const servePath = this.servePath || this.env[require_consts.envKeys.InngestServePath];
if (servePath) ret.pathname = servePath;
if (this.serveOrigin) ret = new URL(ret.pathname + ret.search, this.serveOrigin);
return ret;
}
registerBody({ url, deployId }) {
return {
url: url.href,
deployType: "ping",
framework: this.frameworkName,
appName: this.client.id,
functions: this.configs(url),
sdk: `js:v${require_version.version}`,
v: "0.1",
deployId: deployId || void 0,
capabilities: {
trust_probe: "v1",
connect: "v1"
},
appVersion: this.client.appVersion
};
}
async inBandRegisterBody({ actions, deployId, env, signatureValidation, url }) {
const registerBody = this.registerBody({
deployId,
url
});
const introspectionBody = await this.introspectionBody({
actions,
env,
signatureValidation,
url
});
const body = {
app_id: this.client.id,
appVersion: this.client.appVersion,
capabilities: registerBody.capabilities,
env,
framework: registerBody.framework,
functions: registerBody.functions,
inspection: introspectionBody,
platform: require_env.getPlatformName({
...require_env.allProcessEnv(),
...this.env
}),
sdk_author: "inngest",
sdk_language: "",
sdk_version: "",
sdk: registerBody.sdk,
url: registerBody.url
};
if ("authentication_succeeded" in introspectionBody && introspectionBody.authentication_succeeded) {
body.sdk_language = introspectionBody.sdk_language;
body.sdk_version = introspectionBody.sdk_version;
}
return body;
}
async introspectionBody({ actions, env, signatureValidation, url }) {
const registerBody = this.registerBody({
url: this.reqUrl(url),
deployId: null
});
if (!this.client.mode) throw new Error("No mode set; cannot introspect without mode");
let introspection = {
extra: { native_crypto: globalThis.crypto?.subtle ? true : false },
has_event_key: this.client["eventKeySet"](),
has_signing_key: Boolean(this.client.signingKey),
function_count: registerBody.functions.length,
mode: this.client.mode,
schema_version: "2024-05-24"
};
if (this.client.mode === "cloud") try {
if (!(await signatureValidation).success) throw new Error("Signature validation failed");
introspection = {
...introspection,
authentication_succeeded: true,
api_origin: this.client.apiBaseUrl,
app_id: this.client.id,
capabilities: {
trust_probe: "v1",
connect: "v1"
},
env,
event_api_origin: this.client.eventBaseUrl,
event_key_hash: this.hashedEventKey ?? null,
extra: {
...introspection.extra,
is_streaming: await this.shouldStream(actions),
native_crypto: globalThis.crypto?.subtle ? true : false
},
framework: this.frameworkName,
sdk_language: "js",
sdk_version: require_version.version,
serve_origin: this.serveOrigin ?? null,
serve_path: this.servePath ?? null,
signing_key_fallback_hash: this.hashedSigningKeyFallback ?? null,
signing_key_hash: this.hashedSigningKey ?? null
};
} catch {
introspection = { ...introspection };
}
return introspection;
}
async register(url, deployId, getHeaders) {
const body = this.registerBody({
url,
deployId
});
let res;
const registerUrl = new URL(this.inngestRegisterUrl.href);
if (deployId) registerUrl.searchParams.set(require_consts.queryKeys.DeployId, deployId);
try {
res = await require_net.fetchWithAuthFallback({
authToken: this.hashedSigningKey,
authTokenFallback: this.hashedSigningKeyFallback,
fetch: this.client.fetch,
url: registerUrl.href,
options: {
method: "POST",
body: require_strings.stringify(body),
headers: {
...await getHeaders(),
[require_consts.headerKeys.InngestSyncKind]: require_consts.syncKind.OutOfBand
},
redirect: "follow"
}
});
} catch (err) {
this.client[require_Inngest.internalLoggerSymbol].error({ err }, "Failed to register");
return {
status: 500,
message: `Failed to register${err instanceof Error ? `; ${err.message}` : ""}`,
modified: false
};
}
const raw = await res.text();
let data = {};
try {
data = JSON.parse(raw);
} catch (err) {
this.client[require_Inngest.internalLoggerSymbol].warn({ err }, "Couldn't unpack register response");
let message = "Failed to register";
if (err instanceof Error) message += `; ${err.message}`;
message += `; status code: ${res.status}`;
return {
status: 500,
message,
modified: false
};
}
let status;
let error;
let skipped;
let modified;
try {
({status, error, skipped, modified} = registerResSchema.parse(data));
} catch (err) {
this.client[require_Inngest.internalLoggerSymbol].warn({ err }, "Invalid register response schema");
let message = "Failed to register";
if (err instanceof Error) message += `; ${err.message}`;
message += `; status code: ${res.status}`;
return {
status: 500,
message,
modified: false
};
}
if (!skipped) this.client[require_Inngest.internalLoggerSymbol].debug("Registered inngest functions");
return {
status,
message: error,
modified
};
}
/**
* Check that the current mode has the configuration it requires.
* Returns `true` if valid, `false` if not.
*/
checkModeConfiguration() {
this.client.setEnvVars(this.env);
return require_env.checkModeConfiguration({
mode: this.client.mode,
signingKey: this.client.signingKey,
internalLogger: this.client[require_Inngest.internalLoggerSymbol]
});
}
/**
* Validate the signature of a request and return the signing key used to
* validate it.
*/
async validateSignature(sig, body) {
try {
if (this.skipSignatureValidation) return {
success: true,
keyUsed: ""
};
if (this.client.mode !== "cloud") return {
success: true,
keyUsed: ""
};
if (!this.client.