diff --git a/CHANGELOG.md b/CHANGELOG.md index a0fe6cb6..3660fde4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Supported Versions -Active full support: 1.9.7 (latest). Security maintenance (critical fixes only): 1.1.0. All versions < 1.1.0 are End of Security Support (EoSS). See `SECURITY.md` for the evolving support policy. +Active full support: 2.0.3 (latest). Security maintenance (critical fixes only): 1.1.0. All versions < 1.1.0 are End of Security Support (EoSS). See `SECURITY.md` for the evolving support policy. + +## [2.0.3] - 2026-03-11 + +### Fixed + +- **Cloudflare Pages Build Error** (`app/genres/[slug]/page.tsx`, `app/stories/[id]/page.tsx`): Fixed `Invalid prerender config` and `routes were not configured to run with the Edge Runtime` errors by explicitly exporting `runtime = 'edge'` and removing conflicting static configs. + +## [2.0.2] - 2026-03-11 + +### Fixed + +- **Supabase URL** (`lib/supabase/client.ts`, `lib/supabase/server.ts`): Replaced `dummy.supabase.co` fallback with actual project URL — fixes `ERR_NAME_NOT_RESOLVED` for all engagement/vote/comment queries. +- **TTS Endpoint 404** (`hooks/use-tts.ts`, `app/api/tts/generate/route.ts`, `app/api/tts/audio/route.ts`): Created local Next.js API routes for TTS generation via Sarvam API, replacing non-existent backend endpoints (`/api/v1/tts/generate` and `/api/v1/tts/audio`). +- **Hydration Mismatch** (`app/stories/[id]/page.tsx`): Changed page from `force-static` with `generateStaticParams` to `force-dynamic`, eliminating server/client rendering inconsistencies. +- **Hydration Warnings** (`app/layout.tsx`): Added `suppressHydrationWarning` to the `` tag to prevent errors caused by the theme provider and external scripts modifying body attributes before Next.js hydrates. +- **Supabase SSR Client** (`lib/supabase/server.ts`, `lib/feedService.ts`, `lib/royalty-service.ts`): Refactored `createClient` to be asynchronous (`await cookies()`) and use `getAll()`/`setAll()` to comply with Next.js 15+ and the latest `@supabase/ssr` library requirements. +- **TTS Dropdown Clipping** (`components/book-view.tsx`): Removed `overflow-hidden` from the audio bar's root container so the speaker/language settings popups are no longer clipped when opened inside the story view. + +## [2.0.1] - 2026-03-11 + +### Changed + +- **Interactive Book Preview** (`components/book-view.tsx`): Complete rebuild — the story reader is now a page-flipping book with paragraph-aware pagination, keyboard navigation (←/→/Space), amber parchment color scheme, drop-cap on first paragraphs, running headers (title + author), page numbers, and chapter tabs. +- **Story Page Layout** (`app/stories/[id]/client.tsx`): Fixed element collisions — removed overlapping hero cover, added sticky header bar with back nav/genre/stats, centered title section, clean separation between book, TTS bar, and engagement sections. +- **Book Style Overrides** (`app/globals.css`): Added page-flip keyframe animations and scoped CSS resets to prevent comic global styles (uppercase headings, font-weight 600) from corrupting the book's serif typography. + +## [2.0.0] - 2026-03-11 + +### Added + +- **Clickable Gallery Cards** (`app/gallery/page.tsx`): Gallery story cards are now wrapped with `Link` components — clicking a card navigates to `/stories/{id}` with a smooth scale-up hover animation. +- **Story Engagement System** (`components/story-engagement.tsx`): New component on the story detail page with Reddit-style upvote/downvote, comment section (post + threaded display with avatars), and save/bookmark functionality. +- **Database Tables**: Created `story_comments`, `story_votes`, `saved_stories` tables with full RLS policies (public read, authenticated write, owner-based update/delete). +- **Analytics Warehouse**: Configured Supabase S3/Iceberg analytics warehouse (`analytics-groqtales`) with engagement summary view (`analytics.story_engagement_summary`). + +### Fixed + +- **`user_settings` 406 Error**: Added `service_role` bypass RLS policy to fix REST API queries failing due to missing authenticated session context. +- **Story Page Navigation**: Back button now points to `/gallery` instead of `/marketplace`. + +## [1.9.9] - 2026-03-11 + +### Added + +- **WalletConnect Integration** (`components/wallet-connect.tsx`): WalletConnect v2 is now fully functional — users can connect via the QR modal using any WalletConnect-compatible mobile or desktop wallet. Implements full signature-based authentication flow with backend token issuance. +- **Supabase Storage Buckets**: Created 4 storage buckets (`avatars`, `story-covers`, `comic-panels`, `nft-metadata`) with appropriate file size limits, MIME type restrictions, and Row Level Security policies (public read, authenticated upload, owner-based update/delete). +- **Footer Status Page Link** (`components/footer.tsx`): System health indicator in the footer now links to the public [UptimeRobot Status Page](https://stats.uptimerobot.com/PUi1I3YaBH) with hover effects and external link icon. +- **`@walletconnect/ethereum-provider`** dependency added to `package.json`. + +### Fixed + +- **Authentication Token Persistence** (`components/auth/sign-in-form.tsx`, `components/auth/sign-up-form.tsx`, `app/auth/callback/page.tsx`): **Critical fix** — all three authentication flows (email/password login, email signup, Google OAuth callback) now correctly persist `accessToken` and `refreshToken` to `localStorage` and dispatch `StorageEvent`. Previously, tokens returned from the backend were discarded, causing dashboard and profile pages to always show "Not Logged In". +- **Post-Login Redirect**: Sign-in form now redirects to `/dashboard` instead of `/` after successful authentication. + +### Changed + +- **README.md**: Updated project description, tech stack (Gemini AI, Alchemy, Supabase Storage, WalletConnect, Sarvam TTS), architecture diagram, Quick Start guide, and added uptime monitoring link. + +## [1.9.8] - 2026-03-11 + +### Fixed + +- **"System Offline" False Alarm** (`components/footer.tsx`, `hooks/use-system-health.ts`): The backend `/api/health` returns `status: 'operational'` but the footer and system health hook only accepted `'ok'` or `'healthy'`, causing the website to permanently display "System Offline" despite an operational backend. Now accepts `'operational'` and `'partial'` alongside existing values. +- **AI Endpoint 404** (`lib/api-client.ts`, `lib/gemini-service.ts`): Frontend AI calls (`processAI()` and `GeminiService.generateProse()`) sent `POST /api/v1/ai` but the backend only mounts sub-routes at `/api/v1/ai/generate` and `/api/v1/ai/analyze` — no root handler exists. Changed both callers to use `/api/v1/ai/generate`. +- **Repeated 401 Console Errors** (`lib/feeds-client.ts`): `fetchNotifications()` was calling the auth-required `/api/feeds/notifications/me` endpoint even when no access token was present in `localStorage`, producing repeated `401 Unauthorized` console errors. Added an early-return guard that skips the request when no token is available. ## [1.9.7] - 2026-03-10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1c49428..6b5cdc52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ You can contribute in several ways: - **Reporting Bugs:** Use the `bug_report.md` template and provide detailed steps to reproduce. - **Suggesting Features:** Use the `feature.md` template to propose new ideas. - **Code Contributions:** Pick up issues labeled `good first issue`. -- **Web3/Blockchain:** Use the `web3_issue.md` template for Monad/NFT-related contributions. +- **Web3/Blockchain:** Use the `web3_issue.md` template for Ethereum/NFT-related contributions. - **Security:** Use the `security.md` template for vulnerabilities. - **Documentation:** Help improve the README, Wiki, or code comments. @@ -129,8 +129,13 @@ To get started with development: 3. **Environment Variables:** Copy `.env.example` to `.env.local` and fill in: * `GROQ_API_KEY` – Groq AI key (required) -* `MONAD_RPC_URL` – Monad blockchain endpoint -* `UNSPLASH_API_KEY` – (Optional) for placeholder visuals +* `GEMINI_API_KEY` – Google Gemini AI key (required) +* `NEXT_PUBLIC_SUPABASE_URL` – Supabase project URL +* `NEXT_PUBLIC_SUPABASE_ANON_KEY` – Supabase anon key +* `SUPABASE_SERVICE_ROLE_KEY` – Supabase service role key +* `ALCHEMY_ETH_MAINNET_HTTP_URL` – Alchemy Ethereum RPC (for NFT minting) +* `SARVAM_API_KEY` – Sarvam AI TTS key (optional, for audiobook narration) +* `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` – WalletConnect project ID (optional) 4. **Run Development Server:** ```bash @@ -174,10 +179,10 @@ GroqTales/ ## Architecture Notes - **Frontend:** Built with Next.js, React, TailwindCSS, and shadcn/ui for a modern, responsive UI. -- **Backend:** Node.js API routes handle authentication, story generation, and blockchain interactions. -- **Blockchain:** Monad SDK and Solidity smart contracts manage NFT minting and ownership. -- **AI:** Groq API powers story and comic generation. -- **Database:** MongoDB stores user data, stories, and metadata. +- **Backend:** Node.js + Express.js API handles authentication, story generation, TTS, and blockchain interactions. +- **Blockchain:** Ethereum Mainnet via Alchemy and Solidity smart contracts manage NFT minting and ownership. +- **AI:** Google Gemini (chairman model) + Groq LPU (narrow tasks) + Sarvam AI (TTS audiobook narration). +- **Database & Storage:** Supabase (PostgreSQL) with Row Level Security, Supabase Storage (avatars, story-covers, comic-panels, nft-metadata). - **Testing:** Jest/React Testing Library for frontend; Hardhat/Foundry for smart contracts. --- diff --git a/Dockerfile b/Dockerfile index bb993ae8..109ab380 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,14 @@ # syntax=docker/dockerfile:1 ARG NODE_VERSION=22 -FROM node:${NODE_VERSION}-bookworm as base +FROM node:${NODE_VERSION}-bookworm AS base WORKDIR /usr/src/app ################################################################################ -FROM base as deps +# Install dependencies +################################################################################ +FROM base AS deps RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ @@ -14,22 +16,24 @@ RUN --mount=type=bind,source=package.json,target=package.json \ npm ci ################################################################################ -FROM deps as build +# Build the application +################################################################################ +FROM deps AS build -ENV MONGODB_URI="mongodb://mongo:27017/groqtales" -ENV NEXT_PUBLIC_RPC_URL="http://anvil:8545" +# Build-time env vars (no secrets — those come from runtime) ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_IGNORE_TYPE_ERRORS=1 +ENV NEXT_PUBLIC_BUILD_MODE=true COPY . . RUN npm run build ################################################################################ -FROM base as final +# Production image +################################################################################ +FROM base AS final -ENV NODE_ENV development -ENV MONGODB_URI="mongodb://mongo:27017/groqtales" -ENV NEXT_PUBLIC_RPC_URL="http://anvil:8545" +ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 USER node @@ -42,7 +46,13 @@ COPY --chown=node:node --from=build /usr/src/app/server ./server COPY --chown=node:node --from=build /usr/src/app/scripts ./scripts COPY --chown=node:node --from=build /usr/src/app/next.config.js ./next.config.js +# Frontend (Next.js) EXPOSE 3000 +# Backend API (Express) EXPOSE 3001 -CMD npm start +# Health check — uses the lightweight liveness probe +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -fs http://localhost:3001/healthz || exit 1 + +CMD ["npm", "start"] diff --git a/README.md b/README.md index d85f3e9f..8cfed007 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,10 @@ ## What is GroqTales? GroqTales is an open-source, AI-powered Web3 storytelling platform. Writers and artists can generate -immersive narratives or comic-style stories using Groq AI, then mint and trade them as NFTs on the -Monad blockchain. With a focus on ownership, authenticity, and community, GroqTales bridges the -world of creative writing, generative AI, and decentralized technology. +immersive narratives or comic-style stories using AI (Google Gemini as the chairman model + Groq for +narrow tasks), then mint and trade them as NFTs on Ethereum Mainnet via Alchemy. With a focus on +ownership, authenticity, and community, GroqTales bridges the world of creative writing, generative +AI, and decentralized technology. --- @@ -85,16 +86,29 @@ world of creative writing, generative AI, and decentralized technology. - **Interactive Story Canvas**: A reusable SVG-based canvas supports drag-and-drop node positioning with grid snapping, zoom controls, and real-time info panel. It includes auto-saving to local storage. - **Extensive Story Customization (70+ Parameters)**: Fine-tune every aspect of your story with a powerful Parameter Management System. Includes 70+ parameters across 10 categories, intelligence-level presets, and an advanced UI with search and filtering. - **Guided Onboarding Tours**: Interactive guided tours for each creation mode to help new users get started quickly. -- **NFT Minting on Monad Blockchain** Seamlessly mint your stories as NFTs on Monad (Testnet live, - Mainnet coming soon). Each NFT proves authenticity, ownership, and collectibility. +- **Clickable Gallery → Story Detail Pages** Browse the gallery, click any story card to open a + dedicated story page with a full book-like reading experience. +- **Book-Style Reader with TTS Audiobook** Each story page features a Kindle-style book view with + chapter navigation, serif typography, and an integrated Sarvam AI Bulbul v3 text-to-speech + audiobook player. Choose speaker, language (English default), and playback speed. +- **Reddit-Style Story Engagement** Upvote/downvote stories, post and read comments (with avatars + and timestamps), and bookmark stories to your "Saved Creations" collection. +- **NFT Minting on Ethereum Mainnet** Mint your stories as NFTs on Ethereum Mainnet via Alchemy + with a server-side platform signer. Each NFT proves authenticity, ownership, and collectibility. - **Community Gallery** Publish your stories publicly, browse the gallery, and interact with other creators. Stories can be shared freely or as NFTs. -- **Wallet Integration** Connect with MetaMask, WalletConnect, or Ledger for secure publishing and - minting. Wallet is required for NFT actions. +- **Wallet Integration** Connect with MetaMask or WalletConnect v2 (QR code modal) for secure + publishing and minting. Signature-based authentication with backend token issuance. +- **Supabase Storage** Cloud storage for avatars, story covers, comic panels, and NFT metadata + with Row Level Security policies. +- **Analytics Warehouse** Supabase S3/Iceberg analytics integration with pre-built engagement + summary views for tracking votes, comments, and saves per story. - **Real-Time Story Streaming** Watch your story unfold in real-time as Groq AI generates each segment. - **Mobile-Friendly & Responsive UI** Built with modern web technologies for a seamless experience on any device. +- **Docker Support** Multi-stage Dockerfile with Redis caching + Docker Compose for local + development. - **Extensible & Open Source** Modular codebase with clear separation of frontend, backend, and smart contract logic. Contributions are welcome! @@ -104,10 +118,11 @@ world of creative writing, generative AI, and decentralized technology. - **Frontend:** Next.js, React, TailwindCSS, shadcn/ui - **Backend:** Node.js, Express.js API (Render), Cloudflare Workers -- **AI:** Groq API (story generation with 70+ configurable parameters), Unsplash API (optional visuals) -- **Blockchain:** Monad SDK, Solidity Smart Contracts -- **Database:** Supabase (PostgreSQL) with Row Level Security +- **AI:** Google Gemini (chairman model for story generation), Groq LPU (narrow tasks — parameter validation, classification, outlines), Sarvam AI (Text-to-Speech) +- **Blockchain:** Ethereum Mainnet via Alchemy, Solidity Smart Contracts, WalletConnect, MetaMask +- **Database & Storage:** Supabase (PostgreSQL) with Row Level Security, Supabase Storage (avatars, story-covers, comic-panels, nft-metadata), IPFS via Pinata - **Hosting:** Cloudflare Pages (frontend), Render (backend API) +- **Monitoring:** [UptimeRobot Status Page](https://stats.uptimerobot.com/PUi1I3YaBH) --- @@ -118,10 +133,18 @@ git clone https://github.com/IndieHub25/GroqTales cd GroqTales npm install cp .env.example .env.local -# Add GROQ_API_KEY, UNSPLASH key, Monad network if needed +# Fill in GROQ_API_KEY, GEMINI_API_KEY, Supabase keys, SARVAM_API_KEY, and wallet config npm run dev ``` +### Docker (Optional) + +```bash +cp .env.example .env.local +# Fill in all environment variables +docker compose up --build +``` + 1. Visit [http://localhost:3000](http://localhost:3000) 2. Connect your wallet (optional; required for minting/publishing) 3. Generate your story → Publish or Mint as NFT @@ -139,6 +162,7 @@ For continuous uptime monitoring (e.g., UptimeRobot, Render Health Checks, Datad - **Liveness Probe**: `GET /healthz` — Returns an instant `200 OK` bypassing all middleware, rate limiters, and external database latency. Use this for raw "is the server running?" checks. - **Deep Diagnostics**: `GET /api/health` — Returns extremely detailed server diagnostics including Supabase connectivity, process memory usage, and uptime. (Subject to rate limits). +- **Status Page**: [https://stats.uptimerobot.com/PUi1I3YaBH](https://stats.uptimerobot.com/PUi1I3YaBH) — Public uptime monitoring dashboard. --- @@ -154,19 +178,23 @@ To run this project locally, you must set up your environment variables. Create | `GROQ_API_KEY` | **Required** | Powers the AI story generation engine via Groq LPU. | | `NEXT_PUBLIC_SUPABASE_URL` | **Required** | Your Supabase project URL for database and authentication. | | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | **Required** | Supabase anonymous/public API key for client-side access. | -| `MONAD_RPC_URL` | **Required** | The RPC endpoint for interacting with the Monad Testnet. | +| `ALCHEMY_ETH_MAINNET_HTTP_URL` | **Required** | The Alchemy RPC endpoint for interacting with Ethereum Mainnet. | | `NEXT_PUBLIC_API_URL` | **Required** | Backend API URL (e.g., `https://groqtales-backend-api.onrender.com`).| | `NEXT_PUBLIC_UNSPLASH_API_KEY` | _Optional_ | API key used for fetching high-quality cover images for stories. | | `NEXT_PUBLIC_CONTRACT_ADDR` | **Required** | The smart contract address for the deployed NFT collection. | | `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` | **Required** | WalletConnect project ID for wallet integration. | +| `UPSTASH_REDIS_REST_URL` | _Optional_ | URL for Upstash Redis (used for AI cache & rate limiting). | +| `UPSTASH_REDIS_REST_TOKEN` | _Optional_ | Token for Upstash Redis REST API. | +| `REDIS_URL` | _Optional_ | Alternate Redis connection string (used by notifications/cache if not using Upstash). | +| `MONGODB_URI` | _Optional_ | MongoDB connection string (required for local Mongo-based features). | +| `NEXT_PUBLIC_URL` | _Optional_ | Public base URL of the frontend (used when constructing links in emails, etc.). | ### 🔑 How to get these keys: 1. **Groq API:** Generate a key at [Groq Cloud Console](https://console.groq.com/). 2. **Supabase:** Create a free project at [Supabase](https://supabase.com/) and copy the project URL and anon key from Settings → API. -3. **Monad RPC:** Use the official [Monad Testnet docs](https://docs.monad.xyz/) to find the latest - RPC URL. +3. **Ethereum RPC:** Create a free project at [Alchemy](https://alchemy.com/) to get your `ALCHEMY_ETH_MAINNET_HTTP_URL`. 4. **Unsplash:** Register an application on the [Unsplash Developer Portal](https://unsplash.com/developers). 5. **WalletConnect:** Create a project at [WalletConnect Cloud](https://cloud.walletconnect.com/). @@ -231,11 +259,11 @@ Docker Compose sets these automatically. Override them in a `.env` file or in | ------------------------- | ----------------------------------- | | `NEXT_PUBLIC_SUPABASE_URL` | `http://supabase:54321` | | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | `your-anon-key` | -| `NEXT_PUBLIC_RPC_URL` | `http://anvil:8545` | +| `ALCHEMY_ETH_MAINNET_HTTP_URL` | `https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY` | | `NODE_ENV` | `development` | > [!TIP] -> For production, set `NODE_ENV=production` and add your `GROQ_API_KEY`, `MONAD_RPC_URL`, and other +> For production, set `NODE_ENV=production` and add your `GROQ_API_KEY`, `ALCHEMY_ETH_MAINNET_HTTP_URL`, and other > secrets via environment variables — never bake them into the image. ### References @@ -271,12 +299,15 @@ ownership. ```mermaid graph TD A[User Interface - Next.js] -->|Prompt with 70+ Params| B[Backend API - Node.js] - B -->|Inference Request| C[Groq AI LPU Engine] + B -->|Story Generation| C[Google Gemini Chairman] + B -->|Validation & Outlines| D[Groq LPU Engine] C -->|Structured JSON Story| B - B -->|Metadata| D[Supabase PostgreSQL] - B -->|IPFS Upload| E[Story/Image Data] - A -->|Mint NFT| F[Monad Testnet Blockchain] - F --- G[Smart Contracts - Solidity] + D -->|Classification Data| B + B -->|Metadata & Auth| E[Supabase PostgreSQL] + B -->|Media Files| F[Supabase Storage] + B -->|NFT Metadata| G[IPFS via Pinata] + A -->|MetaMask / WalletConnect| H[Ethereum Mainnet via Alchemy] + H --- I[Smart Contracts - Solidity] ``` --- @@ -295,7 +326,7 @@ graph TD - **Environment Variables:** - `GROQ_API_KEY` – Your Groq AI API key - `NEXT_PUBLIC_UNSPLASH_API_KEY` – (Optional) for placeholder visuals - - `MONAD_RPC_URL` – Monad blockchain RPC endpoint + - `ALCHEMY_ETH_MAINNET_HTTP_URL` – Alchemy Ethereum Mainnet RPC endpoint - **Smart Contract Deployment:** - Contracts are written in Solidity and can be deployed to Monad Testnet/Mainnet. diff --git a/SECURITY.md b/SECURITY.md index b2b1f2b7..252def75 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,14 +8,14 @@ considered End of Security Support (EoSS). | Version | Status | Support Level | Notes | | ------- | -------------------- | ----------------------------- | ----------------------------------- | -| 1.3.104 | ✅ Active (Latest) | Full (features + security) | Current production release | -| 1.3.103 | ✅ Active (Previous) | Security & critical bug fixes | Upgrade recommended | -| > 1.1.0 | ⚠️ Maintenance | Critical security only | Security maintenance — upgrade ASAP | -| < 1.1.0 | ❌ EoSS | No updates | Please upgrade immediately | +| 2.0.2 | ✅ Active (Latest) | Full (features + security) | Current production release | +| 2.0.1 | ✅ Active (Previous) | Security & critical bug fixes | Upgrade recommended | +| 2.0.0 | ⚠️ Maintenance | Critical security only | Security maintenance — upgrade ASAP | +| < 2.0.0 | ❌ EoSS | No updates | Please upgrade immediately | > [!IMPORTANT] -> Version 1.3.7 introduces major cinematic UI/UX overhauls, Supabase interactive authentication flows, global emote removal, and on-chain action steppers with off-chain access control rules. -> Upgrading to **1.3.101** is strongly recommended. +> Version 2.0.2 introduces critical fixes for hydration mismatches, Next.js 15+ async `cookies()` requirement compliance for Supabase `@supabase/ssr` server-side clients, and local TTS generation endpoints. +> Upgrading to **2.0.2** is strongly recommended. ## Reporting a Vulnerability @@ -85,8 +85,10 @@ We welcome reports regarding our backend, smart contracts, AI implementation, an - Rate limiting (`express-rate-limit`) on public API endpoints - Input validation via `express-validator` and Zod schemas - CORS configured to restrict cross-origin access -- Environment secrets managed via `.env` (never committed to version control) +- Environment secrets managed via `.env.local` (never committed to version control) - WCAG 2.1 AA accessibility compliance reduces attack surface from misleading UI +- Supabase Row Level Security (RLS) enforced on all tables +- WalletConnect signature verification for wallet-based authentication ## Protecting Your Data @@ -97,7 +99,10 @@ protect data both in transit and at rest: - PostgreSQL/Supabase DB connections authenticated and encrypted with Row Level Security (RLS) - Secure session management with encrypted JWT tokens managed via Supabase Auth - No secrets exposed in client-side bundles -- Wallet signatures verified server-side (SIWE/Monad) +- Wallet signatures verified server-side (Ethereum `personal_sign`) +- Platform signer private key isolated to server-side only (never exposed to client) +- Supabase Storage buckets with RLS policies (public read, authenticated upload, owner-only delete) +- Story engagement data (votes, comments, saves) protected by per-user RLS policies ## Third-Party & Dependency Security @@ -119,21 +124,30 @@ still report it — include the upstream advisory if available. - Content Security Policy headers via Helmet - Server-side rendering (SSR) safe patterns — no raw `document`/`window` access without guards - Worker endpoints protected by shared `WORKER_SECRET` for internal-only access -- Outbound Groq API calls protected with 30-second `AbortController` timeout +- Outbound AI API calls (Gemini, Groq) protected with 30-second `AbortController` timeout +- Sarvam TTS API calls gated behind authentication middleware +- Docker health checks using `/healthz` liveness probe ## Current Technology Stack (Security-Relevant) -| Component | Technology | Version | -| -------------- | ------------------------- | ---------- | -| Runtime | Node.js | ≥ 20.0.0 | -| Framework | Next.js | 14.1.0 | -| Backend | Express.js | 5.1.0 | -| Database | Supabase (PostgreSQL) | latest | -| Auth | Supabase Auth + SIWE | 2.x | -| HTTP Security | Helmet | 8.x | -| Rate Limiting | express-rate-limit | 8.x | -| Validation | Zod + express-validator | 3.x / 7.x | -| TypeScript | TypeScript (strict) | 5.8.x | +| Component | Technology | Version | +| -------------- | --------------------------------- | ---------- | +| Runtime | Node.js | ≥ 22.0.0 | +| Framework | Next.js | 14.1.0 | +| Backend | Express.js | 5.1.0 | +| Database | Supabase (PostgreSQL) | latest | +| Storage | Supabase Storage (S3-compatible) | latest | +| Auth | Supabase Auth + Wallet Signatures | 2.x | +| AI (Chairman) | Google Gemini | latest | +| AI (Tasks) | Groq LPU | latest | +| TTS | Sarvam AI Bulbul v3 | latest | +| Blockchain | Ethereum Mainnet via Alchemy | latest | +| Wallet | WalletConnect v2 + MetaMask | latest | +| HTTP Security | Helmet | 8.x | +| Rate Limiting | express-rate-limit | 8.x | +| Validation | Zod + express-validator | 3.x / 7.x | +| TypeScript | TypeScript (strict) | 5.8.x | +| Container | Docker (multi-stage build) | latest | --- diff --git a/TESTING_DOUBLE_MINT_PREVENTION.md b/TESTING_DOUBLE_MINT_PREVENTION.md deleted file mode 100644 index c98dbf95..00000000 --- a/TESTING_DOUBLE_MINT_PREVENTION.md +++ /dev/null @@ -1,268 +0,0 @@ -# Testing Double-Minting Prevention - -This guide walks through testing the idempotency protections added to prevent accidental double-minting of stories. - -## What Was Implemented - -1. **Content Hash Tracking** – Each story gets a unique hash before minting (already in `components/story-generator.tsx`) -2. **Minting Status Check** – `/api/mint/check` endpoint validates minting status before allowing new mints -3. **Auth & Rate Limiting** – Each wallet is rate-limited and must be authenticated via NextAuth -4. **Database Scoping** – Mint queries are scoped to `authorAddress` to prevent enumeration attacks -5. **UI Safeguards** – Mint button is disabled during minting; status is shown to user - -## Manual Testing Steps - -### Test 1: Check Mint Status (Happy Path) - -**Precondition:** You're logged in with a connected wallet - -**Steps:** -1. Generate a story in the UI -2. Click **Mint Story as NFT** -3. Wait for the mint to complete (you'll see "NFT Minted Successfully!") -4. Refresh the page or close the dialog -5. Generate the same story again (or use the same hash) -6. Click **Mint Story as NFT** again -7. **Expected:** You should see "Already Minted" or "Minting In Progress" message - -**Test with curl (if running locally):** - -```bash -# 1. First mint check (should return NOT_MINTED or 404) -curl -X POST http://localhost:3000/api/mint/check \ - -H "Content-Type: application/json" \ - -b "your-session-cookie" \ - -d '{"storyHash":"abc123def456"}' - -# Response should be: -# {"success":true,"status":"NOT_MINTED","message":"Story has not been minted yet"} - -# 2. After minting (should return MINTED) -curl -X POST http://localhost:3000/api/mint/check \ - -H "Content-Type: application/json" \ - -b "your-session-cookie" \ - -d '{"storyHash":"abc123def456"}' - -# Response should be: -# {"success":true,"status":"MINTED",...} -``` - -### Test 2: Rate Limiting - -**Steps:** -1. Make 60+ requests to `/api/mint/check` in rapid succession (within 1 minute) from the same wallet -2. After the 60th request, you should receive a 429 (Too Many Requests) response -3. Wait 1 minute for the window to reset -4. Requests should work again - -**Test with curl (rate limit test):** - -```bash -# This will hit rate limit -for i in {1..65}; do - curl -X POST http://localhost:3000/api/mint/check \ - -H "Content-Type: application/json" \ - -b "your-session-cookie" \ - -d "{\"storyHash\":\"test$i\"}" - echo "Request $i" -done -``` - -### Test 3: Validation & Security - -**Steps:** - -1. **Missing storyHash:** -```bash -curl -X POST http://localhost:3000/api/mint/check \ - -H "Content-Type: application/json" \ - -b "your-session-cookie" \ - -d '{}' -``` -Expected: 400 error "Missing or invalid parameter: storyHash" - -2. **Unauthenticated request (no session):** -```bash -curl -X POST http://localhost:3000/api/mint/check \ - -H "Content-Type: application/json" \ - -d '{"storyHash":"abc123"}' -``` -Expected: 401 error "Unauthorized: Wallet not connected" - -3. **Empty string storyHash:** -```bash -curl -X POST http://localhost:3000/api/mint/check \ - -H "Content-Type: application/json" \ - -b "your-session-cookie" \ - -d '{"storyHash":" "}' -``` -Expected: 400 error (after trimming, empty) - -4. **Non-string storyHash:** -```bash -curl -X POST http://localhost:3000/api/mint/check \ - -H "Content-Type: application/json" \ - -b "your-session-cookie" \ - -d '{"storyHash":{"nested":"object"}}' -``` -Expected: 400 error "Missing or invalid parameter: storyHash" - -### Test 4: User-Scoped Queries (Security) - -**Steps:** -1. Login with **Wallet A** -2. Generate and mint a story -3. Logout, then login with **Wallet B** -4. Try to check the mine status of Wallet A's story (if you know the hash) -5. **Expected:** 404 "Mint record not found" (scoped to current user's wallet, not Wallet A's) - -## Integration Testing with Playwright/Cypress - -```typescript -// Example Playwright test -test('prevent double-minting same story', async ({ page }) => { - await page.goto('http://localhost:3000/create/ai-story'); - - // Login - await page.click('text=Connect Wallet'); - // ... complete login flow - - // Generate story - await page.fill('[placeholder="Enter your story idea"]', 'Test story'); - await page.click('text=Generate Story'); - await page.locator('text=Story Generated!').waitFor(); - - // Mint first time - await page.click('text=Mint Story as NFT'); - await page.locator('text=NFT Minted Successfully!').waitFor(); - - // Try mint again (same story hash) - await page.click('text=Mint Story as NFT'); - await page.locator(/Already Minted|Minting In Progress/).waitFor(); - - // Verify button is disabled or error is shown - await expect(page.locator('text=Mint Story as NFT')).toBeDisabled(); -}); -``` - -## Automated Test Script - -Create `tests/double-mint-prevention.test.ts`: - -```typescript -import { expect, test } from '@jest/globals'; - -const API_URL = 'http://localhost:3000/api/mint/check'; -const TEST_HASH = 'test-story-hash-' + Date.now(); - -let sessionCookie: string; - -beforeAll(async () => { - // Setup: Login and capture session - // (mock or use real auth flow here) - sessionCookie = 'your-session-cookie-here'; -}); - -test('should allow checking mint status when authenticated', async () => { - const response = await fetch(API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': sessionCookie, - }, - body: JSON.stringify({ storyHash: TEST_HASH }), - }); - - expect(response.status).toBe(404); // First check returns 404 (not minted) -}); - -test('should reject unauthenticated requests', async () => { - const response = await fetch(API_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ storyHash: TEST_HASH }), - }); - - expect(response.status).toBe(401); - const data = await response.json(); - expect(data.error).toContain('Unauthorized'); -}); - -test('should reject invalid storyHash', async () => { - const response = await fetch(API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': sessionCookie, - }, - body: JSON.stringify({ storyHash: '' }), - }); - - expect(response.status).toBe(400); -}); - -test('should rate-limit after 60 requests/minute', async () => { - const requests = []; - for (let i = 0; i < 65; i++) { - requests.push( - fetch(API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': sessionCookie, - }, - body: JSON.stringify({ storyHash: `hash-${i}` }), - }) - ); - } - - const responses = await Promise.all(requests); - const rateLimited = responses.filter(r => r.status === 429); - - expect(rateLimited.length).toBeGreaterThan(0); -}); -``` - -## Run Tests - -```bash -# Unit tests -npm test -- tests/double-mint-prevention.test.ts - -# Integration tests (if using Playwright) -npx playwright test tests/e2e/double-mint.spec.ts - -# Manual API testing -bash tests/scripts/test-mint-api.sh -``` - -## Expected Behaviors Checklist - -- [ ] First mint succeeds -- [ ] Subsequent attempts with same hash show "Already Minted" -- [ ] UI button is disabled during minting -- [ ] Unauthenticated requests are rejected (401) -- [ ] Invalid storyHash is rejected (400) -- [ ] Rate limit triggers at 60 req/min (429) -- [ ] Different wallets cannot see each other's mint records -- [ ] Empty/whitespace storyHash is rejected -- [ ] Refresh page doesn't reset mint status - -## Troubleshooting - -| Issue | Solution | -|-------|----------| -| 401 Unauthorized | Ensure you're logged in via NextAuth and session cookie is valid | -| 404 Not Found on first check | This is expected; mint record doesn't exist yet | -| Rate limit not triggering | Check that `RateLimiter.checkRateLimit` is being called in route handler | -| Can see other wallet's mints | Verify `authorAddress: user.wallet.toLowerCase()` is in findOne filter | - -## Success Criteria - -✅ The double-minting prevention is working correctly when: - -1. Same story hash cannot be minted twice -2. Unauthenticated and malformed requests are rejected -3. Rate limiting prevents enumeration attacks -4. User data is properly scoped (can't access other wallets' records) -5. UI provides clear feedback on mint status diff --git a/VERSION b/VERSION index 0b9006ea..f93ea0ca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.7 \ No newline at end of file +2.0.2 \ No newline at end of file diff --git a/__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.ts b/__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.ts index 58f7bf2a..c7efdf3d 100644 --- a/__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.ts +++ b/__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.ts @@ -27,25 +27,28 @@ describe('Cloudflare Edge Runtime Profile Route - Bug Condition Exploration', () }); /** - * Property 1: Fault Condition - Cloudflare Build Fails Without Edge Runtime - * - * This property test verifies the bug condition by attempting a Cloudflare build - * and expecting it to fail with the specific error about missing Edge Runtime config. - * - * **Validates: Requirements 1.1, 1.2, 1.3** - * - * Requirement 1.1: Build fails with specific error message - * Requirement 1.2: CLI detects missing Edge Runtime configuration - * Requirement 1.3: Deployment is blocked due to build failure + * Property 1: Requirement update – profile route must NOT use Edge runtime + * + * With Cloudflare Pages now serving a pure static export (`output: 'export'`), + * any page that is imported during the build must be compatible with the Node + * environment. The Edge runtime injects `self` and other browser globals, which + * causes prerender errors when the exporter tries to load the module. Rather + * than fighting the build process, we simply remove the Edge runtime directive + * from `/profile/[slug]` (and other dynamic routes) and assert that the build + * succeeds. + * + * This test verifies the new requirement by running `npm run cf-build` and + * checking that: + * 1. The build completes without failure + * 2. There is no Edge runtime error message + * 3. The source file no longer contains `runtime = 'edge'` */ - test('Property 1: Cloudflare build should fail when /profile/[slug] lacks Edge Runtime configuration', () => { + test('Property 1: Cloudflare build succeeds and profile route has no Edge runtime', () => { let buildOutput = ''; let buildFailed = false; - let errorMessage = ''; try { - // Attempt to run the Cloudflare build - // This should fail on unfixed code + // Run the Cloudflare build; it should now succeed buildOutput = execSync('npm run cf-build', { encoding: 'utf-8', stdio: 'pipe', @@ -53,35 +56,20 @@ describe('Cloudflare Edge Runtime Profile Route - Bug Condition Exploration', () }); } catch (error: any) { buildFailed = true; - errorMessage = error.message || ''; buildOutput = error.stdout?.toString() || error.stderr?.toString() || ''; - - // Log the counterexample for documentation - console.log('\n=== COUNTEREXAMPLE FOUND ==='); - console.log('Build failed as expected (bug exists)'); - console.log('Error output:', buildOutput.substring(0, 500)); - console.log('===========================\n'); + console.log('\n=== BUILD FAILURE ==='); + console.log(buildOutput.substring(0, 500)); + console.log('====================\n'); } - // EXPECTED BEHAVIOR (after fix): - // - Build should succeed (buildFailed = false) - // - No error about Edge Runtime configuration - - // CURRENT BEHAVIOR (bug exists): - // - Build fails (buildFailed = true) - // - Error message contains: "The following routes were not configured to run with the Edge Runtime: - /profile/[slug]" - - // This test encodes the EXPECTED behavior, so it will FAIL on unfixed code + // Build must succeed and no edge-runtime warning should appear expect(buildFailed).toBe(false); - - // Verify no Edge Runtime configuration errors - // Note: The build output will still contain "/profile/[slug]" in the build summary - // showing it as an Edge Function Route, which is correct and expected expect(buildOutput).not.toContain('The following routes were not configured to run with the Edge Runtime'); - - // If we reach here on unfixed code, the test will have failed above - // On fixed code, the build should complete successfully - }, 180000); // 3 minute timeout for the test + + // Confirm the profile page source no longer declares the Edge runtime + const routeContent = fs.readFileSync(profileRoutePath, 'utf-8'); + expect(routeContent).not.toContain("runtime = 'edge'"); + }); /** * Additional verification: Check that the profile route file exists diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index 8a2acac5..7bf72779 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -31,6 +31,16 @@ function CallbackContent() { return; } + // Persist session tokens to localStorage so dashboard/API calls can authenticate + const { data: { session } } = await supabase.auth.getSession(); + if (session?.access_token && typeof window !== 'undefined') { + localStorage.setItem('accessToken', session.access_token); + if (session.refresh_token) { + localStorage.setItem('refreshToken', session.refresh_token); + } + window.dispatchEvent(new StorageEvent('storage', { key: 'accessToken' })); + } + console.log("Supabase Auth successful, redirecting to", next); // Successful login! Redirect the user router.push(next); diff --git a/app/gallery/page.tsx b/app/gallery/page.tsx index 75bbfb60..6c787ced 100644 --- a/app/gallery/page.tsx +++ b/app/gallery/page.tsx @@ -5,6 +5,7 @@ import { Heart, MessageCircle, Share2, ChevronUp, ChevronDown, Book, Layers, ShieldCheck, Hexagon, ChevronRight } from 'lucide-react'; import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -30,10 +31,10 @@ function GalleryCard({ post, onVote }: { post: GalleryPost; onVote: (id: string, return (
-
+
{/* Cover Image */} -
+ {post.cover_image ? ( {post.title} ) : ( @@ -50,9 +51,9 @@ function GalleryCard({ post, onVote }: { post: GalleryPost; onVote: (id: string, )}
-
+ -
+
@@ -80,26 +81,35 @@ function GalleryCard({ post, onVote }: { post: GalleryPost; onVote: (id: string,

"{post.content}"

-
+
-
+
{post.likes}
{post.file_url ? ( - ) : ( diff --git a/app/genres/[slug]/page.tsx b/app/genres/[slug]/page.tsx index d39ebdb4..f65b3b2d 100644 --- a/app/genres/[slug]/page.tsx +++ b/app/genres/[slug]/page.tsx @@ -1,5 +1,4 @@ import { ArrowLeft } from 'lucide-react'; -import { Metadata } from 'next'; import Link from 'next/link'; import React from 'react'; diff --git a/app/globals.css b/app/globals.css index 5c3dc337..3362d4f2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1526,4 +1526,50 @@ .madhava-typing span { animation: none !important; } +} + +/* ═══════════════════════════════════════ + INTERACTIVE BOOK — Page Flip Animations + ═══════════════════════════════════════ */ +@keyframes page-flip-right { + 0% { transform: perspective(2000px) rotateY(0deg); opacity: 1; } + 50% { transform: perspective(2000px) rotateY(-15deg); opacity: 0.6; } + 100% { transform: perspective(2000px) rotateY(0deg); opacity: 1; } +} + +@keyframes page-flip-left { + 0% { transform: perspective(2000px) rotateY(0deg); opacity: 1; } + 50% { transform: perspective(2000px) rotateY(15deg); opacity: 0.6; } + 100% { transform: perspective(2000px) rotateY(0deg); opacity: 1; } +} + +.animate-page-flip-right { + animation: page-flip-right 0.35s ease-in-out; +} + +.animate-page-flip-left { + animation: page-flip-left 0.35s ease-in-out; +} + +/* ═══════════════════════════════════════ + BOOK STYLE OVERRIDES + Prevents comic global styles (uppercase headings, + font-weight:600, Comic Neue font) from corrupting + the book's serif reading experience. + ═══════════════════════════════════════ */ +[style*="perspective"] h1, +[style*="perspective"] h2, +[style*="perspective"] h3, +[style*="perspective"] h4, +[style*="perspective"] p, +[style*="perspective"] span, +[style*="perspective"] div, +[style*="perspective"] button { + text-transform: none; + letter-spacing: normal; + font-family: inherit; +} + +[style*="perspective"] p { + font-weight: 300; } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index a021fc54..48b4e6da 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -176,6 +176,7 @@ export default function RootLayout({ {/* Skip link for keyboard users to jump to main content */} → public profile */ -export const runtime = 'edge'; export const dynamicParams = true; +// When `dynamicParams` is true we allow the route to resolve at request time +// instead of forcing every possible slug to be pre-rendered. Under a normal +// Node build this behaves like classic dynamic routing. However, with +// `output: 'export'` (Cloudflare Pages static export) only the IDs returned by +// `generateStaticParams()` are rendered at build time; any other slug requested +// directly will 404 unless the hosting platform is configured with an SPA +// fallback (e.g. redirect all `/profile/*` to the client entry). Client-side +// navigation after initial load still works because the router can handle the +// slug on the browser. +// +// We pre-render only the special "me" slug for authenticated users; all other +// profiles rely on client-side resolution. +export function generateStaticParams() { + return [ + { + slug: 'me', + }, + ]; +} + import ProfilePageClient from './client'; export default function ProfilePage({ diff --git a/app/stories/[id]/client.tsx b/app/stories/[id]/client.tsx index 659f007b..d1e84492 100644 --- a/app/stories/[id]/client.tsx +++ b/app/stories/[id]/client.tsx @@ -2,14 +2,11 @@ import { useEffect, useState } from 'react'; import BookView from '@/components/book-view'; +import StoryEngagement from '@/components/story-engagement'; import { ArrowLeft, BookOpen, Eye, Heart, Loader2, Tag } from 'lucide-react'; import Link from 'next/link'; -interface Chapter { - index: number; - title: string; - content: string; -} +interface Chapter { index: number; title: string; content: string; } interface Story { id: string; @@ -49,7 +46,6 @@ export default function StoryClient({ id }: { id: string }) { useEffect(() => { let cancelled = false; - async function fetchStory() { setNotFound(false); setLoading(true); @@ -67,10 +63,7 @@ export default function StoryClient({ id }: { id: string }) { setLoading(false); } fetchStory(); - - return () => { - cancelled = true; - }; + return () => { cancelled = true; }; }, [id]); if (loading) { @@ -85,13 +78,12 @@ export default function StoryClient({ id }: { id: string }) { return (
-

Signal Lost

-

Story not found.

- Story Not Found +

This story may have been removed or doesn't exist.

+ - ← Return to Marketplace + style={{ fontWeight: 500 }}> + ← Back to Gallery
@@ -103,68 +95,72 @@ export default function StoryClient({ id }: { id: string }) { return (
- {/* Hero cover image */} - {story.cover_image && ( -
- {story.title} -
-
- )} - -
- {/* Back nav */} - - - {/* Story header */} -
- {story.genre && ( - - - {story.genre} - - )} - -

- {story.title} -

- -
- {story.author_name && By {story.author_name}} - {story.views != null && ( - - {story.views.toLocaleString()} views + + {/* ── Compact Header Bar ───────────────────────────────── */} +
+
+
+ + + Gallery + + +
+ +
+ {story.genre && ( + + {story.genre} + + )} + + {story.title} - )} - {story.likes != null && ( +
+ +
+ {story.views != null && ( + + {story.views.toLocaleString()} + + )} + {story.likes != null && ( + + {story.likes.toLocaleString()} + + )} - {story.likes.toLocaleString()} likes + {chapters.length} - )} - - {chapters.length} chapter{chapters.length !== 1 ? 's' : ''} - +
+
+
- {story.description && ( -

- {story.description} -

- )} -
- - {/* ── BOOK VIEW with TTS audio controls ──────────────────────────── */} + {/* ── Story Title Section ──────────────────────────────── */} +
+

+ {story.title} +

+ {story.author_name && ( +

+ by {story.author_name} +

+ )} + {story.description && ( +

+ {story.description} +

+ )} +
+ + {/* ── Interactive Book ──────────────────────────────────── */} +
-
+ + + {/* ── Engagement Section ───────────────────────────────── */} +
+
+ +
+
); } diff --git a/app/stories/[id]/page.tsx b/app/stories/[id]/page.tsx index 7543af97..e2339b16 100644 --- a/app/stories/[id]/page.tsx +++ b/app/stories/[id]/page.tsx @@ -1,21 +1,18 @@ import StoryClient from './client'; -export const dynamic = 'force-static'; -export const dynamicParams = true; - -/** - * Pre-render a few known IDs at build time; - * `dynamicParams = true` means unknown IDs still fallback to CSR (the 'use client' child handles data fetching). - */ +// A minimal list of params is required when `output: 'export'` is set, otherwise +// Next.js will abort the build with a missing generateStaticParams error. We +// supply a dummy id so one static page is generated; additional story IDs will +// be handled by the client router at runtime thanks to `dynamicParams = true`. +// With Cloudflare Pages a non-prerendered `/stories/` request will fall +// through to the SPA fallback if you've configured one; otherwise the page may +// 404 on direct access but will still work via client-side navigation. export function generateStaticParams() { - const params: { id: string }[] = []; - // Top mock IDs kept for backward compat - for (const id of ['top-1', 'top-2', 'top-3']) params.push({ id }); - // Numeric story IDs - for (let i = 1; i <= 90; i++) params.push({ id: `story-${i}` }); - return params; + return [{ id: 'default' }]; } +export const dynamicParams = true; + export default function StoryPage({ params }: { params: { id: string } }) { return ; } \ No newline at end of file diff --git a/components/auth/sign-in-form.tsx b/components/auth/sign-in-form.tsx index 3fbb4824..a52166d2 100644 --- a/components/auth/sign-in-form.tsx +++ b/components/auth/sign-in-form.tsx @@ -55,6 +55,16 @@ export function SignInForm({ onToggleMode }: { onToggleMode: () => void }) { if (result.error) { throw new Error(result.error); } + + // Persist tokens so dashboard, profile, and API calls can authenticate + if (result.data?.tokens?.accessToken && typeof window !== 'undefined') { + localStorage.setItem('accessToken', result.data.tokens.accessToken); + if (result.data.tokens.refreshToken) { + localStorage.setItem('refreshToken', result.data.tokens.refreshToken); + } + // Notify other tabs/hooks that auth state changed + window.dispatchEvent(new StorageEvent('storage', { key: 'accessToken' })); + } setSuccess(true); toast({ @@ -63,7 +73,7 @@ export function SignInForm({ onToggleMode }: { onToggleMode: () => void }) { }); setTimeout(() => { - router.push('/'); + router.push('/dashboard'); router.refresh(); }, 800); diff --git a/components/auth/sign-up-form.tsx b/components/auth/sign-up-form.tsx index 5f551942..f95b639f 100644 --- a/components/auth/sign-up-form.tsx +++ b/components/auth/sign-up-form.tsx @@ -118,6 +118,16 @@ export function SignUpForm({ onToggleMode }: { onToggleMode: () => void }) { }); if (error) throw error; + + // Persist tokens so user is immediately authenticated if auto-confirmed + const { data: { session } } = await supabase.auth.getSession(); + if (session?.access_token && typeof window !== 'undefined') { + localStorage.setItem('accessToken', session.access_token); + if (session.refresh_token) { + localStorage.setItem('refreshToken', session.refresh_token); + } + window.dispatchEvent(new StorageEvent('storage', { key: 'accessToken' })); + } setSuccess(true); toast({ diff --git a/components/book-view.tsx b/components/book-view.tsx index fe323952..394f4c1b 100644 --- a/components/book-view.tsx +++ b/components/book-view.tsx @@ -1,18 +1,17 @@ 'use client'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { Play, Pause, SkipBack, Mic2, Languages, Zap, Volume2, Loader2, RefreshCw, Headphones, BookOpen } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + Play, Pause, SkipBack, SkipForward, Mic2, Languages, Zap, Volume2, + Loader2, RefreshCw, Headphones, BookOpen, ChevronLeft, ChevronRight, +} from 'lucide-react'; import { useTTS, BULBUL_SPEAKERS, BULBUL_LANGUAGES, SPEEDS } from '@/hooks/use-tts'; import { createClient } from '@/lib/supabase/client'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- -interface Chapter { - index: number; - title: string; - content: string; -} +interface Chapter { index: number; title: string; content: string; } interface BookViewProps { storyId: string; @@ -22,12 +21,12 @@ interface BookViewProps { defaultSpeaker?: string; defaultLanguage?: string; defaultPace?: number; - compact?: boolean; // compact = mini marketplace card mode + compact?: boolean; className?: string; } // --------------------------------------------------------------------------- -// Helper: format seconds → mm:ss +// Helpers // --------------------------------------------------------------------------- function formatTime(s: number): string { if (!s || isNaN(s)) return '0:00'; @@ -36,8 +35,27 @@ function formatTime(s: number): string { return `${m}:${sec.toString().padStart(2, '0')}`; } +/** Split chapter text into pages of ~1200 chars each (paragraph-aware) */ +function paginateContent(text: string, maxChars = 1200): string[] { + if (!text) return ['']; + const paragraphs = text.split(/\n\n+/).map(p => p.trim()).filter(Boolean); + const pages: string[] = []; + let current = ''; + + for (const para of paragraphs) { + if (current.length + para.length + 2 > maxChars && current.length > 0) { + pages.push(current.trim()); + current = para; + } else { + current += (current ? '\n\n' : '') + para; + } + } + if (current.trim()) pages.push(current.trim()); + return pages.length > 0 ? pages : ['']; +} + // --------------------------------------------------------------------------- -// Mini Waveform Indicator (animated bars when playing) +// Waveform Bars // --------------------------------------------------------------------------- function WaveformBars({ isPlaying }: { isPlaying: boolean }) { return ( @@ -45,14 +63,8 @@ function WaveformBars({ isPlaying }: { isPlaying: boolean }) { {[3, 5, 4, 6, 3].map((h, i) => ( ))} @@ -60,18 +72,12 @@ function WaveformBars({ isPlaying }: { isPlaying: boolean }) { } // --------------------------------------------------------------------------- -// TTS Audio Bar +// TTS Audio Bar (kept intact for the audio controls) // --------------------------------------------------------------------------- function TTSAudioBar({ - storyId, - chapterIndex, - chapterText, - compact = false, + storyId, chapterIndex, chapterText, compact = false, }: { - storyId: string; - chapterIndex: number; - chapterText: string; - compact?: boolean; + storyId: string; chapterIndex: number; chapterText: string; compact?: boolean; }) { const supabase = createClient(); const [token, setToken] = useState(); @@ -85,7 +91,6 @@ function TTSAudioBar({ play, pause, seek, setSpeed, setSpeaker, setLanguage, generateAudio, } = useTTS(storyId, chapterIndex); - // Get auth token for generate action useEffect(() => { supabase.auth.getSession().then(({ data }) => { setToken(data.session?.access_token); @@ -93,11 +98,7 @@ function TTSAudioBar({ }, [supabase.auth]); const handlePlayPause = useCallback(() => { - if (isPlaying) { - pause(); - } else if (audioUrl) { - play(); - } + if (isPlaying) pause(); else if (audioUrl) play(); }, [isPlaying, audioUrl, play, pause]); const handleGenerate = useCallback(() => { @@ -105,84 +106,68 @@ function TTSAudioBar({ }, [generateAudio, chapterText, token]); if (compact) { - // ── COMPACT / MARKETPLACE CARD MINI PLAYER ────────────────────────────── return (
{audioUrl ? ( - ) : ( - )} {isPlaying && } - - {audioUrl ? formatTime(currentTime) : 'Tap to preview'} - + {audioUrl ? formatTime(currentTime) : 'Tap to preview'}
); } - // ── FULL TTS AUDIO BAR ──────────────────────────────────────────────────── return ( -
- {/* Background gradient accent */} -
+
+
+
+
- {/* Top row: waveform + title + voice controls */} + {/* Voice controls */}
{isGenerating ? 'Generating narration…' : isLoading ? 'Loading audio…' : audioUrl ? 'Narration Ready' : 'AI Narration'} - {/* Speaker selector */} + {/* Speaker */}
- {showSpeaker && ( -
+
{BULBUL_SPEAKERS.map(s => ( + className={`w-full text-left text-xs px-3 py-1.5 rounded-lg hover:bg-white/10 ${s === speaker ? 'text-emerald-400 font-semibold' : 'text-white/70'}`}>{s} ))}
)}
- {/* Language selector */} + {/* Language */}
- {showLang && ( -
+
{BULBUL_LANGUAGES.map(l => ( ))} @@ -190,21 +175,17 @@ function TTSAudioBar({ )}
- {/* Speed selector */} + {/* Speed */}
- {showSpeed && ( -
+
{SPEEDS.map(sp => ( + className={`w-full text-left text-xs px-3 py-1.5 rounded-lg hover:bg-white/10 ${sp === pace ? 'text-emerald-400 font-semibold' : 'text-white/70'}`}>{sp}x ))}
)} @@ -214,75 +195,33 @@ function TTSAudioBar({ {/* Seek bar */} {audioUrl && (
- - seek(Number(e.target.value))} - className="flex-1 h-1 rounded-full appearance-none cursor-pointer accent-emerald-400" - aria-label="Seek" - /> - - {formatTime(currentTime)} / {formatTime(duration)} - + + seek(Number(e.target.value))} + className="flex-1 h-1 rounded-full appearance-none cursor-pointer accent-emerald-400" aria-label="Seek" /> + {formatTime(currentTime)} / {formatTime(duration)}
)} - {/* Play/Pause + Generate button */} + {/* Play/Generate */}
{audioUrl ? ( - ) : ( - )} - {audioUrl && ( - )} - - {error && ( - {error} - )} + {error && {error}}
@@ -290,96 +229,233 @@ function TTSAudioBar({ } // --------------------------------------------------------------------------- -// BookView – main component +// BookView – Interactive Book with Page Flipping // --------------------------------------------------------------------------- export default function BookView({ - storyId, - title, - author, - chapters, - defaultSpeaker = 'Shubh', - defaultLanguage = 'en-IN', - defaultPace = 1, - compact = false, - className = '', + storyId, title, author, chapters, + defaultSpeaker = 'Shubh', defaultLanguage = 'en-IN', defaultPace = 1, + compact = false, className = '', }: BookViewProps) { const [activeChapter, setActiveChapter] = useState(0); + const [currentPage, setCurrentPage] = useState(0); + const [isFlipping, setIsFlipping] = useState(false); + const [flipDirection, setFlipDirection] = useState<'left' | 'right'>('right'); + const bookRef = useRef(null); const currentChapter = chapters[activeChapter] || chapters[0]; + const pages = useMemo( + () => paginateContent(currentChapter?.content || '', 1200), + [currentChapter?.content] + ); + + // Reset page when chapter changes + useEffect(() => { setCurrentPage(0); }, [activeChapter]); + + const totalPages = pages.length; + const hasNext = currentPage < totalPages - 1 || activeChapter < chapters.length - 1; + const hasPrev = currentPage > 0 || activeChapter > 0; + + const goNext = useCallback(() => { + if (isFlipping) return; + if (currentPage < totalPages - 1) { + setFlipDirection('right'); + setIsFlipping(true); + setTimeout(() => { setCurrentPage(p => p + 1); setIsFlipping(false); }, 350); + } else if (activeChapter < chapters.length - 1) { + setFlipDirection('right'); + setIsFlipping(true); + setTimeout(() => { setActiveChapter(c => c + 1); setCurrentPage(0); setIsFlipping(false); }, 350); + } + }, [isFlipping, currentPage, totalPages, activeChapter, chapters.length]); + + const goPrev = useCallback(() => { + if (isFlipping) return; + if (currentPage > 0) { + setFlipDirection('left'); + setIsFlipping(true); + setTimeout(() => { setCurrentPage(p => p - 1); setIsFlipping(false); }, 350); + } else if (activeChapter > 0) { + setFlipDirection('left'); + setIsFlipping(true); + setTimeout(() => { + const prevChapterContent = chapters[activeChapter - 1]?.content || ''; + const prevPages = paginateContent(prevChapterContent, 1200); + setActiveChapter(c => c - 1); + setCurrentPage(Math.max(0, prevPages.length - 1)); + setIsFlipping(false); + }, 350); + } + }, [isFlipping, currentPage, activeChapter, chapters]); + + // Clamp page for when going backwards to a previous chapter + const safeCurrentPage = Math.min(currentPage, totalPages - 1); + const pageContent = pages[safeCurrentPage] || ''; + + // Keyboard navigation + useEffect(() => { + const handler = (e: KeyboardEvent) => { + // Don't handle keys if in compact mode to avoid side-effects + if (compact) return; + if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); goNext(); } + if (e.key === 'ArrowLeft') { e.preventDefault(); goPrev(); } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [goNext, goPrev, compact]); if (compact) { - // ── COMPACT MODE: Used in marketplace cards ──────────────────────────── - return ( - - ); + return ; } - // ── FULL BOOK VIEW ───────────────────────────────────────────────────────── return ( -
- {/* Book surface */} -
- {/* Paper texture / page glow */} -
-
- - {/* Chapter tabs */} - {chapters.length > 1 && ( -
- - {chapters.map((ch, idx) => ( - - ))} +
+ {/* ── THE BOOK ───────────────────────────────────────────────── */} +
+ + {/* Book Outer Shell */} +
+ + {/* Book spine shadow */} +
+ + {/* Book cover / page area */} +
+ + {/* Page texture overlay */} +
+ + {/* Chapter tabs (above the pages) */} + {chapters.length > 1 && ( +
+ + {chapters.map((ch, idx) => ( + + ))} +
+ )} + + {/* ── Page Content ─────────────────────────────────────── */} +
+ + {/* Flip animation overlay */} + {isFlipping && ( +
+
+
+ )} + + {/* Page header */} +
+ + {chapters.length > 1 ? `Chapter ${activeChapter + 1} · ${currentChapter?.title}` : title} + + + {author || 'Unknown Author'} + +
+ + {/* Chapter title on first page */} + {safeCurrentPage === 0 && ( +
+ {chapters.length > 1 && ( +

+ {currentChapter?.title} +

+ )} +
+
+ )} + + {/* Text content */} +
+
+ {pageContent.split('\n\n').map((para, i) => ( +

0 ? '2em' : undefined, + }}> + {i === 0 && safeCurrentPage === 0 ? ( + <> + + {para.trim()[0]} + + {para.trim().slice(1)} + + ) : para.trim()} +

+ ))} +
+
+ + {/* Page footer */} +
+
+ + {safeCurrentPage + 1} + +
+
+ + {/* Bottom page edge shadow */} +
+
- )} - {/* Chapter content */} -
- {chapters.length > 1 && ( -

- Chapter {activeChapter + 1}: {currentChapter?.title} -

+ {/* ── Page Navigation Buttons ─────────────────────────── */} + {hasPrev && ( + + )} + {hasNext && ( + )} -
- {currentChapter?.content?.split('\n\n').map((para, i) => ( -

- {para.trim()} -

- ))} -
- {/* Bottom page edge shadow */} -
+ {/* ── Page indicator ────────────────────────────────────── */} +
+ Page {safeCurrentPage + 1} of {totalPages} + {chapters.length > 1 && ( + <> + · + Chapter {activeChapter + 1} of {chapters.length} + + )} + · + Use ← → keys to navigate +
- {/* TTS Audio Bar — rendered below the book surface */} -
- -
+ {/* ── TTS Audio Bar ──────────────────────────────────────── */} +
); } diff --git a/components/footer.tsx b/components/footer.tsx index 5ae1f320..d06a7d2d 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -34,7 +34,11 @@ export function Footer({ version }: { version?: string }) { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'https://groqtales-backend-api.onrender.com'}/api/health`, { cache: 'no-store' }); if (res.ok) { const data = await res.json(); - setHealthStatus((data.status === 'ok' || data.status === 'healthy') ? 'ok' : data.status === 'degraded' ? 'degraded' : 'down'); + setHealthStatus( + (data.status === 'ok' || data.status === 'healthy' || data.status === 'operational') ? 'ok' + : (data.status === 'degraded' || data.status === 'partial') ? 'degraded' + : 'down' + ); } else { setHealthStatus('down'); } @@ -235,7 +239,12 @@ export function Footer({ version }: { version?: string }) {
- + +
Promise; disconnectWallet: () => Promise; + setWalletConnection: (account: string, chainId?: number) => void; networkName: string; ensName: string | null; } @@ -186,7 +187,22 @@ export function Web3Provider({ children }: { children: React.ReactNode }) { }; - // const disconnectWallet = () => { + const setWalletConnection = useCallback((newAccount: string, newChainId?: number) => { + setAccount(newAccount); + setConnected(true); + if (newChainId) { + setChainId(newChainId); + if (newChainId === 1) setNetworkName('Ethereum Mainnet'); + else if (newChainId === 5) setNetworkName('Goerli Testnet'); + else if (newChainId === 11155111) setNetworkName('Sepolia Testnet'); + else if (newChainId === 137) setNetworkName('Polygon'); + else if (newChainId === 8453) setNetworkName('Base'); + else if (newChainId === 42161) setNetworkName('Arbitrum'); + else if (newChainId === 10) setNetworkName('Optimism'); + else setNetworkName(`Chain ${newChainId}`); + } + }, []); + const contextValue: Web3ContextType = { account, chainId, @@ -195,6 +211,7 @@ export function Web3Provider({ children }: { children: React.ReactNode }) { connecting, connectWallet, disconnectWallet, + setWalletConnection, networkName, ensName, // buyNFT, diff --git a/components/story-engagement.tsx b/components/story-engagement.tsx new file mode 100644 index 00000000..cbba2657 --- /dev/null +++ b/components/story-engagement.tsx @@ -0,0 +1,407 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { ChevronUp, ChevronDown, MessageCircle, Bookmark, BookmarkCheck, Send, Loader2, User } from 'lucide-react'; +import { createClient } from '@/lib/supabase/client'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +interface Comment { + id: string; + content: string; + created_at: string; + user_id: string; + profiles?: { username: string; avatar_url: string; display_name: string } | null; +} + +interface StoryEngagementProps { + storyId: string; +} + +export default function StoryEngagement({ storyId }: StoryEngagementProps) { + const supabase = createClient(); + + // Auth + const [userId, setUserId] = useState(null); + const [loading, setLoading] = useState(true); + + // Votes + const [voteScore, setVoteScore] = useState(0); + const [userVote, setUserVote] = useState<1 | -1 | null>(null); + const [voteLoading, setVoteLoading] = useState(false); + + // Comments + const [comments, setComments] = useState([]); + const [commentText, setCommentText] = useState(''); + const [commentLoading, setCommentLoading] = useState(false); + const [showComments, setShowComments] = useState(false); + const [commentCount, setCommentCount] = useState(0); + + // Save + const [isSaved, setIsSaved] = useState(false); + const [saveLoading, setSaveLoading] = useState(false); + + // ---------- Auth ---------- + + // Bootstraps Supabase auth state by checking for a persisted access token in + // localStorage. This mirrors the pattern used in other components (header, + // user-nav, etc.) so that when a user logs in/out anywhere in the app the + // session is restored immediately and `onAuthStateChange` will fire. We + // also listen for storage events so that changes originating in other tabs + // are handled. + useEffect(() => { + let subscription: any; + let storageListener: (e: StorageEvent) => void; + + const init = async () => { + if (typeof window !== 'undefined') { + const token = localStorage.getItem('accessToken'); + const refresh = localStorage.getItem('refreshToken'); + if (token) { + try { + await supabase.auth.setSession({ access_token: token, refresh_token: refresh || undefined }); + } catch (err) { + console.error('Failed to restore supabase session from localStorage', err); + } + } + } + + // grab whatever session Supabase currently has (may be empty) + const { data } = await supabase.auth.getSession(); + setUserId(data.session?.user?.id ?? null); + + // subscribe to later changes + const { data: sub } = supabase.auth.onAuthStateChange((_event, session) => { + setUserId(session?.user?.id ?? null); + }); + subscription = sub.subscription; + }; + + init(); + + storageListener = (e: StorageEvent) => { + if (e.key === 'accessToken') { + // token changed (login/logout elsewhere) – re-bootstrap session + init(); + } + }; + window.addEventListener('storage', storageListener); + + return () => { + if (subscription) subscription.unsubscribe(); + window.removeEventListener('storage', storageListener); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ---------- Load engagement data ---------- + useEffect(() => { + if (!storyId) return; + + async function loadData() { + setLoading(true); + + // Load vote score + const { data: votes } = await supabase + .from('story_votes') + .select('vote') + .eq('story_id', storyId); + + if (votes) { + const score = votes.reduce((acc: number, v: any) => acc + v.vote, 0); + setVoteScore(score); + } + + // Load user's vote + if (userId) { + const { data: myVote } = await supabase + .from('story_votes') + .select('vote') + .eq('story_id', storyId) + .eq('user_id', userId) + .maybeSingle(); + + setUserVote(myVote?.vote ?? null); + } + + // Load comment count + const { count } = await supabase + .from('story_comments') + .select('*', { count: 'exact', head: true }) + .eq('story_id', storyId); + + setCommentCount(count ?? 0); + + // Check if saved + if (userId) { + const { data: saved } = await supabase + .from('saved_stories') + .select('story_id') + .eq('story_id', storyId) + .eq('user_id', userId) + .maybeSingle(); + + setIsSaved(!!saved); + } + + setLoading(false); + } + + loadData(); + }, [storyId, userId]); + + // ---------- Load comments ---------- + const loadComments = useCallback(async () => { + const { data } = await supabase + .from('story_comments') + .select('id, content, created_at, user_id, profiles(username, avatar_url, display_name)') + .eq('story_id', storyId) + .order('created_at', { ascending: false }) + .limit(50); + + setComments((data as any) || []); + // Note: Intentionally NOT updating setCommentCount here because the paginated + // result length would overwrite the true total count fetched on initial load. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [storyId]); + + useEffect(() => { + if (showComments) loadComments(); + }, [showComments, loadComments]); + + // ---------- Vote ---------- + const handleVote = async (direction: 1 | -1) => { + if (!userId) return; + setVoteLoading(true); + + const prevVoteScore = voteScore; + const prevUserVote = userVote; + + // Optimistic UI Update + if (userVote === direction) { + setVoteScore(prev => prev - direction); + setUserVote(null); + } else { + setVoteScore(prev => prev - (userVote ?? 0) + direction); + setUserVote(direction); + } + + try { + if (prevUserVote === direction) { + // Remove vote + await supabase.from('story_votes').delete().eq('story_id', storyId).eq('user_id', userId).throwOnError(); + } else { + // Upsert vote + await supabase.from('story_votes').upsert( + { story_id: storyId, user_id: userId, vote: direction }, + { onConflict: 'story_id,user_id' } + ).throwOnError(); + } + } catch (err) { + console.error('Vote error:', err); + // Revert optimistic update on failure + setVoteScore(prevVoteScore); + setUserVote(prevUserVote); + } + setVoteLoading(false); + }; + + // ---------- Comment ---------- + const handleComment = async () => { + if (!userId || !commentText.trim()) return; + setCommentLoading(true); + + try { + await supabase.from('story_comments').insert({ + story_id: storyId, + user_id: userId, + content: commentText.trim(), + }).throwOnError(); + setCommentText(''); + // increment the count immediately so the toggle button reflects the + // new comment without waiting for `loadComments` to refresh the list + setCommentCount(c => c + 1); // TODO: keep this in sync with loadComments + await loadComments(); + } catch (err) { + console.error('Comment error:', err); + } + setCommentLoading(false); + }; + + // ---------- Save ---------- + const handleSave = async () => { + if (!userId) return; + setSaveLoading(true); + + try { + if (isSaved) { + await supabase.from('saved_stories').delete().eq('story_id', storyId).eq('user_id', userId).throwOnError(); + setIsSaved(false); + } else { + await supabase.from('saved_stories').insert({ story_id: storyId, user_id: userId }).throwOnError(); + setIsSaved(true); + } + } catch (err) { + console.error('Save error:', err); + } + setSaveLoading(false); + }; + + // ---------- Time ago ---------- + function timeAgo(dateStr: string) { + const diff = Date.now() - new Date(dateStr).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + const days = Math.floor(hrs / 24); + return `${days}d ago`; + } + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* ── ACTION BAR ─────────────────────────────────────────────── */} +
+ {/* Votes */} +
+ + + {voteScore} + + +
+ + {/* Comments toggle */} + + + {/* Save/Bookmark */} + + + {!userId && ( + Sign in to interact + )} +
+ + {/* ── COMMENTS SECTION ───────────────────────────────────────── */} + {showComments && ( +
+ {/* Comment input */} + {userId ? ( +
+
+ +
+ setCommentText(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleComment()} + placeholder="Add a comment..." + className="flex-1 bg-transparent text-white text-sm placeholder:text-white/30 outline-none" + maxLength={1000} + /> + +
+ ) : ( +
+ Sign in to leave a comment +
+ )} + + {/* Comments list */} +
+ {comments.length === 0 ? ( +
+ No comments yet. Be the first to share your thoughts! +
+ ) : ( + comments.map(c => ( +
+ + + + {(c.profiles?.display_name || c.profiles?.username || 'U')[0]} + + +
+
+ + {c.profiles?.display_name || c.profiles?.username || 'Anonymous'} + + {timeAgo(c.created_at)} +
+

{c.content}

+
+
+ )) + )} +
+
+ )} +
+ ); +} diff --git a/components/wallet-connect.tsx b/components/wallet-connect.tsx index 3b48b380..1c947ab2 100644 --- a/components/wallet-connect.tsx +++ b/components/wallet-connect.tsx @@ -40,8 +40,8 @@ export default function WalletConnect() { disconnectWallet, networkName, ensName, + setWalletConnection, } = useWeb3(); - const { toast } = useToast(); const [showDropdown, setShowDropdown] = useState(false); const [showWalletModal, setShowWalletModal] = useState(false); @@ -88,13 +88,83 @@ export default function WalletConnect() { const handleWalletConnect = async () => { setShowWalletModal(false); - // Placeholder for actual WalletConnect v2 initialization - toast({ - title: 'WalletConnect Initialization', - description: 'Opening mobile QR scan modal...', - }); - console.log("Initiating WalletConnect mobile flow..."); - // await initiateWalletConnectFlow() + + const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + if (!projectId || projectId === 'your_wallet_connect_project_id_here') { + toast({ + title: 'WalletConnect Not Configured', + description: 'WalletConnect Project ID is not set. Please configure NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID.', + variant: 'destructive', + }); + return; + } + + try { + const { default: EthereumProvider } = await import('@walletconnect/ethereum-provider'); + + const provider = await EthereumProvider.init({ + projectId, + chains: [1], // Ethereum Mainnet + showQrModal: true, + optionalChains: [137, 8453, 42161, 10], // Polygon, Base, Arbitrum, Optimism + }); + + await provider.connect(); + + const accounts = provider.accounts; + if (!accounts || accounts.length === 0) { + toast({ title: 'Connection Failed', description: 'No accounts returned from WalletConnect.', variant: 'destructive' }); + return; + } + + const selectedAccount: string = accounts[0]!; + + // Authenticate via backend wallet-login with signature verification + const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'https://groqtales-backend-api.onrender.com'; + + // Step 1: Get nonce from backend + const nonceRes = await fetch(`${baseUrl}/api/v1/auth/nonce?address=${selectedAccount}`); + if (!nonceRes.ok) { + toast({ title: 'Auth Failed', description: 'Failed to get authentication nonce.', variant: 'destructive' }); + return; + } + const { nonce } = await nonceRes.json(); + + // Step 2: Sign the message + const message = `Sign this message to authenticate with Comicraft. Nonce: ${nonce}`; + const signature = await provider.request({ method: 'personal_sign', params: [message, selectedAccount] }); + + // Step 3: Send to backend for verification and token issuance + const authRes = await fetch(`${baseUrl}/api/v1/auth/wallet-login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ address: selectedAccount, signature }), + }); + + if (authRes.ok) { + // use the chain id returned by the provider instead of hardcoding + const connectedChain = typeof provider.chainId === 'string' ? parseInt(provider.chainId, 16) : provider.chainId; + setWalletConnection(selectedAccount, connectedChain || 1); + const authData = await authRes.json(); + if (authData.data?.tokens?.accessToken && typeof window !== 'undefined') { + localStorage.setItem('accessToken', authData.data.tokens.accessToken); + if (authData.data.tokens.refreshToken) { + localStorage.setItem('refreshToken', authData.data.tokens.refreshToken); + } + window.dispatchEvent(new StorageEvent('storage', { key: 'accessToken' })); + } + toast({ title: 'Wallet Connected', description: `Connected via WalletConnect: ${selectedAccount.slice(0, 6)}...${selectedAccount.slice(-4)}` }); + } else { + toast({ title: 'Auth Failed', description: 'Wallet authentication failed.', variant: 'destructive' }); + } + } catch (err: any) { + if (err?.message?.includes('User rejected') || err?.message?.includes('cancelled')) { + // User cancelled — no toast needed + return; + } + console.error('WalletConnect error:', err); + toast({ title: 'WalletConnect Error', description: err?.message || 'Failed to connect via WalletConnect.', variant: 'destructive' }); + } }; if (!connected) { @@ -151,10 +221,8 @@ export default function WalletConnect() { diff --git a/docker-compose.yml b/docker-compose.yml index f05849c7..2186674e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,32 +1,80 @@ +# GroqTales / Comicraft — Docker Compose +# v2.0.0 — Supabase-backed, Ethereum Mainnet via Alchemy +# +# Usage: +# cp .env.example .env.local +# # Fill in all required env vars +# docker compose up --build +# +# The app connects to external managed services: +# - Supabase (PostgreSQL + Auth + Storage) +# - Alchemy (Ethereum RPC) +# - Groq / Gemini AI +# - Sarvam TTS +# - Redis (Render-managed or local) +# - MongoDB (Optional: Required only for Vector Search) + services: - mongo: - image: mongo:7 - restart: always + # --------------------------------------------------------------------------- + # Redis — local cache (optional; production uses Render Redis) + # --------------------------------------------------------------------------- + redis: + image: redis:7-alpine + restart: unless-stopped ports: - - "27017:27017" + - "6379:6379" volumes: - - mongo_data:/data/db - - anvil: - image: ghcr.io/foundry-rs/foundry:v1.0.0 - command: anvil --host 0.0.0.0 --port 8545 - ports: - - "8545:8545" + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 - server: + # --------------------------------------------------------------------------- + # GroqTales App — Next.js frontend + Express backend + # --------------------------------------------------------------------------- + app: build: context: . + dockerfile: Dockerfile depends_on: - - mongo - - anvil + redis: + condition: service_healthy + env_file: + - .env.local environment: - NODE_ENV: development - MONGODB_URI: mongodb://mongo:27017/groqtales - NEXT_PUBLIC_RPC_URL: http://anvil:8545 - command: sh -c "node scripts/seed.js && npm start" + NODE_ENV: production + REDIS_URL: redis://redis:6379 + + # ── Supabase (required) ── + # NEXT_PUBLIC_SUPABASE_URL: (set in .env.local) + # NEXT_PUBLIC_SUPABASE_ANON_KEY: (set in .env.local) + # SUPABASE_SERVICE_ROLE_KEY: (set in .env.local) + + # ── AI Services (required) ── + # GEMINI_API_KEY: (set in .env.local) + # GROQ_API_KEY: (set in .env.local) + + # ── Blockchain (required for NFT minting) ── + # ALCHEMY_ETH_MAINNET_HTTP_URL: (set in .env.local) + # PLATFORM_SIGNER_KEY: (set in .env.local) + + # ── WalletConnect (optional) ── + # NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: (set in .env.local) + + # ── TTS (optional) ── + # SARVAM_API_KEY: (set in .env.local) ports: - "3000:3000" - "3001:3001" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-fs", "http://localhost:3001/healthz"] + interval: 30s + timeout: 5s + start_period: 15s + retries: 3 volumes: - mongo_data: + redis_data: diff --git a/hooks/use-system-health.ts b/hooks/use-system-health.ts index e5aba62b..3f22924b 100644 --- a/hooks/use-system-health.ts +++ b/hooks/use-system-health.ts @@ -47,7 +47,7 @@ export function useSystemHealth(): HealthStatus { }); if (res.ok) { const data = await res.json(); - api = data.status === 'healthy' || data.status === 'degraded'; + api = data.status === 'healthy' || data.status === 'operational' || data.status === 'degraded' || data.status === 'partial'; // The backend returns database info nested under data.database db = data.database?.connected === true; } diff --git a/hooks/use-tts.ts b/hooks/use-tts.ts index c5f0d946..162db992 100644 --- a/hooks/use-tts.ts +++ b/hooks/use-tts.ts @@ -47,7 +47,6 @@ interface TTSState { error: string | null; } -const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://groqtales-backend-api.onrender.com'; export const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2]; export function useTTS(storyId: string, chapterIndex = 0, defaultSpeaker = 'Shubh', defaultLang = 'en-IN') { @@ -79,7 +78,7 @@ export function useTTS(storyId: string, chapterIndex = 0, defaultSpeaker = 'Shub speaker: state.speaker, languageCode: state.languageCode, }); - const res = await fetch(`${API_BASE}/api/v1/tts/audio?${params}`); + const res = await fetch(`/api/tts/audio?${params}`); if (!cancelled && res.ok) { const data = await res.json(); setState(prev => ({ @@ -178,7 +177,7 @@ export function useTTS(storyId: string, chapterIndex = 0, defaultSpeaker = 'Shub if (!text || !storyId) return; setState(prev => ({ ...prev, isGenerating: true, error: null })); try { - const res = await fetch(`${API_BASE}/api/v1/tts/generate`, { + const res = await fetch(`/api/tts/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/lib/api-client.ts b/lib/api-client.ts index 0022fcd8..e58fa066 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -152,7 +152,7 @@ export interface AIProcessingRequest { export async function processAI(request: AIProcessingRequest) { return withRetry(() => - apiFetch('/api/v1/ai', { + apiFetch('/api/v1/ai/generate', { method: 'POST', headers: authHeaders(), body: JSON.stringify(request), diff --git a/lib/feedService.ts b/lib/feedService.ts index d35e8cba..9f419a94 100644 --- a/lib/feedService.ts +++ b/lib/feedService.ts @@ -1,7 +1,7 @@ import { createClient } from '@/lib/supabase/server'; export async function getPersonalizedFeed(userId: string, page = 1, limit = 10) { - const supabase = createClient(); + const supabase = await createClient(); const skip = (page - 1) * limit; // We fetch interactions directly using Supabase @@ -66,7 +66,7 @@ export async function getPersonalizedFeed(userId: string, page = 1, limit = 10) } async function getTrendingFeed(page: number, limit: number) { - const supabase = createClient(); + const supabase = await createClient(); const skip = (page - 1) * limit; const { data, error } = await supabase diff --git a/lib/feeds-client.ts b/lib/feeds-client.ts index 61728bf9..0c67041e 100644 --- a/lib/feeds-client.ts +++ b/lib/feeds-client.ts @@ -66,6 +66,7 @@ export async function fetchNotifications( ): Promise { try { const token = getAuthToken(); + if (!token) return []; const res = await fetch( `${feedsBase()}/api/feeds/notifications/me?unread=${unreadOnly ? 50 : 0}&limit=${limit}`, { diff --git a/lib/gemini-service.ts b/lib/gemini-service.ts index 9a665544..47a96bb4 100644 --- a/lib/gemini-service.ts +++ b/lib/gemini-service.ts @@ -54,7 +54,7 @@ export class GeminiService { model?: string; error?: string; details?: string; - }>('/api/v1/ai', { + }>('/api/v1/ai/generate', { method: 'POST', headers: authHeaders(), body: JSON.stringify({ diff --git a/lib/redis.ts b/lib/redis.ts index 69929e65..1dfaf1fa 100644 --- a/lib/redis.ts +++ b/lib/redis.ts @@ -1,7 +1,15 @@ -// Yo, we don't have the actual Redis module, so let's fake it -// This is a mock implementation of the Redis client +// This module exposes a Redis-like client. In local/dev environments we +// use a lightweight in-memory mock to avoid having to run a real server. In +// production we create a real client (currently Upstash) whenever a connection +// URL is supplied via environment variables (either REDIS_URL or the +// UPSTASH_REDIS_* vars). The rest of the app code can `import { redis }` and +// use it without worrying about which implementation is in use. -// Mock Redis class +import { Redis as UpstashRedis } from '@upstash/redis'; + +// ---------------------------------------------------------- +// Mock Redis class (in-memory, for development without a server) +// ---------------------------------------------------------- class MockRedis { private cache: Map = new Map(); @@ -20,10 +28,35 @@ class MockRedis { this.cache.delete(key); return existed ? 1 : 0; } - // Add other methods as needed + async ping(): Promise { + return 'PONG'; + } + async pipeline(): Promise { + // basic no-op pipeline for compatibility + return { exec: async () => [] }; + } + // add other methods as needed by callers +} + +// ---------------------------------------------------------- +// Factory logic +// ---------------------------------------------------------- +let client: any; + +// Upstash envs take precedence +if ( + process.env.UPSTASH_REDIS_REST_URL && + process.env.UPSTASH_REDIS_REST_TOKEN +) { + client = UpstashRedis.fromEnv(); + console.log('[redis] Using Upstash Redis (from UPSTASH_REDIS_REST_URL)'); +} else if (process.env.REDIS_URL) { + // Fallback: allow a generic REDIS_URL for self-hosted installations + client = new UpstashRedis({ url: process.env.REDIS_URL }); + console.log('[redis] Using Upstash Redis (from REDIS_URL)'); +} else { + client = new MockRedis(); + console.log('[redis] Using mock Redis implementation'); } -// Create and export the redis client instance -export const redis = new MockRedis(); -// Log a message to indicate we're using the mock -console.log('Using mock Redis implementation'); +export const redis = client; diff --git a/lib/royalty-service.ts b/lib/royalty-service.ts index cdbced18..420c60c4 100644 --- a/lib/royalty-service.ts +++ b/lib/royalty-service.ts @@ -30,7 +30,7 @@ export async function configureRoyalty(params: ConfigureRoyaltyParams) { throw new Error('storyId is required for Supabase royalty configs'); } - const supabase = createClient(); + const supabase = await createClient(); const { data, error } = await supabase .from('royalty_configs') .upsert( @@ -69,7 +69,7 @@ export async function recordRoyaltyTransaction(params: RecordTransactionParams) throw new Error('Sale price must be greater than 0'); } - const supabase = createClient(); + const supabase = await createClient(); // Look up royalty config for this story (mapping nftId to storyId temporarily) const { data: config, error: configError } = await (supabase @@ -146,7 +146,7 @@ export async function getCreatorEarnings(walletAddress: string) { throw new Error('Invalid wallet address'); } - const supabase = createClient(); + const supabase = await createClient(); const { data, error } = await supabase .from('creator_earnings') .select('*') @@ -187,7 +187,7 @@ export async function getCreatorTransactions( const limit = Math.min(100, Math.max(1, options.limit || 10)); const skip = (page - 1) * limit; - const supabase = createClient(); + const supabase = await createClient(); let query = supabase .from('royalty_transactions') @@ -226,7 +226,7 @@ interface GetConfigParams { } export async function getRoyaltyConfig(params: GetConfigParams) { - const supabase = createClient(); + const supabase = await createClient(); let query = supabase.from('royalty_configs').select('*'); if (params.storyId || params.nftId) { diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts index 23bb8d1f..d61f8102 100644 --- a/lib/supabase/client.ts +++ b/lib/supabase/client.ts @@ -1,8 +1,50 @@ import { createBrowserClient } from '@supabase/ssr'; +// During a Cloudflare static build we may not have access to the usual +// runtime environment variables. Throwing synchronously in that scenario +// causes every page import that calls `createClient()` (directly or via a +// service) to crash the whole build. To make the export process more +// resilient we provide a lightweight noop client when the vars are missing +// **and** we're in build mode. All callers should be prepared to receive +// back a client whose methods simply resolve to `{ data: null, error: null }`. + +function createNoopClient() { + const handler: ProxyHandler = { + get(_target, prop) { + if (prop === 'auth') { + return { + getSession: async () => ({ data: { session: null } }), + getUser: async () => ({ data: null }), + }; + } + // every method call returns a promise resolving to a harmless + // result object; chained calls are also proxied + return new Proxy(async () => ({ data: null, error: null }), handler); + }, + apply(_target, _thisArg, _args) { + // support invoking the proxy as a function + return Promise.resolve({ data: null, error: null }); + }, + }; + return new Proxy({}, handler) as any; +} + export function createClient() { - return createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://dummy.supabase.co', - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy_key' - ); + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + // if we're performing a static export, return noop client instead of + // throwing; this keeps the build alive and results in empty data. + if (process.env.NEXT_PUBLIC_BUILD_MODE === 'true' || process.env.CF_PAGES === '1') { + console.warn( + '[supabase] missing env vars during build – returning noop client' + ); + return createNoopClient(); + } + throw new Error('Supabase configuration is missing. Ensure NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are set.'); + } + + return createBrowserClient(supabaseUrl, supabaseAnonKey); } + diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts index 55ce6bda..04912d80 100644 --- a/lib/supabase/server.ts +++ b/lib/supabase/server.ts @@ -1,31 +1,60 @@ import { createServerClient, type CookieOptions } from '@supabase/ssr'; import { cookies } from 'next/headers'; -export function createClient() { - const cookieStore = cookies(); +// server.ts handles setup of the Supabase client on the server side. It +// mirrors the logic in `client.ts` by returning a safe noop client during +// static export builds when the public keys may not yet be injected. The +// noop implementation is intentionally minimal: any method invoked resolves +// to `{ data: null, error: null }`. + +function createNoopClient() { + const handler: ProxyHandler = { + get(_target, prop) { + if (prop === 'auth') { + return { + getSession: async () => ({ data: { session: null } }), + getUser: async () => ({ data: null }), + }; + } + return new Proxy(async () => ({ data: null, error: null }), handler); + }, + apply(_target, _thisArg, _args) { + return Promise.resolve({ data: null, error: null }); + }, + }; + return new Proxy({}, handler) as any; +} + +export async function createClient() { + const cookieStore = await cookies(); + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + if (process.env.NEXT_PUBLIC_BUILD_MODE === 'true' || process.env.CF_PAGES === '1') { + console.warn( + '[supabase] missing server env vars during build – returning noop client' + ); + return createNoopClient(); + } + throw new Error('Supabase configuration is missing. Ensure NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are set.'); + } return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://dummy.supabase.co', - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy_key', + supabaseUrl, + supabaseAnonKey, { cookies: { - get(name: string) { - return cookieStore.get(name)?.value; - }, - set(name: string, value: string, options: CookieOptions) { - try { - cookieStore.set({ name, value, ...options }); - } catch (error) { - // The `set` method was called from a Server Component. - // This can be ignored if you have middleware refreshing - // user sessions. - } + getAll() { + return cookieStore.getAll(); }, - remove(name: string, options: CookieOptions) { + setAll(cookiesToSet) { try { - cookieStore.set({ name, value: '', ...options }); + cookiesToSet.forEach(({ name, value, options }) => { + cookieStore.set(name, value, options); + }); } catch (error) { - // The `delete` method was called from a Server Component. + // The `setAll` method was called from a Server Component. // This can be ignored if you have middleware refreshing // user sessions. } diff --git a/next.config.js b/next.config.js index d166b95c..70df7e25 100644 --- a/next.config.js +++ b/next.config.js @@ -238,10 +238,24 @@ const nextConfig = { }), // Output configuration - // For Cloudflare Pages: use undefined to allow @cloudflare/next-on-pages to handle dynamic routes - // 'standalone' = self-contained Node.js server for Render - // undefined = default Next.js behavior with dynamic routes and SSR - output: isCfBuild ? undefined : (process.env.BUILD_STANDALONE === 'true' ? 'standalone' : undefined), + // When building for Cloudflare Pages we deploy a *static export* (output: 'export'). + // This is essential because the frontend is served as static assets and any + // dynamic behavior (API routes, server rendering) happens on the separate + // backend service. The previous behavior (undefined) caused a build that + // generated no `out/` directory and triggered export errors such as + // "Export encountered errors on following paths" in our CI logs. + // + // Fallbacks: + // - 'standalone' = self-contained Node.js server for Render + // - undefined = default Next.js behavior with dynamic routes and SSR + // + // The bug-exploration test ensures this value switches to 'export' when + // NEXT_PUBLIC_BUILD_MODE=true (isCfBuild). + output: isCfBuild + ? 'export' + : process.env.BUILD_STANDALONE === 'true' + ? 'standalone' + : undefined, // Experimental features experimental: { diff --git a/package-lock.json b/package-lock.json index 47ee36a6..0bcd5b72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "groqtales", - "version": "1.9.7", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "groqtales", - "version": "1.9.7", + "version": "2.0.3", "license": "MIT", "dependencies": { "@auth/mongodb-adapter": "^3.11.1", @@ -69,7 +69,6 @@ "concat-stream": "^2.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "cross-env": "^10.1.0", "date-fns": "^3.3.1", "dotenv": "^16.5.0", "embla-carousel-react": "^8.6.0", @@ -210,9 +209,9 @@ } }, "node_modules/@alloralabs/allora-sdk/node_modules/@types/node": { - "version": "22.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", - "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1156,9 +1155,9 @@ } }, "node_modules/@coinbase/cdp-sdk": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.44.1.tgz", - "integrity": "sha512-O7sikX7gTZdF4xy9b0xAMPOKWk8z6E7an4EGVBukPdfChooBL15Zt6B8Wn5th/LC1V1nIf3J/tlTTQCJLkKZ6A==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.45.0.tgz", + "integrity": "sha512-4fgGOhyN9g/pTDE9NtsKUapwFsubrk9wafz8ltmBqSwWqLZWfWxXkVmzMYYFAf+qeGf/X9JqJtmvDVaHFlXWlw==", "license": "MIT", "dependencies": { "@solana-program/system": "^0.10.0", @@ -1381,12 +1380,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", - "license": "MIT" - }, "node_modules/@esbuild/android-arm": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", @@ -2450,33 +2443,33 @@ } }, "node_modules/@hpke/chacha20poly1305": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@hpke/chacha20poly1305/-/chacha20poly1305-1.7.1.tgz", - "integrity": "sha512-Zp8IwRIkdCucu877wCNqDp3B8yOhAnAah/YDDkO94pPr/KKV7IGnBbpwIjDB3BsAySWBMrhhdE0JKYw3N4FCag==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hpke/chacha20poly1305/-/chacha20poly1305-1.8.0.tgz", + "integrity": "sha512-FcBfAQ+Y99vMNJP2yrZ9wpL8V0GOwp1+zMyzvc6alasrBygfFjFm1yeUtyADJCu/27C3Lm5mJzx6u7pwg+cX5w==", "license": "MIT", "dependencies": { - "@hpke/common": "^1.8.1" + "@hpke/common": "^1.10.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@hpke/common": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.9.0.tgz", - "integrity": "sha512-Sdxj4KqtmBt8FiwRkNLxXF+peqLR3FwVxtnsemiiEzClgslckRpFv3yK8mchoCwvyRwLOMS3Y2Z9ND2bq3sRVg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.10.0.tgz", + "integrity": "sha512-uVq9pTNERQ1GcFlHZzQx+a0ZMC81wQzkbNzJPEyR/l3AWM7fASd/qYN2Cnq6uL1NPEfwcD4lgOmfjjZfx2k2XA==", "license": "MIT", "engines": { "node": ">=16.0.0" } }, "node_modules/@hpke/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.8.0.tgz", - "integrity": "sha512-yHuo+2q4HSPUFuxcg87Kiy7QZRk4IeR+cwBB0qW8fHnr71bnRCArM39Cq1bWHBt75gTyeERGD/v1H14yPB2wyw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.9.0.tgz", + "integrity": "sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==", "license": "MIT", "dependencies": { - "@hpke/common": "^1.9.0" + "@hpke/common": "^1.10.0" }, "engines": { "node": ">=16.0.0" @@ -2902,6 +2895,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@jest/console/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/console/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3070,6 +3070,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/core/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3084,9 +3091,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "dev": true, "license": "MIT", "engines": { @@ -3223,9 +3230,9 @@ } }, "node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, "license": "MIT", "dependencies": { @@ -3404,6 +3411,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/expect/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/expect/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3557,6 +3571,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/fake-timers/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/fake-timers/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3929,6 +3950,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@jest/reporters/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/reporters/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11062,9 +11090,9 @@ } }, "node_modules/@splinetool/runtime": { - "version": "1.12.67", - "resolved": "https://registry.npmjs.org/@splinetool/runtime/-/runtime-1.12.67.tgz", - "integrity": "sha512-jKE5asBgqTlYkOgdUDXUAbiCenPTP3hnb4RaeO8YUZLPlUB4RFlxx579n79I/0wS/alBqv6C06h3Csclc+IwQg==", + "version": "1.12.68", + "resolved": "https://registry.npmjs.org/@splinetool/runtime/-/runtime-1.12.68.tgz", + "integrity": "sha512-8QjMoSQUWtN58GrZe7ORekHco1SupQAfXo1AndYqCRrZhs1IkQQ5Uod75IvKiiOffwpCp5UxvPahCDdg8DZI/w==", "dependencies": { "on-change": "4.0.0", "semver-compare": "1.0.0" @@ -11259,9 +11287,9 @@ "license": "MIT" }, "node_modules/@supabase/auth-js": { - "version": "2.98.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.98.0.tgz", - "integrity": "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==", + "version": "2.99.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.99.0.tgz", + "integrity": "sha512-tHiIST/OEoLmWBE+3X69xRY5srJM/lL86KltmMlIfDo9ePJLo14vQQV9T4NF+P+MoGhCwQL1GTmk51zuAFMXKw==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -11271,9 +11299,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.98.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.98.0.tgz", - "integrity": "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==", + "version": "2.99.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.99.0.tgz", + "integrity": "sha512-zA9oad6EqGwMLLu2LfP1bXbqKcJGiotAdbdTfZG7YS7619YZQAEgejj9mp+E5vglKE1yMWbKK+S1J3PbuUtgLg==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -11283,9 +11311,9 @@ } }, "node_modules/@supabase/postgrest-js": { - "version": "2.98.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.98.0.tgz", - "integrity": "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==", + "version": "2.99.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.99.0.tgz", + "integrity": "sha512-8qfOMi2pu9y0IQhUAeFqjrvR49G4ELGevXCWV9qAHXFQ/h2FFh0I8PYjFQj4rHcHSq6hrpozDnS1vbQU8NAQ/A==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -11295,9 +11323,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.98.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.98.0.tgz", - "integrity": "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==", + "version": "2.99.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.99.0.tgz", + "integrity": "sha512-7nFTZhNeANR7FvEY6PfWLCfE8dHqcaJd9SuR7IPEZvBPG9K4uEHMivpjZx4NWRSU7Eji7ZbKy2LG+cJ48DhwHg==", "license": "MIT", "dependencies": { "@types/phoenix": "^1.6.6", @@ -11335,9 +11363,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.98.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.98.0.tgz", - "integrity": "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==", + "version": "2.99.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.99.0.tgz", + "integrity": "sha512-mAEEbfsght5EEALejYrwAP9k8sFBGjfMZT8n4SyMXk2iYuWVeRMs1kA/uKg0uDMctWdZ0bL+L4jZzksUJpCjMA==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -11348,16 +11376,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.98.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz", - "integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==", + "version": "2.99.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.99.0.tgz", + "integrity": "sha512-SP9Sn9tsHDB7N4u2gT13rdeZJewE4xibAxasG7vOz+fYi92+XkMMbWNx0uGK53zKTnAnvTs16isRooyBy4sn5w==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.98.0", - "@supabase/functions-js": "2.98.0", - "@supabase/postgrest-js": "2.98.0", - "@supabase/realtime-js": "2.98.0", - "@supabase/storage-js": "2.98.0" + "@supabase/auth-js": "2.99.0", + "@supabase/functions-js": "2.99.0", + "@supabase/postgrest-js": "2.99.0", + "@supabase/realtime-js": "2.99.0", + "@supabase/storage-js": "2.99.0" }, "engines": { "node": ">=20.0.0" @@ -11427,45 +11455,6 @@ "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/dom/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -11486,6 +11475,13 @@ "yarn": ">=1" } }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", @@ -11764,6 +11760,48 @@ "pretty-format": "^30.0.0" } }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/jsdom": { "version": "20.0.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", @@ -11807,9 +11845,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.35", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", - "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -12420,9 +12458,9 @@ } }, "node_modules/@upstash/redis": { - "version": "1.36.3", - "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.36.3.tgz", - "integrity": "sha512-wxo1ei4OHDHm4UGMgrNVz9QUEela9N/Iwi4p1JlHNSowQiPi+eljlGnfbZVkV0V4PIrjGtGFJt5GjWM5k28enA==", + "version": "1.36.4", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.36.4.tgz", + "integrity": "sha512-w4s/msmyMqxOxaVhC8TQ2whJ77+Zd8YaSFokXL4mULQopaYb4xNJcm/PedtFQyLJn65nneySw9IwYnlMBBmFHg==", "license": "MIT", "dependencies": { "uncrypto": "^0.1.3" @@ -14521,12 +14559,13 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -15554,9 +15593,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001776", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", - "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "funding": [ { "type": "opencollective", @@ -16465,23 +16504,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", - "license": "MIT", - "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -17171,9 +17193,9 @@ } }, "node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT" }, @@ -17264,9 +17286,9 @@ } }, "node_modules/eciesjs": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz", - "integrity": "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==", + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", "license": "MIT", "dependencies": { "@ecies/ciphers": "^0.2.5", @@ -17327,6 +17349,22 @@ "node": ">=8.0.0" } }, + "node_modules/effect/node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/electron-fetch": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/electron-fetch/-/electron-fetch-1.9.1.tgz", @@ -18549,6 +18587,15 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -19302,18 +19349,18 @@ } }, "node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -19363,9 +19410,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", "license": "MIT", "dependencies": { "ip-address": "10.1.0" @@ -19442,9 +19489,9 @@ } }, "node_modules/fast-check": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", - "integrity": "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz", + "integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==", "dev": true, "funding": [ { @@ -19458,29 +19505,12 @@ ], "license": "MIT", "dependencies": { - "pure-rand": "^7.0.0" + "pure-rand": "^8.0.0" }, "engines": { "node": ">=12.17.0" } }, - "node_modules/fast-check/node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -19745,9 +19775,9 @@ } }, "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -20288,9 +20318,9 @@ } }, "node_modules/h3": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", - "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz", + "integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -20520,9 +20550,9 @@ "dev": true }, "node_modules/hono": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -22420,6 +22450,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-circus/node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-circus/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22729,6 +22783,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-config/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22743,37 +22804,41 @@ } }, "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", + "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-diff/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-diff/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-diff/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -22791,6 +22856,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-diff/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-diff/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -22811,6 +22892,28 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22940,6 +23043,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-each/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -23346,38 +23456,49 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-matcher-utils/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-matcher-utils/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-matcher-utils/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -23395,6 +23516,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-matcher-utils/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-matcher-utils/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -23415,6 +23552,28 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -23429,19 +23588,19 @@ } }, "node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -23463,9 +23622,9 @@ } }, "node_modules/jest-message-util/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", "dependencies": { @@ -23488,37 +23647,37 @@ "dev": true, "license": "MIT" }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-message-util/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-message-util/node_modules/color-convert": { @@ -23541,6 +23700,41 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-message-util/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -23555,15 +23749,15 @@ } }, "node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-util": "30.2.0" + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -23583,9 +23777,9 @@ } }, "node_modules/jest-mock/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", "dependencies": { @@ -23974,6 +24168,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-runner/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-runner/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -24202,6 +24403,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-runtime/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-runtime/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -24416,6 +24624,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -24430,18 +24645,18 @@ } }, "node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -24461,9 +24676,9 @@ } }, "node_modules/jest-util/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", "dependencies": { @@ -24680,6 +24895,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-validate/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -24907,9 +25129,9 @@ } }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", + "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -25281,9 +25503,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.38", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.38.tgz", - "integrity": "sha512-vwzxmasAy9hZigxtqTbFEwp8ZdZ975TiqVDwj5bKx5sR+zi5ucUQy9mbVTkKM9GzqdLdxux/hTw2nmN5J7POMA==", + "version": "1.12.39", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.39.tgz", + "integrity": "sha512-MW79m7HuOqBk8mwytiXYTMELJiBbV3Zl9Y39dCCn1yC8K+WGNSq1QGvzywbylp5vGShEztMScCWHX/XFOS0rXg==", "license": "MIT" }, "node_modules/lie": { @@ -26495,13 +26717,13 @@ } }, "node_modules/mongoose": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.2.4.tgz", - "integrity": "sha512-XNh+jiztVMddDFDCv8TWxVxi/rGx+0FfsK3Ftj6hcYzEmhTcos2uC144OJRmUFPHSu3hJr6Pgip++Ab2+Da35Q==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.3.0.tgz", + "integrity": "sha512-Tv2p3DLBkftoGFp+VM/19k0t0RYPAAYjGIbCVGlV6Tf5Dnq6TICfYyeKeYvwQ06nK9sRDvymP3B+tjGHnUlaxw==", "license": "MIT", "dependencies": { "kareem": "3.2.0", - "mongodb": "~7.0", + "mongodb": "~7.1", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", @@ -26534,13 +26756,13 @@ } }, "node_modules/mongoose/node_modules/mongodb": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", - "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz", + "integrity": "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.0.0", + "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "engines": { @@ -28830,40 +29052,20 @@ } }, "node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/pretty-format/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.0" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/pretty-format/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", @@ -29018,9 +29220,10 @@ } }, "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.0.0.tgz", + "integrity": "sha512-7rgWlxG2gAvFPIQfUreo1XYlNvrQ9VnQPFWdncPkdl3icucLK0InOxsaafbvxGTnI6Bk/Rxmslg0lQlRCuzOXw==", + "dev": true, "funding": [ { "type": "individual", @@ -29458,9 +29661,10 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, "license": "MIT" }, "node_modules/react-joyride": { @@ -29733,6 +29937,12 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/redaxios": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/redaxios/-/redaxios-0.5.1.tgz", @@ -31358,9 +31568,9 @@ } }, "node_modules/svix": { - "version": "1.86.0", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.86.0.tgz", - "integrity": "sha512-/HTvXwjLJe1l/MsLXAO1ddCYxElJk4eNR4DzOjDOEmGrPN/3BtBE8perGwMAaJ2sT5T172VkBYzmHcjUfM1JRQ==", + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.87.0.tgz", + "integrity": "sha512-tkmvGiAYGiUtHFpeS5Uc1XUa8LKHvFPzKDfmDR1ceKplU9VkEIFPyZOT3Hxii8pRfCChSriyAeSK5Nvh9OGvKQ==", "license": "MIT", "dependencies": { "standardwebhooks": "1.0.0", @@ -32933,9 +33143,9 @@ } }, "node_modules/viem": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.0.tgz", - "integrity": "sha512-jU5e1E1s5E5M1y+YrELDnNar/34U8NXfVcRfxtVETigs2gS1vvW2ngnBoQUGBwLnNr0kNv+NUu4m10OqHByoFw==", + "version": "2.47.1", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.1.tgz", + "integrity": "sha512-frlK109+X5z2vlZeIGKa6Rxev6CcIpumV/VVhaIPc/QFotiB6t/CgUwkMlYfr4F2YNBZZ2l6jguWz2sY1XrQHw==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index 7ec4c278..d9079e28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "groqtales", - "version": "1.9.7", + "version": "2.0.3", "private": true, "description": "Enterprise-grade AI-powered Web3 storytelling platform with NFT integration", "author": "GroqTales Team", @@ -25,8 +25,8 @@ }, "scripts": { "dev": "concurrently \"next dev\" \"nodemon server/backend.js\"", - "build": "cross-env NEXT_PUBLIC_BUILD_MODE=true next build", - "cf-build": "rm -rf app/api && cross-env NEXT_PUBLIC_BUILD_MODE=true next build && npx @cloudflare/next-on-pages", + "build": "NEXT_PUBLIC_BUILD_MODE=true next build", + "cf-build": "rm -rf app/api && NEXT_PUBLIC_BUILD_MODE=true next build", "start": "concurrently \"next start\" \"node server/backend.js\"", "lint": "next lint", "lint:fix": "next lint --fix", @@ -116,7 +116,6 @@ "concat-stream": "^2.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "cross-env": "^10.1.0", "date-fns": "^3.3.1", "dotenv": "^16.5.0", "embla-carousel-react": "^8.6.0", diff --git a/supabase/schema.sql b/supabase/schema.sql index d9e9403d..1c1b3efe 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -61,6 +61,54 @@ CREATE TABLE IF NOT EXISTS stories ( updated_at TIMESTAMPTZ DEFAULT now() ); +-- -------------------------------------------------------- +-- 2.5 STORY_VOTES TABLE +-- -------------------------------------------------------- +CREATE TABLE IF NOT EXISTS story_votes ( + story_id UUID REFERENCES stories(id) ON DELETE CASCADE, + user_id UUID REFERENCES profiles(id) ON DELETE CASCADE, + vote SMALLINT NOT NULL CHECK (vote IN (1, -1)), + created_at TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (story_id, user_id) +); + +ALTER TABLE story_votes ENABLE ROW LEVEL SECURITY; +CREATE POLICY "Users can view votes for a story" ON story_votes FOR SELECT USING (true); +CREATE POLICY "Users can insert/update their own vote" ON story_votes FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "Users can update their own vote" ON story_votes FOR UPDATE USING (auth.uid() = user_id); +CREATE POLICY "Users can delete their own vote" ON story_votes FOR DELETE USING (auth.uid() = user_id); + +-- -------------------------------------------------------- +-- 2.6 STORY_COMMENTS TABLE +-- -------------------------------------------------------- +CREATE TABLE IF NOT EXISTS story_comments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + story_id UUID REFERENCES stories(id) ON DELETE CASCADE, + user_id UUID REFERENCES profiles(id) ON DELETE CASCADE, + content TEXT NOT NULL CHECK (char_length(content) <= 1000), + created_at TIMESTAMPTZ DEFAULT now() +); + +ALTER TABLE story_comments ENABLE ROW LEVEL SECURITY; +CREATE POLICY "Users can view comments" ON story_comments FOR SELECT USING (true); +CREATE POLICY "Users can insert their own comment" ON story_comments FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "Users can delete their own comment" ON story_comments FOR DELETE USING (auth.uid() = user_id); + +-- -------------------------------------------------------- +-- 2.7 SAVED_STORIES TABLE +-- -------------------------------------------------------- +CREATE TABLE IF NOT EXISTS saved_stories ( + story_id UUID REFERENCES stories(id) ON DELETE CASCADE, + user_id UUID REFERENCES profiles(id) ON DELETE CASCADE, + saved_at TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (story_id, user_id) +); + +ALTER TABLE saved_stories ENABLE ROW LEVEL SECURITY; +CREATE POLICY "Users can view their saved stories" ON saved_stories FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "Users can insert a saved story" ON saved_stories FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "Users can delete their saved story" ON saved_stories FOR DELETE USING (auth.uid() = user_id); + ALTER TABLE stories ENABLE ROW LEVEL SECURITY; CREATE POLICY "Stories are viewable by everyone" diff --git a/tailwind.config.js b/tailwind.config.js index afb01f9f..cdb082a4 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,7 +4,9 @@ module.exports = { './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', - './src/**/*.{js,ts,jsx,tsx,mdx}', + // scan src for TS/JSX/MDX only; excluding plain .js avoids the + // `./src/**/*.js` warning that can unintentionally match node_modules + './src/**/*.{ts,tsx,jsx,mdx}', ], darkMode: ['class'], theme: { @@ -135,8 +137,18 @@ module.exports = { shimmer: { '0%': { backgroundPosition: '-200% 0' }, '100%': { backgroundPosition: '200% 0' } + }, + /* page flip animation used by BookView */ + 'page-flip-right': { + from: { transform: 'rotateY(0deg)' }, + to: { transform: 'rotateY(-180deg)' } + }, + 'page-flip-left': { + from: { transform: 'rotateY(0deg)' }, + to: { transform: 'rotateY(180deg)' } } }, + animation: { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', @@ -145,7 +157,10 @@ module.exports = { 'float-delayed': 'float-delayed 5s ease-in-out infinite 1s', 'float-slow': 'float-slow 6s ease-in-out infinite', 'shimmer_1.5s_infinite': 'shimmer 1.5s infinite linear', - 'shimmer_2s_infinite': 'shimmer 2s infinite linear' + 'shimmer_2s_infinite': 'shimmer 2s infinite linear', + /* book page flip */ + 'page-flip-right': 'page-flip-right 0.35s ease-in-out forwards', + 'page-flip-left': 'page-flip-left 0.35s ease-in-out forwards' } } }, diff --git a/wrangler.toml b/wrangler.toml index 522508f1..b08f06e0 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -5,24 +5,23 @@ # # HOW THIS WORKS # ───────────────────────────────────────────────────────────── -# This project uses @cloudflare/next-on-pages to transform the -# Next.js build output into Cloudflare Pages Functions. -# Dynamic routes (like /profile/[slug]) are handled by Cloudflare -# Functions at runtime, not pre-rendered as static HTML. +# The frontend is deployed as a *static export* using Next.js' built-in +# `output: 'export'` mode. All dynamic server logic (API routes, database +# queries, authentication) runs on our backend service (Render / Cloudflare +# Worker). Cloudflare Pages simply serves the files produced in the `out/` +# directory. This allows us to avoid the @cloudflare/next-on-pages adapter +# (which required Edge runtime for every route) and simplifies the build. # # Build command (set in Cloudflare Pages dashboard): # npm run cf-build # # Build output directory (set in Cloudflare Pages dashboard): -# .vercel/output/static +# out # -# The @cloudflare/next-on-pages adapter transforms the Next.js -# build into a format compatible with Cloudflare Pages, placing -# the output in .vercel/output/static (Vercel Build Output API format). # ============================================================ name = "groqtales" -pages_build_output_dir = ".vercel/output/static" +pages_build_output_dir = "out" compatibility_date = "2024-09-23" compatibility_flags = ["nodejs_compat"]