From bc5c163d2da56e11f6bde045c231ea9415d636e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 23 Apr 2025 15:44:20 +0200 Subject: [PATCH 1/5] Wrap tool parameters of AI SDK --- library/agent/Context.ts | 1 + library/package-lock.json | 258 +++++++++++++++++++++++- library/package.json | 5 +- library/sources/AiSDK.test.ts | 89 ++++++++ library/sources/AiSDK.ts | 42 ++++ library/sources/ai/wrapToolExecution.ts | 31 +++ 6 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 library/sources/AiSDK.test.ts create mode 100644 library/sources/AiSDK.ts create mode 100644 library/sources/ai/wrapToolExecution.ts diff --git a/library/agent/Context.ts b/library/agent/Context.ts index f324feb8d..ca8cb8dbd 100644 --- a/library/agent/Context.ts +++ b/library/agent/Context.ts @@ -30,6 +30,7 @@ export type Context = { */ outgoingRequestRedirects?: { source: URL; destination: URL }[]; executedMiddleware?: boolean; + aiToolParams?: unknown[]; // Parameters send to functions/tools that are called by a LLM }; /** diff --git a/library/package-lock.json b/library/package-lock.json index de866d599..82d02998b 100644 --- a/library/package-lock.json +++ b/library/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "AGPL-3.0-or-later", "devDependencies": { + "@ai-sdk/google": "^1.2.13", "@clickhouse/client": "^1.7.0", "@eslint/js": "^9.23.0", "@fastify/cookie": "^10.0.0", @@ -37,6 +38,7 @@ "@types/sinonjs__fake-timers": "^8.1.5", "@types/supertest": "^6.0.2", "@types/xml2js": "^0.4.14", + "ai": "^4.3.9", "aws-sdk": "^2.1595.0", "axios": "^1.8.4", "better-sqlite3": "^11.2.0", @@ -86,12 +88,111 @@ "undici-v6": "npm:undici@^6.0.0", "undici-v7": "npm:undici@^7.0.0", "xml-js": "^1.6.11", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "zod": "^3.24.3" }, "engines": { "node": ">=16" } }, + "node_modules/@ai-sdk/google": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.13.tgz", + "integrity": "sha512-nnHDzbX1Zst28AjP3718xSWsEqx++qmFuqmnDc2Htelc02HyO6WkWOXMH+YVK3W8zdIyZEKpHL9KKlql7pa10A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.7.tgz", + "integrity": "sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/provider-utils/node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@ai-sdk/react": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.9.tgz", + "integrity": "sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "2.2.7", + "@ai-sdk/ui-utils": "1.2.8", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/ui-utils": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.8.tgz", + "integrity": "sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", @@ -5381,6 +5482,13 @@ "@types/node": "*" } }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -6176,6 +6284,33 @@ "node": ">=8" } }, + "node_modules/ai": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.9.tgz", + "integrity": "sha512-P2RpV65sWIPdUlA4f1pcJ11pB0N1YmqPVLEmC4j8WuBwKY0L3q9vGhYPh0Iv+spKHKyn0wUbMfas+7Z6nTfS0g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7", + "@ai-sdk/react": "1.2.9", + "@ai-sdk/ui-utils": "1.2.8", + "@opentelemetry/api": "1.9.0", + "jsondiffpatch": "0.6.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -7735,6 +7870,13 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -11556,6 +11698,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-ref-resolver": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", @@ -11603,6 +11752,37 @@ "json5": "lib/cli.js" } }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/jsondiffpatch/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -13029,6 +13209,25 @@ "node": ">=12" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -16844,6 +17043,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz", + "integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/sync-content": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-1.0.2.tgz", @@ -17251,6 +17464,19 @@ "real-require": "^0.2.0" } }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -18121,6 +18347,16 @@ "querystring": "0.2.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -18749,6 +18985,26 @@ "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", "dev": true, "license": "MIT" + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/library/package.json b/library/package.json index eb257da5d..339cfc311 100644 --- a/library/package.json +++ b/library/package.json @@ -42,6 +42,7 @@ }, "sideEffects": true, "devDependencies": { + "@ai-sdk/google": "^1.2.13", "@clickhouse/client": "^1.7.0", "@eslint/js": "^9.23.0", "@fastify/cookie": "^10.0.0", @@ -70,6 +71,7 @@ "@types/sinonjs__fake-timers": "^8.1.5", "@types/supertest": "^6.0.2", "@types/xml2js": "^0.4.14", + "ai": "^4.3.9", "aws-sdk": "^2.1595.0", "axios": "^1.8.4", "better-sqlite3": "^11.2.0", @@ -119,7 +121,8 @@ "undici-v6": "npm:undici@^6.0.0", "undici-v7": "npm:undici@^7.0.0", "xml-js": "^1.6.11", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "zod": "^3.24.3" }, "scripts": { "test": "node ../scripts/run-tap.js", diff --git a/library/sources/AiSDK.test.ts b/library/sources/AiSDK.test.ts new file mode 100644 index 000000000..373a0f476 --- /dev/null +++ b/library/sources/AiSDK.test.ts @@ -0,0 +1,89 @@ +import * as t from "tap"; +import { startTestAgent } from "../helpers/startTestAgent"; +import { AiSDK } from "./AiSDK"; +import { getContext, runWithContext, type Context } from "../agent/Context"; + +t.test("It works with agentic", async (t) => { + startTestAgent({ + wrappers: [new AiSDK()], + rewrite: {}, + }); + + const getTestContext = (message: string): Context => { + return { + remoteAddress: "::1", + method: "POST", + url: "http://localhost:4000", + query: { + message, + }, + body: undefined, + headers: {}, + cookies: {}, + routeParams: {}, + source: "express", + route: "/posts/:id", + }; + }; + + const { google } = + require("@ai-sdk/google") as typeof import("@ai-sdk/google"); + const { generateText, tool } = require("ai") as typeof import("ai"); + const { z } = require("zod") as typeof import("zod"); + + const callWithPrompt = async (prompt: string) => { + return await generateText({ + model: google("models/gemini-2.0-flash"), + tools: { + weather: tool({ + description: "Get the weather in a location", + parameters: z.object({ + location: z + .string() + .describe("The location to get the weather for"), + }), + execute: async ({ location }) => { + const temperature = location === "Norway" ? 5 : 24; + return { + temperature, + context: getContext(), + }; + }, + }), + }, + prompt: prompt, + }); + }; + + await runWithContext( + getTestContext("What is the weather in San Francisco?"), + async () => { + const result = await callWithPrompt( + "What is the weather in San Francisco?" + ); + + t.same(result.toolResults.length, 1); + t.same(result.toolResults[0].toolName, "weather"); + t.same(result.toolResults[0].result.temperature, 24); + t.match(result.toolResults[0].result.context, { + remoteAddress: "::1", + method: "POST", + url: "http://localhost:4000", + query: { + message: "What is the weather in San Francisco?", + }, + body: undefined, + headers: {}, + cookies: {}, + routeParams: {}, + source: "express", + route: "/posts/:id", + aiToolParams: [ + { + location: "San Francisco", + }, + ], + }); + } + ); +}); diff --git a/library/sources/AiSDK.ts b/library/sources/AiSDK.ts new file mode 100644 index 000000000..fc7c7c3df --- /dev/null +++ b/library/sources/AiSDK.ts @@ -0,0 +1,42 @@ +import type { Tool } from "ai"; +import type { Hooks } from "../agent/hooks/Hooks"; +import { wrapExport } from "../agent/hooks/wrapExport"; +import type { Wrapper } from "../agent/Wrapper"; +import { isPlainObject } from "../helpers/isPlainObject"; +import { wrapToolExecution } from "./ai/wrapToolExecution"; + +export class AiSDK implements Wrapper { + private wrapToolExecution(args: unknown[]) { + if (!args || args.length === 0) { + return args; + } + + if (!isPlainObject(args[0]) || typeof args[0].execute !== "function") { + return args; + } + + const toolObject = args[0] as Tool; + + toolObject.execute = wrapToolExecution(toolObject.execute); + + return args; + } + + wrap(hooks: Hooks) { + hooks + .addPackage("ai") + .withVersion("^4.0.0") + .onRequire((exports, pkgInfo) => { + const toolFunc = exports.tool; // It's a getter so we can't directly pass it to wrapExport + + const wrappedToolFunc = wrapExport(toolFunc, undefined, pkgInfo, { + modifyArgs: (args) => this.wrapToolExecution(args), + }); + + return { + ...exports, + tool: wrappedToolFunc, + }; + }); + } +} diff --git a/library/sources/ai/wrapToolExecution.ts b/library/sources/ai/wrapToolExecution.ts new file mode 100644 index 000000000..9a3674b71 --- /dev/null +++ b/library/sources/ai/wrapToolExecution.ts @@ -0,0 +1,31 @@ +import { getContext, updateContext } from "../../agent/Context"; +import type { Tool } from "ai"; +import { isPlainObject } from "../../helpers/isPlainObject"; + +export function wrapToolExecution(handler: Tool["execute"]): Tool["execute"] { + return async function execute() { + if (!handler || typeof handler !== "function") { + return; + } + // eslint-disable-next-line prefer-rest-params + const args = Array.from(arguments); + + if (args && args.length > 0 && isPlainObject(args[0])) { + const context = getContext(); + if (context) { + if (Array.isArray(context.xml)) { + updateContext(context, "aiToolParams", context.xml.concat(args[0])); + } else { + updateContext(context, "aiToolParams", [args[0]]); + } + } + } + + return await handler.apply( + // @ts-expect-error We don't now the type of `this` here + this, + // @ts-expect-error We don't now the passed args + args + ); + }; +} From 47b717c5b7c12d25fe1202e7418050a75322f196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 23 Apr 2025 16:26:49 +0200 Subject: [PATCH 2/5] Update readme and sources, fix tests --- .github/workflows/unit-test.yml | 2 + README.md | 2 + library/agent/Source.ts | 1 + library/agent/protect.ts | 2 + library/sources/AiSDK.test.ts | 150 +++++++++++++++++--------------- 5 files changed, 87 insertions(+), 70 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 911a67dfa..1719782a5 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -80,6 +80,8 @@ jobs: - run: npm run install-lib-only - run: npm run build - run: npm run test:ci + env: + GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} - name: "Upload coverage" uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5 with: diff --git a/README.md b/README.md index b931c08fd..f0b31191c 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ See list above for supported database drivers. * ✅ [`@koa/router`](https://www.npmjs.com/package/@koa/router) 13.x, 12.x, 11.x and 10.x +### AI SDKs +* ✅ [`ai`](https://www.npmjs.com/package/ai) 4.x ## Installation diff --git a/library/agent/Source.ts b/library/agent/Source.ts index 2c0148864..622f96e83 100644 --- a/library/agent/Source.ts +++ b/library/agent/Source.ts @@ -9,6 +9,7 @@ export const SOURCES = [ "subdomains", "markUnsafe", "url", + "aiToolParams", ] as const; export type Source = (typeof SOURCES)[number]; diff --git a/library/agent/protect.ts b/library/agent/protect.ts index a84259695..516cd8005 100644 --- a/library/agent/protect.ts +++ b/library/agent/protect.ts @@ -48,6 +48,7 @@ import { Fastify } from "../sources/Fastify"; import { Koa } from "../sources/Koa"; import { ClickHouse } from "../sinks/ClickHouse"; import { Prisma } from "../sinks/Prisma"; +import { AiSDK } from "../sources/AiSDK"; function getLogger(): Logger { if (isDebugging()) { @@ -141,6 +142,7 @@ export function getWrappers() { new ClickHouse(), new Prisma(), // new Function(), Disabled because functionName.constructor === Function is false after patching global + new AiSDK(), ]; } diff --git a/library/sources/AiSDK.test.ts b/library/sources/AiSDK.test.ts index 373a0f476..c8eba38a9 100644 --- a/library/sources/AiSDK.test.ts +++ b/library/sources/AiSDK.test.ts @@ -2,75 +2,29 @@ import * as t from "tap"; import { startTestAgent } from "../helpers/startTestAgent"; import { AiSDK } from "./AiSDK"; import { getContext, runWithContext, type Context } from "../agent/Context"; +import { getMajorNodeVersion } from "../helpers/getNodeVersion"; -t.test("It works with agentic", async (t) => { - startTestAgent({ - wrappers: [new AiSDK()], - rewrite: {}, - }); - - const getTestContext = (message: string): Context => { - return { - remoteAddress: "::1", - method: "POST", - url: "http://localhost:4000", - query: { - message, - }, - body: undefined, - headers: {}, - cookies: {}, - routeParams: {}, - source: "express", - route: "/posts/:id", - }; - }; - - const { google } = - require("@ai-sdk/google") as typeof import("@ai-sdk/google"); - const { generateText, tool } = require("ai") as typeof import("ai"); - const { z } = require("zod") as typeof import("zod"); - - const callWithPrompt = async (prompt: string) => { - return await generateText({ - model: google("models/gemini-2.0-flash"), - tools: { - weather: tool({ - description: "Get the weather in a location", - parameters: z.object({ - location: z - .string() - .describe("The location to get the weather for"), - }), - execute: async ({ location }) => { - const temperature = location === "Norway" ? 5 : 24; - return { - temperature, - context: getContext(), - }; - }, - }), - }, - prompt: prompt, +t.test( + "It works with AI tool execution", + { + skip: + !process.env.GOOGLE_GENERATIVE_AI_API_KEY || getMajorNodeVersion() < 20 + ? "Google API key not set or Node version < 20" + : undefined, + }, + async (t) => { + startTestAgent({ + wrappers: [new AiSDK()], + rewrite: {}, }); - }; - - await runWithContext( - getTestContext("What is the weather in San Francisco?"), - async () => { - const result = await callWithPrompt( - "What is the weather in San Francisco?" - ); - t.same(result.toolResults.length, 1); - t.same(result.toolResults[0].toolName, "weather"); - t.same(result.toolResults[0].result.temperature, 24); - t.match(result.toolResults[0].result.context, { + const getTestContext = (message: string): Context => { + return { remoteAddress: "::1", method: "POST", url: "http://localhost:4000", query: { - message: "What is the weather in San Francisco?", + message, }, body: undefined, headers: {}, @@ -78,12 +32,68 @@ t.test("It works with agentic", async (t) => { routeParams: {}, source: "express", route: "/posts/:id", - aiToolParams: [ - { - location: "San Francisco", - }, - ], + }; + }; + + const { google } = + require("@ai-sdk/google") as typeof import("@ai-sdk/google"); + const { generateText, tool } = require("ai") as typeof import("ai"); + const { z } = require("zod") as typeof import("zod"); + + const callWithPrompt = async (prompt: string) => { + return await generateText({ + model: google("models/gemini-2.0-flash-lite"), + tools: { + weather: tool({ + description: "Get the weather in a location", + parameters: z.object({ + location: z + .string() + .describe("The location to get the weather for"), + }), + execute: async ({ location }) => { + const temperature = location === "Norway" ? 5 : 24; + return { + temperature, + context: getContext(), + }; + }, + }), + }, + prompt: prompt, }); - } - ); -}); + }; + + await runWithContext( + getTestContext("What is the weather in San Francisco?"), + async () => { + const result = await callWithPrompt( + "What is the weather in San Francisco?" + ); + + t.same(result.toolResults.length, 1); + t.same(result.toolResults[0].toolName, "weather"); + t.same(result.toolResults[0].result.temperature, 24); + t.match(result.toolResults[0].result.context, { + remoteAddress: "::1", + method: "POST", + url: "http://localhost:4000", + query: { + message: "What is the weather in San Francisco?", + }, + body: undefined, + headers: {}, + cookies: {}, + routeParams: {}, + source: "express", + route: "/posts/:id", + aiToolParams: [ + { + location: "San Francisco", + }, + ], + }); + } + ); + } +); From 80d5b98107872093d0316f4ba20eec6cb209548a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 23 Apr 2025 17:44:48 +0200 Subject: [PATCH 3/5] Small fixes --- library/sources/AiSDK.ts | 15 +++++++++------ library/sources/ai/wrapToolExecution.ts | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/library/sources/AiSDK.ts b/library/sources/AiSDK.ts index fc7c7c3df..c28df04ad 100644 --- a/library/sources/AiSDK.ts +++ b/library/sources/AiSDK.ts @@ -7,15 +7,18 @@ import { wrapToolExecution } from "./ai/wrapToolExecution"; export class AiSDK implements Wrapper { private wrapToolExecution(args: unknown[]) { - if (!args || args.length === 0) { + if ( + !args || + args.length === 0 || + !isPlainObject(args[0]) || + typeof args[0].execute !== "function" + ) { return args; } - if (!isPlainObject(args[0]) || typeof args[0].execute !== "function") { - return args; - } - - const toolObject = args[0] as Tool; + const toolObject = args[0] as Tool & { + execute: NonNullable; + }; toolObject.execute = wrapToolExecution(toolObject.execute); diff --git a/library/sources/ai/wrapToolExecution.ts b/library/sources/ai/wrapToolExecution.ts index 9a3674b71..9d3d57a82 100644 --- a/library/sources/ai/wrapToolExecution.ts +++ b/library/sources/ai/wrapToolExecution.ts @@ -2,19 +2,22 @@ import { getContext, updateContext } from "../../agent/Context"; import type { Tool } from "ai"; import { isPlainObject } from "../../helpers/isPlainObject"; -export function wrapToolExecution(handler: Tool["execute"]): Tool["execute"] { +export function wrapToolExecution( + handler: NonNullable +): NonNullable { return async function execute() { - if (!handler || typeof handler !== "function") { - return; - } // eslint-disable-next-line prefer-rest-params const args = Array.from(arguments); if (args && args.length > 0 && isPlainObject(args[0])) { const context = getContext(); if (context) { - if (Array.isArray(context.xml)) { - updateContext(context, "aiToolParams", context.xml.concat(args[0])); + if (Array.isArray(context.aiToolParams)) { + updateContext( + context, + "aiToolParams", + context.aiToolParams.concat(args[0]) + ); } else { updateContext(context, "aiToolParams", [args[0]]); } From 33c541f964f7e1fabb6da6c8e33d9472ec30cc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Thu, 24 Apr 2025 10:00:36 +0200 Subject: [PATCH 4/5] Add e2e test --- .github/workflows/end-to-end-tests.yml | 2 + end2end/tests/hono-sqlite-ai.test.js | 114 +++ sample-apps/hono-sqlite-ai/README.md | 7 + sample-apps/hono-sqlite-ai/app.js | 76 ++ sample-apps/hono-sqlite-ai/db.js | 31 + sample-apps/hono-sqlite-ai/package-lock.json | 762 +++++++++++++++++++ sample-apps/hono-sqlite-ai/package.json | 11 + 7 files changed, 1003 insertions(+) create mode 100644 end2end/tests/hono-sqlite-ai.test.js create mode 100644 sample-apps/hono-sqlite-ai/README.md create mode 100644 sample-apps/hono-sqlite-ai/app.js create mode 100644 sample-apps/hono-sqlite-ai/db.js create mode 100644 sample-apps/hono-sqlite-ai/package-lock.json create mode 100644 sample-apps/hono-sqlite-ai/package.json diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index 649a2187f..cafe9fc93 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -66,3 +66,5 @@ jobs: - run: npm install - run: npm run build - run: npm run end2end + env: + GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} diff --git a/end2end/tests/hono-sqlite-ai.test.js b/end2end/tests/hono-sqlite-ai.test.js new file mode 100644 index 000000000..dface205c --- /dev/null +++ b/end2end/tests/hono-sqlite-ai.test.js @@ -0,0 +1,114 @@ +const t = require("tap"); +const { spawn } = require("child_process"); +const { resolve } = require("path"); +const timeout = require("../timeout"); + +const pathToApp = resolve( + __dirname, + "../../sample-apps/hono-sqlite-ai", + "app.js" +); + +t.test("it blocks in blocking mode", (t) => { + const server = spawn(`node`, [pathToApp, "4006"], { + env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCK: "true" }, + }); + + server.on("close", () => { + t.end(); + }); + + server.on("error", (err) => { + t.fail(err.message); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + timeout(2000) + .then(() => { + return Promise.all([ + fetch( + `http://127.0.0.1:4006/weather?prompt=${encodeURIComponent('What is the weather in "Ghent\'; DELETE FROM weather; --" like?')}`, + { + signal: AbortSignal.timeout(5000), + } + ), + ]); + }) + .then(async ([sqlInjection]) => { + t.equal(sqlInjection.status, 500); + + const response = await sqlInjection.json(); + t.equal( + response.error, + "Error executing tool weather: Zen has blocked an SQL injection: better-sqlite3.prepare(...) originating from aiToolParams.[0].location" + ); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); + +t.test("it does not block in monitoring mode", (t) => { + const server = spawn(`node`, [pathToApp, "4007"], { + env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCK: "false" }, + }); + + server.on("close", () => { + t.end(); + }); + + server.on("error", (err) => { + t.fail(err.message); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + timeout(2000) + .then(() => { + return Promise.all([ + fetch( + `http://127.0.0.1:4007/weather?prompt=${encodeURIComponent('What is the weather in "Ghent\'; DELETE FROM weather; --" like?')}`, + { + signal: AbortSignal.timeout(5000), + } + ), + ]); + }) + .then(async ([sqlInjection]) => { + t.equal(sqlInjection.status, 500); + + const response = await sqlInjection.json(); + t.equal( + response.error, + "Error executing tool weather: The supplied SQL string contains more than one statement" + ); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); diff --git a/sample-apps/hono-sqlite-ai/README.md b/sample-apps/hono-sqlite-ai/README.md new file mode 100644 index 000000000..a70d65bc7 --- /dev/null +++ b/sample-apps/hono-sqlite-ai/README.md @@ -0,0 +1,7 @@ +# hono-sqlite-ai + +WARNING: This application contains security issues and should not be used in production (or taken as an example of how to write secure code). + +This example uses an in-memory SQLite database. + +In the root directory run `npm run sample-app hono-sqlite-ai` to start the server. diff --git a/sample-apps/hono-sqlite-ai/app.js b/sample-apps/hono-sqlite-ai/app.js new file mode 100644 index 000000000..67223a629 --- /dev/null +++ b/sample-apps/hono-sqlite-ai/app.js @@ -0,0 +1,76 @@ +const Zen = require("@aikidosec/firewall"); +const { seedDatabase, getTemperature } = require("./db"); +const { Hono } = require("hono"); +const { serve } = require("@hono/node-server"); +const { google } = require("@ai-sdk/google"); +const { generateText, tool } = require("ai"); +const { z } = require("zod"); + +(async () => { + const app = new Hono(); + seedDatabase(); + + Zen.addHonoMiddleware(app); + + // Insecure test api + app.get("/weather", async (c) => { + const prompt = c.req.query("prompt"); + + if (!prompt) { + return c.json( + { + error: "Prompt is required", + }, + 400 + ); + } + + try { + const response = await sendRequestToLLM(prompt); + + return c.json({ + response: response.toolResults, + }); + } catch (error) { + return c.json( + { + error: error.message, + }, + 500 + ); + } + }); + + const port = parseInt(process.argv[2], 10) || 4000; + + if (isNaN(port)) { + console.error("Invalid port"); + process.exit(1); + } + + serve({ + fetch: app.fetch, + port: port, + }).on("listening", () => { + console.log(`Server is running on port ${port}`); + }); +})(); + +async function sendRequestToLLM(prompt) { + return await generateText({ + model: google("models/gemini-2.0-flash-lite"), + tools: { + weather: tool({ + description: "Get the weather in a location", + parameters: z.object({ + location: z.string().describe("The location to get the weather for"), + }), + execute: async ({ location }) => { + return getTemperature(location); + }, + }), + }, + prompt: prompt, + toolChoice: "required", + }); +} diff --git a/sample-apps/hono-sqlite-ai/db.js b/sample-apps/hono-sqlite-ai/db.js new file mode 100644 index 000000000..17ebcc1f9 --- /dev/null +++ b/sample-apps/hono-sqlite-ai/db.js @@ -0,0 +1,31 @@ +const Database = require("better-sqlite3"); + +const db = new Database(":memory:"); + +function seedDatabase() { + db.exec(` + CREATE TABLE IF NOT EXISTS weather ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + city TEXT NOT NULL, + temperature REAL NOT NULL + ); + `); + db.exec("INSERT INTO weather (city, temperature) VALUES ('New York', 25.0);"); + db.exec( + "INSERT INTO weather (city, temperature) VALUES ('Los Angeles', 30.0);" + ); + db.exec("INSERT INTO weather (city, temperature) VALUES ('Ghent', 20.0);"); + db.exec("INSERT INTO weather (city, temperature) VALUES ('Oslo', 15.0);"); +} + +function getTemperature(city) { + // Insecure, vulnerable to SQL injection + return db + .prepare(`SELECT temperature FROM weather WHERE city = '${city}';`) + .get(); +} + +module.exports = { + seedDatabase, + getTemperature, +}; diff --git a/sample-apps/hono-sqlite-ai/package-lock.json b/sample-apps/hono-sqlite-ai/package-lock.json new file mode 100644 index 000000000..eb9987a4a --- /dev/null +++ b/sample-apps/hono-sqlite-ai/package-lock.json @@ -0,0 +1,762 @@ +{ + "name": "hono-sqlite-ai", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@ai-sdk/google": "^1.2.13", + "@aikidosec/firewall": "file:../../build", + "@hono/node-server": "^1.11.2", + "ai": "^4.3.9", + "better-sqlite3": "^11.9.1", + "hono": "^4.4.2", + "zod": "^3.24.3" + } + }, + "../../build": { + "name": "@aikidosec/firewall", + "version": "0.0.0", + "license": "AGPL-3.0-or-later", + "engines": { + "node": ">=16" + } + }, + "node_modules/@ai-sdk/google": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.13.tgz", + "integrity": "sha512-nnHDzbX1Zst28AjP3718xSWsEqx++qmFuqmnDc2Htelc02HyO6WkWOXMH+YVK3W8zdIyZEKpHL9KKlql7pa10A==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.7.tgz", + "integrity": "sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.9.tgz", + "integrity": "sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "2.2.7", + "@ai-sdk/ui-utils": "1.2.8", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/ui-utils": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.8.tgz", + "integrity": "sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@aikidosec/firewall": { + "resolved": "../../build", + "link": true + }, + "node_modules/@hono/node-server": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.1.tgz", + "integrity": "sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "license": "MIT" + }, + "node_modules/ai": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.9.tgz", + "integrity": "sha512-P2RpV65sWIPdUlA4f1pcJ11pB0N1YmqPVLEmC4j8WuBwKY0L3q9vGhYPh0Iv+spKHKyn0wUbMfas+7Z6nTfS0g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.7", + "@ai-sdk/react": "1.2.9", + "@ai-sdk/ui-utils": "1.2.8", + "@opentelemetry/api": "1.9.0", + "jsondiffpatch": "0.6.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.9.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz", + "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/hono": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.7.tgz", + "integrity": "sha512-2PCpQRbN87Crty8/L/7akZN3UyZIAopSoRxCwRbJgUuV1+MHNFHzYFxZTg4v/03cXUm+jce/qa2VSBZpKBm3Qw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "license": "MIT", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/swr": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz", + "integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/sample-apps/hono-sqlite-ai/package.json b/sample-apps/hono-sqlite-ai/package.json new file mode 100644 index 000000000..937fba9ba --- /dev/null +++ b/sample-apps/hono-sqlite-ai/package.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "@ai-sdk/google": "^1.2.13", + "@aikidosec/firewall": "file:../../build", + "@hono/node-server": "^1.11.2", + "ai": "^4.3.9", + "better-sqlite3": "^11.9.1", + "hono": "^4.4.2", + "zod": "^3.24.3" + } +} From 35e894156ed751b1bcecc26900d4b5acb3924717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Thu, 24 Apr 2025 10:33:16 +0200 Subject: [PATCH 5/5] Ignore full ai generated sql queries --- .../checkContextForSqlInjection.ts | 2 +- .../sql-injection/detectSQLInjection.test.ts | 53 +++++++++++++++++++ .../sql-injection/detectSQLInjection.ts | 15 +++++- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/library/vulnerabilities/sql-injection/checkContextForSqlInjection.ts b/library/vulnerabilities/sql-injection/checkContextForSqlInjection.ts index b5542c930..1bf5e824c 100644 --- a/library/vulnerabilities/sql-injection/checkContextForSqlInjection.ts +++ b/library/vulnerabilities/sql-injection/checkContextForSqlInjection.ts @@ -28,7 +28,7 @@ export function checkContextForSqlInjection({ } for (const str of userInput) { - if (detectSQLInjection(sql, str, dialect)) { + if (detectSQLInjection(sql, str, dialect, source)) { return { operation: operation, kind: "sql_injection", diff --git a/library/vulnerabilities/sql-injection/detectSQLInjection.test.ts b/library/vulnerabilities/sql-injection/detectSQLInjection.test.ts index 6a06fa28b..da2a53410 100644 --- a/library/vulnerabilities/sql-injection/detectSQLInjection.test.ts +++ b/library/vulnerabilities/sql-injection/detectSQLInjection.test.ts @@ -259,6 +259,59 @@ t.test("It does not match GROUP keyword", async () => { isNotSqlInjection(query, "ASC"); }); +t.test( + "it ignores full SQL queries from the source aiToolParams", + async (t) => { + const generic = new SQLDialectGeneric(); + t.same( + detectSQLInjection( + "SELECT * FROM 'test';", + "SELECT * FROM 'test';", + generic, + "body" + ), + true + ); + t.same( + detectSQLInjection( + "SELECT * FROM 'test';", + "'test';", + generic, + "aiToolParams" + ), + true + ); + t.same( + detectSQLInjection( + "SELECT * FROM 'test'; DELETE FROM 'test'; -- ';", + "test'; DELETE FROM 'test'; -- ';", + generic, + "aiToolParams" + ), + true + ); + + t.same( + detectSQLInjection( + "SELECT * FROM 'test';", + "SELECT * FROM 'test';", + generic, + "aiToolParams" + ), + false + ); + t.same( + detectSQLInjection( + "DELETE FROM 'test';", + "DELETE FROM 'test';", + generic, + "aiToolParams" + ), + false + ); + } +); + const files = [ // Taken from https://github.com/payloadbox/sql-injection-payload-list/tree/master join(__dirname, "payloads", "Auth_Bypass.txt"), diff --git a/library/vulnerabilities/sql-injection/detectSQLInjection.ts b/library/vulnerabilities/sql-injection/detectSQLInjection.ts index ea5288525..0216118e0 100644 --- a/library/vulnerabilities/sql-injection/detectSQLInjection.ts +++ b/library/vulnerabilities/sql-injection/detectSQLInjection.ts @@ -2,16 +2,29 @@ import { SQLDialect } from "./dialects/SQLDialect"; import { shouldReturnEarly } from "./shouldReturnEarly"; // eslint-disable-next-line camelcase import { wasm_detect_sql_injection } from "../../internals/zen_internals"; +import type { Source } from "../../agent/Source"; export function detectSQLInjection( query: string, userInput: string, - dialect: SQLDialect + dialect: SQLDialect, + source: Source | undefined = undefined ) { if (shouldReturnEarly(query, userInput)) { return false; } + // Ignore full SQL queries from the source aiToolParams + // This is to prevent false positives when the AI tool is generating SQL queries + // It was already checked in shouldReturnEarly that the query includes user input + if ( + source && + source === "aiToolParams" && + query.length === userInput.length + ) { + return false; + } + return wasm_detect_sql_injection( query.toLowerCase(), userInput.toLowerCase(),