diff --git a/.env.local.example b/.env.local.example index 487e3f6f..18ce7ccd 100644 --- a/.env.local.example +++ b/.env.local.example @@ -6,6 +6,9 @@ NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ +# Arcjet related environment variables +ARCJET_KEY=ajkey_**** + # OpenAI related environment variables OPENAI_API_KEY=sk-**** diff --git a/README.md b/README.md index 36fc2a65..1efdac3b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - Text Model: [OpenAI](https://platform.openai.com/docs/models) - Text streaming: [ai sdk](https://github.com/vercel-labs/ai) - Deployment: [Fly](https://fly.io/) +- Security: [Arcjet](https://arcjet.com/) ## Overview - 🚀 [Quickstart](#quickstart) @@ -76,6 +77,10 @@ e. **Supabase API key** - `SUPABASE_PRIVATE_KEY` is the key starts with `ey` under Project API Keys - Now, you should enable pgvector on Supabase and create a schema. You can do this easily by clicking on "SQL editor" on the left hand side on supabase UI and then clicking on "+New Query". Copy paste [this code snippet](https://github.com/a16z-infra/ai-getting-started/blob/main/pgvector.sql) in the SQL editor and click "Run". +f. **Arcjet key** + +Visit https://app.arcjet.com to sign up for free and get your Arcjet key. + ### 4. Generate embeddings There are a few markdown files under `/blogs` directory as examples so you can do Q&A on them. To generate embeddings and store them in the vector database for future queries, you can run the following command: diff --git a/package-lock.json b/package-lock.json index 6937e0b5..d1c96871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "ai-getting-started", "version": "0.0.0", "dependencies": { + "@arcjet/next": "^1.0.0-alpha.13", "@clerk/nextjs": "^4.21.9-snapshot.56dc3e", "@headlessui/react": "^1.7.15", "@pinecone-database/pinecone": "^0.1.6", @@ -56,6 +57,70 @@ "cross-fetch": "^3.1.5" } }, + "node_modules/@arcjet/analyze": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/@arcjet/analyze/-/analyze-1.0.0-alpha.13.tgz", + "integrity": "sha512-k1YhBlJfib8rvU5iQhvNqQgkf7Ty7HqMVt8SwpYhXWG5ziBYs8WdOuTZvZp04ZhY7iPx3PVZOloeZY6/Yx/kpQ==", + "dependencies": { + "@arcjet/logger": "1.0.0-alpha.13" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@arcjet/duration": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/@arcjet/duration/-/duration-1.0.0-alpha.13.tgz", + "integrity": "sha512-347HXBupvpwYvSyBZ5B9MMuRw1Ch59MpXbvMrefNSpqYIwy+ETvOQaK2fh9KoVmLVR9AKj+dGH7Miw02Tu3f0Q==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@arcjet/ip": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/@arcjet/ip/-/ip-1.0.0-alpha.13.tgz", + "integrity": "sha512-h+ZANLBCpwcQ/ugcxu6cOtgZBRu0rtILWcB1VnRzOPO4eGU61Km2cGVwHccmmi0O+foyt4q6TF5/KNNeQqBebQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@arcjet/logger": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/@arcjet/logger/-/logger-1.0.0-alpha.13.tgz", + "integrity": "sha512-QyRcbdbQ4DUAvUFhpq/juRqKJZR3u52ptnr9Rmn8R0L5CLpfua3zCHHF6HiB8Pq3Pe/V+WjFwtu44nUXSv+Yeg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@arcjet/next": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/@arcjet/next/-/next-1.0.0-alpha.13.tgz", + "integrity": "sha512-94yVhsF0rfaWa4OM6wcJxdXucGxCOpjlxTuXFYhGjjtUfDoI2O+aeVEmqyZC8fMh6TbR7NqVHPSY1NYmAs8gZw==", + "dependencies": { + "@arcjet/ip": "1.0.0-alpha.13", + "@connectrpc/connect-web": "1.4.0", + "arcjet": "1.0.0-alpha.13" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "next": ">=13" + } + }, + "node_modules/@arcjet/protocol": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/@arcjet/protocol/-/protocol-1.0.0-alpha.13.tgz", + "integrity": "sha512-nAb+xrgZDHbyS0Pl1j6UtAlV1Pi1eZ1sR1JDOYWR98Oshx+Iyn0I9Er7Co2hMdBigUjf4NZ5Id90LfVD0brW6Q==", + "dependencies": { + "@bufbuild/protobuf": "1.9.0", + "@connectrpc/connect": "1.4.0", + "typeid-js": "0.7.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@babel/parser": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", @@ -79,6 +144,11 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.9.0.tgz", + "integrity": "sha512-W7gp8Q/v1NlCZLsv8pQ3Y0uCu/SHgXOVFK+eUluUKWXmsb6VHkpNx0apdOWWcDbB9sJoKeP8uPrjmehJz6xETQ==" + }, "node_modules/@clerk/backend": { "version": "0.23.6", "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-0.23.6.tgz", @@ -219,6 +289,23 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, + "node_modules/@connectrpc/connect": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.4.0.tgz", + "integrity": "sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==", + "peerDependencies": { + "@bufbuild/protobuf": "^1.4.2" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.4.0.tgz", + "integrity": "sha512-13aO4psFbbm7rdOFGV0De2Za64DY/acMspgloDlcOKzLPPs0yZkhp1OOzAQeiAIr7BM/VOHIA3p8mF0inxCYTA==", + "peerDependencies": { + "@bufbuild/protobuf": "^1.4.2", + "@connectrpc/connect": "1.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1215,6 +1302,20 @@ "node": ">= 8" } }, + "node_modules/arcjet": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/arcjet/-/arcjet-1.0.0-alpha.13.tgz", + "integrity": "sha512-GRsUziHAs7YrrmPUCOYB5EY9ZDRMLH81bDC/zQX3canIlfRs069E8PmQa39hVIbrHEMck6jhXWdzqTp561ppDw==", + "dependencies": { + "@arcjet/analyze": "1.0.0-alpha.13", + "@arcjet/duration": "1.0.0-alpha.13", + "@arcjet/logger": "1.0.0-alpha.13", + "@arcjet/protocol": "1.0.0-alpha.13" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -5889,6 +5990,14 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typeid-js": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/typeid-js/-/typeid-js-0.7.0.tgz", + "integrity": "sha512-eNkOAh7Z7SIGwgdg2BM5n79QhJqHMNMm6kWYJAsIf3OqBLDEO5UeNLknBXh9+U6zel1zC5z2l0cwCswIy+GWyw==", + "dependencies": { + "uuidv7": "^0.6.2" + } + }, "node_modules/typescript": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", @@ -5993,6 +6102,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuidv7": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/uuidv7/-/uuidv7-0.6.3.tgz", + "integrity": "sha512-zV3eW2NlXTsun/aJ7AixxZjH/byQcH/r3J99MI0dDEkU2cJIBJxhEWUHDTpOaLPRNhebPZoeHuykYREkI9HafA==", + "bin": { + "uuidv7": "cli.js" + } + }, "node_modules/vue": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", diff --git a/package.json b/package.json index c519b6d9..cb85db1e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "generate-embeddings-supabase": "node src/scripts/indexBlogPGVector.mjs" }, "dependencies": { + "@arcjet/next": "^1.0.0-alpha.13", "@clerk/nextjs": "^4.21.9-snapshot.56dc3e", "@headlessui/react": "^1.7.15", "@pinecone-database/pinecone": "^0.1.6", diff --git a/src/app/api/qa-pg-vector/route.ts b/src/app/api/qa-pg-vector/route.ts index ce5dc568..f176a8f1 100644 --- a/src/app/api/qa-pg-vector/route.ts +++ b/src/app/api/qa-pg-vector/route.ts @@ -6,10 +6,79 @@ import dotenv from "dotenv"; import { VectorDBQAChain } from "langchain/chains"; import { StreamingTextResponse, LangChainStream } from "ai"; import { CallbackManager } from "langchain/callbacks"; +import { currentUser } from "@clerk/nextjs"; +import arcjet, { shield, fixedWindow, detectBot } from "@arcjet/next"; +import { NextResponse } from "next/server"; dotenv.config({ path: `.env.local` }); +// The arcjet instance is created outside of the handler +const aj = arcjet({ + key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com + rules: [ + // Arcjet Shield protects against common attacks e.g. SQL injection + shield({ + mode: "LIVE", + }), + // Create a fixed window rate limit. Other algorithms are supported. + fixedWindow({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + characteristics: ["userId"], // Rate limit based on the Clerk userId + window: "60s", // 60 second fixed window + max: 10, // allow a maximum of 10 requests + }), + // Blocks all automated clients + detectBot({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + block: ["AUTOMATED"], + }), + ], +}); + export async function POST(req: Request) { + // Get the current user from Clerk + const user = await currentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Use Arcjet to protect the route + const decision = await aj.protect(req, { userId: user.id }); + + if (decision.isDenied()) { + if (decision.reason.isRateLimit()) { + return NextResponse.json( + { + error: "Too Many Requests", + reason: decision.reason, + }, + { + status: 429, + }, + ); + } else if (decision.reason.isBot()) { + return NextResponse.json( + { + error: "Bots are not allowed", + reason: decision.reason, + }, + { + status: 403, + }, + ); + } else { + return NextResponse.json( + { + error: "Unauthorized", + reason: decision.reason, + }, + { + status: 401, + }, + ); + } + } + const { prompt } = await req.json(); const privateKey = process.env.SUPABASE_PRIVATE_KEY; @@ -31,7 +100,7 @@ export async function POST(req: Request) { client, tableName: "documents", queryName: "match_documents", - } + }, ); const { stream, handlers } = LangChainStream(); diff --git a/src/app/api/qa-pinecone/route.ts b/src/app/api/qa-pinecone/route.ts index 4ef68338..667d1706 100644 --- a/src/app/api/qa-pinecone/route.ts +++ b/src/app/api/qa-pinecone/route.ts @@ -6,10 +6,79 @@ import { OpenAI } from "langchain/llms/openai"; import { PineconeStore } from "langchain/vectorstores/pinecone"; import { StreamingTextResponse, LangChainStream } from "ai"; import { CallbackManager } from "langchain/callbacks"; +import { currentUser } from "@clerk/nextjs"; +import arcjet, { shield, fixedWindow, detectBot } from "@arcjet/next"; +import { NextResponse } from "next/server"; dotenv.config({ path: `.env.local` }); +// The arcjet instance is created outside of the handler +const aj = arcjet({ + key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com + rules: [ + // Arcjet Shield protects against common attacks e.g. SQL injection + shield({ + mode: "LIVE", + }), + // Create a fixed window rate limit. Other algorithms are supported. + fixedWindow({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + characteristics: ["userId"], // Rate limit based on the Clerk userId + window: "60s", // 60 second fixed window + max: 10, // allow a maximum of 10 requests + }), + // Blocks all automated clients + detectBot({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + block: ["AUTOMATED"], + }), + ], +}); + export async function POST(request: Request) { + // Get the current user from Clerk + const user = await currentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Use Arcjet to protect the route + const decision = await aj.protect(request, { userId: user.id }); + + if (decision.isDenied()) { + if (decision.reason.isRateLimit()) { + return NextResponse.json( + { + error: "Too Many Requests", + reason: decision.reason, + }, + { + status: 429, + }, + ); + } else if (decision.reason.isBot()) { + return NextResponse.json( + { + error: "Bots are not allowed", + reason: decision.reason, + }, + { + status: 403, + }, + ); + } else { + return NextResponse.json( + { + error: "Unauthorized", + reason: decision.reason, + }, + { + status: 401, + }, + ); + } + } + const { prompt } = await request.json(); const client = new PineconeClient(); await client.init({ @@ -20,7 +89,7 @@ export async function POST(request: Request) { const vectorStore = await PineconeStore.fromExistingIndex( new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY }), - { pineconeIndex } + { pineconeIndex }, ); const { stream, handlers } = LangChainStream(); diff --git a/src/app/api/txt2img/route.ts b/src/app/api/txt2img/route.ts index 4a09895d..c5577225 100644 --- a/src/app/api/txt2img/route.ts +++ b/src/app/api/txt2img/route.ts @@ -1,27 +1,93 @@ import dotenv from "dotenv"; import Replicate from "replicate"; import { NextResponse } from "next/server"; +import { currentUser } from "@clerk/nextjs"; +import arcjet, { shield, fixedWindow, detectBot } from "@arcjet/next"; dotenv.config({ path: `.env.local` }); +// The arcjet instance is created outside of the handler +const aj = arcjet({ + key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com + rules: [ + // Arcjet Shield protects against common attacks e.g. SQL injection + shield({ + mode: "LIVE", + }), + // Create a fixed window rate limit. Other algorithms are supported. + fixedWindow({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + characteristics: ["userId"], // Rate limit based on the Clerk userId + window: "60s", // 60 second fixed window + max: 10, // allow a maximum of 10 requests + }), + // Blocks all automated clients + detectBot({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + block: ["AUTOMATED"], + }), + ], +}); + export async function POST(request: Request) { + // Get the current user from Clerk + const user = await currentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Use Arcjet to protect the route + const decision = await aj.protect(request, { userId: user.id }); + + if (decision.isDenied()) { + if (decision.reason.isRateLimit()) { + return NextResponse.json( + { + error: "Too Many Requests", + reason: decision.reason, + }, + { + status: 429, + }, + ); + } else if (decision.reason.isBot()) { + return NextResponse.json( + { + error: "Bots are not allowed", + reason: decision.reason, + }, + { + status: 403, + }, + ); + } else { + return NextResponse.json( + { + error: "Unauthorized", + reason: decision.reason, + }, + { + status: 401, + }, + ); + } + } + const { prompt } = await request.json(); const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN || "", }); - try{ + try { const output = await replicate.run( "stability-ai/stable-diffusion:db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf", { input: { prompt, }, - } + }, ); return NextResponse.json(output); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); } - catch(error:any){ - return NextResponse.json({error:error.message},{status:500}); - } - }