From 786378e4a50be6962d50e9abcd0dade65b50e072 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 20 Mar 2024 18:57:19 -0500 Subject: [PATCH] Add AI rate limiting and safety documentation --- .vscode/settings.json | 4 +++ README.md | 17 ++++++++-- package.json | 2 ++ pnpm-lock.yaml | 42 ++++++++++++++++++++++++ src/services/openai.ts | 21 ++++++++++++ src/services/vercel-kv.ts | 10 ++++++ src/site/SiteChecklistClient.tsx | 56 ++++++++++++++++++++++---------- src/site/config.ts | 18 +++++++--- 8 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 src/services/vercel-kv.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index ddaa4892..f3ea8edd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,6 +24,7 @@ "jpgs", "Lightbox", "Makernote", + "mitigations", "nanoids", "nextjs", "parameterizes", @@ -31,6 +32,8 @@ "Provia", "qaub", "QRSTUVWXYZ", + "ratelimit", + "ratelimiter", "Reala", "skippable", "sonner", @@ -38,6 +41,7 @@ "thephotoblog", "trpc", "unnest", + "upstash", "UsKSGcbt", "Velvia", "WRHGZC", diff --git a/README.md b/README.md index 456ddc6d..7d84d313 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,25 @@ Installation 2. Click "Speed Insights" tab 3. Follow "Enable Speed Insights" instructions (`@vercel/speed-insights` already included) -### 7. Optional configuration +### 7. Add experimental AI text generation + +_⚠️ READ BEFORE PROCEEDING_ +- _Usage of this feature will result in fees from OpenAI._ +- _When enabling AI text generation, follow all recommended mitigations in order to avoid unexpected charges and attacks._ +- _Make sure your OpenAI secret key is not prefixed with NEXT_PUBLIC._ + +1. Setup OpenAI + - If you don't already have one, create an [OpenAI](https://openai.com) account + - Generate an API key and store as `OPENAI_SECRET_KEY` + - Setup usage limits to avoid unexpected charges (_recommended_) +2. Add rate limiting (_recommended_) + - As an additional precaution, create a [Vercel KV](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) store and link it to your project in order to enable rate limiting + +### 8. Optional configuration - `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage for jpgs (will result in increased storage usage) - `NEXT_PUBLIC_BLUR_DISABLED = 1` prevents image blur data being stored and displayed (potentially useful for limiting Postgres usage) - `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data -- `OPENAI_SECRET_KEY = [Your Key]` enables experimental support for AI-generated text descriptions - `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order - `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api` - `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo diff --git a/package.json b/package.json index 5c86d0da..73431121 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "@types/react-dom": "18.2.21", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", + "@upstash/ratelimit": "^1.0.1", "@vercel/analytics": "^1.2.2", "@vercel/blob": "^0.22.1", + "@vercel/kv": "^1.0.1", "@vercel/postgres": "0.7.2", "@vercel/speed-insights": "^1.0.10", "ai": "^3.0.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c3c822e..70a1fae3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,12 +47,18 @@ dependencies: '@typescript-eslint/parser': specifier: ^7.2.0 version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@upstash/ratelimit': + specifier: ^1.0.1 + version: 1.0.1 '@vercel/analytics': specifier: ^1.2.2 version: 1.2.2(next@14.1.4)(react@18.2.0) '@vercel/blob': specifier: ^0.22.1 version: 0.22.1 + '@vercel/kv': + specifier: ^1.0.1 + version: 1.0.1 '@vercel/postgres': specifier: 0.7.2 version: 0.7.2 @@ -3110,6 +3116,31 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false + /@upstash/core-analytics@0.0.7: + resolution: {integrity: sha512-lC2j5efqb1haX/fpTGaPUx1rue1WUkOZBVHDzCB7eMIVsRdFFp4xiHtyH/G9omiR1zj39fU5SCTWFiKJH3KOpw==} + engines: {node: '>=16.0.0'} + dependencies: + '@upstash/redis': 1.28.4 + dev: false + + /@upstash/ratelimit@1.0.1: + resolution: {integrity: sha512-G9LZ7idhlkuYknbUngCB3qzd7QnkK1xDkFG5jRtEJZuOUS5UKJ0UTKbhalCtp39eX2wu2Ubv8W7HCeaJQOWM0A==} + dependencies: + '@upstash/core-analytics': 0.0.7 + dev: false + + /@upstash/redis@1.25.1: + resolution: {integrity: sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==} + dependencies: + crypto-js: 4.2.0 + dev: false + + /@upstash/redis@1.28.4: + resolution: {integrity: sha512-UalkSAny/dz1m8giEhD3Y5ru1o+CPHI32wFyS3MyzDzj2TRvEN+lTw+mPwi20ojk0H2gs8TBW3qsrvwuLLy+pA==} + dependencies: + crypto-js: 4.2.0 + dev: false + /@vercel/analytics@1.2.2(next@14.1.4)(react@18.2.0): resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==} peerDependencies: @@ -3136,6 +3167,13 @@ packages: undici: 5.28.3 dev: false + /@vercel/kv@1.0.1: + resolution: {integrity: sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==} + engines: {node: '>=14.6'} + dependencies: + '@upstash/redis': 1.25.1 + dev: false + /@vercel/postgres@0.7.2: resolution: {integrity: sha512-IqR/ZAvoPGcPaXl9eWWB5KaA+w/81RzZa/18P4izQRHpNBkTGt9HwGfYi9+wut5UgxNq4QSX9A7HIQR6QDvX2Q==} engines: {node: '>=14.6'} @@ -3991,6 +4029,10 @@ packages: resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} dev: false + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + dev: false + /css-tree@2.3.1: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} diff --git a/src/services/openai.ts b/src/services/openai.ts index 602d5a02..4ef1661b 100644 --- a/src/services/openai.ts +++ b/src/services/openai.ts @@ -2,13 +2,34 @@ import OpenAI from 'openai'; import { createStreamableValue, render } from 'ai/rsc'; +import { kv } from '@/services/vercel-kv'; +import { Ratelimit } from '@upstash/ratelimit'; + +const RATE_LIMIT_IDENTIFIER = 'openai-image-query'; +const RATE_LIMIT_MAX_QUERIES_PER_HOUR = 100; const provider = new OpenAI({ apiKey: process.env.OPENAI_SECRET_KEY }); +// Allows 100 requests per hour +const ratelimit = kv + ? new Ratelimit({ + redis: kv, + limiter: Ratelimit.slidingWindow(RATE_LIMIT_MAX_QUERIES_PER_HOUR, '1h'), + }) + : undefined; + export const streamOpenAiImageQuery = async ( imageBase64: string, query: string, ) => { + if (ratelimit) { + const { success } = await ratelimit.limit(RATE_LIMIT_IDENTIFIER); + if (!success) { + console.error('OpenAI rate limit exceeded'); + throw new Error('OpenAI rate limit exceeded'); + } + } + const stream = createStreamableValue(''); render({ diff --git a/src/services/vercel-kv.ts b/src/services/vercel-kv.ts new file mode 100644 index 00000000..f6ef92e8 --- /dev/null +++ b/src/services/vercel-kv.ts @@ -0,0 +1,10 @@ +import { createClient } from '@vercel/kv'; + +export const kv = + process.env.REDIS_REST_API_URL && + process.env.REDIS_REST_API_TOKEN + ? createClient({ + url: process.env.REDIS_REST_API_URL, + token: process.env.REDIS_REST_API_TOKEN, + }) + : undefined; diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 1fb9ec0d..87c2442d 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -20,10 +20,12 @@ import { toastSuccess } from '@/toast'; import { ConfigChecklistStatus } from './config'; import StatusIcon from '@/components/StatusIcon'; import { labelForStorage } from '@/services/storage'; +import { HiSparkles } from 'react-icons/hi'; export default function SiteChecklistClient({ - hasPostgres, - hasStorage, + hasVercelPostgres, + hasVercelKV, + hasStorageProvider, hasVercelBlobStorage, hasCloudflareR2Storage, hasAwsS3Storage, @@ -140,7 +142,7 @@ export default function SiteChecklistClient({ > {renderLink( @@ -152,13 +154,13 @@ export default function SiteChecklistClient({ and connect to project {renderSubStatus( @@ -266,6 +268,38 @@ export default function SiteChecklistClient({ {renderEnvVars(['NEXT_PUBLIC_SITE_DOMAIN'])} + } + optional + > + + Store your OpenAI secret key in order to add experimental support + for AI-generated text descriptions and enable an invisible field + called {'"Semantic Description"'} used to support CMD-K search + {renderEnvVars(['OPENAI_SECRET_KEY'])} + + + {renderLink( + // eslint-disable-next-line max-len + 'https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database', + 'Create Vercel KV store', + )} + {' '} + and connect to project in order to enable rate limiting + + } @@ -301,18 +335,6 @@ export default function SiteChecklistClient({ collection/display of location-based data {renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])} - - Store your OpenAI secret key in order to add experimental support - for AI-generated text descriptions and enable an invisible field - called {'"Semantic Description"'} used to support CMD-K search - {renderEnvVars(['OPENAI_SECRET_KEY'])} - 0; + +// STORAGE: VERCEL KV +export const HAS_VERCEL_KV = + (process.env.REDIS_REST_API_URL ?? '').length > 0 && + (process.env.REDIS_REST_API_TOKEN ?? '').length > 0; + // STORAGE: VERCEL BLOB export const HAS_VERCEL_BLOB_STORAGE = (process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0; @@ -102,11 +111,12 @@ export const OG_TEXT_BOTTOM_ALIGNMENT = export const HIGH_DENSITY_GRID = GRID_ASPECT_RATIO <= 1; export const CONFIG_CHECKLIST_STATUS = { - hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0, + hasVercelPostgres: HAS_VERCEL_POSTGRES, + hasVercelKV: HAS_VERCEL_KV, hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE, hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE, hasAwsS3Storage: HAS_AWS_S3_STORAGE, - hasStorage: + hasStorageProvider: HAS_VERCEL_BLOB_STORAGE || HAS_CLOUDFLARE_R2_STORAGE || HAS_AWS_S3_STORAGE, @@ -135,7 +145,7 @@ export const CONFIG_CHECKLIST_STATUS = { export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS; export const IS_SITE_READY = - CONFIG_CHECKLIST_STATUS.hasPostgres && - CONFIG_CHECKLIST_STATUS.hasStorage && + CONFIG_CHECKLIST_STATUS.hasVercelPostgres && + CONFIG_CHECKLIST_STATUS.hasStorageProvider && CONFIG_CHECKLIST_STATUS.hasAuthSecret && CONFIG_CHECKLIST_STATUS.hasAdminUser;