diff --git a/.example.env b/.example.env
index 22563af..8e7ea56 100644
--- a/.example.env
+++ b/.example.env
@@ -8,4 +8,7 @@ S3_UPLOAD_REGION=
DATABASE_URL=
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
-CLERK_SECRET_KEY=
\ No newline at end of file
+CLERK_SECRET_KEY=
+
+UPSTASH_REDIS_REST_URL=
+UPSTASH_REDIS_REST_TOKEN=
diff --git a/app/api/generate-comic/route.ts b/app/api/generate-comic/route.ts
index 45b95bd..008778a 100644
--- a/app/api/generate-comic/route.ts
+++ b/app/api/generate-comic/route.ts
@@ -7,6 +7,7 @@ import {
createPage,
getNextPageNumber,
} from "@/lib/db-actions";
+import { freeTierRateLimit } from "@/lib/rate-limit";
const NEW_MODEL = false;
const IMAGE_MODEL = NEW_MODEL
@@ -122,15 +123,47 @@ export async function POST(request: NextRequest) {
previousContext = "",
} = await request.json();
- console.log("Received request:", { storyId, prompt: prompt?.substring(0, 50), characterImagesCount: characterImages.length, userId });
+ console.log("Received request:", { storyId, prompt: prompt?.substring(0, 50), characterImagesCount: characterImages.length, userId, hasApiKey: !!apiKey });
- if (!prompt || !apiKey) {
+ if (!prompt) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 }
);
}
+ // Determine which API key to use
+ let finalApiKey = apiKey;
+ const isUsingFreeTier = !apiKey;
+
+ if (isUsingFreeTier) {
+ // Using free tier - apply rate limiting
+ const { success, reset } = await freeTierRateLimit.limit(userId);
+
+ if (!success) {
+ const resetDate = new Date(reset);
+ const timeUntilReset = Math.ceil((reset - Date.now()) / (1000 * 60 * 60 * 24)); // days
+
+ return NextResponse.json(
+ {
+ error: `Free tier limit reached. You can generate 1 comic per week. Try again in ${timeUntilReset} day(s), or provide your own API key for unlimited access.`,
+ resetDate: resetDate.toISOString(),
+ isRateLimited: true,
+ },
+ { status: 429 }
+ );
+ }
+
+ // Use default API key for free tier
+ finalApiKey = process.env.TOGETHER_API_KEY_DEFAULT;
+ if (!finalApiKey) {
+ return NextResponse.json(
+ { error: "Server configuration error - default API key not available" },
+ { status: 500 }
+ );
+ }
+ }
+
let page;
let story;
@@ -235,9 +268,9 @@ COMPOSITION:
const fullPrompt = `${systemPrompt}\n\nSTORY:\n${prompt}`;
- console.log("Generating comic with prompt length:", fullPrompt.length);
+ console.log("Generating comic with prompt length:", fullPrompt.length, "using tier:", isUsingFreeTier ? "free" : "paid");
- const client = new Together({ apiKey });
+ const client = new Together({ apiKey: finalApiKey });
let response;
try {
diff --git a/clear-db.ts b/clear-db.ts
new file mode 100644
index 0000000..e237c68
--- /dev/null
+++ b/clear-db.ts
@@ -0,0 +1,20 @@
+import { db } from './lib/db';
+import { stories, pages } from './lib/schema';
+
+async function clearDatabase() {
+ try {
+ console.log('Clearing all pages...');
+ await db.delete(pages);
+
+ console.log('Clearing all stories...');
+ await db.delete(stories);
+
+ console.log('Database cleared successfully!');
+ } catch (error) {
+ console.error('Error clearing database:', error);
+ } finally {
+ process.exit(0);
+ }
+}
+
+clearDatabase();
\ No newline at end of file
diff --git a/components/api-key-modal.tsx b/components/api-key-modal.tsx
index 14e1eb3..e4dcc0e 100644
--- a/components/api-key-modal.tsx
+++ b/components/api-key-modal.tsx
@@ -113,7 +113,7 @@ export function ApiKeyModal({ isOpen, onClose, onSubmit }: ApiKeyModalProps) {
- Your API key is stored locally and never sent to our servers.
+ Your API key is stored locally and never stored on our servers.
diff --git a/components/landing/create-button.tsx b/components/landing/create-button.tsx
index 5aa1838..29cb65a 100644
--- a/components/landing/create-button.tsx
+++ b/components/landing/create-button.tsx
@@ -1,44 +1,66 @@
-"use client"
+"use client";
-import { useState, useEffect } from "react"
-import { useRouter } from "next/navigation"
-import { ArrowRight, Loader2 } from "lucide-react"
-import { Button } from "@/components/ui/button"
-import { useToast } from "@/hooks/use-toast"
-import { useS3Upload } from "next-s3-upload"
-import { useAuth, SignInButton } from "@clerk/nextjs"
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { ArrowRight, Loader2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useToast } from "@/hooks/use-toast";
+import { useS3Upload } from "next-s3-upload";
+import { useAuth, SignInButton } from "@clerk/nextjs";
interface CreateButtonProps {
- prompt: string
- style: string
- characterFiles: File[]
+ prompt: string;
+ style: string;
+ characterFiles: File[];
}
-export function CreateButton({ prompt, style, characterFiles }: CreateButtonProps) {
- const router = useRouter()
- const [isLoading, setIsLoading] = useState(false)
- const [loadingStep, setLoadingStep] = useState(0)
- const { toast } = useToast()
- const { uploadToS3 } = useS3Upload()
- const { isSignedIn } = useAuth()
+export function CreateButton({
+ prompt,
+ style,
+ characterFiles,
+}: CreateButtonProps) {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+ const [loadingStep, setLoadingStep] = useState(0);
+ const { toast } = useToast();
+ const { uploadToS3 } = useS3Upload();
+ const { isSignedIn } = useAuth();
+ const [hasApiKey, setHasApiKey] = useState(false);
+
+ // Check if user has their own API key set
+ useEffect(() => {
+ const checkApiKey = () => {
+ const apiKey = localStorage.getItem("together_api_key");
+ setHasApiKey(!!apiKey);
+ };
+
+ checkApiKey();
+ // Listen for storage changes
+ window.addEventListener("storage", checkApiKey);
+ return () => window.removeEventListener("storage", checkApiKey);
+ }, []);
useEffect(() => {
- if (!isLoading) return
+ if (!isLoading) return;
- const steps = ["Enhancing prompt...", "Generating scenes...", "Creating your comic..."]
- let currentStep = 0
+ const steps = [
+ "Enhancing prompt...",
+ "Generating scenes...",
+ "Creating your comic...",
+ ];
+ let currentStep = 0;
const interval = setInterval(() => {
- currentStep += 1
+ currentStep += 1;
if (currentStep < steps.length) {
- setLoadingStep(currentStep)
+ setLoadingStep(currentStep);
} else {
- clearInterval(interval)
+ clearInterval(interval);
}
- }, 2500)
+ }, 2500);
- return () => clearInterval(interval)
- }, [isLoading])
+ return () => clearInterval(interval);
+ }, [isLoading]);
const handleCreate = async () => {
if (!prompt.trim()) {
@@ -47,16 +69,18 @@ export function CreateButton({ prompt, style, characterFiles }: CreateButtonProp
description: "Please enter a prompt to generate your comic",
variant: "destructive",
duration: 3000,
- })
- return
+ });
+ return;
}
- setIsLoading(true)
- setLoadingStep(0)
+ setIsLoading(true);
+ setLoadingStep(0);
try {
- const apiKey = localStorage.getItem("together_api_key")
- const characterUploads = await Promise.all(characterFiles.map((file) => uploadToS3(file).then(({ url }) => url)))
+ const apiKey = localStorage.getItem("together_api_key");
+ const characterUploads = await Promise.all(
+ characterFiles.map((file) => uploadToS3(file).then(({ url }) => url))
+ );
// Use API to create story and generate first page
const response = await fetch("/api/generate-comic", {
@@ -70,54 +94,72 @@ export function CreateButton({ prompt, style, characterFiles }: CreateButtonProp
style,
characterImages: characterUploads,
}),
- })
+ });
if (!response.ok) {
- const errorData = await response.json()
- throw new Error(errorData.error || "Failed to create story")
+ const errorData = await response.json();
+ if (response.status === 429 && errorData.isRateLimited) {
+ throw new Error(errorData.error);
+ }
+ throw new Error(errorData.error || "Failed to create story");
}
- const result = await response.json()
+ const result = await response.json();
// Redirect to the story editor using slug
- router.push(`/editor/${result.storySlug}`)
-
+ router.push(`/editor/${result.storySlug}`);
} catch (error) {
- console.error("Error creating comic:", error)
+ console.error("Error creating comic:", error);
toast({
title: "Creation failed",
- description: error instanceof Error ? error.message : "Failed to create comic. Please try again.",
+ description:
+ error instanceof Error
+ ? error.message
+ : "Failed to create comic. Please try again.",
variant: "destructive",
duration: 4000,
- })
- setIsLoading(false)
+ });
+ setIsLoading(false);
}
- }
+ };
-
-
- const loadingSteps = ["Enhancing prompt...", "Generating scenes...", "Creating your comic..."]
+ const loadingSteps = [
+ "Enhancing prompt...",
+ "Generating scenes...",
+ "Creating your comic...",
+ ];
return (
{isSignedIn ? (
-
+
+
+
+ {hasApiKey ? (
+ <>Using your API key (~$0.01 per comic)>
+ ) : (
+ <>1 credit weekly>
+ )}
+
+
) : (
)}
- )
+ );
}
diff --git a/drizzle/0002_romantic_blob.sql b/drizzle/0002_romantic_blob.sql
new file mode 100644
index 0000000..0778c56
--- /dev/null
+++ b/drizzle/0002_romantic_blob.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "stories" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint
+ALTER TABLE "stories" ALTER COLUMN "user_id" SET NOT NULL;
\ No newline at end of file
diff --git a/drizzle/0003_harsh_molecule_man.sql b/drizzle/0003_harsh_molecule_man.sql
new file mode 100644
index 0000000..1194449
--- /dev/null
+++ b/drizzle/0003_harsh_molecule_man.sql
@@ -0,0 +1 @@
+ALTER TABLE "stories" ALTER COLUMN "user_id" DROP NOT NULL;
\ No newline at end of file
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..477347b
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,164 @@
+{
+ "id": "6e2800e4-49f1-4069-aee3-00451ac4eb01",
+ "prevId": "5ab9181f-d5fe-4f00-8822-d2c4f5be3322",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.pages": {
+ "name": "pages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "story_id": {
+ "name": "story_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "page_number": {
+ "name": "page_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "character_image_urls": {
+ "name": "character_image_urls",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "generated_image_url": {
+ "name": "generated_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pages_story_id_stories_id_fk": {
+ "name": "pages_story_id_stories_id_fk",
+ "tableFrom": "pages",
+ "tableTo": "stories",
+ "columnsFrom": [
+ "story_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.stories": {
+ "name": "stories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "stories_slug_unique": {
+ "name": "stories_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json
new file mode 100644
index 0000000..f419cb4
--- /dev/null
+++ b/drizzle/meta/0003_snapshot.json
@@ -0,0 +1,164 @@
+{
+ "id": "d4fd5445-071f-469e-9bc8-ccb05ead5ef9",
+ "prevId": "6e2800e4-49f1-4069-aee3-00451ac4eb01",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.pages": {
+ "name": "pages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "story_id": {
+ "name": "story_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "page_number": {
+ "name": "page_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "character_image_urls": {
+ "name": "character_image_urls",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "generated_image_url": {
+ "name": "generated_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pages_story_id_stories_id_fk": {
+ "name": "pages_story_id_stories_id_fk",
+ "tableFrom": "pages",
+ "tableTo": "stories",
+ "columnsFrom": [
+ "story_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.stories": {
+ "name": "stories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "stories_slug_unique": {
+ "name": "stories_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 7008510..c464364 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -15,6 +15,20 @@
"when": 1766492848044,
"tag": "0001_windy_ezekiel",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1766674906873,
+ "tag": "0002_romantic_blob",
+ "breakpoints": true
+ },
+ {
+ "idx": 3,
+ "version": "7",
+ "when": 1766674926877,
+ "tag": "0003_harsh_molecule_man",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/lib/rate-limit.ts b/lib/rate-limit.ts
new file mode 100644
index 0000000..dc4bbdd
--- /dev/null
+++ b/lib/rate-limit.ts
@@ -0,0 +1,15 @@
+import { Ratelimit } from "@upstash/ratelimit"
+import { Redis } from "@upstash/redis"
+
+const redis = new Redis({
+ url: process.env.UPSTASH_REDIS_REST_URL!,
+ token: process.env.UPSTASH_REDIS_REST_TOKEN!,
+})
+
+// Free tier: 1 comic per week (fixed window)
+export const freeTierRateLimit = new Ratelimit({
+ redis,
+ limiter: Ratelimit.fixedWindow(1, "7 d"),
+ analytics: true,
+ prefix: "ratelimit:free-comics",
+})
\ No newline at end of file
diff --git a/lib/schema.ts b/lib/schema.ts
index 7525db2..090ccce 100644
--- a/lib/schema.ts
+++ b/lib/schema.ts
@@ -7,7 +7,7 @@ export const stories = pgTable('stories', {
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
description: text('description'),
- userId: uuid('user_id').notNull(), // Required Clerk user ID
+ userId: text('user_id').notNull(), // Required Clerk user ID
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
diff --git a/package.json b/package.json
index 4cad12f..378c6d4 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
+ "@upstash/ratelimit": "^2.0.7",
+ "@upstash/redis": "^1.36.0",
"@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d740237..bd88410 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,6 +101,12 @@ importers:
'@radix-ui/react-tooltip':
specifier: 1.1.6
version: 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@upstash/ratelimit':
+ specifier: ^2.0.7
+ version: 2.0.7(@upstash/redis@1.36.0)
+ '@upstash/redis':
+ specifier: ^1.36.0
+ version: 1.36.0
'@vercel/analytics':
specifier: 1.3.1
version: 1.3.1(next@16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
@@ -121,7 +127,7 @@ importers:
version: 4.1.0
drizzle-orm:
specifier: ^0.45.1
- version: 0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0)
+ version: 0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0)(@upstash/redis@1.36.0)
embla-carousel-react:
specifier: 8.5.1
version: 8.5.1(react@19.2.0)
@@ -1994,6 +2000,18 @@ packages:
'@types/react@19.2.7':
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
+ '@upstash/core-analytics@0.0.10':
+ resolution: {integrity: sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==}
+ engines: {node: '>=16.0.0'}
+
+ '@upstash/ratelimit@2.0.7':
+ resolution: {integrity: sha512-qNQW4uBPKVk8c4wFGj2S/vfKKQxXx1taSJoSGBN36FeiVBBKHQgsjPbKUijZ9Xu5FyVK+pfiXWKIsQGyoje8Fw==}
+ peerDependencies:
+ '@upstash/redis': ^1.34.3
+
+ '@upstash/redis@1.36.0':
+ resolution: {integrity: sha512-9zN2UV9QJGPnXfWU3yZBLVQaqqENDh7g+Y4J2vJuSxBCi9FQ0aUOtaXlzuFhnsiZvCqM+eS27ic+tgmkWUsfOg==}
+
'@vercel/analytics@1.3.1':
resolution: {integrity: sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA==}
peerDependencies:
@@ -2709,6 +2727,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ uncrypto@0.1.3:
+ resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -4890,6 +4911,19 @@ snapshots:
dependencies:
csstype: 3.2.3
+ '@upstash/core-analytics@0.0.10':
+ dependencies:
+ '@upstash/redis': 1.36.0
+
+ '@upstash/ratelimit@2.0.7(@upstash/redis@1.36.0)':
+ dependencies:
+ '@upstash/core-analytics': 0.0.10
+ '@upstash/redis': 1.36.0
+
+ '@upstash/redis@1.36.0':
+ dependencies:
+ uncrypto: 0.1.3
+
'@vercel/analytics@1.3.1(next@16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
dependencies:
server-only: 0.0.1
@@ -5027,10 +5061,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- drizzle-orm@0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0):
+ drizzle-orm@0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0)(@upstash/redis@1.36.0):
optionalDependencies:
'@neondatabase/serverless': 1.0.2
'@types/pg': 8.16.0
+ '@upstash/redis': 1.36.0
electron-to-chromium@1.5.267: {}
@@ -5512,6 +5547,8 @@ snapshots:
typescript@5.9.3: {}
+ uncrypto@0.1.3: {}
+
undici-types@6.21.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.1):