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,045 lines (1,044 loc) • 63.4 kB
JavaScript
"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _RequestSignature_instances, _RequestSignature_verifySignature;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InngestCommHandler = void 0;
const debug_1 = __importDefault(require("debug"));
const zod_1 = require("zod");
const ServerTiming_js_1 = require("../helpers/ServerTiming.js");
const consts_js_1 = require("../helpers/consts.js");
const devserver_js_1 = require("../helpers/devserver.js");
const enum_js_1 = require("../helpers/enum.js");
const env_js_1 = require("../helpers/env.js");
const errors_js_1 = require("../helpers/errors.js");
const functions_js_1 = require("../helpers/functions.js");
const net_js_1 = require("../helpers/net.js");
const promises_js_1 = require("../helpers/promises.js");
const stream_js_1 = require("../helpers/stream.js");
const strings_js_1 = require("../helpers/strings.js");
const types_js_1 = require("../types.js");
const version_js_1 = require("../version.js");
const InngestExecution_js_1 = require("./execution/InngestExecution.js");
/**
* A schema for the response from Inngest when registering.
*/
const registerResSchema = zod_1.z.object({
status: zod_1.z.number().default(200),
skipped: zod_1.z.boolean().optional().default(false),
modified: zod_1.z.boolean().optional().default(false),
error: zod_1.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
*/
class InngestCommHandler {
constructor(options) {
var _a;
/**
* A private collection of functions that are being served. This map is used
* to find and register functions when interacting with Inngest Cloud.
*/
this.fns = {};
this.env = (0, env_js_1.allProcessEnv)();
// Set input options directly so we can reference them later
this._options = options;
/**
* v2 -> v3 migration error.
*
* 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.prototype.hasOwnProperty.call(options, "eventKey")) {
throw new Error(`${consts_js_1.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;
if (options.id) {
console.warn(`${consts_js_1.logPrefix} The \`id\` serve option is deprecated and will be removed in v4`);
}
this.id = options.id || this.client.id;
this.handler = options.handler;
/**
* Provide a hidden option to allow expired signatures to be accepted during
* testing.
*/
this.allowExpiredSignatures = Boolean(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, prefer-rest-params
(_a = arguments["0"]) === null || _a === void 0 ? void 0 : _a.__testingAllowExpiredSignatures);
// Ensure we filter any undefined functions in case of missing imports.
this.rawFns = options.functions.filter(Boolean);
if (this.rawFns.length !== options.functions.length) {
// TODO PrettyError
console.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.id,
});
const fns = configs.reduce((acc, { id }, index) => {
return Object.assign(Object.assign({}, acc), { [id]: { fn, onFailure: Boolean(index) } });
}, {});
configs.forEach(({ id }) => {
if (acc[id]) {
// TODO PrettyError
throw new Error(`Duplicate function ID "${id}"; please change a function's name or provide an explicit ID to avoid conflicts.`);
}
});
return Object.assign(Object.assign({}, acc), fns);
}, {});
this.inngestRegisterUrl = new URL("/fn/register", this.apiBaseUrl);
this.signingKey = options.signingKey;
this.signingKeyFallback = options.signingKeyFallback;
this._serveHost = options.serveHost || this.env[consts_js_1.envKeys.InngestServeHost];
this._servePath = options.servePath || this.env[consts_js_1.envKeys.InngestServePath];
this.skipSignatureValidation = options.skipSignatureValidation || false;
const defaultLogLevel = "info";
this.logLevel = zod_1.z
.enum(types_js_1.logLevels)
.default(defaultLogLevel)
.catch((ctx) => {
this.log("warn", `Unknown log level passed: ${String(ctx.input)}; defaulting to ${defaultLogLevel}`);
return defaultLogLevel;
})
.parse(options.logLevel || this.env[consts_js_1.envKeys.InngestLogLevel]);
if (this.logLevel === "debug") {
/**
* `debug` is an old library; sometimes its runtime detection doesn't work
* for newer pairings of framework/runtime.
*
* One silly symptom of this is that `Debug()` returns an anonymous
* function with no extra properties instead of a `Debugger` instance if
* the wrong code is consumed following a bad detection. This results in
* the following `.enable()` call failing, so we just try carefully to
* enable it here.
*/
if (debug_1.default.enable && typeof debug_1.default.enable === "function") {
debug_1.default.enable(`${consts_js_1.debugPrefix}:*`);
}
}
const defaultStreamingOption = false;
this.streaming = zod_1.z
.union([zod_1.z.enum(["allow", "force"]), zod_1.z.literal(false)])
.default(defaultStreamingOption)
.catch((ctx) => {
this.log("warn", `Unknown streaming option passed: ${String(ctx.input)}; defaulting to ${String(defaultStreamingOption)}`);
return defaultStreamingOption;
})
.parse(options.streaming || this.env[consts_js_1.envKeys.InngestStreaming]);
this.fetch = options.fetch ? (0, env_js_1.getFetch)(options.fetch) : this.client["fetch"];
}
/**
* Get the API base URL for the Inngest API.
*
* This is a getter to encourage checking the environment for the API base URL
* each time it's accessed, as it may change during execution.
*/
get apiBaseUrl() {
return (this._options.baseUrl ||
this.env[consts_js_1.envKeys.InngestApiBaseUrl] ||
this.env[consts_js_1.envKeys.InngestBaseUrl] ||
this.client.apiBaseUrl ||
consts_js_1.defaultInngestApiBaseUrl);
}
/**
* Get the event API base URL for the Inngest API.
*
* This is a getter to encourage checking the environment for the event API
* base URL each time it's accessed, as it may change during execution.
*/
get eventApiBaseUrl() {
return (this._options.baseUrl ||
this.env[consts_js_1.envKeys.InngestEventApiBaseUrl] ||
this.env[consts_js_1.envKeys.InngestBaseUrl] ||
this.client.eventBaseUrl ||
consts_js_1.defaultInngestEventBaseUrl);
}
/**
* The host 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 hostname here to ensure that the path is reported
* correctly when registering functions with Inngest.
*
* To also provide a custom path, use `servePath`.
*/
get serveHost() {
return this._serveHost || this.env[consts_js_1.envKeys.InngestServeHost];
}
/**
* 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 `serveHost`.
*
* 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[consts_js_1.envKeys.InngestServePath];
}
get hashedEventKey() {
if (!this.client["eventKey"] || this.client["eventKey"] === consts_js_1.dummyEventKey) {
return undefined;
}
return (0, strings_js_1.hashEventKey)(this.client["eventKey"]);
}
// hashedSigningKey creates a sha256 checksum of the signing key with the
// same signing key prefix.
get hashedSigningKey() {
if (!this.signingKey) {
return undefined;
}
return (0, strings_js_1.hashSigningKey)(this.signingKey);
}
get hashedSigningKeyFallback() {
if (!this.signingKeyFallback) {
return undefined;
}
return (0, strings_js_1.hashSigningKey)(this.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) {
const rawProbe = await actions.queryStringWithDefaults("testing for probe", consts_js_1.queryKeys.Probe);
if (rawProbe !== undefined) {
return false;
}
// We must be able to stream responses to continue.
if (!actions.transformStreamingResponse) {
return false;
}
// If the user has forced streaming, we should always stream.
if (this.streaming === "force") {
return true;
}
// If the user has allowed streaming, we should stream if the platform
// supports it.
return (this.streaming === "allow" &&
(0, env_js_1.platformSupportsStreaming)(this.frameworkName, this.env));
}
/**
* `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() {
const handler = async (...args) => {
var _a, _b;
const timer = new ServerTiming_js_1.ServerTiming();
/**
* 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 = Object.assign(Object.assign({}, (await timer
.wrap("handler", () => this.handler(...args))
.catch((0, errors_js_1.rethrowError)("Serve handler failed to run")))), actionOverrides);
/**
* Map over every `action` in `rawActions` and create a new `actions`
* object where each function is safely promisified with each access
* requiring a reason.
*
* This helps us provide high quality errors about what's going wrong for
* each access without having to wrap every access in a try/catch.
*/
const promisifiedActions = Object.entries(rawActions).reduce((acc, [key, value]) => {
if (typeof value !== "function") {
return acc;
}
return Object.assign(Object.assign({}, acc), { [key]: (reason, ...args) => {
const errMessage = [
`Failed calling \`${key}\` from serve handler`,
reason,
]
.filter(Boolean)
.join(" when ");
const fn = () => value(...args);
return (0, promises_js_1.runAsPromise)(fn)
.catch((0, errors_js_1.rethrowError)(errMessage))
.catch((err) => {
this.log("error", err);
throw err;
});
} });
}, {});
/**
* Mapped promisified handlers from userland `serve()` function mixed in
* with some helpers.
*/
const actions = Object.assign(Object.assign(Object.assign({}, promisifiedActions), { queryStringWithDefaults: async (reason, key) => {
var _a;
const url = await actions.url(reason);
const ret = (await ((_a = actions.queryString) === null || _a === void 0 ? void 0 : _a.call(actions, reason, key, url))) ||
url.searchParams.get(key) ||
undefined;
return ret;
} }), actionOverrides);
const [env, expectedServerKind] = await Promise.all([
(_a = actions.env) === null || _a === void 0 ? void 0 : _a.call(actions, "starting to handle request"),
actions.headers("checking expected server kind", consts_js_1.headerKeys.InngestServerKind),
]);
// Always make sure to merge whatever env we've been given with
// `process.env`; some platforms may not provide all the necessary
// environment variables or may use two sources.
this.env = Object.assign(Object.assign({}, (0, env_js_1.allProcessEnv)()), env);
const getInngestHeaders = () => (0, env_js_1.inngestHeaders)({
env: this.env,
framework: this.frameworkName,
client: this.client,
expectedServerKind: expectedServerKind || undefined,
extras: {
"Server-Timing": timer.getHeader(),
},
});
const assumedMode = (0, env_js_1.getMode)({ env: this.env, client: this.client });
if (assumedMode.isExplicit) {
this._mode = assumedMode;
}
else {
const serveIsProd = await ((_b = actions.isProduction) === null || _b === void 0 ? void 0 : _b.call(actions, "starting to handle request"));
if (typeof serveIsProd === "boolean") {
this._mode = new env_js_1.Mode({
type: serveIsProd ? "cloud" : "dev",
isExplicit: false,
});
}
else {
this._mode = assumedMode;
}
}
this.upsertKeysFromEnv();
const methodP = actions.method("starting to handle request");
const headerPromises = [
consts_js_1.headerKeys.TraceParent,
consts_js_1.headerKeys.TraceState,
].map(async (header) => {
const value = await actions.headers(`fetching ${header} for forwarding`, header);
return { header, value };
});
const contentLength = await actions
.headers("checking signature for request", consts_js_1.headerKeys.ContentLength)
.then((value) => {
if (!value) {
return undefined;
}
return parseInt(value, 10);
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const [signature, method, body] = await Promise.all([
actions
.headers("checking signature for request", consts_js_1.headerKeys.Signature)
.then((headerSignature) => {
return headerSignature !== null && headerSignature !== void 0 ? headerSignature : undefined;
}),
methodP,
methodP.then((method) => {
if (method === "POST" || method === "PUT") {
if (!contentLength) {
// Return empty string because req.json() will throw an error.
return "";
}
return actions.body(`checking body for request signing as method is ${method}`);
}
return "";
}),
]);
const signatureValidation = this.validateSignature(signature, body);
const headersToForwardP = Promise.all(headerPromises).then((fetchedHeaders) => {
return fetchedHeaders.reduce((acc, { header, value }) => {
if (value) {
acc[header] = value;
}
return acc;
}, {});
});
const actionRes = timer.wrap("action", () => this.handleAction({
actions,
timer,
getInngestHeaders,
reqArgs: args,
signatureValidation,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
body,
method,
headers: headersToForwardP,
}));
/**
* 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) => {
var _a;
const headers = Object.assign(Object.assign(Object.assign(Object.assign({}, getInngestHeaders()), (await headersToForwardP)), res.headers), (res.version === null
? {}
: {
[consts_js_1.headerKeys.RequestVersion]: ((_a = res.version) !== null && _a !== void 0 ? _a : InngestExecution_js_1.PREFERRED_EXECUTION_VERSION).toString(),
}));
let signature;
try {
signature = await signatureValidation.then((result) => {
if (!result.success || !result.keyUsed) {
return undefined;
}
return this.getResponseSignature(result.keyUsed, res.body);
});
}
catch (err) {
// If we fail to sign, retun a 500 with the error.
return Object.assign(Object.assign({}, res), { headers, body: (0, strings_js_1.stringify)((0, errors_js_1.serializeError)(err)), status: 500 });
}
if (signature) {
headers[consts_js_1.headerKeys.Signature] = signature;
}
return Object.assign(Object.assign({}, res), { headers });
};
if (await this.shouldStream(actions)) {
const method = await actions.method("starting streaming response");
if (method === "POST") {
const { stream, finalize } = await (0, stream_js_1.createStream)();
/**
* Errors are handled by `handleAction` here to ensure that an
* appropriate response is always given.
*/
void actionRes.then((res) => {
return finalize(prepareActionRes(res));
});
return timer.wrap("res", () => {
var _a;
return (_a = actions.transformStreamingResponse) === null || _a === void 0 ? void 0 : _a.call(actions, "starting streaming response", {
status: 201,
headers: getInngestHeaders(),
body: stream,
version: null,
});
});
}
}
return timer.wrap("res", async () => {
return actionRes.then(prepareActionRes).then((actionRes) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return actions.transformResponse("sending back response", actionRes);
});
});
};
/**
* 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;
}
get mode() {
return this._mode;
}
set mode(m) {
this._mode = m;
if (m) {
this.client["mode"] = m;
}
}
/**
* 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, getInngestHeaders, reqArgs, signatureValidation, body, method, headers, }) {
var _a;
// This is when the request body is completely missing; it does not
// include an empty body. This commonly happens when the HTTP framework
// doesn't have body parsing middleware.
const isMissingBody = body === undefined;
try {
let url = await actions.url("starting to handle request");
if (method === "POST") {
if (isMissingBody) {
this.log("error", "Missing body when executing, possibly due to missing request body middleware");
return {
status: 500,
headers: {
"Content-Type": "application/json",
},
body: (0, strings_js_1.stringify)((0, errors_js_1.serializeError)(new Error("Missing request body when executing, possibly due to missing request body middleware"))),
version: undefined,
};
}
const validationResult = await signatureValidation;
if (!validationResult.success) {
return {
status: 401,
headers: {
"Content-Type": "application/json",
},
body: (0, strings_js_1.stringify)((0, errors_js_1.serializeError)(validationResult.err)),
version: undefined,
};
}
const rawProbe = await actions.queryStringWithDefaults("testing for probe", consts_js_1.queryKeys.Probe);
if (rawProbe) {
const probe = (0, enum_js_1.enumFromValue)(consts_js_1.probe, rawProbe);
if (!probe) {
// If we're here, we've received a probe that we don't recognize.
// Fail.
return {
status: 400,
headers: {
"Content-Type": "application/json",
},
body: (0, strings_js_1.stringify)((0, errors_js_1.serializeError)(new Error(`Unknown probe "${rawProbe}"`))),
version: undefined,
};
}
// Provide actions for every probe available.
const probeActions = {
[consts_js_1.probe.Trust]: () => ({
status: 200,
headers: {
"Content-Type": "application/json",
},
body: "",
version: undefined,
}),
};
return probeActions[probe]();
}
const fnId = await actions.queryStringWithDefaults("processing run request", consts_js_1.queryKeys.FnId);
if (!fnId) {
// TODO PrettyError
throw new Error("No function ID found in request");
}
const stepId = (await actions.queryStringWithDefaults("processing run request", consts_js_1.queryKeys.StepId)) || null;
const { version, result } = this.runStep({
functionId: fnId,
data: body,
stepId,
timer,
reqArgs,
headers: await headers,
});
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 = (0, functions_js_1.undefinedToNull)(op.data);
return op;
};
const resultHandlers = {
"function-rejected": (result) => {
return {
status: result.retriable ? 500 : 400,
headers: Object.assign({ "Content-Type": "application/json", [consts_js_1.headerKeys.NoRetry]: result.retriable ? "false" : "true" }, (typeof result.retriable === "string"
? { [consts_js_1.headerKeys.RetryAfter]: result.retriable }
: {})),
body: (0, strings_js_1.stringify)((0, functions_js_1.undefinedToNull)(result.error)),
version,
};
},
"function-resolved": (result) => {
return {
status: 200,
headers: {
"Content-Type": "application/json",
},
body: (0, strings_js_1.stringify)((0, functions_js_1.undefinedToNull)(result.data)),
version,
};
},
"step-not-found": (result) => {
return {
status: 500,
headers: {
"Content-Type": "application/json",
[consts_js_1.headerKeys.NoRetry]: "false",
},
body: (0, strings_js_1.stringify)({
error: `Could not find step "${result.step.displayName || result.step.id}" to run; timed out`,
}),
version,
};
},
"step-ran": (result) => {
const step = opDataUndefinedToNull(result.step);
return {
status: 206,
headers: Object.assign({ "Content-Type": "application/json" }, (typeof result.retriable !== "undefined"
? Object.assign({ [consts_js_1.headerKeys.NoRetry]: result.retriable ? "false" : "true" }, (typeof result.retriable === "string"
? { [consts_js_1.headerKeys.RetryAfter]: result.retriable }
: {})) : {})),
body: (0, strings_js_1.stringify)([step]),
version,
};
},
"steps-found": (result) => {
const steps = result.steps.map(opDataUndefinedToNull);
return {
status: 206,
headers: {
"Content-Type": "application/json",
},
body: (0, strings_js_1.stringify)(steps),
version,
};
},
};
const handler = resultHandlers[stepOutput.type];
try {
return await handler(stepOutput);
}
catch (err) {
this.log("error", "Error handling execution result", err);
throw err;
}
}
// TODO: This feels hacky, so we should probably make it not hacky.
const env = (_a = getInngestHeaders()[consts_js_1.headerKeys.Environment]) !== null && _a !== void 0 ? _a : null;
if (method === "GET") {
return {
status: 200,
body: (0, strings_js_1.stringify)(await this.introspectionBody({
actions,
env,
signatureValidation,
url,
})),
headers: {
"Content-Type": "application/json",
},
version: undefined,
};
}
if (method === "PUT") {
const [deployId, inBandSyncRequested] = await Promise.all([
actions
.queryStringWithDefaults("processing deployment request", consts_js_1.queryKeys.DeployId)
.then((deployId) => {
return deployId === "undefined" ? undefined : deployId;
}),
Promise.resolve((0, env_js_1.parseAsBoolean)(this.env[consts_js_1.envKeys.InngestAllowInBandSync]))
.then((allowInBandSync) => {
if (allowInBandSync !== undefined && !allowInBandSync) {
return consts_js_1.syncKind.OutOfBand;
}
return actions.headers("processing deployment request", consts_js_1.headerKeys.InngestSyncKind);
})
.then((kind) => {
return kind === consts_js_1.syncKind.InBand;
}),
]);
if (inBandSyncRequested) {
if (isMissingBody) {
this.log("error", "Missing body when syncing, possibly due to missing request body middleware");
return {
status: 500,
headers: {
"Content-Type": "application/json",
},
body: (0, strings_js_1.stringify)((0, errors_js_1.serializeError)(new Error("Missing request body when syncing, possibly due to missing request body middleware"))),
version: undefined,
};
}
// Validation can be successful if we're in dev mode and did not
// actually validate a key. In this case, also check that we did indeed
// use a particular key to validate.
const sigCheck = await signatureValidation;
if (!sigCheck.success) {
return {
status: 401,
body: (0, strings_js_1.stringify)({
code: "sig_verification_failed",
}),
headers: {
"Content-Type": "application/json",
},
version: undefined,
};
}
const res = types_js_1.inBandSyncRequestBodySchema.safeParse(body);
if (!res.success) {
return {
status: 400,
body: (0, strings_js_1.stringify)({
code: "invalid_request",
message: res.error.message,
}),
headers: {
"Content-Type": "application/json",
},
version: undefined,
};
}
// We can trust the URL here because it's coming from
// signature-verified request.
url = this.reqUrl(new URL(res.data.url));
// This should be an in-band sync
const respBody = await this.inBandRegisterBody({
actions,
deployId,
env,
signatureValidation,
url,
});
return {
status: 200,
body: (0, strings_js_1.stringify)(respBody),
headers: {
"Content-Type": "application/json",
[consts_js_1.headerKeys.InngestSyncKind]: consts_js_1.syncKind.InBand,
},
version: undefined,
};
}
// If we're here, this is a legacy out-of-band sync
const { status, message, modified } = await this.register(this.reqUrl(url), deployId, getInngestHeaders);
return {
status,
body: (0, strings_js_1.stringify)({ message, modified }),
headers: {
"Content-Type": "application/json",
[consts_js_1.headerKeys.InngestSyncKind]: consts_js_1.syncKind.OutOfBand,
},
version: undefined,
};
}
}
catch (err) {
return {
status: 500,
body: (0, strings_js_1.stringify)(Object.assign({ type: "internal" }, (0, errors_js_1.serializeError)(err))),
headers: {
"Content-Type": "application/json",
},
version: undefined,
};
}
return {
status: 405,
body: JSON.stringify({
message: "No action found; request was likely not POST, PUT, or GET",
mode: this._mode,
}),
headers: {},
version: undefined,
};
}
runStep({ functionId, stepId, data, timer, reqArgs, headers, }) {
var _a, _b;
const fn = this.fns[functionId];
if (!fn) {
// TODO PrettyError
throw new Error(`Could not find function with ID "${functionId}"`);
}
const immediateFnData = (0, functions_js_1.parseFnData)(data);
let { version } = immediateFnData;
// Handle opting in to optimized parallelism in v3.
if (version === InngestExecution_js_1.ExecutionVersion.V1 &&
((_b = (_a = fn.fn)["shouldOptimizeParallelism"]) === null || _b === void 0 ? void 0 : _b.call(_a))) {
version = InngestExecution_js_1.ExecutionVersion.V2;
}
const result = (0, promises_js_1.runAsPromise)(async () => {
const anyFnData = await (0, functions_js_1.fetchAllFnData)({
data: immediateFnData,
api: this.client["inngestApi"],
version,
});
if (!anyFnData.ok) {
throw new Error(anyFnData.error);
}
const executionStarters = ((s) => s)({
[InngestExecution_js_1.ExecutionVersion.V0]: ({ event, events, steps, ctx, version }) => {
var _a, _b, _c;
const stepState = Object.entries(steps !== null && steps !== void 0 ? steps : {}).reduce((acc, [id, data]) => {
return Object.assign(Object.assign({}, acc), {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
[id]: { id, data } });
}, {});
return {
version,
partialOptions: {
client: this.client,
runId: (ctx === null || ctx === void 0 ? void 0 : ctx.run_id) || "",
data: {
event: event,
events: events,
runId: (ctx === null || ctx === void 0 ? void 0 : ctx.run_id) || "",
attempt: (_a = ctx === null || ctx === void 0 ? void 0 : ctx.attempt) !== null && _a !== void 0 ? _a : 0,
},
stepState,
requestedRunStep: stepId === "step" ? undefined : stepId || undefined,
timer,
isFailureHandler: fn.onFailure,
stepCompletionOrder: (_c = (_b = ctx === null || ctx === void 0 ? void 0 : ctx.stack) === null || _b === void 0 ? void 0 : _b.stack) !== null && _c !== void 0 ? _c : [],
reqArgs,
headers,
},
};
},
[InngestExecution_js_1.ExecutionVersion.V1]: ({ event, events, steps, ctx, version }) => {
var _a, _b, _c;
const stepState = Object.entries(steps !== null && steps !== void 0 ? steps : {}).reduce((acc, [id, result]) => {
return Object.assign(Object.assign({}, acc), { [id]: result.type === "data"
? // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
{ id, data: result.data }
: result.type === "input"
? // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
{ id, input: result.input }
: { id, error: result.error } });
}, {});
return {
version,
partialOptions: {
client: this.client,
runId: (ctx === null || ctx === void 0 ? void 0 : ctx.run_id) || "",
data: {
event: event,
events: events,
runId: (ctx === null || ctx === void 0 ? void 0 : ctx.run_id) || "",
attempt: (_a = ctx === null || ctx === void 0 ? void 0 : ctx.attempt) !== null && _a !== void 0 ? _a : 0,
},
stepState,
requestedRunStep: stepId === "step" ? undefined : stepId || undefined,
timer,
isFailureHandler: fn.onFailure,
disableImmediateExecution: ctx === null || ctx === void 0 ? void 0 : ctx.disable_immediate_execution,
stepCompletionOrder: (_c = (_b = ctx === null || ctx === void 0 ? void 0 : ctx.stack) === null || _b === void 0 ? void 0 : _b.stack) !== null && _c !== void 0 ? _c : [],
reqArgs,
headers,
},
};
},
[InngestExecution_js_1.ExecutionVersion.V2]: ({ event, events, steps, ctx, version }) => {
var _a, _b, _c;
const stepState = Object.entries(steps !== null && steps !== void 0 ? steps : {}).reduce((acc, [id, result]) => {
return Object.assign(Object.assign({}, acc), { [id]: result.type === "data"
? // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
{ id, data: result.data }
: result.type === "input"
? // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
{ id, input: result.input }
: { id, error: result.error } });
}, {});
return {
version,
partialOptions: {
client: this.client,
runId: (ctx === null || ctx === void 0 ? void 0 : ctx.run_id) || "",
data: {
event: event,
events: events,
runId: (ctx === null || ctx === void 0 ? void 0 : ctx.run_id) || "",
attempt: (_a = ctx === null || ctx === void 0 ? void 0 : ctx.attempt) !== null && _a !== void 0 ? _a : 0,
},
stepState,
requestedRunStep: stepId === "step" ? undefined : stepId || undefined,
timer,
isFailureHandler: fn.onFailure,
disableImmediateExecution: ctx === null || ctx === void 0 ? void 0 : ctx.disable_immediate_execution,
stepCompletionOrder: (_c = (_b = ctx === null || ctx === void 0 ? void 0 : ctx.stack) === null || _b === void 0 ? void 0 : _b.stack) !== null && _c !== void 0 ? _c : [],
reqArgs,
headers,
},
};
},
});
const executionOptions = await executionStarters[version](anyFnData.value);
return fn.fn["createExecution"](executionOptions).start();
});
return { version, result };
}
configs(url) {
const configs = Object.values(this.rawFns).reduce((acc, fn) => [
...acc,
...fn["getConfig"]({ baseUrl: url, appPrefix: this.id }),
], []);
for (const config of configs) {
const check = types_js_1.functionConfigSchema.safeParse(config);
if (!check.success) {
const errors = check.error.errors.map((err) => err.message).join("; ");
this.log("warn", `Config invalid for function "${config.id}" : ${errors}`);
}
}
return configs;
}
/**
* Return an Inngest serve endpoint URL given a potential `path` and `host`.
*
* Will automatically use the `serveHost` and `servePath` if they have been
* set when registering.
*/
reqUrl(url) {
let ret = new URL(url);
const serveHost = this.serveHost || this.env[consts_js_1.envKeys.InngestServeHost];
const servePath = this.servePath || this.env[consts_js_1.envKeys.InngestServePath];
if (servePath) {
ret.pathname = servePath;
}
if (serveHost) {
ret = new URL(ret.pathname + ret.search, serveHost);
}
return ret;
}
registerBody({ url, deployId, }) {
const body = {
url: url.href,
deployType: "ping",
framework: this.frameworkName,
appName: this.id,
functions: this.configs(url),
sdk: `js:v${version_js_1.version}`,
v: "0.1",
deployId: deployId || undefined,
capabilities: {
trust_probe: "v1",
connect: "v1",
},
appVersion: this.client.appVersion,
};
return body;
}
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.id,
appVersion: this.client.appVersion,
capabilities: registerBody.capabilities,
env,
framework: registerBody.framework,