From 3d2429d02e18b46b79aab3f17fcf7441c13d3331 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 23 Feb 2024 12:32:03 +0000 Subject: [PATCH] INN-2754 Add support for `INNGEST_DEV` (#488) ## Summary Adds support for `INNGEST_DEV` and a new `isDev` option on the client. This lightly refactors the current checks based around `isProd` and `skipDevServer()`, which were getting a little difficult to read. - The SDK now has two "modes:" `"dev"` and `"cloud"`. - Each mode is either **explicit** or **inferred**. An inferred mode means that the current (`v3.x.x`) version of the SDK can make a decision to attempt to contact the Dev Server. Future versions will remove this and default to `"cloud"` mode. - Setting the `INNGEST_DEV` environment variable or the `isDev` client option **explicitly** sets the mode to either `"cloud"` or `"dev"`. - `INNGEST_DEV` accepts some sensible defaults. We'll recommend `1` to explicitly set `"dev"` mode and `0` to explicitly set `"cloud"` mode, though it also accepts `"true"`, `"y"`, `"no"`, etc. - Explicitly setting either mode also sets the event ingestion and syncing URLs. They continue to be further overwritten by passing `INNGEST_BASE_URL`, `INNGEST_API_BASE_URL`, and `INNGEST_EVENT_API_BASE_URL`. > [!NOTE] > To support many runtimes and environments, environment variables are not always accessible a) at all times, and b) on `process.env`. Sometimes environment variables are accessed via different global objects, or sometimes runtime objects that are passed to requests. > > For this reason, handling environment variables is more complex and relies on making best guesses during instantiation, then later making another decision when we have access to the environment. Supersedes both #424 and #425. ## Checklist - [x] Added a [docs PR](https://github.com/inngest/website) documenting these modes and the new environment variables that references this PR - [x] Added unit/integration tests - [x] Added changesets if applicable - [x] Push env-related changes to the OS SDK Spec ## Related - INN-2754 - Supersedes #424 - Supersedes #425 - inngest/website#679 --- .changeset/many-elephants-smoke.md | 5 + packages/inngest/etc/inngest.api.md | 17 +- .../inngest/src/components/Inngest.test.ts | 105 +++++++++ packages/inngest/src/components/Inngest.ts | 62 +++-- .../src/components/InngestCommHandler.ts | 62 ++--- packages/inngest/src/helpers/consts.ts | 3 +- packages/inngest/src/helpers/env.ts | 215 ++++++++++++++---- packages/inngest/src/test/helpers.ts | 7 +- packages/inngest/src/types.ts | 17 ++ 9 files changed, 390 insertions(+), 103 deletions(-) create mode 100644 .changeset/many-elephants-smoke.md diff --git a/.changeset/many-elephants-smoke.md b/.changeset/many-elephants-smoke.md new file mode 100644 index 000000000..092fa39f6 --- /dev/null +++ b/.changeset/many-elephants-smoke.md @@ -0,0 +1,5 @@ +--- +"inngest": minor +--- + +INN-2754 Add support for `INNGEST_DEV` and the `isDev` option, allowing a devleoper to explicitly set either Cloud or Dev mode diff --git a/packages/inngest/etc/inngest.api.md b/packages/inngest/etc/inngest.api.md index 8b9885d52..b731390f6 100644 --- a/packages/inngest/etc/inngest.api.md +++ b/packages/inngest/etc/inngest.api.md @@ -26,6 +26,7 @@ export interface ClientOptions { eventKey?: string; fetch?: typeof fetch; id: string; + isDev?: boolean; // Warning: (ae-forgotten-export) The symbol "Logger" needs to be exported by the entry point index.d.ts logger?: Logger; // Warning: (ae-forgotten-export) The symbol "MiddlewareStack" needs to be exported by the entry point index.d.ts @@ -231,7 +232,7 @@ export enum headerKeys { // @public export class Inngest { - constructor({ id, eventKey, baseUrl, fetch, env, logger, middleware, }: TOpts); + constructor({ id, eventKey, baseUrl, fetch, env, logger, middleware, isDev, }: TOpts); // Warning: (ae-forgotten-export) The symbol "ExclusiveKeys" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -273,9 +274,10 @@ export class InngestCommHandler Record): Promise<{ status: number; @@ -306,7 +308,6 @@ export class InngestCommHandler { + describe("mode", () => { + const createTestClient = ({ + env, + opts, + }: { + env?: Record; + opts?: Omit[0], "id">; + } = {}): Inngest.Any => { + let ogKeys: Record = {}; + + if (env) { + ogKeys = Object.keys(env).reduce>( + (acc, key) => { + acc[key] = process.env[key]; + process.env[key] = env[key]; + return acc; + }, + {} + ); + } + + const inngest = new Inngest({ id: "test", ...opts }); + + if (env) { + Object.keys(ogKeys).forEach((key) => { + process.env[key] = ogKeys[key]; + }); + } + + return inngest; + }; + + test("should default to inferred dev mode", () => { + const inngest = createTestClient(); + expect(inngest["mode"].isDev).toBe(true); + expect(inngest["mode"].isExplicit).toBe(false); + }); + + test("`isDev: true` sets explicit dev mode", () => { + const inngest = createTestClient({ opts: { isDev: true } }); + expect(inngest["mode"].isDev).toBe(true); + expect(inngest["mode"].isExplicit).toBe(true); + }); + + test("`isDev: false` sets explict cloud mode", () => { + const inngest = createTestClient({ opts: { isDev: false } }); + expect(inngest["mode"].isCloud).toBe(true); + expect(inngest["mode"].isExplicit).toBe(true); + }); + + test("`INNGEST_DEV=1 sets explicit dev mode", () => { + const inngest = createTestClient({ + env: { [envKeys.InngestDevMode]: "1" }, + }); + expect(inngest["mode"].isDev).toBe(true); + expect(inngest["mode"].isExplicit).toBe(true); + }); + + test("`INNGEST_DEV=true` sets explicit dev mode", () => { + const inngest = createTestClient({ + env: { [envKeys.InngestDevMode]: "true" }, + }); + expect(inngest["mode"].isDev).toBe(true); + expect(inngest["mode"].isExplicit).toBe(true); + }); + + test("`INNGEST_DEV=false` sets explicit cloud mode", () => { + const inngest = createTestClient({ + env: { [envKeys.InngestDevMode]: "false" }, + }); + expect(inngest["mode"].isCloud).toBe(true); + expect(inngest["mode"].isExplicit).toBe(true); + }); + + test("`INNGEST_DEV=0 sets explicit cloud mode", () => { + const inngest = createTestClient({ + env: { [envKeys.InngestDevMode]: "0" }, + }); + expect(inngest["mode"].isCloud).toBe(true); + expect(inngest["mode"].isExplicit).toBe(true); + }); + + test("`isDev` overwrites `INNGEST_DEV`", () => { + const inngest = createTestClient({ + env: { [envKeys.InngestDevMode]: "1" }, + opts: { isDev: false }, + }); + expect(inngest["mode"].isDev).toBe(false); + expect(inngest["mode"].isExplicit).toBe(true); + }); + + test("`INNGEST_DEV=URL sets explicit dev mode", () => { + const inngest = createTestClient({ + env: { [envKeys.InngestDevMode]: "http://localhost:3000" }, + }); + expect(inngest["mode"].isDev).toBe(true); + expect(inngest["mode"].isExplicit).toBe(true); + expect(inngest["mode"].explicitDevUrl?.href).toBe( + "http://localhost:3000/" + ); + }); + }); +}); + describe("send", () => { describe("runtime", () => { const originalProcessEnv = process.env; diff --git a/packages/inngest/src/components/Inngest.ts b/packages/inngest/src/components/Inngest.ts index 5c1c42afc..ed55abe3c 100644 --- a/packages/inngest/src/components/Inngest.ts +++ b/packages/inngest/src/components/Inngest.ts @@ -12,9 +12,10 @@ import { import { devServerAvailable, devServerUrl } from "../helpers/devserver"; import { getFetch, + getMode, inngestHeaders, processEnv, - skipDevServer, + type Mode, } from "../helpers/env"; import { fixEventKeyMissingSteps, prettyError } from "../helpers/errors"; import { stringify } from "../helpers/strings"; @@ -131,6 +132,18 @@ export class Inngest { */ private readonly middleware: Promise; + /** + * Whether the client is running in a production environment. This can + * sometimes be `undefined` if the client has expressed no preference or + * perhaps environment variables are only available at a later stage in the + * runtime, for example when receiving a request. + * + * An {@link InngestCommHandler} should prioritize this value over all other + * settings, but should still check for the presence of an environment + * variable if it is not set. + */ + private readonly mode: Mode; + /** * A client used to interact with the Inngest API by sending or reacting to * events. @@ -159,6 +172,7 @@ export class Inngest { env, logger = new DefaultLogger(), middleware, + isDev, }: TOpts) { if (!id) { // TODO PrettyError @@ -167,15 +181,22 @@ export class Inngest { this.id = id; + this.mode = getMode({ + explicitMode: + typeof isDev === "boolean" ? (isDev ? "dev" : "cloud") : undefined, + }); + this.apiBaseUrl = baseUrl || processEnv(envKeys.InngestApiBaseUrl) || - processEnv(envKeys.InngestBaseUrl); + processEnv(envKeys.InngestBaseUrl) || + this.mode.getExplicitUrl(defaultInngestApiBaseUrl); this.eventBaseUrl = baseUrl || processEnv(envKeys.InngestEventApiBaseUrl) || - processEnv(envKeys.InngestBaseUrl); + processEnv(envKeys.InngestBaseUrl) || + this.mode.getExplicitUrl(defaultInngestEventBaseUrl); this.setEventKey(eventKey || processEnv(envKeys.InngestEventKey) || ""); @@ -407,21 +428,9 @@ export class Inngest { let url = this.sendEventUrl.href; /** - * INNGEST_BASE_URL is used to set both dev server and prod URLs, so if a - * user has set this it means they have already chosen a URL to hit. + * If in prod mode and key is not present, fail now. */ - if (!skipDevServer()) { - if (!this.eventBaseUrl) { - const devAvailable = await devServerAvailable( - defaultDevServerHost, - this.fetch - ); - - if (devAvailable) { - url = devServerUrl(defaultDevServerHost, `e/${this.eventKey}`).href; - } - } - } else if (!this.eventKeySet()) { + if (this.mode.isCloud && !this.eventKeySet()) { throw new Error( prettyError({ whatHappened: "Failed to send event", @@ -432,6 +441,25 @@ export class Inngest { ); } + /** + * If dev mode has been inferred, try to hit the dev server first to see if + * it exists. If it does, use it, otherwise fall back to whatever server we + * have configured. + * + * `INNGEST_BASE_URL` is used to set both dev server and prod URLs, so if a + * user has set this it means they have already chosen a URL to hit. + */ + if (this.mode.isDev && this.mode.isInferred && !this.eventBaseUrl) { + const devAvailable = await devServerAvailable( + defaultDevServerHost, + this.fetch + ); + + if (devAvailable) { + url = devServerUrl(defaultDevServerHost, `e/${this.eventKey}`).href; + } + } + const response = await this.fetch(url, { method: "POST", body: stringify(payloads), diff --git a/packages/inngest/src/components/InngestCommHandler.ts b/packages/inngest/src/components/InngestCommHandler.ts index 251192143..eed8dcfcb 100644 --- a/packages/inngest/src/components/InngestCommHandler.ts +++ b/packages/inngest/src/components/InngestCommHandler.ts @@ -13,13 +13,13 @@ import { } from "../helpers/consts"; import { devServerAvailable, devServerUrl } from "../helpers/devserver"; import { + Mode, allProcessEnv, devServerHost, getFetch, + getMode, inngestHeaders, - isProd, platformSupportsStreaming, - skipDevServer, type Env, } from "../helpers/env"; import { rethrowError, serializeError } from "../helpers/errors"; @@ -252,14 +252,7 @@ export class InngestCommHandler< * * Should be set every time a request is received. */ - protected _isProd = false; - - /** - * Whether we should attempt to use the dev server. - * - * Should be set every time a request is received. - */ - protected _skipDevServer = false; + protected _mode: Mode | undefined; /** * The localized `fetch` implementation used by this handler. @@ -670,17 +663,23 @@ export class InngestCommHandler< getInngestHeaders: () => Record; reqArgs: unknown[]; }): Promise { - this._isProd = - (await actions.isProduction?.("starting to handle request")) ?? - isProd(this.env); + const assumedMode = getMode({ env: this.env, client: this.client }); - /** - * If we've been explicitly passed an Inngest dev sever URL, assume that - * we shouldn't skip the dev server. - */ - this._skipDevServer = devServerHost(this.env) - ? false - : this._isProd ?? skipDevServer(this.env); + if (assumedMode.isExplicit) { + this._mode = assumedMode; + } else { + const serveIsProd = await actions.isProduction?.( + "starting to handle request" + ); + if (typeof serveIsProd === "boolean") { + this._mode = new Mode({ + type: serveIsProd ? "cloud" : "dev", + isExplicit: false, + }); + } else { + this._mode = assumedMode; + } + } this.upsertKeysFromEnv(); @@ -834,6 +833,7 @@ export class InngestCommHandler< hasEventKey: this.client["eventKeySet"](), hasSigningKey: Boolean(this.signingKey), functionsFound: registerBody.functions.length, + mode: this._mode, }; return { @@ -888,8 +888,7 @@ export class InngestCommHandler< status: 405, body: JSON.stringify({ message: "No action found; request was likely not POST, PUT, or GET", - isProd: this._isProd, - skipDevServer: this._skipDevServer, + mode: this._mode, }), headers: {}, version: undefined, @@ -1093,12 +1092,17 @@ export class InngestCommHandler< // mutating the property between requests. let registerURL = new URL(this.inngestRegisterUrl.href); - if (!this._skipDevServer) { + const inferredDevMode = + this._mode && this._mode.isInferred && this._mode.isDev; + + if (inferredDevMode) { const host = devServerHost(this.env); const hasDevServer = await devServerAvailable(host, this.fetch); if (hasDevServer) { registerURL = devServerUrl(host, "/fn/register"); } + } else if (this._mode?.explicitDevUrl) { + registerURL = new URL(this._mode.explicitDevUrl); } if (deployId) { @@ -1156,10 +1160,6 @@ export class InngestCommHandler< return { status, message: error, modified }; } - private get isProd() { - return this._isProd; - } - /** * Given an environment, upsert any missing keys. This is useful in * situations where environment variables are passed directly to handlers or @@ -1188,8 +1188,10 @@ export class InngestCommHandler< } protected validateSignature(sig: string | undefined, body: unknown) { - // Never validate signatures in development. - if (!this.isProd) { + // Never validate signatures outside of prod. Make sure to check the mode + // exists here instead of using nullish coalescing to confirm that the check + // has been completed. + if (this._mode && !this._mode.isCloud) { return; } @@ -1427,7 +1429,7 @@ export interface ActionResponse< * This enables us to provide accurate errors for each access without having to * wrap every access in a try/catch. */ -type HandlerResponseWithErrors = { +export type HandlerResponseWithErrors = { [K in keyof HandlerResponse]: NonNullable extends ( ...args: infer Args ) => infer R diff --git a/packages/inngest/src/helpers/consts.ts b/packages/inngest/src/helpers/consts.ts index c78c57092..19994d731 100644 --- a/packages/inngest/src/helpers/consts.ts +++ b/packages/inngest/src/helpers/consts.ts @@ -31,6 +31,7 @@ export enum envKeys { InngestServePath = "INNGEST_SERVE_PATH", InngestLogLevel = "INNGEST_LOG_LEVEL", InngestStreaming = "INNGEST_STREAMING", + InngestDevMode = "INNGEST_DEV", BranchName = "BRANCH_NAME", @@ -97,9 +98,7 @@ export enum envKeys { * {@link https://docs.railway.app/develop/variables#railway-provided-variables} */ RailwayEnvironment = "RAILWAY_ENVIRONMENT", -} -export enum prodEnvKeys { VercelEnvKey = "VERCEL_ENV", } diff --git a/packages/inngest/src/helpers/env.ts b/packages/inngest/src/helpers/env.ts index a34d65775..1f7488300 100644 --- a/packages/inngest/src/helpers/env.ts +++ b/packages/inngest/src/helpers/env.ts @@ -6,7 +6,7 @@ import { type Inngest } from "../components/Inngest"; import { type SupportedFrameworkName } from "../types"; import { version } from "../version"; -import { envKeys, headerKeys, prodEnvKeys } from "./consts"; +import { defaultDevServerHost, envKeys, headerKeys } from "./consts"; import { stringifyUnknown } from "./strings"; /** @@ -36,13 +36,26 @@ export const devServerHost = (env: Env = allProcessEnv()): EnvValue => { // processed using webpack's DefinePlugin, which is dumb and does a straight // text replacement instead of actually understanding the AST, despite webpack // being fully capable of understanding the AST. - const values = [ - env[envKeys.InngestBaseUrl], - env[`REACT_APP_${envKeys.InngestBaseUrl}`], - env[`NEXT_PUBLIC_${envKeys.InngestBaseUrl}`], - ]; + const prefixes = ["REACT_APP_", "NEXT_PUBLIC_"]; + const keys = [envKeys.InngestBaseUrl, envKeys.InngestDevMode]; - return values.find((a) => !!a); + const values = keys.flatMap((key) => { + return prefixes.map((prefix) => { + return env[prefix + key]; + }); + }); + + return values.find((v) => { + if (!v) { + return; + } + + try { + return Boolean(new URL(v)); + } catch { + // no-op + } + }); }; const checkFns = (< @@ -69,55 +82,139 @@ const prodChecks: [ ["NODE_ENV", "starts with", "prod"], ["VERCEL_ENV", "starts with", "prod"], ["DENO_DEPLOYMENT_ID", "is truthy"], -]; - -// platformDeployChecks are a series of predicates that attempt to check whether -// we're deployed outside of localhost testing. This extends prodChecks with -// platform specific checks, ensuring that if you deploy to eg. vercel without -// NODE_ENV=production we still use prod mode. -const platformDeployChecks: [ - key: string, - customCheck: keyof typeof checkFns, - value?: string, -][] = [ - // Extend prod checks, then check if we're deployed to a platform. - [prodEnvKeys.VercelEnvKey, "is truthy but not", "development"], + [envKeys.VercelEnvKey, "is truthy but not", "development"], [envKeys.IsNetlify, "is truthy"], [envKeys.IsRender, "is truthy"], [envKeys.RailwayBranch, "is truthy"], [envKeys.IsCloudflarePages, "is truthy"], ]; -const skipDevServerChecks = prodChecks.concat(platformDeployChecks); - -/** - * Returns `true` if we're running in production or on a platform, based off of - * either passed environment variables or `process.env`. - */ -export const skipDevServer = ( +interface IsProdOptions { /** * The optional environment variables to use instead of `process.env`. */ - env: Record = allProcessEnv() -): boolean => { - return skipDevServerChecks.some(([key, checkKey, expected]) => { - return checkFns[checkKey](stringifyUnknown(env[key]), expected); - }); -}; + env?: Record; -/** - * Returns `true` if we believe the current environment is production based on - * either passed environment variables or `process.env`. - */ -export const isProd = ( /** - * The optional environment variables to use instead of `process.env`. + * The Inngest client that's being used when performing this check. This is + * used to check if the client has an explicit mode set, and if so, to use + * that mode instead of inferring it from the environment. */ - env: Record = allProcessEnv() -): boolean => { - return prodChecks.some(([key, checkKey, expected]) => { + client?: Inngest.Any; + + /** + * If specified as a `boolean`, this will be returned as the result of the + * function. Useful for options that may or may not be set by users. + */ + explicitMode?: Mode["type"]; +} + +export interface ModeOptions { + type: "cloud" | "dev"; + + /** + * Whether the mode was explicitly set, or inferred from other sources. + */ + isExplicit: boolean; + + /** + * If the mode was explicitly set as a dev URL, this is the URL that was set. + */ + explicitDevUrl?: URL; +} + +export class Mode { + private readonly type: "cloud" | "dev"; + + /** + * Whether the mode was explicitly set, or inferred from other sources. + */ + public readonly isExplicit: boolean; + + public readonly explicitDevUrl?: URL; + + constructor({ type, isExplicit, explicitDevUrl }: ModeOptions) { + this.type = type; + this.isExplicit = isExplicit || Boolean(explicitDevUrl); + this.explicitDevUrl = explicitDevUrl; + } + + public get isDev(): boolean { + return this.type === "dev"; + } + + public get isCloud(): boolean { + return this.type === "cloud"; + } + + public get isInferred(): boolean { + return !this.isExplicit; + } + + /** + * If we are explicitly in a particular mode, retrieve the URL that we are + * sure we should be using, not considering any environment variables or other + * influences. + */ + public getExplicitUrl(defaultCloudUrl: string): string | undefined { + if (!this.isExplicit) { + return undefined; + } + + if (this.explicitDevUrl) { + return this.explicitDevUrl.href; + } + + if (this.isCloud) { + return defaultCloudUrl; + } + + if (this.isDev) { + return defaultDevServerHost; + } + + return undefined; + } +} + +/** + * Returns the mode of the current environment, based off of either passed + * environment variables or `process.env`, or explicit settings. + */ +export const getMode = ({ + env = allProcessEnv(), + client, + explicitMode, +}: IsProdOptions = {}): Mode => { + if (explicitMode) { + return new Mode({ type: explicitMode, isExplicit: true }); + } + + if (client?.["mode"].isExplicit) { + return client["mode"]; + } + + if (envKeys.InngestDevMode in env) { + if (typeof env[envKeys.InngestDevMode] === "string") { + try { + const explicitDevUrl = new URL(env[envKeys.InngestDevMode]); + return new Mode({ type: "dev", isExplicit: true, explicitDevUrl }); + } catch { + // no-op + } + } + + const envIsDev = parseAsBoolean(env[envKeys.InngestDevMode]); + if (typeof envIsDev === "boolean") { + return new Mode({ type: envIsDev ? "dev" : "cloud", isExplicit: true }); + } + } + + const isProd = prodChecks.some(([key, checkKey, expected]) => { return checkFns[checkKey](stringifyUnknown(env[key]), expected); }); + + return new Mode({ type: isProd ? "cloud" : "dev", isExplicit: false }); }; /** @@ -388,3 +485,37 @@ export const getResponse = (): typeof Response => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires return require("cross-fetch").Response; }; + +/** + * Given an unknown value, try to parse it as a `boolean`. Useful for parsing + * environment variables that could be a selection of different values such as + * `"true"`, `"1"`. + * + * If the value could not be confidently parsed as a `boolean` or was seen to be + * `undefined`, this function returns `undefined`. + */ +export const parseAsBoolean = (value: unknown): boolean | undefined => { + if (typeof value === "boolean") { + return value; + } + + if (typeof value === "number") { + return Boolean(value); + } + + if (typeof value === "string") { + const trimmed = value.trim().toLowerCase(); + + if (trimmed === "undefined") { + return undefined; + } + + if (["true", "1"].includes(trimmed)) { + return true; + } + + return false; + } + + return undefined; +}; diff --git a/packages/inngest/src/test/helpers.ts b/packages/inngest/src/test/helpers.ts index 405c82194..0abd48f6e 100644 --- a/packages/inngest/src/test/helpers.ts +++ b/packages/inngest/src/test/helpers.ts @@ -550,7 +550,7 @@ export const testFramework = ( const ret = await run( [ { - client: new Inngest({ id: "Test", env: "FOO" }), + client: new Inngest({ id: "Test", env: "FOO", isDev: false }), functions: [], }, ], @@ -698,6 +698,7 @@ export const testFramework = ( DENO_DEPLOYMENT_ID: "1", NODE_ENV: "production", ENVIRONMENT: "production", + INNGEST_DEV: "0", }; test("should throw an error in prod with no signature", async () => { const ret = await run( @@ -816,9 +817,7 @@ export const testFramework = ( () => "fn" ); const env = { - DENO_DEPLOYMENT_ID: undefined, - NODE_ENV: "development", - ENVIRONMENT: "development", + INNGEST_DEV: "1", }; test("should throw an error with an invalid JSON body", async () => { diff --git a/packages/inngest/src/types.ts b/packages/inngest/src/types.ts index 8702000a6..706e621f6 100644 --- a/packages/inngest/src/types.ts +++ b/packages/inngest/src/types.ts @@ -14,6 +14,7 @@ import { } from "./components/InngestMiddleware"; import { type createStepTools } from "./components/InngestStepTools"; import { type internalEvents } from "./helpers/consts"; +import { type Mode } from "./helpers/env"; import { type IsStringLiteral, type ObjectPaths, @@ -585,6 +586,16 @@ export interface ClientOptions { */ logger?: Logger; middleware?: MiddlewareStack; + + /** + * Can be used to explicitly set the client to Development Mode, which will + * turn off signature verification and default to using a local URL to access + * a local Dev Server. + * + * This is useful for forcing the client to use a local Dev Server while also + * running in a production-like environment. + */ + isDev?: boolean; } /** @@ -1100,6 +1111,12 @@ export interface IntrospectRequest { * The number of Inngest functions found at this handler. */ functionsFound: number; + + /** + * The mode that this handler is running in and whether it has been inferred + * or explicitly set. + */ + mode: Mode; } /**