From 298875fe305bd5e25a026402ebf36b8a44b37fe9 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:08:16 -0400 Subject: [PATCH 1/4] fix(examples): migrate AI integrations to patched SDK 6 --- .github/workflows/test_ai_examples.yml | 73 + examples/nextjs-app-router/.gitignore | 5 + .../nextjs-app-router/app/api/chat/route.ts | 8 +- examples/nextjs-app-router/package-lock.json | 1233 ++++++++++++++++ examples/nextjs-app-router/package.json | 15 +- examples/nextjs-app-router/test/chat.test.mjs | 152 ++ examples/nextjs-app-router/tsconfig.json | 1 + examples/vercel-ai-sdk/.gitignore | 5 + examples/vercel-ai-sdk/README.md | 31 +- examples/vercel-ai-sdk/SECURITY-MIGRATION.md | 154 ++ examples/vercel-ai-sdk/app/api/chat/route.ts | 10 +- examples/vercel-ai-sdk/app/page.tsx | 26 +- examples/vercel-ai-sdk/package-lock.json | 1295 +++++++++++++++++ examples/vercel-ai-sdk/package.json | 16 +- examples/vercel-ai-sdk/test/chat.test.mjs | 115 ++ .../template-nextjs/_gitignore | 2 + .../template-nextjs/app/api/chat/route.ts | 8 +- .../template-nextjs/app/page.tsx | 29 +- .../template-nextjs/package.json | 16 +- .../template-nextjs/test/chat.test.mjs | 119 ++ .../template-nextjs/tsconfig.json | 20 + 21 files changed, 3283 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/test_ai_examples.yml create mode 100644 examples/nextjs-app-router/.gitignore create mode 100644 examples/nextjs-app-router/package-lock.json create mode 100644 examples/nextjs-app-router/test/chat.test.mjs create mode 100644 examples/vercel-ai-sdk/.gitignore create mode 100644 examples/vercel-ai-sdk/SECURITY-MIGRATION.md create mode 100644 examples/vercel-ai-sdk/package-lock.json create mode 100644 examples/vercel-ai-sdk/test/chat.test.mjs create mode 100644 packages/create-askable-app/template-nextjs/test/chat.test.mjs create mode 100644 packages/create-askable-app/template-nextjs/tsconfig.json diff --git a/.github/workflows/test_ai_examples.yml b/.github/workflows/test_ai_examples.yml new file mode 100644 index 00000000..021e426c --- /dev/null +++ b/.github/workflows/test_ai_examples.yml @@ -0,0 +1,73 @@ +name: test / AI examples + +on: + push: + branches: [main] + paths: + - "examples/nextjs-app-router/**" + - "examples/vercel-ai-sdk/**" + - "packages/create-askable-app/**" + - ".github/workflows/test_ai_examples.yml" + pull_request: + branches: [main] + paths: + - "examples/nextjs-app-router/**" + - "examples/vercel-ai-sdk/**" + - "packages/create-askable-app/**" + - ".github/workflows/test_ai_examples.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ai-examples: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + app: [nextjs-app-router, vercel-ai-sdk, template-nextjs] + env: + NEXT_TELEMETRY_DISABLED: "1" + OPENAI_API_KEY: "" + ANTHROPIC_API_KEY: "" + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Select example + if: matrix.app != 'template-nextjs' + run: echo "APP_DIR=$GITHUB_WORKSPACE/examples/${{ matrix.app }}" >> "$GITHUB_ENV" + - name: Scaffold a standalone Next.js app + if: matrix.app == 'template-nextjs' + working-directory: ${{ runner.temp }} + run: | + node "$GITHUB_WORKSPACE/packages/create-askable-app/bin/create-askable-app.js" ai-template --template nextjs + echo "APP_DIR=$RUNNER_TEMP/ai-template" >> "$GITHUB_ENV" + - name: Resolve generated template dependencies + if: matrix.app == 'template-nextjs' + working-directory: ${{ env.APP_DIR }} + run: npm install --package-lock-only --workspaces=false + - name: Clean install + working-directory: ${{ env.APP_DIR }} + run: npm ci --workspaces=false + - name: Secrets-free provider and stream regressions + working-directory: ${{ env.APP_DIR }} + run: npm test + - name: Typecheck + working-directory: ${{ env.APP_DIR }} + run: npm run typecheck + - name: Production build (no inference) + working-directory: ${{ env.APP_DIR }} + run: npm run build + - name: Audit all dependencies + working-directory: ${{ env.APP_DIR }} + run: npm audit --workspaces=false diff --git a/examples/nextjs-app-router/.gitignore b/examples/nextjs-app-router/.gitignore new file mode 100644 index 00000000..3427d7fb --- /dev/null +++ b/examples/nextjs-app-router/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.next/ +next-env.d.ts +*.tsbuildinfo +.env*.local diff --git a/examples/nextjs-app-router/app/api/chat/route.ts b/examples/nextjs-app-router/app/api/chat/route.ts index 4f9cb74c..8941629f 100644 --- a/examples/nextjs-app-router/app/api/chat/route.ts +++ b/examples/nextjs-app-router/app/api/chat/route.ts @@ -1,4 +1,4 @@ -import { streamText } from 'ai'; +import { convertToModelMessages, generateId, streamText, type UIMessage } from 'ai'; import { createAnthropic } from '@ai-sdk/anthropic'; export const runtime = 'edge'; @@ -7,7 +7,7 @@ const anthropic = createAnthropic(); export async function POST(req: Request) { const { messages, uiContext } = await req.json() as { - messages: Array<{ role: 'user' | 'assistant'; content: string }>; + messages: UIMessage[]; uiContext?: string; }; @@ -21,8 +21,8 @@ export async function POST(req: Request) { const result = streamText({ model: anthropic('claude-sonnet-4-6'), system: systemPrompt, - messages, + messages: await convertToModelMessages(messages), }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse({ originalMessages: messages, generateMessageId: generateId }); } diff --git a/examples/nextjs-app-router/package-lock.json b/examples/nextjs-app-router/package-lock.json new file mode 100644 index 00000000..cf901bdd --- /dev/null +++ b/examples/nextjs-app-router/package-lock.json @@ -0,0 +1,1233 @@ +{ + "name": "nextjs-app-router", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextjs-app-router", + "version": "0.0.0", + "dependencies": { + "@ai-sdk/anthropic": "^3.0.118", + "@askable-ui/react": "^0.17.3", + "ai": "^6.0.286", + "next": "~15.5.25", + "react": "~19.1.9", + "react-dom": "~19.1.9" + }, + "devDependencies": { + "@types/node": "^22.15.21", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.5", + "typescript": "^5.8.3" + } + }, + "node_modules/@ai-sdk/anthropic": { + "version": "3.0.118", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.118.tgz", + "integrity": "sha512-5j8Cc9owORxhhZxFtpznXPTdLxeWm6bPCVbndM0e+NdvGVxe9GDgNqQq1V0KqOV54hZ6hyQVEXHkgTRPyXzcQg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.16", + "@ai-sdk/provider-utils": "4.0.51" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.196", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.196.tgz", + "integrity": "sha512-YMsbvS2UiPKDdg+dSusbC+SR9KkKyWEvRcURysYrsGK9A3ZIzC7n+Pi+67exlXT2FXC+yvCP1O8z8bbvOJ2ABA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.16", + "@ai-sdk/provider-utils": "4.0.51", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.16.tgz", + "integrity": "sha512-9Av6kg0t/IN/dcYAAmEJ4B9OPhcEJqwSR+GfHEA8olRkindXpUHadc3p3cyvgFUQFTVm1G5thtsmgZ9yVb2w3A==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.51", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.51.tgz", + "integrity": "sha512-ukLTs9x1Xm6lxSIwbJIxYQxbZHmVeIczNntARZrBcOa6pjdBhqeZnATNnJMHa7M9dZ8Ji5NJbpKpvz7o5XyBtg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.16", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^6.28.0" + }, + "engines": { + "node": ">=18.17" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@askable-ui/context": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@askable-ui/context/-/context-0.17.3.tgz", + "integrity": "sha512-6pp0qnkd/JiH7ACuQLTB6pXqnCyCpeH5OO0WwT2ykPZzBuu/8hC7dede8sqxJcJIL1kjmHZMr+FL4SYfpWe+YA==", + "license": "MIT" + }, + "node_modules/@askable-ui/core": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@askable-ui/core/-/core-0.17.3.tgz", + "integrity": "sha512-W1FWOXRtN//mHkKeu7IEjDwSXWCJYj5ydCKgMaO5z/sJ+2K66BeIFFee+7XCan+eZxfmJsVzKguB0eqpyd/viQ==", + "license": "MIT", + "dependencies": { + "@askable-ui/context": "^0.17.3" + } + }, + "node_modules/@askable-ui/react": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@askable-ui/react/-/react-0.17.3.tgz", + "integrity": "sha512-r9w/ZOAWYXJkVn4ZF2cxcpHk8o8xl85QSA16afqgq06xoBMZXp3MBFEUA8t4lVrPCnZ4D0b9DPmY4aM2zqLrAg==", + "license": "MIT", + "dependencies": { + "@askable-ui/core": "^0.17.3" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.25.tgz", + "integrity": "sha512-42h1lLr07vl4gawALP1hsgRZjHB1xYa58JfUfHwr0f7jG/zhPakh5GHkADHXOC9ZxUvlQFOPIrp7s6qX4DezPQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.25.tgz", + "integrity": "sha512-w+RR0v/QuApnWEjRGm1z6gcObKwGMb5YPA7V3bzBEVSBpMFUXprer0tS27UxjUcEnqbhL7Zuzohej79B6rYmBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.25.tgz", + "integrity": "sha512-QiGGBUSakt8S1H4Lt9Ehsh6Ja87axiBnQQgysOObvCbI7iUfJnRGntF1P64S4/ijuHFnSB8KLsEddkY3nN26uw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.25.tgz", + "integrity": "sha512-ehLos/66zo0d/mJCU5u96a/VDcr01aaUrX0o/i16UdInxz8qPTCDSxGtjk/Lps1sIr1RJFdiX3hxc0fxJo+cPA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.25.tgz", + "integrity": "sha512-ZVMrqLiJ7DiChgmbkQwFtdhAnUkSH/4p7tB29QY+giATb0Q/XGHNRSKAb/B8XGDHRUaA67NepOW5W8u3ZRJBAA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.25.tgz", + "integrity": "sha512-UOewtDGkTMJTiODrEdeLZ50yGb59xCZSriNpXkfPMxRRgwDkGc7i8mLWqV5076wEdb+Ca/XN7MhJyM3CupyNyQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.25.tgz", + "integrity": "sha512-UBHwA8AhkCZgtRfU1aJpunuAJe/6gZv6jDESQe4p5MjTb5V0YEeJBWCdNqx15Vj3x+5jmauRfeMJSjfQj9HGFQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.25.tgz", + "integrity": "sha512-QcFcPRr16djk5IqK5+e8O80eZfgWzIvVBXfitIq0tQ/uc+eyfdoZ0NmKc0cnbIJyfVwREapKuG97YcxWA9gcpA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.25.tgz", + "integrity": "sha512-zREeykps3ndWr9egJgvJKqVkkDuaw6Zrrg23cYBos0ygydFkAWYU4+PaPVwXzP1eAYQJe53ShSK45iDM529BOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/ai": { + "version": "6.0.286", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.286.tgz", + "integrity": "sha512-Wr4gUpAYrXF3XCxcyA1WYwD25nD5k9uCFM5gTNkof+S3zmzR9Nh+E2QrHswV+uV6EtHQv+Cqb1iVzKT+4XBVvg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.196", + "@ai-sdk/provider": "3.0.16", + "@ai-sdk/provider-utils": "4.0.51", + "@opentelemetry/api": "^1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "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/next": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.25.tgz", + "integrity": "sha512-OMWNulIIqKM2ykvC2qMjIt0IoavB4UB2SCs4iXJ6z6847FvyH8jBmBWcvrF5iuhTu8Przh20Fo/aoszIdqx4PA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.25", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.25", + "@next/swc-darwin-x64": "15.5.25", + "@next/swc-linux-arm64-gnu": "15.5.25", + "@next/swc-linux-arm64-musl": "15.5.25", + "@next/swc-linux-x64-gnu": "15.5.25", + "@next/swc-linux-x64-musl": "15.5.25", + "@next/swc-win32-arm64-msvc": "15.5.25", + "@next/swc-win32-x64-msvc": "15.5.25", + "sharp": "^0.34.3 || ^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.9.tgz", + "integrity": "sha512-vcH++wcuFsbaGkMBoghOSs7Caq9Xg4KbdGwNEC8hWXye9NbV/ryKpxuElvsQxj6/38TmVUw2BofUb7shrdQogA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-uDpATCDcImkLtNYyqOe1RqW8bNn/dNwyD5/4IBFekbqQ9UKz3xs4+W3pVF0TOw2p2tuVlVc0v4uBnpDglT3gig==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.9" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/examples/nextjs-app-router/package.json b/examples/nextjs-app-router/package.json index 395fc326..e831e26a 100644 --- a/examples/nextjs-app-router/package.json +++ b/examples/nextjs-app-router/package.json @@ -5,15 +5,20 @@ "scripts": { "dev": "next dev", "build": "next build", + "typecheck": "tsc --noEmit", + "test": "node --test test/*.test.mjs", "start": "next start" }, "dependencies": { - "@ai-sdk/anthropic": "^1.2.12", + "@ai-sdk/anthropic": "^3.0.118", "@askable-ui/react": "^0.17.3", - "ai": "^4.3.15", - "next": "^15.3.3", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "ai": "^6.0.286", + "next": "~15.5.25", + "react": "~19.1.9", + "react-dom": "~19.1.9" + }, + "overrides": { + "next": { "postcss": "^8.5.28" } }, "devDependencies": { "@types/node": "^22.15.21", diff --git a/examples/nextjs-app-router/test/chat.test.mjs b/examples/nextjs-app-router/test/chat.test.mjs new file mode 100644 index 00000000..74830ee6 --- /dev/null +++ b/examples/nextjs-app-router/test/chat.test.mjs @@ -0,0 +1,152 @@ +import assert from 'node:assert/strict'; +import { after, beforeEach, mock, test } from 'node:test'; +import { DefaultChatTransport, readUIMessageStream } from 'ai'; + +// No provider request is ever forwarded to the network. +const MODEL = 'claude-sonnet-4-6'; +const PROVIDER_URL = 'https://api.anthropic.com/v1/messages'; +const keyName = 'ANTHROPIC_API_KEY'; +const previousKey = process.env[keyName]; +process.env[keyName] = 'test-only-not-a-real-key'; +after(() => { + if (previousKey === undefined) delete process.env[keyName]; + else process.env[keyName] = previousKey; +}); + +const requests = []; +let providerError = false; +mock.method(globalThis, 'fetch', async (url, options) => { + assert.equal(String(url), PROVIDER_URL, 'unexpected outbound request blocked'); + assert.equal(options.method, 'POST'); + const body = JSON.parse(options.body); + assert.equal(body.model, MODEL); + assert.equal(body.stream, true); + requests.push(body); + if (providerError) { + return Response.json({ error: { type: 'authentication_error', message: 'private-provider-detail' } }, { status: 401 }); + } + const events = [ + { type: 'message_start', message: { id: 'mock-message', model: MODEL, role: 'assistant', usage: { input_tokens: 1, output_tokens: 0 } } }, + { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, + ...['Hello ', 'dashboard'].map((text) => ({ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } })), + { type: 'content_block_stop', index: 0 }, + { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 2 } }, + { type: 'message_stop' }, + ].map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(''); + return new Response(events, { headers: { 'Content-Type': 'text/event-stream' } }); +}); + +const { POST } = await import('../app/api/chat/route.ts'); +const { POST: askablePOST } = await import('../app/api/askable-chat/route.ts'); +assert.equal(requests.length, 0, 'route imports must not invoke a provider'); + +beforeEach(() => { + requests.length = 0; + providerError = false; +}); + +const userMessage = (id, text) => ({ id, role: 'user', parts: [{ type: 'text', text }] }); +const textOf = (message) => typeof message.content === 'string' + ? message.content + : message.content.filter((part) => part.type === 'text').map((part) => part.text).join(''); +const systemOf = (request) => request.system.map((part) => part.text).join('\n'); +const conversationOf = (request) => request.messages.filter((message) => message.role !== 'system') + .map((message) => ({ role: message.role, text: textOf(message) })); + +async function send(messages, uiContext) { + const transport = new DefaultChatTransport({ + api: '/api/chat', + fetch: async (url, options) => { + assert.equal(url, '/api/chat'); + const response = await POST(new Request('http://example.test/api/chat', options)); + assert.match(response.headers.get('content-type'), /text\/event-stream/); + assert.equal(response.headers.get('x-vercel-ai-ui-message-stream'), 'v1'); + return response; + }, + }); + const stream = await transport.sendMessages({ + chatId: 'test-chat', + trigger: 'submit-message', + messages, + body: { uiContext }, + }); + let message; + for await (const update of readUIMessageStream({ stream, terminateOnError: true })) message = update; + return message; +} + +test('UI-message stream round trips through the installed provider and SDK transport', async () => { + const first = userMessage('first', 'What is focused?'); + first.parts.push({ type: 'text', text: ' Explain briefly.' }); + const answer = await send([first], 'Revenue: $2.4M'); + assert.equal(answer.role, 'assistant'); + assert.equal(answer.parts.filter((part) => part.type === 'text').map((part) => part.text).join(''), 'Hello dashboard'); + assert.ok(answer.id); + assert.equal(requests.length, 1); + assert.match(systemOf(requests[0]), /Revenue: \$2.4M/); + assert.deepEqual(conversationOf(requests[0]), [{ role: 'user', text: 'What is focused? Explain briefly.' }]); + + const second = await send([first, answer, userMessage('second', 'And now?')], 'Churn: 2.1%'); + assert.notEqual(second.id, answer.id); + assert.equal(requests.length, 2); + assert.match(systemOf(requests[1]), /Churn: 2.1%/); + assert.doesNotMatch(systemOf(requests[1]), /Revenue: \$2.4M/); + assert.deepEqual(conversationOf(requests[1]), [ + { role: 'user', text: 'What is focused? Explain briefly.' }, + { role: 'assistant', text: 'Hello dashboard' }, + { role: 'user', text: 'And now?' }, + ]); +}); + +test('missing context still allows a text conversation', async () => { + const answer = await send([userMessage('first', 'Hello')]); + assert.equal(answer.role, 'assistant'); + assert.doesNotMatch(systemOf(requests[0]), /undefined/); +}); + +test('provider errors are masked in the UI stream', async (t) => { + providerError = true; + t.mock.method(console, 'error', () => {}); + await assert.rejects(send([userMessage('first', 'Hello')]), (error) => { + assert.doesNotMatch(error.message, /private-provider-detail|test-only-not-a-real-key/); + return true; + }); + assert.equal(requests.length, 1); +}); + +test('malformed JSON is rejected before any provider request', async () => { + await assert.rejects(POST(new Request('http://example.test/api/chat', { method: 'POST', body: '{' }))); + assert.equal(requests.length, 0); +}); + +test('Askable endpoint preserves plain text, history order, and one current question', async () => { + const response = await askablePOST(new Request('http://example.test/api/askable-chat', { + method: 'POST', + body: JSON.stringify({ + question: 'And now?', + context: 'Churn: 2.1%', + messages: [ + { role: 'user', content: 'What is focused?' }, + { role: 'assistant', content: 'Revenue' }, + { role: 'user', content: 'And now?' }, + ], + }), + })); + assert.match(response.headers.get('content-type'), /text\/plain/); + assert.equal(response.headers.get('x-vercel-ai-ui-message-stream'), null); + assert.equal(await response.text(), 'Hello dashboard'); + assert.match(systemOf(requests[0]), /Churn: 2.1%/); + assert.deepEqual(conversationOf(requests[0]), [ + { role: 'user', text: 'What is focused?' }, + { role: 'assistant', text: 'Revenue' }, + { role: 'user', text: 'And now?' }, + ]); +}); + +test('Askable endpoint accepts a single question without history', async () => { + const response = await askablePOST(new Request('http://example.test/api/askable-chat', { + method: 'POST', body: JSON.stringify({ question: 'Hello', context: '' }), + })); + assert.equal(await response.text(), 'Hello dashboard'); + assert.deepEqual(conversationOf(requests[0]), [{ role: 'user', text: 'Hello' }]); +}); diff --git a/examples/nextjs-app-router/tsconfig.json b/examples/nextjs-app-router/tsconfig.json index 49e4cf3b..fba2bf37 100644 --- a/examples/nextjs-app-router/tsconfig.json +++ b/examples/nextjs-app-router/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/examples/vercel-ai-sdk/.gitignore b/examples/vercel-ai-sdk/.gitignore new file mode 100644 index 00000000..3427d7fb --- /dev/null +++ b/examples/vercel-ai-sdk/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.next/ +next-env.d.ts +*.tsbuildinfo +.env*.local diff --git a/examples/vercel-ai-sdk/README.md b/examples/vercel-ai-sdk/README.md index 7a74009a..7ab664d8 100644 --- a/examples/vercel-ai-sdk/README.md +++ b/examples/vercel-ai-sdk/README.md @@ -1,6 +1,6 @@ # askable-ui + Vercel AI SDK -A minimal Next.js 15 app showing how **askable-ui** and the **Vercel AI SDK** work together. +A minimal Next.js 15 app showing how **askable-ui** and **Vercel AI SDK 6** work together. Click any metric card or deal row. The AI panel knows exactly what you're looking at — no screenshots, no form input, no extra wiring. @@ -40,30 +40,47 @@ const { promptContext } = useAskable(); // → "User is focused on: {"id":"revenue","value":"$2.34M","delta":"+12%"}" ``` -**3. Send it with every AI request** via Vercel AI SDK's `body` option: +**3. Send it with every AI request** using request-level options so the current context is captured at submission time: ```tsx -const { messages, input, handleSubmit } = useChat({ - api: '/api/chat', - body: { uiContext: promptContext }, // ← live UI context rides along +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; +import { useState } from 'react'; + +const [input, setInput] = useState(''); +const { messages, sendMessage, status } = useChat({ + transport: new DefaultChatTransport({ api: '/api/chat' }), }); + +// In the form submit handler: +sendMessage({ text: input }, { body: { uiContext: promptContext } }); +setInput(''); +// Render text from message.parts, and use status for loading state. ``` **4. Use it in your system prompt** on the server: ```ts // app/api/chat/route.ts +import { convertToModelMessages, generateId, streamText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + const { messages, uiContext } = await req.json(); const result = streamText({ - model: openai('gpt-4o-mini'), + model: openai.chat('gpt-4o-mini'), system: `You are a dashboard assistant.\n\n${uiContext ?? 'Nothing focused.'}`, - messages, + messages: await convertToModelMessages(messages), }); + +return result.toUIMessageStreamResponse({ originalMessages: messages, generateMessageId: generateId }); ``` That's the entire integration — four steps, no SDK-specific adapters needed. +See [the security migration notes](./SECURITY-MIGRATION.md) for dependency +selection, compatibility details, and secrets-free validation commands. + ## Quickstart ```bash diff --git a/examples/vercel-ai-sdk/SECURITY-MIGRATION.md b/examples/vercel-ai-sdk/SECURITY-MIGRATION.md new file mode 100644 index 00000000..81847fa4 --- /dev/null +++ b/examples/vercel-ai-sdk/SECURITY-MIGRATION.md @@ -0,0 +1,154 @@ +# AI SDK security migration + +Verified on 2026-09-20. Scope: both Next.js examples, the Next.js scaffold +template, and the additive AI examples workflow. No package release version +was changed. + +## Version decision + +The current stable AI SDK is 7, but this migration deliberately uses the +smallest audited-safe major from the requested 5-or-6 choices. + +- Vercel still publishes an `ai-v5` line: `ai@5.0.261` was released on + September 18. However, a clean install with its matching v2 providers brought + in `@ai-sdk/provider-utils@3.0.37`, which depends on `undici@^5.29.0`. + npm audit reported unresolved Undici findings. Using it would require an + out-of-range transitive major override. +- The maintained `ai-v6` line has `ai@6.0.286`, also released September 18. + Its matching provider utilities use `undici@^6.28.0`; the tested installs + resolved `6.28.1` without an override. This is the selected fallback. +- Next.js stays on the Maintenance LTS 15.5 line, with a minimum of `15.5.25`, + above the August security release's `15.5.24` minimum. React and React DOM + stay together on the patched 19.1 line at `19.1.9`. +- Next.js 15.5.25 still pins PostCSS 8.4.31. A narrowly scoped `next.postcss` + override raises it to `^8.5.28`, fixing the current source-map disclosure + and stringify advisories while staying within PostCSS major 8. Revisit this + override when Next.js updates its dependency. + +| Dependency | Manifest floor | Tested resolution | +| --- | --- | --- | +| ai | ^6.0.286 | 6.0.286 | +| @ai-sdk/react | ^3.0.289 | 3.0.289 | +| @ai-sdk/anthropic | ^3.0.118 | 3.0.118 | +| @ai-sdk/openai | ^3.0.114 | 3.0.114 | +| next | ~15.5.25 | 15.5.25 | +| react / react-dom | ~19.1.9 | 19.1.9 | +| PostCSS under Next.js | ^8.5.28 | 8.5.28 | + +Only the providers and React adapter actually used by each app are installed. +The template's Anthropic provider is now a production dependency. + +## Sources and API checks + +- [Official v4-to-v5 migration](https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0): + React hook import, local input state, request-level context, message parts, + UI-message streaming, and explicit OpenAI Chat Completions selection. +- [Official v5-to-v6 migration](https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0): + asynchronous `convertToModelMessages` and matching provider major versions. +- [Official SDK 5 release](https://github.com/vercel/ai/releases/tag/ai%405.0.261) + and [SDK 6 release](https://github.com/vercel/ai/releases/tag/ai%406.0.286): + current maintenance evidence, not a promise of a future support window. +- [Vercel's original input-validation advisory](https://vercel.com/changelog/cve-2025-48985-input-validation-bypass-on-ai-sdk). +- [Next.js August 2026 security release](https://nextjs.org/blog/august-2026-security-release). +- [PostCSS source-map advisory](https://github.com/postcss/postcss/security/advisories/GHSA-fxqj-rqcc-2cmp). + +Checked installed `ai/dist/index.d.ts`, `@ai-sdk/react/dist/index.d.ts`, and +provider types and stream schemas. The three `/api/chat` routes now await +UI-to-model conversion and return UI-message SSE with message IDs. Both +`useChat` clients render text parts and pass current context in `sendMessage` +options, avoiding a transport closure over the first render's focus. +The OpenAI route explicitly uses `openai.chat`, preserving the previous +Chat Completions backend and model. Model names are otherwise unchanged. + +`examples/nextjs-app-router/app/api/askable-chat/route.ts` and its client page +are unchanged. Its text stream, history ordering, and single current question +are covered by regression tests. + +## Reproduction + +Use Node 22.18 or later for native TypeScript loading in the Node test runner. +Validation used Node 22.23.2. Run these commands in each example directory: + +```sh +npm ci --workspaces=false +npm test +npm run typecheck +NEXT_TELEMETRY_DISABLED=1 OPENAI_API_KEY= ANTHROPIC_API_KEY= npm run build +npm audit --workspaces=false +``` + +Generate the template through the real CLI from a separate temporary working +directory, using the absolute path to this checkout's +`packages/create-askable-app/bin/create-askable-app.js`: + +```sh +node /path/to/askable/packages/create-askable-app/bin/create-askable-app.js ai-template --template nextjs +cd ai-template +npm install --package-lock-only --workspaces=false +``` + +Then run the same five validation commands. The template intentionally has no +checked-in lockfile containing placeholders. Each generated application resolves +its own lock, then CI performs a clean `npm ci`. Example lockfiles are committed. + +| App | Clean install | Route/stream tests | Typecheck | Production build | npm audit | +| --- | --- | --- | --- | --- | --- | +| nextjs-app-router | pass | 6 passed | pass | pass | 0 findings | +| vercel-ai-sdk | pass | 4 passed | pass | pass | 0 findings | +| generated ai-template | pass | 4 passed | pass | pass | 0 findings | + +The tests exercise the installed providers, route handlers, UI stream parser, +multi-turn history, changed context, missing context, masked provider failures, +and malformed JSON. Fetch is replaced with in-memory provider responses; +unexpected URLs fail closed. The only keys used are explicit fake test values. +Additional local Chromium checks against all three production builds verified +current focus, text rendering, two turns, disabled inputs while pending, cleared +inputs, and no page errors. Both migrated hook clients also passed generic error +display and recovery checks. Browser responses were intercepted locally. + +The existing scaffold suite passed all 6 tests without modifications. +`actionlint` 1.7.12 accepted the new workflow. It runs the validation commands in +three independent, 15-minute Node 22 jobs, with read-only repository permission, +no stored checkout credentials, no provider secrets, and no inference step. + +## Limitations + +- No live model calls, credentials, remote CI run, or deployment was used. +- The npm audit result is a point-in-time dependency check, not a guarantee that + the application has no security issues. Other repository dependencies and + remote Dependabot alert states are outside this change. +- Existing demo endpoint authentication, rate limits, and input-validation + policies were not expanded as part of this compatibility migration. +- Next.js emits a harmless multiple-lockfile root inference warning for the + in-repository examples and an Edge static-generation warning where applicable. + Node emits a module-type detection warning when directly testing route files. +- Root manifests/lockfiles, release versions, and other agents' files are + untouched. No remote push, PR, merge, or alert dismissal is part of this work. + +## Changed files + +Paths below are relative to the repository root: + +```text +.github/workflows/test_ai_examples.yml +examples/nextjs-app-router/.gitignore +examples/nextjs-app-router/app/api/chat/route.ts +examples/nextjs-app-router/package-lock.json +examples/nextjs-app-router/package.json +examples/nextjs-app-router/test/chat.test.mjs +examples/nextjs-app-router/tsconfig.json +examples/vercel-ai-sdk/.gitignore +examples/vercel-ai-sdk/README.md +examples/vercel-ai-sdk/SECURITY-MIGRATION.md +examples/vercel-ai-sdk/app/api/chat/route.ts +examples/vercel-ai-sdk/app/page.tsx +examples/vercel-ai-sdk/package-lock.json +examples/vercel-ai-sdk/package.json +examples/vercel-ai-sdk/test/chat.test.mjs +packages/create-askable-app/template-nextjs/_gitignore +packages/create-askable-app/template-nextjs/app/api/chat/route.ts +packages/create-askable-app/template-nextjs/app/page.tsx +packages/create-askable-app/template-nextjs/package.json +packages/create-askable-app/template-nextjs/test/chat.test.mjs +packages/create-askable-app/template-nextjs/tsconfig.json +``` diff --git a/examples/vercel-ai-sdk/app/api/chat/route.ts b/examples/vercel-ai-sdk/app/api/chat/route.ts index c8ca2e74..1a900ac3 100644 --- a/examples/vercel-ai-sdk/app/api/chat/route.ts +++ b/examples/vercel-ai-sdk/app/api/chat/route.ts @@ -1,11 +1,11 @@ import { openai } from '@ai-sdk/openai'; -import { streamText } from 'ai'; +import { convertToModelMessages, generateId, streamText, type UIMessage } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages, uiContext } = await req.json() as { - messages: { role: string; content: string }[]; + messages: UIMessage[]; uiContext?: string; }; @@ -19,10 +19,10 @@ export async function POST(req: Request) { ].join('\n'); const result = streamText({ - model: openai('gpt-4o-mini'), + model: openai.chat('gpt-4o-mini'), system: systemPrompt, - messages: messages as Parameters[0]['messages'], + messages: await convertToModelMessages(messages), }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse({ originalMessages: messages, generateMessageId: generateId }); } diff --git a/examples/vercel-ai-sdk/app/page.tsx b/examples/vercel-ai-sdk/app/page.tsx index dafa8392..c1c93632 100644 --- a/examples/vercel-ai-sdk/app/page.tsx +++ b/examples/vercel-ai-sdk/app/page.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useChat } from 'ai/react'; +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; import { Askable, useAskable, @@ -8,7 +9,7 @@ import { useAskableViewport, useAskableCompose, } from '@askable-ui/react'; -import { useRef } from 'react'; +import { useRef, useState } from 'react'; // --- sample data ----------------------------------------------------------- @@ -44,11 +45,19 @@ export default function Dashboard() { ], }); - const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ - api: '/api/chat', - // send the composed context with every request - body: { uiContext: promptContext }, + const [input, setInput] = useState(''); + const { messages, sendMessage, status, error } = useChat({ + transport: new DefaultChatTransport({ api: '/api/chat' }), }); + const isLoading = status === 'submitted' || status === 'streaming'; + + function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (!input.trim() || isLoading) return; + // Request-level options capture the current focus, not the initial render. + void sendMessage({ text: input.trim() }, { body: { uiContext: promptContext } }); + setInput(''); + } return (
@@ -125,17 +134,18 @@ export default function Dashboard() { {messages.map((m) => (
{m.role === 'user' ? 'You' : 'AI'} -

{m.content}

+

{m.parts.map((part) => part.type === 'text' ? part.text : null)}

))} {isLoading && thinking…} + {error &&

Unable to complete the response. Please try again.

}
setInput(event.target.value)} placeholder="Ask about what you're looking at…" style={styles.chatInput} disabled={isLoading} diff --git a/examples/vercel-ai-sdk/package-lock.json b/examples/vercel-ai-sdk/package-lock.json new file mode 100644 index 00000000..d15293d4 --- /dev/null +++ b/examples/vercel-ai-sdk/package-lock.json @@ -0,0 +1,1295 @@ +{ + "name": "askable-vercel-ai-sdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "askable-vercel-ai-sdk", + "version": "0.1.0", + "dependencies": { + "@ai-sdk/openai": "^3.0.114", + "@ai-sdk/react": "^3.0.289", + "@askable-ui/react": "^0.17.3", + "ai": "^6.0.286", + "next": "~15.5.25", + "react": "~19.1.9", + "react-dom": "~19.1.9" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5.8.3" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.196", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.196.tgz", + "integrity": "sha512-YMsbvS2UiPKDdg+dSusbC+SR9KkKyWEvRcURysYrsGK9A3ZIzC7n+Pi+67exlXT2FXC+yvCP1O8z8bbvOJ2ABA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.16", + "@ai-sdk/provider-utils": "4.0.51", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/openai": { + "version": "3.0.114", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.114.tgz", + "integrity": "sha512-1Y8EYQpeY+Sb1YtRTN077X2v8DEhGw6RvGHudPRbNNP9JPDC5/y/M9OUx9WX/s/adU7IcSY8AENxurdqaEwJDg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.16", + "@ai-sdk/provider-utils": "4.0.51" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.16.tgz", + "integrity": "sha512-9Av6kg0t/IN/dcYAAmEJ4B9OPhcEJqwSR+GfHEA8olRkindXpUHadc3p3cyvgFUQFTVm1G5thtsmgZ9yVb2w3A==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.51", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.51.tgz", + "integrity": "sha512-ukLTs9x1Xm6lxSIwbJIxYQxbZHmVeIczNntARZrBcOa6pjdBhqeZnATNnJMHa7M9dZ8Ji5NJbpKpvz7o5XyBtg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.16", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^6.28.0" + }, + "engines": { + "node": ">=18.17" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "3.0.289", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.289.tgz", + "integrity": "sha512-WBP8AHYyIH1uJIHolWZVwDNHqk6+rcJjkwSjxXu5/dkCLboWa0uJE9TuMZC/DekLIOP9gVH5PvWYtAI6jI0U6g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "4.0.51", + "ai": "6.0.286", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" + } + }, + "node_modules/@askable-ui/context": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@askable-ui/context/-/context-0.17.3.tgz", + "integrity": "sha512-6pp0qnkd/JiH7ACuQLTB6pXqnCyCpeH5OO0WwT2ykPZzBuu/8hC7dede8sqxJcJIL1kjmHZMr+FL4SYfpWe+YA==", + "license": "MIT" + }, + "node_modules/@askable-ui/core": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@askable-ui/core/-/core-0.17.3.tgz", + "integrity": "sha512-W1FWOXRtN//mHkKeu7IEjDwSXWCJYj5ydCKgMaO5z/sJ+2K66BeIFFee+7XCan+eZxfmJsVzKguB0eqpyd/viQ==", + "license": "MIT", + "dependencies": { + "@askable-ui/context": "^0.17.3" + } + }, + "node_modules/@askable-ui/react": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@askable-ui/react/-/react-0.17.3.tgz", + "integrity": "sha512-r9w/ZOAWYXJkVn4ZF2cxcpHk8o8xl85QSA16afqgq06xoBMZXp3MBFEUA8t4lVrPCnZ4D0b9DPmY4aM2zqLrAg==", + "license": "MIT", + "dependencies": { + "@askable-ui/core": "^0.17.3" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.25.tgz", + "integrity": "sha512-42h1lLr07vl4gawALP1hsgRZjHB1xYa58JfUfHwr0f7jG/zhPakh5GHkADHXOC9ZxUvlQFOPIrp7s6qX4DezPQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.25.tgz", + "integrity": "sha512-w+RR0v/QuApnWEjRGm1z6gcObKwGMb5YPA7V3bzBEVSBpMFUXprer0tS27UxjUcEnqbhL7Zuzohej79B6rYmBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.25.tgz", + "integrity": "sha512-QiGGBUSakt8S1H4Lt9Ehsh6Ja87axiBnQQgysOObvCbI7iUfJnRGntF1P64S4/ijuHFnSB8KLsEddkY3nN26uw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.25.tgz", + "integrity": "sha512-ehLos/66zo0d/mJCU5u96a/VDcr01aaUrX0o/i16UdInxz8qPTCDSxGtjk/Lps1sIr1RJFdiX3hxc0fxJo+cPA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.25.tgz", + "integrity": "sha512-ZVMrqLiJ7DiChgmbkQwFtdhAnUkSH/4p7tB29QY+giATb0Q/XGHNRSKAb/B8XGDHRUaA67NepOW5W8u3ZRJBAA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.25.tgz", + "integrity": "sha512-UOewtDGkTMJTiODrEdeLZ50yGb59xCZSriNpXkfPMxRRgwDkGc7i8mLWqV5076wEdb+Ca/XN7MhJyM3CupyNyQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.25.tgz", + "integrity": "sha512-UBHwA8AhkCZgtRfU1aJpunuAJe/6gZv6jDESQe4p5MjTb5V0YEeJBWCdNqx15Vj3x+5jmauRfeMJSjfQj9HGFQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.25.tgz", + "integrity": "sha512-QcFcPRr16djk5IqK5+e8O80eZfgWzIvVBXfitIq0tQ/uc+eyfdoZ0NmKc0cnbIJyfVwREapKuG97YcxWA9gcpA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.25.tgz", + "integrity": "sha512-zREeykps3ndWr9egJgvJKqVkkDuaw6Zrrg23cYBos0ygydFkAWYU4+PaPVwXzP1eAYQJe53ShSK45iDM529BOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/ai": { + "version": "6.0.286", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.286.tgz", + "integrity": "sha512-Wr4gUpAYrXF3XCxcyA1WYwD25nD5k9uCFM5gTNkof+S3zmzR9Nh+E2QrHswV+uV6EtHQv+Cqb1iVzKT+4XBVvg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.196", + "@ai-sdk/provider": "3.0.16", + "@ai-sdk/provider-utils": "4.0.51", + "@opentelemetry/api": "^1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "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.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "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/next": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.25.tgz", + "integrity": "sha512-OMWNulIIqKM2ykvC2qMjIt0IoavB4UB2SCs4iXJ6z6847FvyH8jBmBWcvrF5iuhTu8Przh20Fo/aoszIdqx4PA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.25", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.25", + "@next/swc-darwin-x64": "15.5.25", + "@next/swc-linux-arm64-gnu": "15.5.25", + "@next/swc-linux-arm64-musl": "15.5.25", + "@next/swc-linux-x64-gnu": "15.5.25", + "@next/swc-linux-x64-musl": "15.5.25", + "@next/swc-win32-arm64-msvc": "15.5.25", + "@next/swc-win32-x64-msvc": "15.5.25", + "sharp": "^0.34.3 || ^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.9.tgz", + "integrity": "sha512-vcH++wcuFsbaGkMBoghOSs7Caq9Xg4KbdGwNEC8hWXye9NbV/ryKpxuElvsQxj6/38TmVUw2BofUb7shrdQogA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-uDpATCDcImkLtNYyqOe1RqW8bNn/dNwyD5/4IBFekbqQ9UKz3xs4+W3pVF0TOw2p2tuVlVc0v4uBnpDglT3gig==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.9" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/swr": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.5.1.tgz", + "integrity": "sha512-BRw55e8r0B7SpDN20CAzoQAHl7y1yP7/Zt7oqUjMv0vSt2u2Xnkm88Ws+VypbV9BXHQVuSuyVq7zMjO16wSExw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/examples/vercel-ai-sdk/package.json b/examples/vercel-ai-sdk/package.json index 002b14e3..1128a5df 100644 --- a/examples/vercel-ai-sdk/package.json +++ b/examples/vercel-ai-sdk/package.json @@ -5,15 +5,21 @@ "scripts": { "dev": "next dev", "build": "next build", + "typecheck": "tsc --noEmit", + "test": "node --test test/*.test.mjs", "start": "next start" }, "dependencies": { "@askable-ui/react": "^0.17.3", - "ai": "^4.3.16", - "@ai-sdk/openai": "^1.3.22", - "next": "^15.3.3", - "react": "^19.1.0", - "react-dom": "^19.1.0" + "ai": "^6.0.286", + "@ai-sdk/react": "^3.0.289", + "@ai-sdk/openai": "^3.0.114", + "next": "~15.5.25", + "react": "~19.1.9", + "react-dom": "~19.1.9" + }, + "overrides": { + "next": { "postcss": "^8.5.28" } }, "devDependencies": { "@types/node": "^22", diff --git a/examples/vercel-ai-sdk/test/chat.test.mjs b/examples/vercel-ai-sdk/test/chat.test.mjs new file mode 100644 index 00000000..a0f72e4b --- /dev/null +++ b/examples/vercel-ai-sdk/test/chat.test.mjs @@ -0,0 +1,115 @@ +import assert from 'node:assert/strict'; +import { after, beforeEach, mock, test } from 'node:test'; +import { DefaultChatTransport, readUIMessageStream } from 'ai'; + +// No provider request is ever forwarded to the network. +const MODEL = 'gpt-4o-mini'; +const PROVIDER_URL = 'https://api.openai.com/v1/chat/completions'; +const keyName = 'OPENAI_API_KEY'; +const previousKey = process.env[keyName]; +process.env[keyName] = 'test-only-not-a-real-key'; +after(() => { + if (previousKey === undefined) delete process.env[keyName]; + else process.env[keyName] = previousKey; +}); + +const requests = []; +let providerError = false; +mock.method(globalThis, 'fetch', async (url, options) => { + assert.equal(String(url), PROVIDER_URL, 'unexpected outbound request blocked'); + assert.equal(options.method, 'POST'); + const body = JSON.parse(options.body); + assert.equal(body.model, MODEL); + assert.equal(body.stream, true); + requests.push(body); + if (providerError) { + return Response.json({ error: { type: 'authentication_error', message: 'private-provider-detail' } }, { status: 401 }); + } + const events = [ + ...['Hello ', 'dashboard'].map((content) => ({ id: 'mock-message', model: MODEL, choices: [{ index: 0, delta: { role: 'assistant', content }, finish_reason: null }] })), + { id: 'mock-message', model: MODEL, choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 } }, + ].map((event) => `data: ${JSON.stringify(event)}\n\n`).join('') + 'data: [DONE]\n\n'; + return new Response(events, { headers: { 'Content-Type': 'text/event-stream' } }); +}); + +const { POST } = await import('../app/api/chat/route.ts'); +assert.equal(requests.length, 0, 'route imports must not invoke a provider'); + +beforeEach(() => { + requests.length = 0; + providerError = false; +}); + +const userMessage = (id, text) => ({ id, role: 'user', parts: [{ type: 'text', text }] }); +const textOf = (message) => typeof message.content === 'string' + ? message.content + : message.content.filter((part) => part.type === 'text').map((part) => part.text).join(''); +const systemOf = (request) => request.messages.filter((message) => message.role === 'system').map(textOf).join('\n'); +const conversationOf = (request) => request.messages.filter((message) => message.role !== 'system') + .map((message) => ({ role: message.role, text: textOf(message) })); + +async function send(messages, uiContext) { + const transport = new DefaultChatTransport({ + api: '/api/chat', + fetch: async (url, options) => { + assert.equal(url, '/api/chat'); + const response = await POST(new Request('http://example.test/api/chat', options)); + assert.match(response.headers.get('content-type'), /text\/event-stream/); + assert.equal(response.headers.get('x-vercel-ai-ui-message-stream'), 'v1'); + return response; + }, + }); + const stream = await transport.sendMessages({ + chatId: 'test-chat', + trigger: 'submit-message', + messages, + body: { uiContext }, + }); + let message; + for await (const update of readUIMessageStream({ stream, terminateOnError: true })) message = update; + return message; +} + +test('UI-message stream round trips through the installed provider and SDK transport', async () => { + const first = userMessage('first', 'What is focused?'); + first.parts.push({ type: 'text', text: ' Explain briefly.' }); + const answer = await send([first], 'Revenue: $2.4M'); + assert.equal(answer.role, 'assistant'); + assert.equal(answer.parts.filter((part) => part.type === 'text').map((part) => part.text).join(''), 'Hello dashboard'); + assert.ok(answer.id); + assert.equal(requests.length, 1); + assert.match(systemOf(requests[0]), /Revenue: \$2.4M/); + assert.deepEqual(conversationOf(requests[0]), [{ role: 'user', text: 'What is focused? Explain briefly.' }]); + + const second = await send([first, answer, userMessage('second', 'And now?')], 'Churn: 2.1%'); + assert.notEqual(second.id, answer.id); + assert.equal(requests.length, 2); + assert.match(systemOf(requests[1]), /Churn: 2.1%/); + assert.doesNotMatch(systemOf(requests[1]), /Revenue: \$2.4M/); + assert.deepEqual(conversationOf(requests[1]), [ + { role: 'user', text: 'What is focused? Explain briefly.' }, + { role: 'assistant', text: 'Hello dashboard' }, + { role: 'user', text: 'And now?' }, + ]); +}); + +test('missing context still allows a text conversation', async () => { + const answer = await send([userMessage('first', 'Hello')]); + assert.equal(answer.role, 'assistant'); + assert.doesNotMatch(systemOf(requests[0]), /undefined/); +}); + +test('provider errors are masked in the UI stream', async (t) => { + providerError = true; + t.mock.method(console, 'error', () => {}); + await assert.rejects(send([userMessage('first', 'Hello')]), (error) => { + assert.doesNotMatch(error.message, /private-provider-detail|test-only-not-a-real-key/); + return true; + }); + assert.equal(requests.length, 1); +}); + +test('malformed JSON is rejected before any provider request', async () => { + await assert.rejects(POST(new Request('http://example.test/api/chat', { method: 'POST', body: '{' }))); + assert.equal(requests.length, 0); +}); diff --git a/packages/create-askable-app/template-nextjs/_gitignore b/packages/create-askable-app/template-nextjs/_gitignore index 3cecc383..7262e249 100644 --- a/packages/create-askable-app/template-nextjs/_gitignore +++ b/packages/create-askable-app/template-nextjs/_gitignore @@ -1,3 +1,5 @@ node_modules .next .env.local +next-env.d.ts +*.tsbuildinfo diff --git a/packages/create-askable-app/template-nextjs/app/api/chat/route.ts b/packages/create-askable-app/template-nextjs/app/api/chat/route.ts index ab13e453..9a17d16c 100644 --- a/packages/create-askable-app/template-nextjs/app/api/chat/route.ts +++ b/packages/create-askable-app/template-nextjs/app/api/chat/route.ts @@ -1,4 +1,4 @@ -import { streamText } from 'ai'; +import { convertToModelMessages, generateId, streamText, type UIMessage } from 'ai'; import { createAnthropic } from '@ai-sdk/anthropic'; export const runtime = 'edge'; @@ -7,7 +7,7 @@ const anthropic = createAnthropic(); export async function POST(req: Request) { const { messages, uiContext } = await req.json() as { - messages: Array<{ role: 'user' | 'assistant'; content: string }>; + messages: UIMessage[]; uiContext?: string; }; @@ -17,8 +17,8 @@ export async function POST(req: Request) { 'You are a helpful assistant embedded in a web app.', uiContext ? `\n## Current UI context\n${uiContext}` : '', ].join('\n'), - messages, + messages: await convertToModelMessages(messages), }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse({ originalMessages: messages, generateMessageId: generateId }); } diff --git a/packages/create-askable-app/template-nextjs/app/page.tsx b/packages/create-askable-app/template-nextjs/app/page.tsx index d6d30a72..f19261b0 100644 --- a/packages/create-askable-app/template-nextjs/app/page.tsx +++ b/packages/create-askable-app/template-nextjs/app/page.tsx @@ -1,6 +1,8 @@ 'use client'; -import { useChat } from 'ai/react'; +import { useState } from 'react'; +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; import { useAskable, Askable } from '@askable-ui/react'; const ITEMS = [ @@ -11,10 +13,19 @@ const ITEMS = [ export default function Page() { const { promptContext } = useAskable(); - const { messages, input, handleInputChange, handleSubmit } = useChat({ - api: '/api/chat', - body: { uiContext: promptContext }, + const [input, setInput] = useState(''); + const { messages, sendMessage, status, error } = useChat({ + transport: new DefaultChatTransport({ api: '/api/chat' }), }); + const isLoading = status === 'submitted' || status === 'streaming'; + + function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (!input.trim() || isLoading) return; + // Request-level options capture the current focus, not the initial render. + void sendMessage({ text: input.trim() }, { body: { uiContext: promptContext } }); + setInput(''); + } return (
@@ -40,18 +51,22 @@ export default function Page() {
{messages.map((m) => (

- {m.role === 'user' ? 'You' : 'AI'}: {m.content} + {m.role === 'user' ? 'You' : 'AI'}: + {m.parts.map((part) => part.type === 'text' ? part.text : null)}

))} + {isLoading &&

Thinking...

} + {error &&

Unable to complete the response. Please try again.

}
setInput(event.target.value)} + disabled={isLoading} placeholder="Ask about what you see..." style={{ flex: 1, padding: '0.5rem', border: '1px solid #e5e7eb', borderRadius: 6 }} /> - diff --git a/packages/create-askable-app/template-nextjs/package.json b/packages/create-askable-app/template-nextjs/package.json index b88a8469..ebad6ec4 100644 --- a/packages/create-askable-app/template-nextjs/package.json +++ b/packages/create-askable-app/template-nextjs/package.json @@ -5,17 +5,23 @@ "scripts": { "dev": "next dev", "build": "next build", + "typecheck": "tsc --noEmit", + "test": "node --test test/*.test.mjs", "start": "next start" }, "dependencies": { "@askable-ui/react": "^__ASKABLE_VERSION__", - "ai": "^4.3.15", - "next": "^15.3.3", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "@ai-sdk/anthropic": "^3.0.118", + "@ai-sdk/react": "^3.0.289", + "ai": "^6.0.286", + "next": "~15.5.25", + "react": "~19.1.9", + "react-dom": "~19.1.9" + }, + "overrides": { + "next": { "postcss": "^8.5.28" } }, "devDependencies": { - "@ai-sdk/anthropic": "^1.2.12", "@types/node": "^22.15.21", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", diff --git a/packages/create-askable-app/template-nextjs/test/chat.test.mjs b/packages/create-askable-app/template-nextjs/test/chat.test.mjs new file mode 100644 index 00000000..5a4f31e9 --- /dev/null +++ b/packages/create-askable-app/template-nextjs/test/chat.test.mjs @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { after, beforeEach, mock, test } from 'node:test'; +import { DefaultChatTransport, readUIMessageStream } from 'ai'; + +// No provider request is ever forwarded to the network. +const MODEL = 'claude-haiku-4-5-20251001'; +const PROVIDER_URL = 'https://api.anthropic.com/v1/messages'; +const keyName = 'ANTHROPIC_API_KEY'; +const previousKey = process.env[keyName]; +process.env[keyName] = 'test-only-not-a-real-key'; +after(() => { + if (previousKey === undefined) delete process.env[keyName]; + else process.env[keyName] = previousKey; +}); + +const requests = []; +let providerError = false; +mock.method(globalThis, 'fetch', async (url, options) => { + assert.equal(String(url), PROVIDER_URL, 'unexpected outbound request blocked'); + assert.equal(options.method, 'POST'); + const body = JSON.parse(options.body); + assert.equal(body.model, MODEL); + assert.equal(body.stream, true); + requests.push(body); + if (providerError) { + return Response.json({ error: { type: 'authentication_error', message: 'private-provider-detail' } }, { status: 401 }); + } + const events = [ + { type: 'message_start', message: { id: 'mock-message', model: MODEL, role: 'assistant', usage: { input_tokens: 1, output_tokens: 0 } } }, + { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, + ...['Hello ', 'dashboard'].map((text) => ({ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } })), + { type: 'content_block_stop', index: 0 }, + { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 2 } }, + { type: 'message_stop' }, + ].map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(''); + return new Response(events, { headers: { 'Content-Type': 'text/event-stream' } }); +}); + +const { POST } = await import('../app/api/chat/route.ts'); +assert.equal(requests.length, 0, 'route imports must not invoke a provider'); + +beforeEach(() => { + requests.length = 0; + providerError = false; +}); + +const userMessage = (id, text) => ({ id, role: 'user', parts: [{ type: 'text', text }] }); +const textOf = (message) => typeof message.content === 'string' + ? message.content + : message.content.filter((part) => part.type === 'text').map((part) => part.text).join(''); +const systemOf = (request) => request.system.map((part) => part.text).join('\n'); +const conversationOf = (request) => request.messages.filter((message) => message.role !== 'system') + .map((message) => ({ role: message.role, text: textOf(message) })); + +async function send(messages, uiContext) { + const transport = new DefaultChatTransport({ + api: '/api/chat', + fetch: async (url, options) => { + assert.equal(url, '/api/chat'); + const response = await POST(new Request('http://example.test/api/chat', options)); + assert.match(response.headers.get('content-type'), /text\/event-stream/); + assert.equal(response.headers.get('x-vercel-ai-ui-message-stream'), 'v1'); + return response; + }, + }); + const stream = await transport.sendMessages({ + chatId: 'test-chat', + trigger: 'submit-message', + messages, + body: { uiContext }, + }); + let message; + for await (const update of readUIMessageStream({ stream, terminateOnError: true })) message = update; + return message; +} + +test('UI-message stream round trips through the installed provider and SDK transport', async () => { + const first = userMessage('first', 'What is focused?'); + first.parts.push({ type: 'text', text: ' Explain briefly.' }); + const answer = await send([first], 'Revenue: $2.4M'); + assert.equal(answer.role, 'assistant'); + assert.equal(answer.parts.filter((part) => part.type === 'text').map((part) => part.text).join(''), 'Hello dashboard'); + assert.ok(answer.id); + assert.equal(requests.length, 1); + assert.match(systemOf(requests[0]), /Revenue: \$2.4M/); + assert.deepEqual(conversationOf(requests[0]), [{ role: 'user', text: 'What is focused? Explain briefly.' }]); + + const second = await send([first, answer, userMessage('second', 'And now?')], 'Churn: 2.1%'); + assert.notEqual(second.id, answer.id); + assert.equal(requests.length, 2); + assert.match(systemOf(requests[1]), /Churn: 2.1%/); + assert.doesNotMatch(systemOf(requests[1]), /Revenue: \$2.4M/); + assert.deepEqual(conversationOf(requests[1]), [ + { role: 'user', text: 'What is focused? Explain briefly.' }, + { role: 'assistant', text: 'Hello dashboard' }, + { role: 'user', text: 'And now?' }, + ]); +}); + +test('missing context still allows a text conversation', async () => { + const answer = await send([userMessage('first', 'Hello')]); + assert.equal(answer.role, 'assistant'); + assert.doesNotMatch(systemOf(requests[0]), /undefined/); +}); + +test('provider errors are masked in the UI stream', async (t) => { + providerError = true; + t.mock.method(console, 'error', () => {}); + await assert.rejects(send([userMessage('first', 'Hello')]), (error) => { + assert.doesNotMatch(error.message, /private-provider-detail|test-only-not-a-real-key/); + return true; + }); + assert.equal(requests.length, 1); +}); + +test('malformed JSON is rejected before any provider request', async () => { + await assert.rejects(POST(new Request('http://example.test/api/chat', { method: 'POST', body: '{' }))); + assert.equal(requests.length, 0); +}); diff --git a/packages/create-askable-app/template-nextjs/tsconfig.json b/packages/create-askable-app/template-nextjs/tsconfig.json new file mode 100644 index 00000000..a514bbb1 --- /dev/null +++ b/packages/create-askable-app/template-nextjs/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 252ba2d4036afccd919828757fc0f6b9a6554ba8 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:10:38 -0400 Subject: [PATCH 2/4] docs(examples): focus SDK migration notes on maintenance guidance --- examples/vercel-ai-sdk/SECURITY-MIGRATION.md | 37 +++----------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/examples/vercel-ai-sdk/SECURITY-MIGRATION.md b/examples/vercel-ai-sdk/SECURITY-MIGRATION.md index 81847fa4..db7b9ec8 100644 --- a/examples/vercel-ai-sdk/SECURITY-MIGRATION.md +++ b/examples/vercel-ai-sdk/SECURITY-MIGRATION.md @@ -6,8 +6,8 @@ was changed. ## Version decision -The current stable AI SDK is 7, but this migration deliberately uses the -smallest audited-safe major from the requested 5-or-6 choices. +This migration uses the maintained AI SDK 6 line to address dependency +findings without also adopting the latest SDK 7 major. - Vercel still publishes an `ai-v5` line: `ai@5.0.261` was released on September 18. However, a clean install with its matching v2 providers brought @@ -113,7 +113,8 @@ no stored checkout credentials, no provider secrets, and no inference step. ## Limitations -- No live model calls, credentials, remote CI run, or deployment was used. +- Local validation uses mocked model responses, not live inference credentials. + The workflow repeats the install, test, build, and audit checks in hosted CI. - The npm audit result is a point-in-time dependency check, not a guarantee that the application has no security issues. Other repository dependencies and remote Dependabot alert states are outside this change. @@ -122,33 +123,3 @@ no stored checkout credentials, no provider secrets, and no inference step. - Next.js emits a harmless multiple-lockfile root inference warning for the in-repository examples and an Edge static-generation warning where applicable. Node emits a module-type detection warning when directly testing route files. -- Root manifests/lockfiles, release versions, and other agents' files are - untouched. No remote push, PR, merge, or alert dismissal is part of this work. - -## Changed files - -Paths below are relative to the repository root: - -```text -.github/workflows/test_ai_examples.yml -examples/nextjs-app-router/.gitignore -examples/nextjs-app-router/app/api/chat/route.ts -examples/nextjs-app-router/package-lock.json -examples/nextjs-app-router/package.json -examples/nextjs-app-router/test/chat.test.mjs -examples/nextjs-app-router/tsconfig.json -examples/vercel-ai-sdk/.gitignore -examples/vercel-ai-sdk/README.md -examples/vercel-ai-sdk/SECURITY-MIGRATION.md -examples/vercel-ai-sdk/app/api/chat/route.ts -examples/vercel-ai-sdk/app/page.tsx -examples/vercel-ai-sdk/package-lock.json -examples/vercel-ai-sdk/package.json -examples/vercel-ai-sdk/test/chat.test.mjs -packages/create-askable-app/template-nextjs/_gitignore -packages/create-askable-app/template-nextjs/app/api/chat/route.ts -packages/create-askable-app/template-nextjs/app/page.tsx -packages/create-askable-app/template-nextjs/package.json -packages/create-askable-app/template-nextjs/test/chat.test.mjs -packages/create-askable-app/template-nextjs/tsconfig.json -``` From 09c74c6d0f8497445e9c574a9c29f79be829b837 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:20:20 -0400 Subject: [PATCH 3/4] fix(create-app): isolate scaffold tests from generated app suites --- packages/create-askable-app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-askable-app/package.json b/packages/create-askable-app/package.json index ee180eeb..ffc8c5c8 100644 --- a/packages/create-askable-app/package.json +++ b/packages/create-askable-app/package.json @@ -40,6 +40,6 @@ "homepage": "https://askable-ui.com", "license": "MIT", "scripts": { - "test": "node --test" + "test": "node --test test/*.test.js" } } From 15e5a715cad8347c10c8e5467dc6fb0681114af9 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:30:23 -0400 Subject: [PATCH 4/4] docs(examples): align chat integrations with AI SDK 6 --- site/docs/examples/ai-sdk.md | 323 ++++++++++++++++++++------------ site/docs/examples/dashboard.md | 210 ++++++++++++--------- 2 files changed, 330 insertions(+), 203 deletions(-) diff --git a/site/docs/examples/ai-sdk.md b/site/docs/examples/ai-sdk.md index bf7aa3ca..e372469c 100644 --- a/site/docs/examples/ai-sdk.md +++ b/site/docs/examples/ai-sdk.md @@ -4,53 +4,82 @@ Askable works with any LLM SDK. Here are drop-in patterns for the most common on ## Vercel AI SDK +These examples target AI SDK 6: `ai@^6.0.286`, `@ai-sdk/openai@^3.0.114`, and the client package for your framework: `@ai-sdk/react@^3.0.289`, `@ai-sdk/vue@^3.0.286`, or `@ai-sdk/svelte@^4.0.286`. Keep provider credentials on the server. + +All three clients use `DefaultChatTransport` and the same `/api/chat` endpoint. The endpoint accepts `UIMessage[]`, awaits `convertToModelMessages`, and returns a UI-message SSE stream, not plain text or a JSON response. Vue uses the SDK's `Chat` class, not `useChat`. The Svelte example requires **Svelte 5.31+**; it is not a Svelte 4 example. See the SDK's [Vue client source](https://github.com/vercel/ai/blob/ai%406.0.286/packages/vue/src/chat.vue.ts) and [Svelte client source](https://github.com/vercel/ai/blob/ai%406.0.286/packages/svelte/src/chat.svelte.ts). + +Read context inside the send handler and pass it in `sendMessage`'s second argument. That captures the current focus and history for each request, including immediately after `ctx.select()` or `ctx.push()`. + ::: code-group ```ts [API route] // app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; -import { streamText } from 'ai'; +import { convertToModelMessages, generateId, streamText, type UIMessage } from 'ai'; export async function POST(req: Request) { - const { messages, uiContext, historyContext } = await req.json(); + const { messages, uiContext, historyContext } = await req.json() as { + messages: UIMessage[]; + uiContext?: string; + historyContext?: string; + }; const systemParts = ['You are a helpful UI assistant.']; if (uiContext) systemParts.push(`Current UI context:\n${uiContext}`); if (historyContext) systemParts.push(`Recent interactions:\n${historyContext}`); const result = streamText({ - model: openai('gpt-4o'), + model: openai.chat('gpt-4o-mini'), system: systemParts.join('\n\n'), - messages, + messages: await convertToModelMessages(messages), }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse({ + originalMessages: messages, + generateMessageId: generateId, + }); } ``` ```tsx [React client] 'use client'; -import { useChat } from 'ai/react'; +import { useState, type FormEvent } from 'react'; +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; import { useAskable } from '@askable-ui/react'; export function Chat() { - const { ctx, promptContext } = useAskable(); - - const { messages, input, handleInputChange, handleSubmit } = useChat({ - api: '/api/chat', - body: { - uiContext: promptContext, - historyContext: ctx.toHistoryContext(5), - }, + const { ctx } = useAskable(); + const [input, setInput] = useState(''); + const { messages, sendMessage, status, error, stop } = useChat({ + transport: new DefaultChatTransport({ api: '/api/chat' }), }); + const isLoading = status === 'submitted' || status === 'streaming'; + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (!input.trim() || isLoading) return; + void sendMessage({ text: input.trim() }, { + body: { + uiContext: ctx.toPromptContext(), + historyContext: ctx.toHistoryContext(5), + }, + }); + setInput(''); + } return (
{messages.map((m) => ( -
{m.content}
+
+ {m.parts.map((part) => part.type === 'text' ? part.text : null)} +
))} - - + {isLoading &&

Receiving response...

} + {error &&

Unable to complete the response. Please try again.

} + setInput(event.target.value)} disabled={isLoading} placeholder="Ask..." /> + + {isLoading && }
); } @@ -58,139 +87,202 @@ export function Chat() { ```vue [Vue client] ``` -```svelte [Svelte client] +```svelte [Svelte 5 client] -
- {#each messages as m (m.id)} -
{m.content}
+ + {#each chat.messages as m (m.id)} +
+ {#each m.parts as part} + {#if part.type === 'text'}{part.text}{/if} + {/each} +
{/each} - - + {#if isLoading}

Receiving response...

{/if} + {#if chat.error}

Unable to complete the response. Please try again.

{/if} + + + {#if isLoading}{/if}
``` ::: +The snippets render text parts only; add renderers before using tools, files, or other message parts. The server's type assertion documents the payload, but does not validate untrusted JSON. Add authentication, runtime message/context validation, request-size limits, and rate limits before deployment. Treat UI context as untrusted data, not instructions or authorization. + ### Streaming updates during generation -When a response is already streaming, you can keep the server-side session fresh by subscribing to Askable context changes and posting debounced updates tied to the active chat request. +The ordinary route above takes one context snapshot per request. Changing focus does **not** retroactively change the model call already generating tokens. For a custom multi-step agent, you can post debounced context updates for later model/tool steps. This requires an application-owned `/api/chat/context` endpoint and server-side session storage; neither is supplied by the route above or by Askable. + +This complete client assigns a request ID in the send handler, before dispatch. It subscribes only while streaming, checks update responses, and cleans up pending updates on completion, failure, cancellation, or unmount. There is no SDK 6 `onResponse` callback involved. ```tsx 'use client'; -import { useEffect, useMemo, useRef } from 'react'; -import { useChat } from 'ai/react'; +import { useEffect, useRef, useState, type FormEvent } from 'react'; +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; import { useAskable } from '@askable-ui/react'; export function StreamingChat() { - const { ctx, promptContext } = useAskable(); + const { ctx } = useAskable(); + const [input, setInput] = useState(''); + const [contextError, setContextError] = useState(false); const requestIdRef = useRef(null); - const chatBody = useMemo(() => ({ - requestId: crypto.randomUUID(), - uiContext: promptContext, - historyContext: ctx.toHistoryContext(5), - }), [ctx, promptContext]); - - const { status, ...chat } = useChat({ - api: '/api/chat', - body: chatBody, - onResponse() { - requestIdRef.current = chatBody.requestId; - }, + const { messages, sendMessage, status, error, stop } = useChat({ + transport: new DefaultChatTransport({ api: '/api/chat' }), onFinish() { requestIdRef.current = null; }, + onError() { + requestIdRef.current = null; + }, }); + const isLoading = status === 'submitted' || status === 'streaming'; + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (!input.trim() || isLoading || requestIdRef.current) return; + const requestId = crypto.randomUUID(); + requestIdRef.current = requestId; + setContextError(false); + void sendMessage({ text: input.trim() }, { + body: { + requestId, + uiContext: ctx.toPromptContext(), + historyContext: ctx.toHistoryContext(5), + }, + }); + setInput(''); + } - useEffect(() => { - if (status !== 'streaming') return; + useEffect(() => () => { + requestIdRef.current = null; + void stop(); + }, [stop]); - return ctx.subscribeAsync(async (context) => { - if (!requestIdRef.current) return; - await fetch('/api/chat/context', { + useEffect(() => { + const requestId = requestIdRef.current; + if (status !== 'streaming' || !requestId) return; + const controller = new AbortController(); + let sequence = 0; + + const unsubscribe = ctx.subscribeAsync(async (context) => { + if (requestIdRef.current !== requestId) return; + const response = await fetch('/api/chat/context', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - requestId: requestIdRef.current, - context, - }), + signal: controller.signal, + body: JSON.stringify({ requestId, sequence: ++sequence, context }), }); + if (!response.ok) throw new Error(`Context update failed: ${response.status}`); }, { history: 5, - sources: [{ id: 'accounts', mode: 'summary', timeoutMs: 750 }], debounce: 100, - onError(error) { - console.error(error); + emitInitial: true, + onError() { + if (!controller.signal.aborted && requestIdRef.current === requestId) { + setContextError(true); + } }, }); + + return () => { + unsubscribe(); + controller.abort(); + }; }, [ctx, status]); - return ; + return ( +
+ {messages.map((m) => ( +
+ {m.parts.map((part) => part.type === 'text' ? part.text : null)} +
+ ))} + {isLoading &&

Receiving response...

} + {error &&

Unable to complete the response. Please try again.

} + {contextError &&

Live context updates failed; later steps may use older context.

} + setInput(event.target.value)} disabled={isLoading} placeholder="Ask..." /> + + {isLoading && } +
+ ); } ``` -On the server, store these incremental updates by `requestId` and let your streaming route read the latest Askable context between model/tool steps. +The custom chat route must register the request before streaming. Authenticate both endpoints, bind `requestId` to the authenticated user, validate and bound `context`, accept only increasing `sequence` values, and reject updates for finished/expired requests. Store updates in shared storage if routes run in separate serverless instances. Do not treat a client-supplied request ID as authorization. + +For an SDK multi-step tool loop, read the latest stored context in [`prepareStep`](https://github.com/vercel/ai/blob/ai%406.0.286/packages/ai/src/generate-text/prepare-step.ts) and return a new `system` value for the **next** step. Configure tools and stopping conditions separately. A single-step `streamText` call cannot consume later updates; stop and start a new request if the current answer must use new context. Aborting the client does not undo an update the server has already accepted. ## Anthropic SDK @@ -296,39 +388,32 @@ export async function POST(req: Request) { } ``` +Using the React client's `ctx` and `sendMessage` above, read both values at the point of sending: + ```ts -// In your client component -const { promptContext, ctx } = useAskable(); - -async function ask(question: string) { - await fetch('/api/chat', { - method: 'POST', - body: JSON.stringify({ - messages: [...], - uiContext: promptContext, +function ask(question: string) { + return sendMessage({ text: question }, { + body: { + uiContext: ctx.toPromptContext(), historyContext: ctx.toHistoryContext(5), - }), + }, }); } ``` ## Structured context with JSON format -When your backend needs to parse or log the context, use `{ format: 'json' }`: +When your backend needs to parse context, use `{ format: 'json' }`. Keep the serialized value in `uiContext` to use the same chat endpoint: ```ts -const { ctx } = useAskable(); - -// Returns '{"meta":{"metric":"revenue"},"text":"Revenue","timestamp":...}' -const structuredContext = ctx.toPromptContext({ format: 'json' }); - -await fetch('/api/chat', { - method: 'POST', - body: JSON.stringify({ - messages: [...], - context: JSON.parse(structuredContext), // parsed object - }), -}); +function askWithStructuredContext(question: string) { + return sendMessage({ text: question }, { + body: { + uiContext: ctx.toPromptContext({ format: 'json' }), + historyContext: ctx.toHistoryContext(5), + }, + }); +} ``` ## Token budget diff --git a/site/docs/examples/dashboard.md b/site/docs/examples/dashboard.md index 0b786a4f..020f30e7 100644 --- a/site/docs/examples/dashboard.md +++ b/site/docs/examples/dashboard.md @@ -9,22 +9,25 @@ The pattern: 1. Annotate each dashboard widget with `data-askable` metadata 2. Track active focus with `useAskable` / `useAskable()` / `createAskableStore()` 3. Add explicit tools for Ask AI buttons, region/circle/lasso capture, and highlighted text when the user needs more precise context than a single widget -4. Inject `promptContext` into every AI request +4. Read current context when sending and pass it in the AI request body 5. Optionally inject `historyContext` or structured Context packets for a conversation-aware assistant +These examples use AI SDK 6 and the shared UI-message streaming route below. Use `ai@^6.0.286` with `@ai-sdk/react@^3.0.289`, `@ai-sdk/vue@^3.0.286`, or `@ai-sdk/svelte@^4.0.286`. The Svelte client requires **Svelte 5.31+**, not Svelte 4. See [AI SDK integration patterns](./ai-sdk) for the package/API details and production validation requirements. + ::: code-group ```tsx [React] // components/Dashboard.tsx 'use client'; -import { useRef, useState } from 'react'; +import { useRef, useState, type FormEvent } from 'react'; import { Askable, useAskable, useAskableRegionCapture, useAskableTextSelectionCapture, } from '@askable-ui/react'; -import { useChat } from 'ai/react'; +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; const widgets = [ { id: 'revenue', label: 'Revenue', value: '$2.3M', delta: '+12%', period: 'Q3 2024' }, @@ -33,8 +36,9 @@ const widgets = [ ]; export function Dashboard() { - const { ctx, promptContext } = useAskable(); + const { ctx } = useAskable(); const [chatOpen, setChatOpen] = useState(false); + const [input, setInput] = useState(''); const refs = useRef>({}); const region = useAskableRegionCapture({ ctx, @@ -53,13 +57,22 @@ export function Dashboard() { }, }); - // Include last 5 interactions so the AI can answer follow-up questions - const historyContext = ctx.toHistoryContext(5); - - const { messages, input, handleInputChange, handleSubmit } = useChat({ - api: '/api/chat', - body: { uiContext: promptContext, historyContext }, + const { messages, sendMessage, status, error, stop } = useChat({ + transport: new DefaultChatTransport({ api: '/api/chat' }), }); + const isLoading = status === 'submitted' || status === 'streaming'; + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (!input.trim() || isLoading) return; + void sendMessage({ text: input.trim() }, { + body: { + uiContext: ctx.toPromptContext(), + historyContext: ctx.toHistoryContext(5), + }, + }); + setInput(''); + } return (
@@ -96,19 +109,25 @@ export function Dashboard() { {chatOpen && ( )} @@ -120,9 +139,10 @@ export function Dashboard() { ```vue [Vue] @@ -237,29 +264,35 @@ function selectWidget(id: string) {
{#each widgets as w (w.id)}

{w.label}

{w.value}

{w.delta}

- +
{/each}
{#if chatOpen} {/if} @@ -270,15 +303,19 @@ function selectWidget(id: string) { ## API route -All three examples above POST to `/api/chat` with `uiContext` and `historyContext`. Here's a Next.js App Router handler using the Vercel AI SDK: +All three examples above POST SDK `UIMessage[]` to `/api/chat` with current `uiContext` and `historyContext`. The transport decodes the UI-message SSE response and updates `messages`, `status`, and `error`; do not parse it with `res.json()` or concatenate the raw response bytes as assistant text. Here's a Next.js App Router handler using `@ai-sdk/openai@^3.0.114` (set `OPENAI_API_KEY` on the server): ```ts // app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; -import { streamText } from 'ai'; +import { convertToModelMessages, generateId, streamText, type UIMessage } from 'ai'; export async function POST(req: Request) { - const { messages, uiContext, historyContext } = await req.json(); + const { messages, uiContext, historyContext } = await req.json() as { + messages: UIMessage[]; + uiContext?: string; + historyContext?: string; + }; const systemParts = [ 'You are a helpful analytics assistant. Answer questions about the metrics the user is asking about.', @@ -288,18 +325,23 @@ export async function POST(req: Request) { if (historyContext) systemParts.push(`\nRecent interactions:\n${historyContext}`); const result = streamText({ - model: openai('gpt-4o'), + model: openai.chat('gpt-4o-mini'), system: systemParts.join('\n'), - messages, + messages: await convertToModelMessages(messages), }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse({ + originalMessages: messages, + generateMessageId: generateId, + }); } ``` +The type assertion is not runtime validation. Authenticate requests, validate and limit messages/context, and apply rate limits before deployment. Context is untrusted data, not an authorization decision. These clients render text parts only. + ## Passive hover tracking -If you prefer not to add "Ask AI" buttons, enable passive hover tracking instead. The AI always receives context for whatever the user is currently hovering over: +If you prefer not to add "Ask AI" buttons, enable passive hover tracking instead. Each new request reads context for the most recently tracked element: ```ts // One-time setup — usually at app root @@ -309,4 +351,4 @@ ctx.observe(document, { }); ``` -With this setup, `promptContext` / `ctx.toPromptContext()` always reflects the last interacted element — no explicit button click needed. +With this setup, `promptContext` / `ctx.toPromptContext()` reflects the last tracked element after the hover debounce; no explicit button click is needed. Moving the pointer while an answer streams does not change the context already sent to the active model call. See [streaming updates during generation](./ai-sdk#streaming-updates-during-generation) for an application-owned multi-step update pattern.