From 255416c4478ac367381da0c166b6762056d94e1d Mon Sep 17 00:00:00 2001 From: Tony Holdstock-Brown Date: Wed, 27 Nov 2024 06:51:46 -0800 Subject: [PATCH 1/2] `ai.infer`: anthropic types and adapters (#762) This add types from the Anthropic SDK to support Anthropic models. --------- Co-authored-by: Jack Williams <1736957+jpwilliams@users.noreply.github.com> Co-authored-by: Jack Williams --- .changeset/twenty-buttons-retire.md | 5 + packages/inngest/src/components/ai/adapter.ts | 4 +- .../src/components/ai/adapters/anthropic.ts | 633 ++++++++++++++++++ packages/inngest/src/components/ai/index.ts | 2 + .../src/components/ai/models/anthropic.ts | 89 +++ .../src/components/ai/models/openai.ts | 2 +- packages/inngest/src/helpers/consts.ts | 1 + 7 files changed, 734 insertions(+), 2 deletions(-) create mode 100644 .changeset/twenty-buttons-retire.md create mode 100644 packages/inngest/src/components/ai/adapters/anthropic.ts create mode 100644 packages/inngest/src/components/ai/models/anthropic.ts diff --git a/.changeset/twenty-buttons-retire.md b/.changeset/twenty-buttons-retire.md new file mode 100644 index 00000000..880654cf --- /dev/null +++ b/.changeset/twenty-buttons-retire.md @@ -0,0 +1,5 @@ +--- +"inngest": minor +--- + +Add `anthropic()` model for `step.ai.*()` diff --git a/packages/inngest/src/components/ai/adapter.ts b/packages/inngest/src/components/ai/adapter.ts index ab195425..ee22662a 100644 --- a/packages/inngest/src/components/ai/adapter.ts +++ b/packages/inngest/src/components/ai/adapter.ts @@ -1,3 +1,4 @@ +import { type AnthropicAiAdapter } from "./adapters/anthropic.js"; import { type OpenAiAiAdapter } from "./adapters/openai.js"; /** @@ -91,7 +92,7 @@ export namespace AiAdapter { /** * Supported I/O formats for AI models. */ - export type Format = "openai-chat"; // | "anthropic" | "gemini" | "bedrock"; + export type Format = "openai-chat" | "anthropic"; /** * A function that creates a model that adheres to an existng AI adapter @@ -108,6 +109,7 @@ export namespace AiAdapter { */ const adapters = { "openai-chat": null as unknown as OpenAiAiAdapter, + anthropic: null as unknown as AnthropicAiAdapter, } satisfies Record; /** diff --git a/packages/inngest/src/components/ai/adapters/anthropic.ts b/packages/inngest/src/components/ai/adapters/anthropic.ts new file mode 100644 index 00000000..c1f4c60d --- /dev/null +++ b/packages/inngest/src/components/ai/adapters/anthropic.ts @@ -0,0 +1,633 @@ +import { type AiAdapter, type types } from "../adapter.js"; + +export interface AnthropicAiAdapter extends AiAdapter { + /** + * Format of the IO for this model + */ + format: "anthropic"; + + [types]: { + input: AnthropicAiAdapter.Input; + output: AnthropicAiAdapter.Output; + }; +} + +export namespace AnthropicAiAdapter { + export type Input = MessageCreateParamsNonStreaming; + + export type Output = Message; + + /** + * The model that will complete your prompt.\n\nSee + * [models](https://docs.anthropic.com/en/docs/models-overview) for additional + * details and options. + */ + export type Model = + // eslint-disable-next-line @typescript-eslint/ban-types + | (string & {}) + | "claude-3-5-haiku-latest" + | "claude-3-5-haiku-20241022" + | "claude-3-5-sonnet-latest" + | "claude-3-5-sonnet-20241022" + | "claude-3-5-sonnet-20240620" + | "claude-3-opus-latest" + | "claude-3-opus-20240229" + | "claude-3-sonnet-20240229" + | "claude-3-haiku-20240307" + | "claude-2.1" + | "claude-2.0" + | "claude-instant-1.2"; + + export type Beta = + // eslint-disable-next-line @typescript-eslint/ban-types + | (string & {}) + | "message-batches-2024-09-24" + | "prompt-caching-2024-07-31" + | "computer-use-2024-10-22" + | "pdfs-2024-09-25" + | "token-counting-2024-11-01"; + + export interface MessageCreateParamsNonStreaming + extends MessageCreateParamsBase { + /** + * Whether to incrementally stream the response using server-sent events. + * + * See [streaming](https://docs.anthropic.com/en/api/messages-streaming) for + * details. + */ + stream?: false; + } + + export interface MessageParam { + content: + | string + | Array< + | TextBlockParam + | ImageBlockParam + | ToolUseBlockParam + | ToolResultBlockParam + >; + + role: "user" | "assistant"; + } + + export interface TextBlockParam { + text: string; + + type: "text"; + } + + export namespace ImageBlockParam { + export interface Source { + data: string; + + media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; + + type: "base64"; + } + } + + export interface ToolUseBlockParam { + id: string; + + input: unknown; + + name: string; + + type: "tool_use"; + } + + export interface ToolResultBlockParam { + tool_use_id: string; + + type: "tool_result"; + + content?: string | Array; + + is_error?: boolean; + } + + export interface ImageBlockParam { + source: ImageBlockParam.Source; + + type: "image"; + } + + export namespace ImageBlockParam { + export interface Source { + data: string; + + media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; + + type: "base64"; + } + } + + export interface Message { + /** + * Unique object identifier. + * + * The format and length of IDs may change over time. + */ + id: string; + + /** + * Content generated by the model. + * + * This is an array of content blocks, each of which has a `type` that determines + * its shape. + * + * Example: + * + * ```json + * [{ "type": "text", "text": "Hi, I'm Claude." }] + * ``` + * + * If the request input `messages` ended with an `assistant` turn, then the + * response `content` will continue directly from that last turn. You can use this + * to constrain the model's output. + * + * For example, if the input `messages` were: + * + * ```json + * [ + * { + * "role": "user", + * "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun" + * }, + * { "role": "assistant", "content": "The best answer is (" } + * ] + * ``` + * + * Then the response `content` might be: + * + * ```json + * [{ "type": "text", "text": "B)" }] + * ``` + */ + content: Array; + + /** + * The model that will complete your prompt.\n\nSee + * [models](https://docs.anthropic.com/en/docs/models-overview) for additional + * details and options. + */ + model: Model; + + /** + * Conversational role of the generated message. + * + * This will always be `"assistant"`. + */ + role: "assistant"; + + /** + * The reason that we stopped. + * + * This may be one the following values: + * + * - `"end_turn"`: the model reached a natural stopping point + * - `"max_tokens"`: we exceeded the requested `max_tokens` or the model's maximum + * - `"stop_sequence"`: one of your provided custom `stop_sequences` was generated + * - `"tool_use"`: the model invoked one or more tools + * + * In non-streaming mode this value is always non-null. In streaming mode, it is + * null in the `message_start` event and non-null otherwise. + */ + stop_reason: + | "end_turn" + | "max_tokens" + | "stop_sequence" + | "tool_use" + | null; + + /** + * Which custom stop sequence was generated, if any. + * + * This value will be a non-null string if one of your custom stop sequences was + * generated. + */ + stop_sequence: string | null; + + /** + * Object type. + * + * For Messages, this is always `"message"`. + */ + type: "message"; + + /** + * Billing and rate-limit usage. + * + * Anthropic's API bills and rate-limits by token counts, as tokens represent the + * underlying cost to our systems. + * + * Under the hood, the API transforms requests into a format suitable for the + * model. The model's output then goes through a parsing stage before becoming an + * API response. As a result, the token counts in `usage` will not match one-to-one + * with the exact visible content of an API request or response. + * + * For example, `output_tokens` will be non-zero, even for an empty string response + * from Claude. + */ + usage: Usage; + } + + export type ContentBlock = TextBlock | ToolUseBlock; + + export interface TextBlock { + text: string; + + type: "text"; + } + + export interface ToolUseBlock { + id: string; + + input: unknown; + + name: string; + + type: "tool_use"; + } + + export interface Usage { + /** + * The number of input tokens which were used. + */ + input_tokens: number; + + /** + * The number of output tokens which were used. + */ + output_tokens: number; + } + + export interface Metadata { + /** + * An external identifier for the user who is associated with the request. + * + * This should be a uuid, hash value, or other opaque identifier. Anthropic may use + * this id to help detect abuse. Do not include any identifying information such as + * name, email address, or phone number. + */ + user_id?: string | null; + } + + /** + * How the model should use the provided tools. The model can use a specific tool, + * any available tool, or decide by itself. + */ + export type ToolChoice = ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool; + + /** + * The model will use any available tools. + */ + export interface ToolChoiceAny { + type: "any"; + + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output exactly one tool + * use. + */ + disable_parallel_tool_use?: boolean; + } + + /** + * The model will automatically decide whether to use tools. + */ + export interface ToolChoiceAuto { + type: "auto"; + + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output at most one tool + * use. + */ + disable_parallel_tool_use?: boolean; + } + + /** + * The model will use the specified tool with `tool_choice.name`. + */ + export interface ToolChoiceTool { + /** + * The name of the tool to use. + */ + name: string; + + type: "tool"; + + /** + * Whether to disable parallel tool use. + * + * Defaults to `false`. If set to `true`, the model will output exactly one tool + * use. + */ + disable_parallel_tool_use?: boolean; + } + + export interface Tool { + /** + * [JSON schema](https://json-schema.org/) for this tool's input. + * + * This defines the shape of the `input` that your tool accepts and that the model + * will produce. + */ + input_schema: Tool.InputSchema; + + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in tool_use blocks. + */ + name: string; + + /** + * Description of what this tool does. + * + * Tool descriptions should be as detailed as possible. The more information that + * the model has about what the tool is and how to use it, the better it will + * perform. You can use natural language descriptions to reinforce important + * aspects of the tool input JSON schema. + */ + description?: string; + } + + export namespace Tool { + /** + * [JSON schema](https://json-schema.org/) for this tool's input. + * + * This defines the shape of the `input` that your tool accepts and that the model + * will produce. + */ + export interface InputSchema { + type: "object"; + + properties?: unknown; + [k: string]: unknown; + } + } + + export interface MessageCreateParamsBase { + /** + * The maximum number of tokens to generate before stopping. + * + * Note that our models may stop _before_ reaching this maximum. This parameter + * only specifies the absolute maximum number of tokens to generate. + * + * Different models have different maximum values for this parameter. See + * [models](https://docs.anthropic.com/en/docs/models-overview) for details. + */ + max_tokens: number; + + /** + * Input messages. + * + * Our models are trained to operate on alternating `user` and `assistant` + * conversational turns. When creating a new `Message`, you specify the prior + * conversational turns with the `messages` parameter, and the model then generates + * the next `Message` in the conversation. Consecutive `user` or `assistant` turns + * in your request will be combined into a single turn. + * + * Each input message must be an object with a `role` and `content`. You can + * specify a single `user`-role message, or you can include multiple `user` and + * `assistant` messages. + * + * If the final message uses the `assistant` role, the response content will + * continue immediately from the content in that message. This can be used to + * constrain part of the model's response. + * + * Example with a single `user` message: + * + * ```json + * [{ "role": "user", "content": "Hello, Claude" }] + * ``` + * + * Example with multiple conversational turns: + * + * ```json + * [ + * { "role": "user", "content": "Hello there." }, + * { "role": "assistant", "content": "Hi, I'm Claude. How can I help you?" }, + * { "role": "user", "content": "Can you explain LLMs in plain English?" } + * ] + * ``` + * + * Example with a partially-filled response from Claude: + * + * ```json + * [ + * { + * "role": "user", + * "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun" + * }, + * { "role": "assistant", "content": "The best answer is (" } + * ] + * ``` + * + * Each input message `content` may be either a single `string` or an array of + * content blocks, where each block has a specific `type`. Using a `string` for + * `content` is shorthand for an array of one content block of type `"text"`. The + * following input messages are equivalent: + * + * ```json + * { "role": "user", "content": "Hello, Claude" } + * ``` + * + * ```json + * { "role": "user", "content": [{ "type": "text", "text": "Hello, Claude" }] } + * ``` + * + * Starting with Claude 3 models, you can also send image content blocks: + * + * ```json + * { + * "role": "user", + * "content": [ + * { + * "type": "image", + * "source": { + * "type": "base64", + * "media_type": "image/jpeg", + * "data": "/9j/4AAQSkZJRg..." + * } + * }, + * { "type": "text", "text": "What is in this image?" } + * ] + * } + * ``` + * + * We currently support the `base64` source type for images, and the `image/jpeg`, + * `image/png`, `image/gif`, and `image/webp` media types. + * + * See [examples](https://docs.anthropic.com/en/api/messages-examples#vision) for + * more input examples. + * + * Note that if you want to include a + * [system prompt](https://docs.anthropic.com/en/docs/system-prompts), you can use + * the top-level `system` parameter — there is no `"system"` role for input + * messages in the Messages API. + */ + messages: Array; + + /** + * The model that will complete your prompt.\n\nSee + * [models](https://docs.anthropic.com/en/docs/models-overview) for additional + * details and options. + */ + model: Model; + + /** + * An object describing metadata about the request. + */ + metadata?: Metadata; + + /** + * Custom text sequences that will cause the model to stop generating. + * + * Our models will normally stop when they have naturally completed their turn, + * which will result in a response `stop_reason` of `"end_turn"`. + * + * If you want the model to stop generating when it encounters custom strings of + * text, you can use the `stop_sequences` parameter. If the model encounters one of + * the custom sequences, the response `stop_reason` value will be `"stop_sequence"` + * and the response `stop_sequence` value will contain the matched stop sequence. + */ + stop_sequences?: Array; + + /** + * Whether to incrementally stream the response using server-sent events. + * + * See [streaming](https://docs.anthropic.com/en/api/messages-streaming) for + * details. + */ + stream?: boolean; + + /** + * System prompt. + * + * A system prompt is a way of providing context and instructions to Claude, such + * as specifying a particular goal or role. See our + * [guide to system prompts](https://docs.anthropic.com/en/docs/system-prompts). + */ + system?: string | Array; + + /** + * Amount of randomness injected into the response. + * + * Defaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` + * for analytical / multiple choice, and closer to `1.0` for creative and + * generative tasks. + * + * Note that even with `temperature` of `0.0`, the results will not be fully + * deterministic. + */ + temperature?: number; + + /** + * How the model should use the provided tools. The model can use a specific tool, + * any available tool, or decide by itself. + */ + tool_choice?: ToolChoice; + + /** + * Definitions of tools that the model may use. + * + * If you include `tools` in your API request, the model may return `tool_use` + * content blocks that represent the model's use of those tools. You can then run + * those tools using the tool input generated by the model and then optionally + * return results back to the model using `tool_result` content blocks. + * + * Each tool definition includes: + * + * - `name`: Name of the tool. + * - `description`: Optional, but strongly-recommended description of the tool. + * - `input_schema`: [JSON schema](https://json-schema.org/) for the tool `input` + * shape that the model will produce in `tool_use` output content blocks. + * + * For example, if you defined `tools` as: + * + * ```json + * [ + * { + * "name": "get_stock_price", + * "description": "Get the current stock price for a given ticker symbol.", + * "input_schema": { + * "type": "object", + * "properties": { + * "ticker": { + * "type": "string", + * "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." + * } + * }, + * "required": ["ticker"] + * } + * } + * ] + * ``` + * + * And then asked the model "What's the S&P 500 at today?", the model might produce + * `tool_use` content blocks in the response like this: + * + * ```json + * [ + * { + * "type": "tool_use", + * "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "name": "get_stock_price", + * "input": { "ticker": "^GSPC" } + * } + * ] + * ``` + * + * You might then run your `get_stock_price` tool with `{"ticker": "^GSPC"}` as an + * input, and return the following back to the model in a subsequent `user` + * message: + * + * ```json + * [ + * { + * "type": "tool_result", + * "tool_use_id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV", + * "content": "259.75 USD" + * } + * ] + * ``` + * + * Tools can be used for workflows that include running client-side tools and + * functions, or more generally whenever you want the model to produce a particular + * JSON structure of output. + * + * See our [guide](https://docs.anthropic.com/en/docs/tool-use) for more details. + */ + tools?: Array; + + /** + * Only sample from the top K options for each subsequent token. + * + * Used to remove "long tail" low probability responses. + * [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277). + * + * Recommended for advanced use cases only. You usually only need to use + * `temperature`. + */ + top_k?: number; + + /** + * Use nucleus sampling. + * + * In nucleus sampling, we compute the cumulative distribution over all the options + * for each subsequent token in decreasing probability order and cut it off once it + * reaches a particular probability specified by `top_p`. You should either alter + * `temperature` or `top_p`, but not both. + * + * Recommended for advanced use cases only. You usually only need to use + * `temperature`. + */ + top_p?: number; + } +} diff --git a/packages/inngest/src/components/ai/index.ts b/packages/inngest/src/components/ai/index.ts index 97f9e7c8..fcd65dcd 100644 --- a/packages/inngest/src/components/ai/index.ts +++ b/packages/inngest/src/components/ai/index.ts @@ -1,8 +1,10 @@ export type { AiAdapter, AiAdapters } from "./adapter.js"; // Adapters +export * from "./adapters/anthropic.js"; export * from "./adapters/openai.js"; // Models +export * from "./models/anthropic.js"; export * from "./models/gemini.js"; export * from "./models/openai.js"; diff --git a/packages/inngest/src/components/ai/models/anthropic.ts b/packages/inngest/src/components/ai/models/anthropic.ts new file mode 100644 index 00000000..fc1c0a85 --- /dev/null +++ b/packages/inngest/src/components/ai/models/anthropic.ts @@ -0,0 +1,89 @@ +import { envKeys } from "../../../helpers/consts.js"; +import { processEnv } from "../../../helpers/env.js"; +import { type AiAdapter } from "../adapter.js"; +import { type AnthropicAiAdapter } from "../adapters/anthropic.js"; + +/** + * Create an Anthropic model using the Anthropic chat format. + * + * By default it targets the `https://api.anthropic.com/v1/` base URL, with the + * "2023-06-01" anthropic-version header. + */ +export const anthropic: AiAdapter.ModelCreator< + [options: Anthropic.AiModelOptions], + Anthropic.AiModel +> = (options) => { + const authKey = options.apiKey || processEnv(envKeys.AnthropicApiKey) || ""; + + // Ensure we add a trailing slash to our base URL if it doesn't have one, + // otherwise we'll replace the path instead of appending it. + let baseUrl = options.baseUrl || "https://api.anthropic.com/v1/"; + if (!baseUrl.endsWith("/")) { + baseUrl += "/"; + } + + const url = new URL("messages", baseUrl); + + const headers: Record = { + "anthropic-version": "2023-06-01", + }; + + if ((options.betaHeaders?.length || 0) > 0) { + headers["anthropic-beta"] = options.betaHeaders?.join(",") || ""; + } + + return { + url: url.href, + authKey, + format: "anthropic", + onCall(_, body) { + body.model ||= options.model; + }, + headers, + } as Anthropic.AiModel; +}; + +export namespace Anthropic { + /** + * IDs of models to use. See the [model endpoint + * compatibility](https://docs.anthropic.com/en/docs/about-claude/models) + * table for details on which models work with the Anthropic API. + */ + export type Model = AnthropicAiAdapter.Model; + + /** + * Options for creating an Anthropic model. + */ + export interface AiModelOptions { + /** + * ID of the model to use. See the [model endpoint + * compatibility](https://docs.anthropic.com/en/docs/about-claude/models) + * table for details on which models work with the Anthropic API. + */ + model: Model; + + /** + * The Anthropic API key to use for authenticating your request. By default + * we'll search for and use the `ANTHROPIC_API_KEY` environment variable. + */ + apiKey?: string; + + /** + * The beta headers to enable, eg. for computer use, prompt caching, and so + * on + */ + betaHeaders?: AnthropicAiAdapter.Beta[]; + + /** + * The base URL for the Anthropic API. + * + * @default "https://api.anthropic.com/v1/" + */ + baseUrl?: string; + } + + /** + * An Anthropic model using the Anthropic format for I/O. + */ + export type AiModel = AnthropicAiAdapter; +} diff --git a/packages/inngest/src/components/ai/models/openai.ts b/packages/inngest/src/components/ai/models/openai.ts index 189ad6e0..38584317 100644 --- a/packages/inngest/src/components/ai/models/openai.ts +++ b/packages/inngest/src/components/ai/models/openai.ts @@ -66,7 +66,7 @@ export namespace OpenAi { /** * The base URL for the OpenAI API. * - * @default "https://api.openai.com" + * @default "https://api.openai.com/v1/" */ baseUrl?: string; } diff --git a/packages/inngest/src/helpers/consts.ts b/packages/inngest/src/helpers/consts.ts index 22d57509..0c639a7a 100644 --- a/packages/inngest/src/helpers/consts.ts +++ b/packages/inngest/src/helpers/consts.ts @@ -114,6 +114,7 @@ export enum envKeys { OpenAiApiKey = "OPENAI_API_KEY", GeminiApiKey = "GEMINI_API_KEY", + AnthropicApiKey = "ANTHROPIC_API_KEY", } /** From 0c2bb8e048f39500e25ed0b521db210bbc4a757d Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Wed, 27 Nov 2024 15:15:28 +0000 Subject: [PATCH 2/2] Add `@inngest/middleware-validation` (#744) ## Summary Adds `@inngest/middleware-validation` which will validate incoming and outgoing events using Zod schemas defined using `new EventSchemas().fromZod()`. While while supports only Zod currently, this can support any number of schemas as we add them. ```ts import { Inngest, EventSchemas } from "inngest"; import { validationMiddleware } from "@inngest/middleware-validation"; import { z } from "zod"; const inngest = new Inngest({ id: "my-app", middleware: [validationMiddleware()], // just add this schemas: new EventSchemas().fromZod({ "example/event": { data: z.object({ message: z.string(), }), }, }), }); ``` By default, simply adding `validationMiddleware()` to an existing client that uses Zod schemas will validate all incoming and outgoing events. You can provide some extra options to customize the behaviour: ```ts validationMiddleware({ ... }); { /** * Disallow events that don't have a schema defined. * * For this to happen, it probably means that the event is typed using * `.fromRecord()` or some other type-only method, and we have no way of * validating the payload at runtime. * * @default false */ disallowSchemalessEvents?: boolean; /** * Disallow events that have a schema defined, but the schema is unknown and * not handled in this code. * * This is most likely to happen if schemas can be defined using a library not yet * supported by this middleware. * * @default false */ disallowUnknownSchemas?: boolean; /** * Disable validation of incoming events. * * @default false */ disableIncomingValidation?: boolean; /** * Disable validation of outgoing events using `inngest.send()` or * `step.sendEvent()`. * * @default false */ disableOutgoingValidation?: boolean; } ``` Note that due to current typing restrictions within middleware, _transforming_ types is currently unsupported, for example using `z.transform()`. This can be introduced in a later update once more mature middleware typing is available, likely in `inngest@^4.0.0`. ## Checklist - [ ] ~Added a [docs PR](https://github.com/inngest/website) that references this PR~ N/A Will ship after - [x] Add a `README.md` - [x] Added unit/integration tests - [x] Added changesets if applicable --- .changeset/quick-buckets-cover.md | 5 + .github/workflows/pr.yml | 15 + packages/middleware-validation/.gitignore | 2 + packages/middleware-validation/CHANGELOG.md | 0 packages/middleware-validation/LICENSE.md | 201 +++++++ packages/middleware-validation/README.md | 95 ++++ .../middleware-validation/eslint.config.js | 9 + packages/middleware-validation/jest.config.js | 8 + packages/middleware-validation/jsr.json | 11 + packages/middleware-validation/package.json | 59 ++ packages/middleware-validation/pnpm-lock.yaml | 327 +++++++++++ packages/middleware-validation/src/index.ts | 1 + .../src/middleware.test.ts | 506 ++++++++++++++++++ .../middleware-validation/src/middleware.ts | 241 +++++++++ .../middleware-validation/tsconfig.build.json | 4 + packages/middleware-validation/tsconfig.json | 14 + pnpm-lock.yaml | 397 +++++++++++++- 17 files changed, 1866 insertions(+), 29 deletions(-) create mode 100644 .changeset/quick-buckets-cover.md create mode 100644 packages/middleware-validation/.gitignore create mode 100644 packages/middleware-validation/CHANGELOG.md create mode 100644 packages/middleware-validation/LICENSE.md create mode 100644 packages/middleware-validation/README.md create mode 100644 packages/middleware-validation/eslint.config.js create mode 100644 packages/middleware-validation/jest.config.js create mode 100644 packages/middleware-validation/jsr.json create mode 100644 packages/middleware-validation/package.json create mode 100644 packages/middleware-validation/pnpm-lock.yaml create mode 100644 packages/middleware-validation/src/index.ts create mode 100644 packages/middleware-validation/src/middleware.test.ts create mode 100644 packages/middleware-validation/src/middleware.ts create mode 100644 packages/middleware-validation/tsconfig.build.json create mode 100644 packages/middleware-validation/tsconfig.json diff --git a/.changeset/quick-buckets-cover.md b/.changeset/quick-buckets-cover.md new file mode 100644 index 00000000..c24ce41e --- /dev/null +++ b/.changeset/quick-buckets-cover.md @@ -0,0 +1,5 @@ +--- +"@inngest/middleware-validation": patch +--- + +Initial release of `@inngest/middleware-validation` diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 92caa06b..83f55c08 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -161,6 +161,21 @@ jobs: - run: pnpm install - run: pnpm test + "middleware-validation_test": + name: "middleware-validation: Test" + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/middleware-validation + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-and-build + with: + install-dependencies: false + build: false + - run: pnpm install + - run: pnpm test + package_inngest: name: "inngest: Package" runs-on: ubuntu-latest diff --git a/packages/middleware-validation/.gitignore b/packages/middleware-validation/.gitignore new file mode 100644 index 00000000..de4d1f00 --- /dev/null +++ b/packages/middleware-validation/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/middleware-validation/CHANGELOG.md b/packages/middleware-validation/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/middleware-validation/LICENSE.md b/packages/middleware-validation/LICENSE.md new file mode 100644 index 00000000..3ad56957 --- /dev/null +++ b/packages/middleware-validation/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Inngest Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/middleware-validation/README.md b/packages/middleware-validation/README.md new file mode 100644 index 00000000..6a49a5d9 --- /dev/null +++ b/packages/middleware-validation/README.md @@ -0,0 +1,95 @@ +# @inngest/middleware-validation + +This package provides a validation middleware for Inngest, enabling parsing of +incoming and outgoing events using Zod schemas provided using `new +EventSchemas().fromZod()`. + +## Features + +- Validates incoming event payloads when a function is run +- Validates outgoing event payloads using `inngest.send()` or `step.sendEvent()` +- Optionally disallow events without specified schemas + +## Installation + +```sh +npm install @inngest/middleware-validation +``` + +> [!NOTE] +> Requires TypeScript SDK >= 3.23.1 + +## Usage + +To use the validation middleware, import and initialize it. + +```ts +import { Inngest, EventSchemas } from "inngest"; +import { validationMiddleware } from "@inngest/middleware-validation"; +import { z } from "zod"; + +const inngest = new Inngest({ + id: "my-app", + middleware: [validationMiddleware()], // just add this + schemas: new EventSchemas().fromZod({ + "example/event": { + data: z.object({ + message: z.string(), + }), + }, + }), +}); +``` + +By default, simply adding `validationMiddleware()` to an existing client that uses Zod schemas will validate all incoming and outgoing events. + +You can provide some extra options to customize the behaviour: + +```ts +validationMiddleware({ ... }); + +{ + /** + * Disallow events that don't have a schema defined. + * + * For this to happen, it probably means that the event is typed using + * `.fromRecord()` or some other type-only method, and we have no way of + * validating the payload at runtime. + * + * @default false + */ + disallowSchemalessEvents?: boolean; + + /** + * Disallow events that have a schema defined, but the schema is unknown and + * not handled in this code. + * + * This is most likely to happen if schemas can be defined using a library not yet + * supported by this middleware. + * + * @default false + */ + disallowUnknownSchemas?: boolean; + + /** + * Disable validation of incoming events. + * + * @default false + */ + disableIncomingValidation?: boolean; + + /** + * Disable validation of outgoing events using `inngest.send()` or + * `step.sendEvent()`. + * + * @default false + */ + disableOutgoingValidation?: boolean; +} +``` + +> [!NOTE] +> Due to current typing restrictions within middleware, _transforming_ +> types is currently unsupported, for example using `z.transform()`. This can be +> introduced in a later update once more mature middleware typing is available, +> likely in `inngest@^4.0.0`. diff --git a/packages/middleware-validation/eslint.config.js b/packages/middleware-validation/eslint.config.js new file mode 100644 index 00000000..749472ea --- /dev/null +++ b/packages/middleware-validation/eslint.config.js @@ -0,0 +1,9 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended +); diff --git a/packages/middleware-validation/jest.config.js b/packages/middleware-validation/jest.config.js new file mode 100644 index 00000000..8e30a241 --- /dev/null +++ b/packages/middleware-validation/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest", {}], + }, + roots: ["/src"], +}; diff --git a/packages/middleware-validation/jsr.json b/packages/middleware-validation/jsr.json new file mode 100644 index 00000000..c0639560 --- /dev/null +++ b/packages/middleware-validation/jsr.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://jsr.io/schema/config-file.v1.json", + "name": "@inngest/middleware-validation", + "description": "Schema validation middleware for Inngest.", + "version": "0.0.0", + "include": ["./src/**/*.ts"], + "exclude": ["**/*.test.*", "*.js", "**/tsconfig.*"], + "exports": { + ".": "./src/index.ts" + } +} diff --git a/packages/middleware-validation/package.json b/packages/middleware-validation/package.json new file mode 100644 index 00000000..ea46156a --- /dev/null +++ b/packages/middleware-validation/package.json @@ -0,0 +1,59 @@ +{ + "name": "@inngest/middleware-validation", + "version": "0.0.0", + "description": "Schema validation middleware for Inngest.", + "main": "./index.js", + "types": "./index.d.ts", + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "scripts": { + "test": "jest", + "build": "pnpm run build:clean && pnpm run build:tsc && pnpm run build:copy", + "build:clean": "rm -rf ./dist", + "build:tsc": "tsc --project tsconfig.build.json", + "build:copy": "cp package.json LICENSE.md README.md CHANGELOG.md dist", + "postversion": "pnpm run build", + "release:version": "node ../../scripts/release/jsrVersion.js", + "release": "cross-env DIST_DIR=dist node ../../scripts/release/publish.js && pnpm dlx jsr publish --allow-dirty", + "pack": "pnpm run build && yarn pack --verbose --frozen-lockfile --filename inngest-middleware-validation.tgz --cwd dist" + }, + "exports": { + ".": { + "require": "./index.js", + "import": "./index.js", + "types": "./index.d.ts" + } + }, + "keywords": [ + "inngest-middleware", + "inngest", + "middleware", + "validation" + ], + "homepage": "https://github.com/inngest/inngest-js/tree/main/packages/middleware-validation#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/inngest/inngest-js.git", + "directory": "packages/middleware-validation" + }, + "author": "Inngest Inc. ", + "license": "Apache-2.0", + "dependencies": { + "inngest": "^3.23.1", + "zod": "^3.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.7.0", + "@inngest/test": "0.1.1-pr-741.0", + "@types/eslint__js": "^8.42.3", + "@types/jest": "^29.5.14", + "eslint": "^8.30.0", + "fetch-mock-jest": "^1.5.1", + "jest": "^29.3.1", + "nock": "^13.2.9", + "ts-jest": "^29.1.0", + "typescript": "^5.6.3", + "typescript-eslint": "^7.16.1" + } +} diff --git a/packages/middleware-validation/pnpm-lock.yaml b/packages/middleware-validation/pnpm-lock.yaml new file mode 100644 index 00000000..6fba5bd8 --- /dev/null +++ b/packages/middleware-validation/pnpm-lock.yaml @@ -0,0 +1,327 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@inngest/test': + specifier: file:../test/inngest-test.tgz + version: file:../test/inngest-test.tgz + inngest: + specifier: file:../inngest/inngest.tgz + version: file:../inngest/inngest.tgz + +packages: + + '@inngest/test@file:../test/inngest-test.tgz': + resolution: {integrity: sha512-62KnW5692EADFbSuBMWG4jX0pTgPkk06LwZJThTW8HOlmdXCraxL+80hxXggB9dPOdbBLKmNcrzdhPPa1ErkMQ==, tarball: file:../test/inngest-test.tgz} + version: 0.1.0 + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + canonicalize@1.0.8: + resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inngest@3.25.1: + resolution: {integrity: sha512-d/0Fa9A3MeqSDh+N0cH628vi4uE2dQbrsTT684ilKMtIYNhWhkSh5xswQtCfOE8Wr2zxmmuQTfB+sdk6sDy+OA==} + engines: {node: '>=14'} + peerDependencies: + '@sveltejs/kit': '>=1.27.3' + '@vercel/node': '>=2.15.9' + aws-lambda: '>=1.0.7' + express: '>=4.19.2' + fastify: '>=4.21.0' + h3: '>=1.8.1' + hono: '>=4.2.7' + koa: '>=2.14.2' + next: '>=12.0.0' + typescript: '>=4.7.2' + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/node': + optional: true + aws-lambda: + optional: true + express: + optional: true + fastify: + optional: true + h3: + optional: true + hono: + optional: true + koa: + optional: true + next: + optional: true + typescript: + optional: true + + inngest@file:../inngest/inngest.tgz: + resolution: {integrity: sha512-+3lVubE9kGoICGk5FYwF8JAxth5HE8hEu2BhIMyijnbBbW+J+Gg2KToaPUIq83/0k0+FYfUr6rMZnsLZn4gBFg==, tarball: file:../inngest/inngest.tgz} + version: 3.25.1 + engines: {node: '>=14'} + peerDependencies: + '@sveltejs/kit': '>=1.27.3' + '@vercel/node': '>=2.15.9' + aws-lambda: '>=1.0.7' + express: '>=4.19.2' + fastify: '>=4.21.0' + h3: '>=1.8.1' + hono: '>=4.2.7' + koa: '>=2.14.2' + next: '>=12.0.0' + typescript: '>=4.7.2' + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/node': + optional: true + aws-lambda: + optional: true + express: + optional: true + fastify: + optional: true + h3: + optional: true + hono: + optional: true + koa: + optional: true + next: + optional: true + typescript: + optional: true + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + serialize-error-cjs@0.1.3: + resolution: {integrity: sha512-GXwbHkufrNZ87O7DUEvWhR8eBnOqiXtHsOXakkJliG7eLDmjh6gDlbJbMZFFbUx0J5sXKgwq4NFCs41dF5MhiA==} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ulid@2.3.0: + resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + zod@3.22.5: + resolution: {integrity: sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==} + +snapshots: + + '@inngest/test@file:../test/inngest-test.tgz': + dependencies: + inngest: 3.25.1 + tinyspy: 3.0.2 + ulid: 2.3.0 + transitivePeerDependencies: + - '@sveltejs/kit' + - '@vercel/node' + - aws-lambda + - encoding + - express + - fastify + - h3 + - hono + - koa + - next + - supports-color + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/ms@0.7.34': {} + + ansi-regex@4.1.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + canonicalize@1.0.8: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-fetch@4.0.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + has-flag@4.0.0: {} + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + inherits@2.0.4: {} + + inngest@3.25.1: + dependencies: + '@types/debug': 4.1.12 + canonicalize: 1.0.8 + chalk: 4.1.2 + cross-fetch: 4.0.0 + debug: 4.3.7 + hash.js: 1.1.7 + json-stringify-safe: 5.0.1 + ms: 2.1.3 + serialize-error-cjs: 0.1.3 + strip-ansi: 5.2.0 + zod: 3.22.5 + transitivePeerDependencies: + - encoding + - supports-color + + inngest@file:../inngest/inngest.tgz: + dependencies: + '@types/debug': 4.1.12 + canonicalize: 1.0.8 + chalk: 4.1.2 + cross-fetch: 4.0.0 + debug: 4.3.7 + hash.js: 1.1.7 + json-stringify-safe: 5.0.1 + ms: 2.1.3 + serialize-error-cjs: 0.1.3 + strip-ansi: 5.2.0 + zod: 3.22.5 + transitivePeerDependencies: + - encoding + - supports-color + + json-stringify-safe@5.0.1: {} + + minimalistic-assert@1.0.1: {} + + ms@2.1.3: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + serialize-error-cjs@0.1.3: {} + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyspy@3.0.2: {} + + tr46@0.0.3: {} + + ulid@2.3.0: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + zod@3.22.5: {} diff --git a/packages/middleware-validation/src/index.ts b/packages/middleware-validation/src/index.ts new file mode 100644 index 00000000..758c3c68 --- /dev/null +++ b/packages/middleware-validation/src/index.ts @@ -0,0 +1 @@ +export * from "./middleware"; diff --git a/packages/middleware-validation/src/middleware.test.ts b/packages/middleware-validation/src/middleware.test.ts new file mode 100644 index 00000000..a905a83b --- /dev/null +++ b/packages/middleware-validation/src/middleware.test.ts @@ -0,0 +1,506 @@ +import { InngestTestEngine } from "@inngest/test"; +import FetchMock from "fetch-mock-jest"; +import { EventSchemas, Inngest } from "inngest"; +import { Logger } from "inngest/middleware/logger"; +import { z } from "zod"; +import { validationMiddleware } from "./middleware"; + +const baseUrl = "https://unreachable.com"; +const eventKey = "123"; +const fetchMock = FetchMock.sandbox(); + +describe("validationMiddleware", () => { + test("should allow an event through with no schema", async () => { + const inngest = new Inngest({ + id: "test", + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test" }], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + + test("should allow an event through with a non-Zod schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromRecord<{ + test: { + data: { + message: string; + }; + }; + }>(), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test" }], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + + test("should validate a correct event with a Zod schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test", data: { message: "hello" } }], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + + test("should not allow an event through with an incorrect Zod schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test", data: { message: 123 } }], + }); + + const { result, error } = await t.execute(); + + expect(JSON.stringify(error)).toContain("failed validation"); + expect(result).toBeUndefined(); + }); + + describe("inngest/function.invoked", () => { + test("should test against multiple schemas for `inngest/function.invoked`", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + a: { + data: z.object({ + a: z.boolean(), + }), + }, + b: { + data: z.object({ + b: z.boolean(), + }), + }, + }), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "b" }, + () => "success" + ), + events: [{ name: "inngest/function.invoked", data: { b: true } }], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + }); + + describe("disallowSchemalessEvents", () => { + test("should fail if an event has no schema", async () => { + const inngest = new Inngest({ + id: "test", + middleware: [validationMiddleware({ disallowSchemalessEvents: true })], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test" }], + }); + + const { result, error } = await t.execute(); + + expect(JSON.stringify(error)).toContain("has no schema defined"); + expect(result).toBeUndefined(); + }); + + test("should fail if an event has a type-only schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromRecord<{ + test: { + data: { + message: string; + }; + }; + }>(), + middleware: [validationMiddleware({ disallowSchemalessEvents: true })], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test" }], + }); + + const { result, error } = await t.execute(); + + expect(JSON.stringify(error)).toContain("has no schema defined"); + expect(result).toBeUndefined(); + }); + + test("should succeed if an event has a schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [validationMiddleware({ disallowSchemalessEvents: true })], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test", data: { message: "hello" } }], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + + test("should succeed if an `inngest/function.invoked` event has a schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [validationMiddleware({ disallowSchemalessEvents: true })], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [ + { name: "inngest/function.invoked", data: { message: "hello" } }, + ], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + }); + + test("handles a literal Zod schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod([ + z.object({ + name: z.literal("test"), + data: z.object({ + message: z.string(), + }), + }), + ]), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test", data: { message: "hello" } }], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + + test("handles a nested Zod schema", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.object({ + content: z.string(), + }), + }), + }, + }), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [{ name: "test", data: { message: { content: "hello" } } }], + }); + + const { result, error } = await t.execute(); + + expect(error).toBeUndefined(); + expect(result).toEqual("success"); + }); + + test("validates all events in a batch", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => "success" + ), + events: [ + { name: "test", data: { message: "hello" } }, + { name: "test", data: { message: 123 } }, + ], + }); + + const { result, error } = await t.execute(); + + expect(JSON.stringify(error)).toContain("failed validation"); + expect(result).toBeUndefined(); + }); + + describe("onSendEvent", () => { + describe("inngest.send()", () => { + afterEach(() => { + fetchMock.mockReset(); + }); + + test("should validate an event before sending it", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [validationMiddleware()], + }); + + const t = new InngestTestEngine({ + function: inngest.createFunction( + { id: "test" }, + { event: "test" }, + () => + inngest.send({ + name: "test", + data: { message: 123 as unknown as string }, + }) + ), + events: [{ name: "test", data: { message: "hello" } }], + }); + + const { result, error } = await t.execute(); + + expect(JSON.stringify(error)).toContain("failed validation"); + expect(result).toBeUndefined(); + }); + + test("should not validate an event before sending it if disabled", async () => { + fetchMock.postOnce(`${baseUrl}/e/${eventKey}`, { + status: 200, + ids: ["123"], + }); + + const inngest = new Inngest({ + id: "test", + fetch: fetchMock as typeof fetch, + baseUrl, + eventKey, + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [ + validationMiddleware({ disableOutgoingValidation: true }), + ], + }); + + await expect( + inngest.send({ + name: "test", + data: { message: 123 as unknown as string }, + }) + ).resolves.not.toThrow(); + }); + }); + + describe("step.sendEvent()", () => { + afterEach(() => { + fetchMock.mockReset(); + }); + + test("should validate an event before sending it", async () => { + const inngest = new Inngest({ + id: "test", + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [validationMiddleware()], + logger: { error: () => undefined } as Logger, + }); + + const fn = inngest.createFunction( + { id: "test" }, + { event: "test" }, + async ({ step }) => { + await step.sendEvent("id", { + name: "test", + data: { message: 123 as unknown as string }, + }); + } + ); + + const t = new InngestTestEngine({ + function: fn, + events: [{ name: "test", data: { message: "hello" } }], + }); + + const { result, error } = await t.execute(); + + expect(JSON.stringify(error)).toContain("failed validation"); + expect(result).toBeUndefined(); + }); + + test("should not validate an event before sending it if disabled", async () => { + fetchMock.post(`${baseUrl}/e/${eventKey}`, { + status: 200, + ids: ["123"], + }); + + const inngest = new Inngest({ + id: "test", + fetch: fetchMock as typeof fetch, + baseUrl, + eventKey, + schemas: new EventSchemas().fromZod({ + test: { + data: z.object({ + message: z.string(), + }), + }, + }), + middleware: [ + validationMiddleware({ disableOutgoingValidation: true }), + ], + }); + + const fn = inngest.createFunction( + { id: "test" }, + { event: "test" }, + async ({ step }) => { + await step.sendEvent("id", { + name: "test", + data: { message: 123 as unknown as string }, + }); + } + ); + + const t = new InngestTestEngine({ + function: fn, + events: [{ name: "test", data: { message: "hello" } }], + }); + + await expect(t.execute()).resolves.not.toThrow(); + }); + }); + }); +}); diff --git a/packages/middleware-validation/src/middleware.ts b/packages/middleware-validation/src/middleware.ts new file mode 100644 index 00000000..0d76be67 --- /dev/null +++ b/packages/middleware-validation/src/middleware.ts @@ -0,0 +1,241 @@ +import { + InngestMiddleware, + internalEvents, + NonRetriableError, + type EventPayload, + type InngestFunction, + type MiddlewareOptions, +} from "inngest"; +import { ZodType, type ZodObject } from "zod"; + +/** + * Middleware that validates events using Zod schemas passed using + * `EventSchemas.fromZod()`. + */ +export const validationMiddleware = (opts?: { + /** + * Disallow events that don't have a schema defined. + * + * @default false + */ + disallowSchemalessEvents?: boolean; + + /** + * Disallow events that have a schema defined, but the schema is unknown and + * not handled in this code. + * + * @default false + */ + disallowUnknownSchemas?: boolean; + + /** + * Disable validation of incoming events. + * + * @default false + */ + disableIncomingValidation?: boolean; + + /** + * Disable validation of outgoing events using `inngest.send()` or + * `step.sendEvent()`. + * + * @default false + */ + disableOutgoingValidation?: boolean; +}): InngestMiddleware => { + const mw = new InngestMiddleware({ + name: "Inngest: Runtime schema validation", + init({ client }) { + /** + * Given an `event`, validate it against its schema. + */ + const validateEvent = async ( + event: EventPayload, + potentialInvokeEvents: string[] = [] + ): Promise => { + let schemasToAttempt = new Set([event.name]); + let hasSchema = false; + + /** + * Trust internal events; don't allow overwriting their typing. + */ + if (event.name.startsWith("inngest/")) { + if (event.name !== internalEvents.FunctionInvoked) { + return event; + } + + /** + * If this is an `inngest/function.invoked` event, try validating the + * payload against one of the function's schemas. + */ + schemasToAttempt = new Set(potentialInvokeEvents); + + hasSchema = [...schemasToAttempt.values()].some((schemaName) => { + return Boolean(client["schemas"]?.["runtimeSchemas"][schemaName]); + }); + } else { + hasSchema = Boolean( + client["schemas"]?.["runtimeSchemas"][event.name] + ); + } + + if (!hasSchema) { + if (opts?.disallowSchemalessEvents) { + throw new NonRetriableError( + `Event "${event.name}" has no schema defined; disallowing` + ); + } + + return event; + } + + const errors: Record = {}; + + for (const schemaName of schemasToAttempt) { + try { + const schema = client["schemas"]?.["runtimeSchemas"][schemaName]; + + /** + * The schema could be a full Zod object. + */ + if (helpers.isZodObject(schema)) { + const check = await schema.passthrough().safeParseAsync(event); + + if (check.success) { + return check.data as unknown as EventPayload; + } + + throw new NonRetriableError( + `${check.error.name}: ${check.error.message}` + ); + } + + /** + * The schema could also be a regular object with Zod objects + * inside. + */ + if (helpers.isObject(schema)) { + // It could be a partial schema; validate each field + return await Object.keys(schema).reduce>( + async (acc, key) => { + const fieldSchema = schema[key]; + const eventField = event[key as keyof EventPayload]; + + if (!helpers.isZodObject(fieldSchema) || !eventField) { + return acc; + } + + const check = await fieldSchema + .passthrough() + .safeParseAsync(eventField); + + if (check.success) { + return { ...(await acc), [key]: check.data }; + } + + throw new NonRetriableError( + `${check.error.name}: ${check.error.message}` + ); + }, + Promise.resolve({ ...event }) + ); + } + + /** + * Didn't find anything? Throw or warn. + * + * We only allow this for assessing single schemas, as otherwise + * we're assessing an invocation would could be multiple. + */ + if (opts?.disallowUnknownSchemas && schemasToAttempt.size === 1) { + throw new NonRetriableError( + `Event "${event.name}" has an unknown schema; disallowing` + ); + } else { + console.warn( + "Unknown schema found; cannot validate, but allowing" + ); + } + } catch (err) { + errors[schemaName] = err as Error; + } + } + + if (Object.keys(errors).length) { + throw new NonRetriableError( + `Event "${event.name}" failed validation:\n\n${Object.keys(errors) + .map((key) => `Using ${key}: ${errors[key].message}`) + .join("\n\n")}` + ); + } + + return event; + }; + + return { + ...(opts?.disableIncomingValidation + ? {} + : { + async onFunctionRun({ fn }) { + const backupEvents = ( + (fn.opts as InngestFunction.Options).triggers || [] + ).reduce((acc, trigger) => { + if (trigger.event) { + return [...acc, trigger.event]; + } + + return acc; + }, []); + + return { + async transformInput({ ctx: { events } }) { + const validatedEvents = await Promise.all( + events.map((event) => { + return validateEvent(event, backupEvents); + }) + ); + + return { + ctx: { + event: validatedEvents[0], + events: validatedEvents, + } as {}, + }; + }, + }; + }, + }), + + ...(opts?.disableOutgoingValidation + ? {} + : { + async onSendEvent() { + return { + async transformInput({ payloads }) { + return { + payloads: await Promise.all( + payloads.map((payload) => { + return validateEvent(payload); + }) + ), + }; + }, + }; + }, + }), + }; + }, + }); + + return mw; +}; + +const helpers = { + isZodObject: (value: unknown): value is ZodObject => { + return value instanceof ZodType && value._def.typeName === "ZodObject"; + }, + + isObject: (value: unknown): value is Record => { + return typeof value === "object" && value !== null && !Array.isArray(value); + }, +}; diff --git a/packages/middleware-validation/tsconfig.build.json b/packages/middleware-validation/tsconfig.build.json new file mode 100644 index 00000000..f19b0916 --- /dev/null +++ b/packages/middleware-validation/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/middleware-validation/tsconfig.json b/packages/middleware-validation/tsconfig.json new file mode 100644 index 00000000..64549502 --- /dev/null +++ b/packages/middleware-validation/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2016", + "module": "commonjs", + "rootDir": "./src", + "declaration": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["./src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69fdbe3d..f8ca0966 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,6 +300,49 @@ importers: specifier: ~5.4.0 version: 5.4.2 + packages/middleware-validation: + dependencies: + inngest: + specifier: ^3.23.1 + version: 3.25.1(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.6.3) + zod: + specifier: ^3.0.0 + version: 3.22.3 + devDependencies: + '@eslint/js': + specifier: ^9.7.0 + version: 9.7.0 + '@inngest/test': + specifier: 0.1.1-pr-741.0 + version: 0.1.1-pr-741.0(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.6.3) + '@types/eslint__js': + specifier: ^8.42.3 + version: 8.42.3 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + eslint: + specifier: ^8.30.0 + version: 8.53.0 + fetch-mock-jest: + specifier: ^1.5.1 + version: 1.5.1(node-fetch@2.7.0) + jest: + specifier: ^29.3.1 + version: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)) + nock: + specifier: ^13.2.9 + version: 13.2.9 + ts-jest: + specifier: ^29.1.0 + version: 29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)))(typescript@5.6.3) + typescript: + specifier: ^5.6.3 + version: 5.6.3 + typescript-eslint: + specifier: ^7.16.1 + version: 7.16.1(eslint@8.53.0)(typescript@5.6.3) + packages/test: dependencies: inngest: @@ -960,6 +1003,9 @@ packages: resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} deprecated: Use @eslint/object-schema instead + '@inngest/test@0.1.1-pr-741.0': + resolution: {integrity: sha512-qqgGcxjxdFOHeJzfNhuAOVMTd7WQNQ62TB74/WVR4Ul5DlmDFpaC/A7jZp+fWAyD2QIJYl6qFcvevOLKY1I+wQ==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1362,6 +1408,9 @@ packages: '@types/jest@29.5.12': resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1880,9 +1929,6 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001546: - resolution: {integrity: sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==} - caniuse-lite@1.0.30001571: resolution: {integrity: sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==} @@ -3064,6 +3110,42 @@ packages: typescript: optional: true + inngest@3.25.1: + resolution: {integrity: sha512-d/0Fa9A3MeqSDh+N0cH628vi4uE2dQbrsTT684ilKMtIYNhWhkSh5xswQtCfOE8Wr2zxmmuQTfB+sdk6sDy+OA==} + engines: {node: '>=14'} + peerDependencies: + '@sveltejs/kit': '>=1.27.3' + '@vercel/node': '>=2.15.9' + aws-lambda: '>=1.0.7' + express: '>=4.19.2' + fastify: '>=4.21.0' + h3: '>=1.8.1' + hono: '>=4.2.7' + koa: '>=2.14.2' + next: '>=12.0.0' + typescript: '>=4.7.2' + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/node': + optional: true + aws-lambda: + optional: true + express: + optional: true + fastify: + optional: true + h3: + optional: true + hono: + optional: true + koa: + optional: true + next: + optional: true + typescript: + optional: true + inquirer@9.2.10: resolution: {integrity: sha512-tVVNFIXU8qNHoULiazz612GFl+yqNfjMTbLuViNJE/d860Qxrd3NMrse8dm40VUQLOQeULvaQF8lpAhvysjeyA==} engines: {node: '>=14.18.0'} @@ -3573,10 +3655,6 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - magic-string@0.30.5: - resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} - engines: {node: '>=12'} - make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -4373,10 +4451,6 @@ packages: sonic-boom@3.3.0: resolution: {integrity: sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==} - source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4748,6 +4822,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.7.2: resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} @@ -5665,6 +5744,25 @@ snapshots: '@humanwhocodes/object-schema@2.0.1': {} + '@inngest/test@0.1.1-pr-741.0(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.6.3)': + dependencies: + inngest: 3.25.1(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.6.3) + tinyspy: 3.0.2 + ulid: 2.3.0 + transitivePeerDependencies: + - '@sveltejs/kit' + - '@vercel/node' + - aws-lambda + - encoding + - express + - fastify + - h3 + - hono + - koa + - next + - supports-color + - typescript + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5727,6 +5825,40 @@ snapshots: - supports-color - ts-node + '@jest/core@29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3))': + dependencies: + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 20.14.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.5.0 + jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)) + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 + micromatch: 4.0.5 + pretty-format: 29.5.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + - ts-node + '@jest/core@29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2))': dependencies: '@jest/console': 29.5.0 @@ -5915,8 +6047,8 @@ snapshots: '@jridgewell/trace-mapping@0.3.9': dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 '@ljharb/through@2.3.9': {} @@ -6081,7 +6213,7 @@ snapshots: devalue: 4.3.2 esm-env: 1.0.0 kleur: 4.1.5 - magic-string: 0.30.5 + magic-string: 0.30.11 mrmime: 1.0.1 sade: 1.8.1 set-cookie-parser: 2.6.0 @@ -6108,7 +6240,7 @@ snapshots: debug: 4.3.4 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.5 + magic-string: 0.30.11 svelte: 4.2.5 svelte-hmr: 0.15.3(svelte@4.2.5) vite: 4.5.3(@types/node@20.14.8) @@ -6252,6 +6384,11 @@ snapshots: expect: 29.5.0 pretty-format: 29.5.0 + '@types/jest@29.5.14': + dependencies: + expect: 29.5.0 + pretty-format: 29.5.0 + '@types/json-schema@7.0.15': {} '@types/json-stringify-safe@5.0.3': {} @@ -6374,6 +6511,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.53.0)(typescript@5.6.3))(eslint@8.53.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.16.1(eslint@8.53.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 7.16.1 + '@typescript-eslint/type-utils': 7.16.1(eslint@8.53.0)(typescript@5.6.3) + '@typescript-eslint/utils': 7.16.1(eslint@8.53.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 7.16.1 + eslint: 8.53.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@6.11.0(eslint@8.36.0)(typescript@5.7.2)': dependencies: '@typescript-eslint/scope-manager': 6.11.0 @@ -6400,6 +6555,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@7.16.1(eslint@8.53.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.16.1 + '@typescript-eslint/types': 7.16.1 + '@typescript-eslint/typescript-estree': 7.16.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 7.16.1 + debug: 4.3.4 + eslint: 8.53.0 + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/rule-tester@6.11.0(@eslint/eslintrc@2.1.3)(eslint@8.53.0)(typescript@5.5.2)': dependencies: '@eslint/eslintrc': 2.1.3 @@ -6447,6 +6615,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@7.16.1(eslint@8.53.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.16.1(typescript@5.6.3) + '@typescript-eslint/utils': 7.16.1(eslint@8.53.0)(typescript@5.6.3) + debug: 4.3.4 + eslint: 8.53.0 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@6.11.0': {} '@typescript-eslint/types@7.16.1': {} @@ -6494,6 +6674,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@7.16.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 7.16.1 + '@typescript-eslint/visitor-keys': 7.16.1 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@6.11.0(eslint@8.36.0)(typescript@5.7.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.36.0) @@ -6533,6 +6728,17 @@ snapshots: - supports-color - typescript + '@typescript-eslint/utils@7.16.1(eslint@8.53.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@typescript-eslint/scope-manager': 7.16.1 + '@typescript-eslint/types': 7.16.1 + '@typescript-eslint/typescript-estree': 7.16.1(typescript@5.6.3) + eslint: 8.53.0 + transitivePeerDependencies: + - supports-color + - typescript + '@typescript-eslint/visitor-keys@6.11.0': dependencies: '@typescript-eslint/types': 6.11.0 @@ -6920,8 +7126,6 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001546: {} - caniuse-lite@1.0.30001571: {} canonicalize@1.0.8: {} @@ -8213,6 +8417,34 @@ snapshots: - encoding - supports-color + inngest@3.25.1(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.6.3): + dependencies: + '@types/debug': 4.1.12 + canonicalize: 1.0.8 + chalk: 4.1.2 + cross-fetch: 4.0.0 + debug: 4.3.4 + hash.js: 1.1.7 + json-stringify-safe: 5.0.1 + ms: 2.1.3 + serialize-error-cjs: 0.1.3 + strip-ansi: 5.2.0 + zod: 3.22.3 + optionalDependencies: + '@sveltejs/kit': 1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)) + '@vercel/node': 2.15.9 + aws-lambda: 1.0.7 + express: 4.19.2 + fastify: 4.21.0 + h3: 1.8.1 + hono: 4.2.7 + koa: 2.14.2 + next: 13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + typescript: 5.6.3 + transitivePeerDependencies: + - encoding + - supports-color + inquirer@9.2.10: dependencies: '@ljharb/through': 2.3.9 @@ -8468,6 +8700,25 @@ snapshots: - supports-color - ts-node + jest-cli@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)): + dependencies: + '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)) + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.1.0 + jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)) + jest-util: 29.5.0 + jest-validate: 29.5.0 + prompts: 2.4.2 + yargs: 17.7.1 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + jest-cli@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2)): dependencies: '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2)) @@ -8517,6 +8768,36 @@ snapshots: transitivePeerDependencies: - supports-color + jest-config@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)): + dependencies: + '@babel/core': 7.23.6 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 + babel-jest: 29.5.0(@babel/core@7.23.6) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.5.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.14.8 + ts-node: 10.9.1(@types/node@20.14.8)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color + jest-config@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2)): dependencies: '@babel/core': 7.23.6 @@ -8799,6 +9080,17 @@ snapshots: - supports-color - ts-node + jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)): + dependencies: + '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)) + '@jest/types': 29.5.0 + import-local: 3.1.0 + jest-cli: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)) + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2)): dependencies: '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2)) @@ -8975,10 +9267,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.5: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -9093,7 +9381,7 @@ snapshots: '@next/env': 13.5.4 '@swc/helpers': 0.5.2 busboy: 1.6.0 - caniuse-lite: 1.0.30001546 + caniuse-lite: 1.0.30001571 postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -9377,8 +9665,8 @@ snapshots: postcss@8.4.31: dependencies: nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.0.2 + picocolors: 1.1.0 + source-map-js: 1.2.1 postcss@8.4.45: dependencies: @@ -9745,8 +10033,6 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - source-map-js@1.0.2: {} - source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -9976,6 +10262,10 @@ snapshots: dependencies: typescript: 5.5.2 + ts-api-utils@1.3.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + ts-jest@29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)))(typescript@5.5.2): dependencies: bs-logger: 0.2.6 @@ -9993,6 +10283,23 @@ snapshots: '@jest/types': 29.5.0 babel-jest: 29.5.0(@babel/core@7.23.6) + ts-jest@29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)))(typescript@5.6.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3)) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 5.6.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.23.6 + '@jest/types': 29.5.0 + babel-jest: 29.5.0(@babel/core@7.23.6) + ts-jest@29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2)))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 @@ -10023,7 +10330,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 14.18.33 - acorn: 8.11.2 + acorn: 8.12.1 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 @@ -10041,7 +10348,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 20.14.8 - acorn: 8.11.2 + acorn: 8.12.1 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 @@ -10052,6 +10359,25 @@ snapshots: yn: 3.1.1 optional: true + ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.14.8 + acorn: 8.12.1 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-node@10.9.1(@types/node@20.14.8)(typescript@5.7.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -10060,7 +10386,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 20.14.8 - acorn: 8.11.2 + acorn: 8.12.1 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 @@ -10144,12 +10470,25 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@7.16.1(eslint@8.53.0)(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.53.0)(typescript@5.6.3))(eslint@8.53.0)(typescript@5.6.3) + '@typescript-eslint/parser': 7.16.1(eslint@8.53.0)(typescript@5.6.3) + '@typescript-eslint/utils': 7.16.1(eslint@8.53.0)(typescript@5.6.3) + eslint: 8.53.0 + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + typescript@4.9.5: {} typescript@5.4.2: {} typescript@5.5.2: {} + typescript@5.6.3: {} + typescript@5.7.2: {} ufo@1.3.0: {}