├── .nvmrc ├── supabase ├── .branches │ └── _current_branch ├── schema.sql └── migrations │ ├── archive │ ├── 20250124021402_add_googleplaces_source_to_search_activities.sql │ └── 20251030002000_vault_role_hardening.sql │ └── 20251220000000_delete_user_memories_rpc.sql ├── src ├── app │ ├── favicon.ico │ ├── (auth) │ │ ├── layout.tsx │ │ └── error.tsx │ ├── dashboard │ │ ├── search │ │ │ ├── page.tsx │ │ │ ├── hotels │ │ │ │ ├── loading.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── actions.ts │ │ │ ├── flights │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── unified │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── activities │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── destinations │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── settings │ │ │ └── api-keys │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── loading.tsx │ ├── loading.tsx │ ├── chat │ │ ├── page.tsx │ │ ├── __tests__ │ │ │ ├── resume.test.tsx │ │ │ ├── midstream-resume-continuity.test.tsx │ │ │ ├── page.test.tsx │ │ │ └── page.smoke.test.tsx │ │ └── layout.tsx │ ├── api │ │ ├── agents │ │ │ ├── budget │ │ │ │ └── route.ts │ │ │ ├── flights │ │ │ │ └── route.ts │ │ │ ├── itineraries │ │ │ │ └── route.ts │ │ │ ├── accommodations │ │ │ │ └── route.ts │ │ │ └── destinations │ │ │ │ └── route.ts │ │ ├── accommodations │ │ │ └── search │ │ │ │ └── route.ts │ │ ├── activities │ │ │ └── search │ │ │ │ └── route.ts │ │ ├── rag │ │ │ ├── index │ │ │ │ ├── _handler.ts │ │ │ │ └── route.ts │ │ │ └── search │ │ │ │ ├── route.ts │ │ │ │ └── _handler.ts │ │ ├── keys │ │ │ └── _error-mapping.ts │ │ ├── flights │ │ │ └── search │ │ │ │ └── route.ts │ │ ├── security │ │ │ ├── events │ │ │ │ └── route.ts │ │ │ └── sessions │ │ │ │ ├── [sessionId] │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ ├── dashboard │ │ │ └── __tests__ │ │ │ │ └── route.test.ts │ │ ├── hooks │ │ │ └── cache │ │ │ │ └── route.ts │ │ ├── auth │ │ │ └── mfa │ │ │ │ ├── factors │ │ │ │ └── list │ │ │ │ │ └── route.ts │ │ │ │ └── challenge │ │ │ │ └── route.ts │ │ └── calendar │ │ │ └── freebusy │ │ │ └── route.ts │ ├── auth │ │ ├── me │ │ │ └── route.ts │ │ ├── email │ │ │ └── resend │ │ │ │ └── route.ts │ │ ├── delete │ │ │ └── route.ts │ │ ├── logout │ │ │ └── route.ts │ │ └── confirm │ │ │ └── route.ts │ └── error.tsx ├── test │ ├── mocks │ │ ├── server-only.ts │ │ ├── rehype-harden.ts │ │ ├── toast.ts │ │ └── botid.ts │ ├── helpers │ │ ├── unsafe-cast.ts │ │ ├── query.tsx │ │ ├── query-client.ts │ │ ├── make-request.ts │ │ ├── store.ts │ │ └── supabase-storage.ts │ ├── msw │ │ ├── handlers │ │ │ ├── utils.ts │ │ │ ├── stripe.ts │ │ │ ├── telemetry.ts │ │ │ └── ai-routes.ts │ │ └── constants.ts │ ├── upstash │ │ ├── constants.ts │ │ ├── index.qstash.test.ts │ │ ├── upstash.unit.test.ts │ │ └── emulator.ts │ ├── setup.ts │ ├── fixtures │ │ └── flights.ts │ └── factories │ │ ├── reset.ts │ │ ├── chat-factory.ts │ │ └── calendar-factory.ts ├── domain │ ├── accommodations │ │ ├── constants.ts │ │ ├── errors.ts │ │ └── container.ts │ ├── schemas │ │ ├── shared │ │ │ ├── media.ts │ │ │ ├── money.ts │ │ │ └── person.ts │ │ ├── telemetry.ts │ │ ├── ui │ │ │ └── loading.ts │ │ ├── embeddings.ts │ │ └── providers.ts │ ├── activities │ │ ├── container.ts │ │ ├── errors.ts │ │ └── types.ts │ └── flights │ │ └── service.ts ├── components │ ├── ui │ │ ├── current-year.tsx │ │ ├── separator.tsx │ │ ├── label.tsx │ │ ├── collapsible.tsx │ │ ├── __mocks__ │ │ │ └── use-toast.ts │ │ ├── toaster.tsx │ │ ├── textarea.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── loading.tsx │ │ ├── checkbox.tsx │ │ ├── input.tsx │ │ └── popover.tsx │ ├── providers │ │ ├── theme-provider.tsx │ │ ├── performance-provider.tsx │ │ └── telemetry-provider.tsx │ ├── layouts │ │ └── auth-layout.tsx │ ├── search │ │ └── search-page-skeleton.tsx │ ├── features │ │ ├── search │ │ │ ├── forms │ │ │ │ └── __tests__ │ │ │ │ │ └── destination-search-form.helpers.test.ts │ │ │ ├── cards │ │ │ │ ├── rating-stars.tsx │ │ │ │ └── amenities.tsx │ │ │ └── common │ │ │ │ ├── recent-items.ts │ │ │ │ └── use-search-form.ts │ │ └── agent-monitoring │ │ │ └── dashboard │ │ │ └── agent-status-dashboard-lazy.tsx │ └── ai-elements │ │ └── __tests__ │ │ ├── sources.test.tsx │ │ ├── conversation.test.tsx │ │ └── message.test.tsx ├── stores │ ├── currency │ │ └── types.ts │ ├── search-params │ │ └── handlers │ │ │ ├── index.ts │ │ │ └── activity-handler.ts │ ├── __tests__ │ │ └── budget-store │ │ │ └── budget-validation.test.ts │ ├── ui │ │ ├── features.ts │ │ ├── navigation.ts │ │ ├── sidebar.ts │ │ └── theme.ts │ └── auth │ │ └── reset-auth.ts ├── hooks │ └── __tests__ │ │ └── use-realtime-channel.test.ts ├── lib │ ├── utils │ │ ├── build-phase.ts │ │ └── type-guards.ts │ ├── telemetry │ │ ├── tracer.ts │ │ ├── __tests__ │ │ │ └── tracer.test.ts │ │ ├── constants.ts │ │ └── route-key.ts │ ├── constants │ │ └── images.ts │ ├── flights │ │ └── popular-routes-cache.ts │ ├── rag │ │ ├── pgvector.ts │ │ └── __tests__ │ │ │ └── pgvector.test.ts │ ├── env │ │ ├── index.ts │ │ └── runtime-env.ts │ ├── supabase │ │ ├── index.ts │ │ ├── server.ts │ │ └── guards.ts │ ├── cache │ │ └── hash.ts │ ├── auth │ │ ├── actions.ts │ │ └── supabase-errors.ts │ ├── trips │ │ └── parse-trip-date.ts │ ├── security │ │ ├── __tests__ │ │ │ ├── internal-key.test.ts │ │ │ └── webhook.test.ts │ │ └── internal-key.ts │ ├── qstash │ │ └── config.ts │ ├── client │ │ └── session.ts │ ├── __tests__ │ │ ├── zod-v4-resolver.dom.test.tsx │ │ └── utils.test.ts │ ├── routes.ts │ ├── geo.ts │ ├── api │ │ └── __tests__ │ │ │ └── factory.body-limit.test.ts │ ├── url │ │ ├── safe-href.ts │ │ └── __tests__ │ │ │ └── safe-href.test.ts │ ├── ratelimit │ │ ├── __tests__ │ │ │ └── identifier.test.ts │ │ └── identifier.ts │ ├── realtime │ │ └── status.ts │ ├── google │ │ └── places-format.ts │ ├── http │ │ └── __tests__ │ │ │ └── ip.test.ts │ ├── config │ │ └── helpers.ts │ └── query │ │ └── config.ts ├── __tests__ │ └── contracts │ │ └── upstash-int.integration.test.ts ├── ai │ ├── tools │ │ ├── server │ │ │ ├── constants.ts │ │ │ └── planning.schema.ts │ │ └── schemas │ │ │ ├── travel-advisory.ts │ │ │ ├── activities.ts │ │ │ └── __tests__ │ │ │ └── tools.test.ts │ └── constants.ts ├── instrumentation.ts ├── styles │ └── streamdown.css ├── scripts │ └── __tests__ │ │ └── check-ai-tools.test.ts └── config │ └── bot-protection.ts ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── vercel.json ├── @types ├── lucide-react.d.ts └── vitest-globals.d.ts ├── .github ├── secret_scanning.yml └── actions │ └── cache-vite-vitest │ └── action.yml ├── tailwind.config.mjs ├── .npmrc ├── components.json ├── docs ├── README.md ├── architecture │ └── decisions │ │ ├── adr-0033-rag-advanced-v6.md │ │ ├── template.md │ │ ├── adr-0034-structured-outputs-object-generation.md │ │ └── adr-0017-adopt-node-js-v24-lts-baseline.md ├── specs │ └── archive │ │ ├── 0006-spec-zod-v4-migration.md │ │ ├── 0016-spec-react-compiler-enable.md │ │ └── 0003-spec-session-resume.md └── operations │ └── README.md ├── tsconfig.json ├── LICENSE ├── release.config.mjs ├── docker-compose.yml └── docker └── Dockerfile.dev /.nvmrc: -------------------------------------------------------------------------------- 1 | v24.11.0 2 | -------------------------------------------------------------------------------- /supabase/.branches/_current_branch: -------------------------------------------------------------------------------- 1 | main -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornMelin/tripsage-ai/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /src/test/mocks/server-only.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Vitest shim for Next.js `server-only` virtual module. 3 | */ 4 | 5 | export {}; 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "deploymentEnabled": false 4 | }, 5 | "functions": { 6 | "src/app/api/**/route.*": { 7 | "maxDuration": 60 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /@types/lucide-react.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Module augmentation to expose Lucide React suffixed build types. 3 | */ 4 | 5 | declare module "lucide-react" { 6 | export * from "lucide-react/dist/lucide-react.suffixed"; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AuthLayout } from "@/components/layouts/auth-layout"; 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return {children}; 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/accommodations/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared accommodations domain constants. 3 | */ 4 | 5 | /** Cache TTL for accommodation search results (seconds). */ 6 | export const ACCOM_SEARCH_CACHE_TTL_SECONDS = 300; 7 | -------------------------------------------------------------------------------- /.github/secret_scanning.yml: -------------------------------------------------------------------------------- 1 | paths-ignore: 2 | - ".env.example" 3 | - ".env.test" 4 | - ".env.test.example" 5 | - "tests/.env.test.example" 6 | - "supabase/.env.example" 7 | # Keep ignores minimal and precise; review quarterly 8 | 9 | -------------------------------------------------------------------------------- /supabase/schema.sql: -------------------------------------------------------------------------------- 1 | -- Canonical TripSage schema loader (squashed) 2 | -- Apply this file with psql to provision a fresh database. 3 | 4 | \i ./migrations/20251122000000_base_schema.sql 5 | \i ./migrations/202511220002_agent_config_seed.sql 6 | -------------------------------------------------------------------------------- /src/components/ui/current-year.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /** 4 | * Displays the current year on the client to avoid server prerender time coupling. 5 | */ 6 | export function CurrentYear() { 7 | return <>{new Date().getFullYear()}; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/dashboard/search/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server page for search hub (RSC shell). 3 | */ 4 | 5 | import SearchHubClient from "./search-hub-client"; 6 | 7 | export default function SearchPage() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /@types/vitest-globals.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Global Vitest types. 3 | * 4 | * The repo sets `typeRoots`, so Vitest's bundled globals are not auto-included. 5 | * Importing `vitest/globals` here restores `describe/it/expect` typing. 6 | */ 7 | 8 | import "vitest/globals"; 9 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import { PageLoading } from "@/components/ui/loading"; 2 | 3 | /** 4 | * Root loading component for Next.js App Router 5 | * Shown when navigating between pages or during Suspense boundaries 6 | */ 7 | export default function Loading() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/test/mocks/rehype-harden.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Minimal rehype-harden stub for tests to avoid ESM/CJS packaging issues 3 | * in downstream dependencies when running under Vitest. 4 | */ 5 | 6 | export default function rehypeHarden() { 7 | // Return a no-op transformer 8 | return (_tree: unknown) => { 9 | // no-op 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/stores/currency/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared types for currency store slices. 3 | */ 4 | 5 | export type StoreLogger = { 6 | error: (message: string, details?: Record) => void; 7 | info: (message: string, details?: Record) => void; 8 | warn: (message: string, details?: Record) => void; 9 | }; 10 | -------------------------------------------------------------------------------- /src/test/helpers/unsafe-cast.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test-only unsafe type coercion helper for complex SDK mocks. 3 | * 4 | * Prefer real implementations or `satisfies` where practical. Use this when a 5 | * third-party type is too large to model for unit tests. 6 | */ 7 | 8 | export function unsafeCast(value: unknown): T { 9 | return value as T; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/dashboard/search/hotels/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Loading UI for hotel search page route. 3 | */ 4 | 5 | import { SearchPageSkeleton } from "@/components/search/search-page-skeleton"; 6 | 7 | /** 8 | * Route-level loading component shown during page transitions. 9 | */ 10 | export default function Loading() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dashboard/search/flights/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Loading UI for flight search page route. 3 | */ 4 | 5 | import { SearchPageSkeleton } from "@/components/search/search-page-skeleton"; 6 | 7 | /** 8 | * Route-level loading component shown during page transitions. 9 | */ 10 | export default function Loading() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dashboard/search/unified/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Loading UI for unified search page route. 3 | */ 4 | 5 | import { SearchPageSkeleton } from "@/components/search/search-page-skeleton"; 6 | 7 | /** 8 | * Route-level loading component shown during page transitions. 9 | */ 10 | export default function Loading() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Minimal Tailwind CSS configuration (ESM). 3 | * Tailwind v4 supports zero-config via the PostCSS plugin; this exists to 4 | * satisfy tooling (e.g., shadcn/ui CLI) that references a Tailwind config path. 5 | */ 6 | export default { 7 | content: ["./src/**/*.{ts,tsx,mdx}"], 8 | plugins: [], 9 | theme: { 10 | extend: {}, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/dashboard/search/activities/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Loading UI for activity search page route. 3 | */ 4 | 5 | import { SearchPageSkeleton } from "@/components/search/search-page-skeleton"; 6 | 7 | /** 8 | * Route-level loading component shown during page transitions. 9 | */ 10 | export default function Loading() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Hoist specific packages to root node_modules for compatibility 2 | # sharp: Required by Next.js for image optimization 3 | public-hoist-pattern[]=*sharp* 4 | # @tailwindcss/oxide: Required by @tailwindcss/postcss (Tailwind CSS v4) 5 | public-hoist-pattern[]=*@tailwindcss/oxide* 6 | 7 | # Do not auto-install peers (avoids pulling unused native/tooling chains). 8 | auto-install-peers=false 9 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/dashboard/search/destinations/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Loading UI for destination search page route. 3 | */ 4 | 5 | import { SearchPageSkeleton } from "@/components/search/search-page-skeleton"; 6 | 7 | /** 8 | * Route-level loading component shown during page transitions. 9 | */ 10 | export default function Loading() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /src/test/msw/handlers/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Small helpers for composing MSW handler sets in tests. 3 | */ 4 | 5 | import type { HttpHandler } from "msw"; 6 | 7 | /** 8 | * Flatten multiple handler groups into a single array for `server.use(...)`. 9 | */ 10 | export const composeHandlers = ( 11 | ...groups: ReadonlyArray[] 12 | ): HttpHandler[] => groups.flat(); 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "aliases": { 4 | "components": "@/components", 5 | "utils": "@/lib/utils" 6 | }, 7 | "rsc": true, 8 | "style": "default", 9 | "tailwind": { 10 | "baseColor": "zinc", 11 | "config": "tailwind.config.mjs", 12 | "css": "src/app/globals.css", 13 | "cssVariables": true 14 | }, 15 | "tsx": true 16 | } 17 | -------------------------------------------------------------------------------- /src/test/msw/handlers/stripe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview MSW handlers for Stripe API calls used in tests. 3 | */ 4 | 5 | import type { HttpHandler } from "msw"; 6 | import { HttpResponse, http } from "msw"; 7 | 8 | export const stripeHandlers: HttpHandler[] = [ 9 | http.post("https://api.stripe.com/:path*", async () => 10 | HttpResponse.json({ id: "pi_mock", status: "succeeded" }) 11 | ), 12 | ]; 13 | -------------------------------------------------------------------------------- /src/hooks/__tests__/use-realtime-channel.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment jsdom */ 2 | 3 | import { describe, expect, it } from "vitest"; 4 | import { useRealtimeChannel } from "@/hooks/supabase/use-realtime-channel"; 5 | 6 | describe("useRealtimeChannel", () => { 7 | it("exports a callable hook without throwing on import", () => { 8 | expect(typeof useRealtimeChannel).toBe("function"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/test/msw/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared MSW test constants (determinism + safety). 3 | */ 4 | 5 | export const MSW_FIXED_ISO_DATE = "2024-01-01T00:00:00.000Z" as const; 6 | 7 | // OpenAI `created` is typically a Unix timestamp in seconds. 8 | export const MSW_FIXED_UNIX_SECONDS = 1_704_067_200 as const; // 2024-01-01T00:00:00Z 9 | 10 | export const MSW_SUPABASE_URL = "http://localhost:54321" as const; 11 | -------------------------------------------------------------------------------- /src/domain/schemas/shared/media.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared media/attachment primitives. 3 | */ 4 | 5 | import { z } from "zod"; 6 | 7 | export const ATTACHMENT_SCHEMA = z.object({ 8 | contentType: z.string().optional(), 9 | id: z.string(), 10 | name: z.string().optional(), 11 | size: z.number().optional(), 12 | url: z.string(), 13 | }); 14 | 15 | export type Attachment = z.infer; 16 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/api-keys/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Dashboard API keys management page (route wrapper). 3 | * 4 | * Renders the shared API keys UI while allowing this route group to own its page 5 | * boundaries (metadata/layout) independently. 6 | */ 7 | 8 | import { ApiKeysContent } from "@/components/settings/api-keys-content"; 9 | 10 | export default function ApiKeysPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dashboard/search/unified/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server page for unified search experience (RSC shell). 3 | */ 4 | 5 | import { searchHotelsAction } from "./actions"; 6 | import UnifiedSearchClient from "./unified-search-client"; 7 | 8 | /** Server page for unified search experience (RSC shell). */ 9 | export default function UnifiedSearchPage() { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /src/test/msw/handlers/telemetry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview MSW handlers for telemetry endpoints used in tests. 3 | */ 4 | 5 | import { HttpResponse, http } from "msw"; 6 | 7 | export const telemetryHandlers = [ 8 | http.post("/api/telemetry/activities", () => { 9 | return HttpResponse.json({ ok: true }); 10 | }), 11 | 12 | http.post("/api/telemetry/ai-demo", () => { 13 | return HttpResponse.json({ success: true }); 14 | }), 15 | ]; 16 | -------------------------------------------------------------------------------- /src/app/dashboard/search/flights/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server page for flight search (RSC shell). 3 | */ 4 | 5 | import { submitFlightSearch } from "./actions"; 6 | import FlightsSearchClient from "./flights-search-client"; 7 | 8 | /** Flight search page that renders the client component and handles server submission. */ 9 | export default function FlightSearchPage() { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/utils/build-phase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Next.js build-phase detection shared across server/client. 3 | */ 4 | 5 | import { PHASE_EXPORT, PHASE_PRODUCTION_BUILD } from "next/constants"; 6 | 7 | /** 8 | * Check if we're in Next.js build or export phase. 9 | */ 10 | export function isBuildPhase(): boolean { 11 | return ( 12 | process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD || 13 | process.env.NEXT_PHASE === PHASE_EXPORT 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /supabase/migrations/archive/20250124021402_add_googleplaces_source_to_search_activities.sql: -------------------------------------------------------------------------------- 1 | -- Migration: Add 'googleplaces' and 'ai_fallback' to search_activities.source CHECK constraint 2 | -- Related: ADR-0053, SPEC-0030 3 | 4 | ALTER TABLE public.search_activities 5 | DROP CONSTRAINT IF EXISTS search_activities_source_check, 6 | ADD CONSTRAINT search_activities_source_check 7 | CHECK (source IN ('viator','getyourguide','googleplaces','ai_fallback','external_api','cached')); 8 | 9 | -------------------------------------------------------------------------------- /src/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | /** 4 | * @fileoverview Chat page shell that hosts the client chat experience with an error boundary. 5 | */ 6 | 7 | import type { ReactElement } from "react"; 8 | import { ErrorBoundary } from "@/components/error/error-boundary"; 9 | import { ChatClient } from "./chat-client"; 10 | 11 | export default function ChatPage(): ReactElement { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/test/mocks/toast.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | type ToastProps = Record; 4 | 5 | export const mockToast = vi.fn((_props: ToastProps = {}) => ({ 6 | dismiss: vi.fn(), 7 | id: `toast-${Date.now()}`, 8 | update: vi.fn(), 9 | })); 10 | 11 | export const mockUseToast = vi.fn(() => ({ 12 | dismiss: vi.fn(), 13 | toast: mockToast, 14 | toasts: [], 15 | })); 16 | 17 | export const resetToastMocks = () => { 18 | mockToast.mockClear(); 19 | mockUseToast.mockClear(); 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/api/agents/budget/route.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createBudgetAgent } from "@ai/agents"; 4 | import { agentSchemas } from "@schemas/agents"; 5 | import { createAgentRoute } from "@/lib/api/factory"; 6 | 7 | export const maxDuration = 60; 8 | 9 | export const POST = createAgentRoute({ 10 | agentFactory: createBudgetAgent, 11 | agentType: "budgetAgent", 12 | rateLimit: "agents:budget", 13 | schema: agentSchemas.budgetPlanRequestSchema, 14 | telemetry: "agent.budgetPlanning", 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/telemetry/tracer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared tracer utilities for TripSage telemetry. 3 | */ 4 | 5 | import { type Tracer, trace } from "@opentelemetry/api"; 6 | import { TELEMETRY_SERVICE_NAME } from "./constants"; 7 | 8 | /** 9 | * Returns the shared tracer instance for TripSage telemetry. 10 | * 11 | * @returns OpenTelemetry tracer bound to the canonical shared service name. 12 | */ 13 | export function getTelemetryTracer(): Tracer { 14 | return trace.getTracer(TELEMETRY_SERVICE_NAME); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/api/agents/flights/route.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createFlightAgent } from "@ai/agents"; 4 | import { flightSearchRequestSchema } from "@schemas/flights"; 5 | import { createAgentRoute } from "@/lib/api/factory"; 6 | 7 | export const maxDuration = 60; 8 | 9 | export const POST = createAgentRoute({ 10 | agentFactory: createFlightAgent, 11 | agentType: "flightAgent", 12 | rateLimit: "agents:flight", 13 | schema: flightSearchRequestSchema, 14 | telemetry: "agent.flightSearch", 15 | }); 16 | -------------------------------------------------------------------------------- /src/domain/schemas/shared/money.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared money/currency primitives. 3 | */ 4 | 5 | import { z } from "zod"; 6 | import { primitiveSchemas } from "../registry"; 7 | 8 | export const CURRENCY_CODE_SCHEMA = primitiveSchemas.isoCurrency; 9 | 10 | export const PRICE_SCHEMA = z.strictObject({ 11 | amount: z.number().positive(), 12 | currency: CURRENCY_CODE_SCHEMA, 13 | }); 14 | 15 | export type CurrencyCode = z.infer; 16 | export type Price = z.infer; 17 | -------------------------------------------------------------------------------- /src/app/api/agents/itineraries/route.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createItineraryAgent } from "@ai/agents"; 4 | import { agentSchemas } from "@schemas/agents"; 5 | import { createAgentRoute } from "@/lib/api/factory"; 6 | 7 | export const maxDuration = 60; 8 | 9 | export const POST = createAgentRoute({ 10 | agentFactory: createItineraryAgent, 11 | agentType: "itineraryAgent", 12 | rateLimit: "agents:itineraries", 13 | schema: agentSchemas.itineraryPlanRequestSchema, 14 | telemetry: "agent.itineraryPlanning", 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/dashboard/search/hotels/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server page for hotel/accommodation search (RSC shell). 3 | */ 4 | 5 | import { submitHotelSearch } from "./actions"; 6 | import HotelsSearchClient from "./hotels-search-client"; 7 | 8 | /** 9 | * Hotel search page that renders the client component and handles server submission. 10 | * 11 | * @returns {JSX.Element} The hotel search page. 12 | */ 13 | export default function HotelSearchPage() { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/api/agents/accommodations/route.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createAccommodationAgent } from "@ai/agents"; 4 | import { agentSchemas } from "@schemas/agents"; 5 | import { createAgentRoute } from "@/lib/api/factory"; 6 | 7 | export const maxDuration = 60; 8 | 9 | export const POST = createAgentRoute({ 10 | agentFactory: createAccommodationAgent, 11 | agentType: "accommodationAgent", 12 | rateLimit: "agents:accommodations", 13 | schema: agentSchemas.accommodationSearchRequestSchema, 14 | telemetry: "agent.accommodationSearch", 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/api/agents/destinations/route.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createDestinationAgent } from "@ai/agents"; 4 | import { agentSchemas } from "@schemas/agents"; 5 | import { createAgentRoute } from "@/lib/api/factory"; 6 | 7 | export const maxDuration = 60; 8 | 9 | export const POST = createAgentRoute({ 10 | agentFactory: createDestinationAgent, 11 | agentType: "destinationResearchAgent", 12 | rateLimit: "agents:destinations", 13 | schema: agentSchemas.destinationResearchRequestSchema, 14 | telemetry: "agent.destinationResearch", 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/dashboard/search/activities/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server page for activity search (RSC shell). 3 | */ 4 | 5 | import { submitActivitySearch } from "./actions"; 6 | import ActivitiesSearchClient from "./activities-search-client"; 7 | 8 | /** 9 | * Activity search page that renders the client component and handles server submission. 10 | * 11 | * @returns {JSX.Element} The activity search page. 12 | */ 13 | export default function ActivitiesSearchPage() { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/__tests__/contracts/upstash-int.integration.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { describe, expect, it } from "vitest"; 4 | import { getEmulatorConfig } from "@/test/upstash/emulator"; 5 | 6 | describe("Upstash emulator contract", () => { 7 | const config = getEmulatorConfig(); 8 | const shouldRun = config.enabled && !!config.redisUrl && !!config.qstashUrl; 9 | 10 | it.skipIf(!shouldRun)("detects emulator configuration", () => { 11 | expect(config.redisUrl).toBeDefined(); 12 | expect(config.qstashUrl).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/lib/constants/images.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared image constants used across the application. 3 | */ 4 | 5 | /** 6 | * Public fallback image URL/path for hotel listings. 7 | * 8 | * `NEXT_PUBLIC_*` values are inlined at build time (not read at runtime). This module 9 | * trims the inlined `NEXT_PUBLIC_FALLBACK_HOTEL_IMAGE` value and falls back to 10 | * `"/globe.svg"` when not provided. Expected format is a public URL or a public path. 11 | */ 12 | export const FALLBACK_HOTEL_IMAGE = 13 | process.env.NEXT_PUBLIC_FALLBACK_HOTEL_IMAGE?.trim() || "/globe.svg"; 14 | -------------------------------------------------------------------------------- /src/app/dashboard/search/destinations/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server page for destination search (RSC shell). 3 | */ 4 | 5 | import { submitDestinationSearch } from "./actions"; 6 | import DestinationsSearchClient from "./destinations-search-client"; 7 | 8 | /** 9 | * Destination search page that renders the client component and handles server submission. 10 | * 11 | * @returns {JSX.Element} The destination search page. 12 | */ 13 | export default function DestinationsSearchPage() { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/flights/popular-routes-cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Cache keys/constants for popular flight routes. 3 | * 4 | * Kept outside route modules because Next.js route handlers may only export 5 | * specific symbols (e.g. GET/POST, runtime, etc.). 6 | */ 7 | 8 | /** 9 | * Bump this version whenever the popular routes response structure changes 10 | * to force cache invalidation. 11 | */ 12 | export const POPULAR_ROUTES_CACHE_VERSION = "v1" as const; 13 | 14 | export const POPULAR_ROUTES_CACHE_KEY_GLOBAL = 15 | `popular-routes:${POPULAR_ROUTES_CACHE_VERSION}:global` as const; 16 | -------------------------------------------------------------------------------- /src/app/chat/__tests__/resume.test.tsx: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { DefaultChatTransport } from "ai"; 4 | import { describe, expect, it } from "vitest"; 5 | 6 | describe("Chat resume wiring (lightweight)", () => { 7 | it("enables reconnect support via DefaultChatTransport", () => { 8 | const transport = new DefaultChatTransport(); 9 | expect(transport).toBeInstanceOf(DefaultChatTransport); 10 | }); 11 | 12 | it("supports resume flag in chat config", () => { 13 | const chatConfig = { resume: true }; 14 | expect(chatConfig.resume).toBe(true); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/domain/schemas/telemetry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Schemas for telemetry-related API boundaries. 3 | */ 4 | 5 | import { z } from "zod"; 6 | 7 | // ===== API REQUEST SCHEMAS ===== 8 | 9 | export const TELEMETRY_AI_DEMO_MAX_DETAIL_LENGTH = 2000; 10 | 11 | export const telemetryAiDemoRequestSchema = z.strictObject({ 12 | detail: z 13 | .string() 14 | .max(TELEMETRY_AI_DEMO_MAX_DETAIL_LENGTH, { error: "detail exceeds limit" }) 15 | .optional(), 16 | status: z.enum(["success", "error"]), 17 | }); 18 | 19 | export type TelemetryAiDemoRequest = z.infer; 20 | -------------------------------------------------------------------------------- /src/test/upstash/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test constants for Upstash mocks. 3 | * 4 | * Use these instead of hardcoded strings for clarity and maintainability. 5 | * These tokens are only used in test environments and have no production value. 6 | */ 7 | 8 | export const TEST_QSTASH_TOKEN = "test-qstash-token"; 9 | 10 | export const TEST_QSTASH_SIGNING_KEY = "test-signing-key"; 11 | 12 | export const TEST_QSTASH_NEXT_SIGNING_KEY = "test-next-signing-key"; 13 | 14 | export const TEST_REDIS_URL = "https://test-redis.upstash.io"; 15 | 16 | export const TEST_REDIS_TOKEN = "test-redis-token"; 17 | -------------------------------------------------------------------------------- /src/test/helpers/query.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Lightweight typed mocks for TanStack Query primitives used in tests. 3 | */ 4 | 5 | import { QueryClient as TanStackQueryClient } from "@tanstack/react-query"; 6 | 7 | /** 8 | * Create a test QueryClient with retries disabled and zero cache persistence. 9 | * @returns QueryClient configured for deterministic unit tests. 10 | */ 11 | export const createMockQueryClient = (): TanStackQueryClient => 12 | new TanStackQueryClient({ 13 | defaultOptions: { 14 | mutations: { retry: false }, 15 | queries: { gcTime: 0, retry: false, staleTime: 0 }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/lib/rag/pgvector.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview pgvector serialization helpers (number[] -> string literal). 3 | */ 4 | 5 | import "server-only"; 6 | 7 | export function toPgvector(embedding: readonly number[]): string { 8 | const parts = embedding.map((value, idx) => { 9 | if (!Number.isFinite(value)) { 10 | throw new Error(`Invalid embedding value at index ${idx}`); 11 | } 12 | // Intentionally preserve JavaScript numeric string formatting (including scientific 13 | // notation) to avoid rounding/precision changes; pgvector accepts this input form. 14 | return String(value); 15 | }); 16 | return `[${parts.join(",")}]`; 17 | } 18 | -------------------------------------------------------------------------------- /.github/actions/cache-vite-vitest/action.yml: -------------------------------------------------------------------------------- 1 | name: Cache Vite/Vitest/TS/Next.js 2 | description: Reusable cache step for Vite, Vitest, TypeScript, and Next.js build info 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Cache Vite/Vitest/TS/Next.js caches 7 | uses: actions/cache@v4 8 | with: 9 | key: ${{ runner.os }}-frontend-build-${{ hashFiles('pnpm-lock.yaml','vitest.config.ts','tsconfig.json','package.json','next.config.ts') }} 10 | restore-keys: | 11 | ${{ runner.os }}-frontend-build- 12 | path: | 13 | node_modules/.vite 14 | node_modules/.vitest 15 | .vitest-cache 16 | .next/cache 17 | -------------------------------------------------------------------------------- /src/ai/tools/server/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared constants for planning tools. 3 | */ 4 | 5 | export const RATE_CREATE_PER_DAY = 20; // per user 6 | export const RATE_UPDATE_PER_MIN = 60; // per plan 7 | 8 | export const TTL_DRAFT_SECONDS = 86400 * 7; // 7 days 9 | export const TTL_FINAL_SECONDS = 86400 * 30; // 30 days 10 | 11 | /** 12 | * Cache TTL for accommodation search results (5 minutes). 13 | * Used by searchAccommodations tool. 14 | */ 15 | export const ACCOM_SEARCH_CACHE_TTL_SECONDS = 300; 16 | 17 | /** 18 | * Cache TTL for weather results (10 minutes). 19 | * Used by getCurrentWeather tool. 20 | */ 21 | export const WEATHER_CACHE_TTL_SECONDS = 600; 22 | -------------------------------------------------------------------------------- /src/components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Wrapper for Next.js ThemeProvider component. 3 | */ 4 | 5 | "use client"; 6 | 7 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 8 | import type { ComponentProps } from "react"; 9 | 10 | /** 11 | * ThemeProvider component. 12 | * 13 | * @param children - React children to wrap with ThemeProvider. 14 | * @param props - Component props aligned with Next.js ThemeProvider. 15 | * @returns ThemeProvider component wrapping the children. 16 | */ 17 | export function ThemeProvider({ 18 | children, 19 | ...props 20 | }: ComponentProps) { 21 | return {children}; 22 | } 23 | -------------------------------------------------------------------------------- /src/test/mocks/botid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared BotID mock helpers for deterministic test behavior. 3 | * 4 | * Route tests commonly stub `botid/server` to avoid noisy warnings and to keep 5 | * bot-detection behavior consistent across suites. 6 | */ 7 | 8 | import type { BotIdVerification } from "@/lib/security/botid"; 9 | 10 | /** 11 | * Deterministic "human" BotID response used across tests. 12 | * 13 | * Matches the shape used by `createMockBotIdResponse` in `src/lib/security/__tests__/botid.test.ts`. 14 | */ 15 | export const mockBotIdHumanResponse: BotIdVerification = { 16 | bypassed: true, 17 | isBot: false, 18 | isHuman: true, 19 | isVerifiedBot: false, 20 | verifiedBotCategory: undefined, 21 | verifiedBotName: undefined, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/env/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Environment variable access module. 3 | * 4 | * This barrel file exports explicit server and client entrypoints. 5 | * Import from './server' for server-only access, './client' for client-safe access. 6 | * 7 | * DO NOT import from this index in client components - use explicit paths. 8 | */ 9 | 10 | // Client-safe exports 11 | export { 12 | getClientEnv, 13 | getClientEnvVar, 14 | getClientEnvVarWithFallback, 15 | getGoogleMapsBrowserKey, 16 | publicEnv, 17 | } from "./client"; 18 | // Server-only exports (will fail if imported in client) 19 | export { 20 | env as serverEnv, 21 | getGoogleMapsServerKey, 22 | getServerEnv, 23 | getServerEnvVar, 24 | getServerEnvVarWithFallback, 25 | } from "./server"; 26 | -------------------------------------------------------------------------------- /src/lib/supabase/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Browser/client-safe Supabase exports. 3 | * 4 | * Server entrypoints live in `./server` (Route Handlers / Server Components) and 5 | * `./factory` (middleware/proxy cookie adapters). 6 | */ 7 | 8 | // Browser client helpers 9 | export { 10 | createClient, 11 | getBrowserClient, 12 | type TypedSupabaseClient, 13 | useSupabase, 14 | useSupabaseRequired, 15 | } from "./client"; 16 | 17 | // Shared types (type-only exports are safe - they don't cause server-only imports) 18 | export type { 19 | BrowserSupabaseClient, 20 | CreateServerSupabaseOptions, 21 | GetCurrentUserResult, 22 | ServerSupabaseClient, 23 | } from "./factory"; 24 | 25 | // Runtime guards 26 | export { isSupabaseClient } from "./guards"; 27 | -------------------------------------------------------------------------------- /src/domain/activities/container.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Dependency container for activities domain. 3 | * 4 | * Centralizes construction of the activities service so callers (AI tools, routes) 5 | * do not hard-wire dependencies at import time. 6 | */ 7 | 8 | import { ActivitiesService } from "@domain/activities/service"; 9 | import { createServerSupabase } from "@/lib/supabase/server"; 10 | 11 | let singleton: ActivitiesService | undefined; 12 | 13 | /** 14 | * Returns a singleton ActivitiesService configured with Supabase factory. 15 | */ 16 | export function getActivitiesService(): ActivitiesService { 17 | if (singleton) return singleton; 18 | 19 | singleton = new ActivitiesService({ 20 | supabase: createServerSupabase, 21 | }); 22 | 23 | return singleton; 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/cache/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared cache key hashing utilities. 3 | * 4 | * Provides consistent SHA-256 hashing for cache keys across tools and agents. 5 | */ 6 | 7 | import { createHash } from "node:crypto"; 8 | 9 | /** 10 | * Hash input data using SHA-256 and return first 16 hex characters. 11 | * 12 | * Used for creating deterministic cache key suffixes from complex input objects. 13 | * The 16-character hash provides sufficient uniqueness while keeping keys readable. 14 | * 15 | * @param input - Value to hash (will be JSON-stringified). 16 | * @returns First 16 hex characters of SHA-256 hash. 17 | */ 18 | export function hashInputForCache(input: unknown): string { 19 | return createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16); 20 | } 21 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared DOM shims used across Vitest projects. 3 | * 4 | * Some component libraries assume these DOM APIs exist; jsdom omits them. 5 | * Guarded so non-DOM test projects (e.g. node) can still load this file. 6 | */ 7 | 8 | if (typeof HTMLElement !== "undefined") { 9 | if (!HTMLElement.prototype.hasPointerCapture) { 10 | HTMLElement.prototype.hasPointerCapture = (_pointerId: number) => false; 11 | } 12 | 13 | if (!HTMLElement.prototype.releasePointerCapture) { 14 | HTMLElement.prototype.releasePointerCapture = (_pointerId: number) => undefined; 15 | } 16 | 17 | if (!HTMLElement.prototype.scrollIntoView) { 18 | HTMLElement.prototype.scrollIntoView = (_arg?: boolean | ScrollIntoViewOptions) => 19 | undefined; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/domain/schemas/ui/loading.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Loading states and skeleton UI props schemas (UI only). 3 | */ 4 | 5 | import { z } from "zod"; 6 | 7 | export const loadingStateSchema = z.object({ 8 | data: z.unknown().nullable(), 9 | error: z.string().nullable(), 10 | isLoading: z.boolean(), 11 | }); 12 | export type LoadingState = z.infer; 13 | 14 | export const skeletonPropsSchema = z.object({ 15 | className: z.string().optional(), 16 | count: z.number().min(1).max(20).optional(), 17 | height: z.union([z.string(), z.number()]).optional(), 18 | variant: z.enum(["default", "circular", "rectangular", "text"]).optional(), 19 | width: z.union([z.string(), z.number()]).optional(), 20 | }); 21 | export type SkeletonProps = z.infer; 22 | -------------------------------------------------------------------------------- /src/test/helpers/query-client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared QueryClient helpers for tests. 3 | * 4 | * Keep this module React-free so it can be used from setup files without 5 | * pulling in component providers. 6 | */ 7 | 8 | import type { QueryClient } from "@tanstack/react-query"; 9 | import { createMockQueryClient } from "./query"; 10 | 11 | let sharedQueryClient: QueryClient | null = null; 12 | 13 | export const getTestQueryClient = (): QueryClient => { 14 | if (!sharedQueryClient) { 15 | sharedQueryClient = createMockQueryClient(); 16 | } 17 | return sharedQueryClient; 18 | }; 19 | 20 | export const resetTestQueryClient = (): void => { 21 | if (!sharedQueryClient) return; 22 | sharedQueryClient.getQueryCache().clear(); 23 | sharedQueryClient.getMutationCache().clear(); 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/env/runtime-env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Runtime environment detection helpers. 3 | */ 4 | 5 | import "server-only"; 6 | 7 | /** 8 | * Determine the runtime environment for server-side feature gating. 9 | * 10 | * Prefers `VERCEL_ENV` when present and otherwise falls back to a normalized 11 | * value derived from `NODE_ENV`. 12 | */ 13 | export function getRuntimeEnv(): "production" | "preview" | "development" | "test" { 14 | const vercelEnv = process.env.VERCEL_ENV; 15 | if ( 16 | vercelEnv === "production" || 17 | vercelEnv === "preview" || 18 | vercelEnv === "development" 19 | ) { 20 | return vercelEnv; 21 | } 22 | 23 | if (process.env.NODE_ENV === "production") return "production"; 24 | if (process.env.NODE_ENV === "test") return "test"; 25 | return "development"; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/auth/actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server actions for authentication. 3 | */ 4 | 5 | "use server"; 6 | 7 | import "server-only"; 8 | 9 | import { revalidatePath } from "next/cache"; 10 | import { redirect } from "next/navigation"; 11 | import { createServerSupabase } from "@/lib/supabase/server"; 12 | import { createServerLogger } from "@/lib/telemetry/logger"; 13 | 14 | const logger = createServerLogger("auth.actions"); 15 | 16 | /** 17 | * Signs the user out and redirects to the login page. 18 | */ 19 | export async function logoutAction(): Promise { 20 | const supabase = await createServerSupabase(); 21 | try { 22 | await supabase.auth.signOut(); 23 | } catch (error) { 24 | logger.error("Logout error", { error }); 25 | } 26 | revalidatePath("/"); 27 | redirect("/login"); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/providers/performance-provider.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Wrapper for performance monitoring provider. 3 | */ 4 | 5 | "use client"; 6 | 7 | import type { ReactNode } from "react"; 8 | import { useWebVitals } from "@/hooks/use-performance"; 9 | 10 | /** Props for the PerformanceMonitor component. */ 11 | interface PerformanceMonitorProps { 12 | children: ReactNode; 13 | } 14 | 15 | /** 16 | * PerformanceMonitor component. 17 | * 18 | * Initializes Web Vitals monitoring and returns the children. 19 | * 20 | * @param children - React children to wrap. 21 | * @returns The children wrapped in a PerformanceMonitor component. 22 | */ 23 | export function PerformanceMonitor({ children }: PerformanceMonitorProps) { 24 | // Initialize Web Vitals monitoring 25 | useWebVitals(); 26 | 27 | return <>{children}; 28 | } 29 | -------------------------------------------------------------------------------- /src/stores/search-params/handlers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Registry for all search parameter handlers 3 | * Each handler is responsible for a specific search type's parameter validation, defaults, and serialization. 4 | * Handlers are automatically registered when imported. 5 | */ 6 | 7 | import { accommodationHandler } from "./accommodation-handler"; 8 | import { activityHandler } from "./activity-handler"; 9 | import { destinationHandler } from "./destination-handler"; 10 | import { flightHandler } from "./flight-handler"; 11 | 12 | export const registerAllHandlers = () => { 13 | // Handlers self-register on import via registerHandler. 14 | return [destinationHandler, accommodationHandler, activityHandler, flightHandler]; 15 | }; 16 | 17 | export { accommodationHandler, activityHandler, destinationHandler, flightHandler }; 18 | -------------------------------------------------------------------------------- /src/stores/__tests__/budget-store/budget-validation.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment jsdom */ 2 | 3 | import { act } from "@testing-library/react"; 4 | import { beforeEach, describe, expect, it } from "vitest"; 5 | import { useBudgetStore } from "@/stores/budget-store"; 6 | 7 | describe("Budget Store - Budget Validation", () => { 8 | beforeEach(() => { 9 | act(() => { 10 | useBudgetStore.setState({ 11 | activeBudgetId: null, 12 | alerts: {}, 13 | baseCurrency: "USD", 14 | budgets: {}, 15 | currencies: {}, 16 | expenses: {}, 17 | }); 18 | }); 19 | }); 20 | 21 | it("resets budgets state before validation flows", () => { 22 | const state = useBudgetStore.getState(); 23 | expect(state.budgets).toEqual({}); 24 | expect(state.activeBudgetId).toBeNull(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/layouts/auth-layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | export function AuthLayout({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
8 |
9 | 10 | TripSage AI 11 | 12 |
13 |
14 |
{children}
15 |
16 |
17 |

© {new Date().getFullYear()} TripSage AI. All rights reserved.

18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # TripSage Documentation 2 | 3 | ## **AI-powered travel planning platform documentation** 4 | 5 | ### Users (./users/) 6 | 7 | End-user guides and getting started. 8 | 9 | ### API (./api/) 10 | 11 | External developer integration and API reference. 12 | 13 | ### Development (./development/) 14 | 15 | Development resources and guidelines. 16 | 17 | ### Operations (./operations/) 18 | 19 | Deployment and operations. 20 | 21 | ### Architecture (./architecture/) 22 | 23 | System design and technical decisions. 24 | 25 | ### Architecture Decisions (./architecture/decisions/) 26 | 27 | Architecture Decision Records documenting technical choices. 28 | 29 | ## Quick Links 30 | 31 | - Users: ./users/user-guide.md 32 | - API: ./api/api-reference.md 33 | - Development: ./development/development-guide.md 34 | - Operations: ./operations/operators-reference.md 35 | -------------------------------------------------------------------------------- /src/lib/rag/__tests__/pgvector.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { describe, expect, it, vi } from "vitest"; 4 | import { toPgvector } from "../pgvector"; 5 | 6 | vi.mock("server-only", () => ({})); 7 | 8 | describe("toPgvector", () => { 9 | it("formats numeric arrays as pgvector literals", () => { 10 | expect(toPgvector([1, 2, 3])).toBe("[1,2,3]"); 11 | expect(toPgvector([0.1, -0.2, 3.5])).toBe("[0.1,-0.2,3.5]"); 12 | }); 13 | 14 | it("throws on non-finite values", () => { 15 | expect(() => toPgvector([Number.NaN])).toThrow(/Invalid embedding value/); 16 | expect(() => toPgvector([Number.POSITIVE_INFINITY])).toThrow( 17 | /Invalid embedding value/ 18 | ); 19 | }); 20 | 21 | it("preserves scientific notation when present", () => { 22 | expect(toPgvector([1e-7, -1e-7])).toBe("[1e-7,-1e-7]"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/search/search-page-skeleton.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared loading skeleton for search result pages. 3 | */ 4 | 5 | import { Skeleton } from "@/components/ui/skeleton"; 6 | 7 | /** 8 | * Skeleton UI for search pages. 9 | * 10 | * @returns {JSX.Element} Placeholder content while search pages load. 11 | */ 12 | export function SearchPageSkeleton() { 13 | return ( 14 | // biome-ignore lint/a11y/useSemanticElements: Loading skeleton uses role="status" live region; no semantic element fits this container. 15 |
16 | Loading search results 17 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/features/search/forms/__tests__/destination-search-form.helpers.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { describe, expect, it } from "vitest"; 4 | import { 5 | type DestinationSearchFormValues, 6 | mapDestinationValuesToParams, 7 | } from "../destination-search-form"; 8 | 9 | describe("mapDestinationValuesToParams", () => { 10 | it("converts form values to search params", () => { 11 | const formValues: DestinationSearchFormValues = { 12 | language: "fr", 13 | limit: 7, 14 | query: "Paris", 15 | region: "eu", 16 | types: ["locality", "establishment"], 17 | }; 18 | 19 | expect(mapDestinationValuesToParams(formValues)).toEqual({ 20 | language: "fr", 21 | limit: 7, 22 | query: "Paris", 23 | region: "eu", 24 | types: ["locality", "establishment"], 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/lib/utils/type-guards.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared type-guard utilities. 3 | */ 4 | 5 | /** 6 | * Determines whether a value is a plain object (object literal or Object.create(null)), 7 | * excluding arrays, null, and class instances like Date. 8 | * 9 | * @param {unknown} value - The value to evaluate. 10 | * @returns {value is Record} True when the value is a plain object. 11 | * @example isPlainObject({ a: 1 }) // true 12 | * @example isPlainObject([1]) // false 13 | * @example isPlainObject(new Date()) // false 14 | */ 15 | export function isPlainObject(value: unknown): value is Record { 16 | if (typeof value !== "object" || value === null || Array.isArray(value)) { 17 | return false; 18 | } 19 | const prototype = Object.getPrototypeOf(value); 20 | return prototype === Object.prototype || prototype === null; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/telemetry/__tests__/tracer.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { describe, expect, it, vi } from "vitest"; 4 | 5 | describe("getTelemetryTracer", () => { 6 | it("returns tracer bound to the canonical service name", async () => { 7 | vi.resetModules(); 8 | const GetTracer = vi.fn(); 9 | 10 | vi.doMock("@opentelemetry/api", () => ({ 11 | trace: { getTracer: GetTracer }, 12 | })); 13 | 14 | const { TELEMETRY_SERVICE_NAME } = await import("@/lib/telemetry/constants"); 15 | const { getTelemetryTracer } = await import("@/lib/telemetry/tracer"); 16 | 17 | const fakeTracer = { startActiveSpan: vi.fn() }; 18 | GetTracer.mockReturnValue(fakeTracer); 19 | 20 | const tracer = getTelemetryTracer(); 21 | 22 | expect(GetTracer).toHaveBeenCalledWith(TELEMETRY_SERVICE_NAME); 23 | expect(tracer).toBe(fakeTracer); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/__tests__/midstream-resume-continuity.test.tsx: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { describe, expect, it } from "vitest"; 4 | 5 | type Message = { id: string; role: "user" | "assistant" | "system" }; 6 | 7 | // Mirrors the visibleMessages filtering in chat/page.tsx 8 | function filterVisibleMessages(messages: Message[]): Message[] { 9 | return messages.filter((m) => m.role !== "system"); 10 | } 11 | 12 | describe("mid-stream resume continuity (lightweight)", () => { 13 | it("retains non-system messages after resume", () => { 14 | const initial: Message[] = [ 15 | { id: "u1", role: "user" }, 16 | { id: "s1", role: "system" }, 17 | { id: "a1", role: "assistant" }, 18 | ]; 19 | 20 | const visible = filterVisibleMessages(initial); 21 | expect(visible).toHaveLength(2); 22 | expect(visible.map((m) => m.id)).toEqual(["u1", "a1"]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/domain/activities/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Domain-level errors for activities service. 3 | */ 4 | 5 | /** 6 | * Error thrown when an activity cannot be found. 7 | */ 8 | export class NotFoundError extends Error { 9 | readonly code = "NOT_FOUND" as const; 10 | 11 | constructor(message: string) { 12 | super(message); 13 | this.name = "NotFoundError"; 14 | Object.setPrototypeOf(this, NotFoundError.prototype); 15 | } 16 | } 17 | 18 | /** 19 | * Type guard to check if an error is a NotFoundError. 20 | * 21 | * @param error - Error to check. 22 | * @returns True if error is a NotFoundError. 23 | */ 24 | export function isNotFoundError(error: unknown): error is NotFoundError { 25 | return ( 26 | error instanceof NotFoundError || 27 | (error instanceof Error && 28 | "code" in error && 29 | (error as { code: unknown }).code === "NOT_FOUND") 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | export const Separator = React.forwardRef< 9 | React.ComponentRef, 10 | React.ComponentPropsWithoutRef 11 | >(function Separator( 12 | { className, orientation = "horizontal", decorative = true, ...props }, 13 | ref 14 | ) { 15 | return ( 16 | 27 | ); 28 | }); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Dashboard root layout (RSC shell) enforcing auth and providing the 3 | * shared dashboard chrome. 4 | * 5 | * Caching is handled at the app level via `cacheComponents`; this layout intentionally 6 | * does not opt into per-file caching directives because it relies on authenticated 7 | * user context. 8 | */ 9 | 10 | import { Suspense } from "react"; 11 | import { DashboardLayout } from "@/components/layouts/dashboard-layout"; 12 | import { requireUser } from "@/lib/auth/server"; 13 | import DashboardLoading from "./loading"; 14 | 15 | export default async function Layout({ children }: { children: React.ReactNode }) { 16 | // Enforce Supabase SSR auth for all dashboard routes. 17 | await requireUser(); 18 | 19 | return ( 20 | }> 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/test/msw/handlers/ai-routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview MSW handlers for internal AI routes used in tests. 3 | * 4 | * These are application routes (e.g. demo endpoints), not upstream provider APIs. 5 | * Upstream provider mocks live in `ai-providers.ts`. 6 | */ 7 | 8 | import { HttpResponse, http } from "msw"; 9 | 10 | export const aiRouteHandlers = [ 11 | // POST /api/ai/stream - AI streaming endpoint for demo 12 | http.post("/api/ai/stream", () => { 13 | const encoder = new TextEncoder(); 14 | const stream = new ReadableStream({ 15 | start(controller) { 16 | controller.enqueue( 17 | encoder.encode('data: {"type":"text","text":"Hello from AI"}\n\n') 18 | ); 19 | controller.close(); 20 | }, 21 | }); 22 | 23 | return new HttpResponse(stream, { 24 | headers: { "Content-Type": "text/event-stream" }, 25 | status: 200, 26 | }); 27 | }), 28 | ]; 29 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import type * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const LabelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ); 12 | 13 | export interface LabelProps 14 | extends React.ComponentPropsWithoutRef, 15 | VariantProps { 16 | ref?: React.Ref>; 17 | } 18 | 19 | export function Label({ className, ref, ...props }: LabelProps) { 20 | return ( 21 | 26 | ); 27 | } 28 | Label.displayName = LabelPrimitive.Root.displayName; 29 | -------------------------------------------------------------------------------- /src/lib/trips/parse-trip-date.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared trip date parsing helper with consistent error handling. 3 | */ 4 | 5 | import { DateUtils } from "@/lib/dates/unified-date-utils"; 6 | import { recordClientErrorOnActiveSpan } from "@/lib/telemetry/client-errors"; 7 | 8 | type TripDateParseTelemetry = { 9 | action?: string; 10 | context?: string; 11 | }; 12 | 13 | export function parseTripDate( 14 | value?: string | null, 15 | telemetry?: TripDateParseTelemetry 16 | ): Date | null { 17 | if (!value) return null; 18 | try { 19 | return DateUtils.parse(value); 20 | } catch (error) { 21 | recordClientErrorOnActiveSpan( 22 | error instanceof Error ? error : new Error(String(error)), 23 | { 24 | action: telemetry?.action ?? "parseTripDate", 25 | context: telemetry?.context ?? "parseTripDate", 26 | value, 27 | } 28 | ); 29 | return null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/chat/__tests__/page.test.tsx: -------------------------------------------------------------------------------- 1 | /** @vitest-environment jsdom */ 2 | 3 | import { render, screen } from "@testing-library/react"; 4 | import { describe, expect, it, vi } from "vitest"; 5 | import ChatPage from "../../chat/page"; 6 | 7 | // Mock Streamdown-backed Response to avoid rehype/ESM issues in node test runner 8 | vi.mock("@/components/ai-elements/response", () => ({ 9 | Response: ({ children }: { children?: React.ReactNode }) => ( 10 |
{children}
11 | ), 12 | })); 13 | 14 | describe("ChatPage", () => { 15 | it("renders empty state and input controls", () => { 16 | render(); 17 | expect( 18 | screen.getByText(/Start a conversation to see messages here/i) 19 | ).toBeInTheDocument(); 20 | expect(screen.getByLabelText(/Chat prompt/i)).toBeInTheDocument(); 21 | expect(screen.getByRole("button", { name: /Submit/i })).toBeInTheDocument(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/lib/supabase/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Server-only Supabase client entrypoint wired to Next.js cookies(). 3 | */ 4 | 5 | import "server-only"; 6 | 7 | import type { SupabaseClient } from "@supabase/supabase-js"; 8 | import { cookies } from "next/headers"; 9 | import type { Database } from "./database.types"; 10 | import { createCookieAdapter, createServerSupabaseClient } from "./factory"; 11 | 12 | export type TypedServerSupabase = SupabaseClient; 13 | 14 | /** 15 | * Creates server Supabase client with Next.js cookies. 16 | * @returns Promise resolving to typed Supabase server client 17 | */ 18 | export async function createServerSupabase(): Promise { 19 | const cookieStore = await cookies(); 20 | return createServerSupabaseClient({ 21 | cookies: createCookieAdapter(cookieStore), 22 | }); 23 | } 24 | 25 | // Re-export factory utilities 26 | export { getCurrentUser } from "./factory"; 27 | -------------------------------------------------------------------------------- /src/test/fixtures/flights.ts: -------------------------------------------------------------------------------- 1 | import type { UpcomingFlight } from "@/hooks/use-trips"; 2 | 3 | export const UPCOMING_FLIGHT_A: UpcomingFlight = { 4 | airline: "NH", 5 | airlineName: "ANA", 6 | arrivalTime: "2025-01-15T14:20:00Z", 7 | cabinClass: "economy", 8 | currency: "USD", 9 | departureTime: "2025-01-15T10:00:00Z", 10 | destination: "HND", 11 | duration: 260, 12 | flightNumber: "NH203", 13 | id: "f1", 14 | origin: "NRT", 15 | price: 999, 16 | status: "upcoming", 17 | stops: 0, 18 | }; 19 | 20 | export const UPCOMING_FLIGHT_B: UpcomingFlight = { 21 | airline: "UA", 22 | airlineName: "United", 23 | arrivalTime: "2025-01-10T16:30:00Z", 24 | cabinClass: "business", 25 | currency: "USD", 26 | departureTime: "2025-01-10T12:00:00Z", 27 | destination: "SFO", 28 | duration: 270, 29 | flightNumber: "UA837", 30 | id: "f2", 31 | origin: "NRT", 32 | price: 1200, 33 | status: "upcoming", 34 | stops: 0, 35 | }; 36 | -------------------------------------------------------------------------------- /src/ai/tools/schemas/travel-advisory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Centralized Zod schemas for travel advisory tools. 3 | * 4 | * Contains input validation schemas for getTravelAdvisory tool. 5 | */ 6 | 7 | import { safetyResultSchema } from "@ai/tools/schemas/tools"; 8 | import { z } from "zod"; 9 | 10 | /** Schema for travel advisory tool input. */ 11 | export const travelAdvisoryInputSchema = z.strictObject({ 12 | destination: z 13 | .string() 14 | .min(1, "Destination must be a non-empty string") 15 | .describe("The destination city, country, or region to get travel advisory for"), 16 | }); 17 | 18 | // ===== TOOL OUTPUT SCHEMAS ===== 19 | 20 | /** Schema for travel advisory tool output. */ 21 | export const travelAdvisoryOutputSchema = safetyResultSchema.extend({ 22 | fromCache: z.boolean(), 23 | }); 24 | 25 | /** TypeScript type for travel advisory tool output. */ 26 | export type TravelAdvisoryOutput = z.infer; 27 | -------------------------------------------------------------------------------- /src/app/api/accommodations/search/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview POST /api/accommodations/search route handler. 3 | */ 4 | 5 | import "server-only"; 6 | 7 | import { getAccommodationsService } from "@domain/accommodations/container"; 8 | import { accommodationSearchInputSchema } from "@schemas/accommodations"; 9 | import { withApiGuards } from "@/lib/api/factory"; 10 | import { getCurrentUser } from "@/lib/supabase/server"; 11 | 12 | export const POST = withApiGuards({ 13 | auth: false, // Allow anonymous searches 14 | rateLimit: "accommodations:search", 15 | schema: accommodationSearchInputSchema, 16 | telemetry: "accommodations.search", 17 | })(async (_req, { supabase }, body) => { 18 | const userResult = await getCurrentUser(supabase); 19 | const service = getAccommodationsService(); 20 | 21 | const result = await service.search(body, { 22 | userId: userResult.user?.id ?? undefined, 23 | }); 24 | 25 | return Response.json(result); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 | 5 | function Collapsible({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return ; 9 | } 10 | 11 | function CollapsibleTrigger({ 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 19 | ); 20 | } 21 | 22 | function CollapsibleContent({ 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 30 | ); 31 | } 32 | 33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 34 | -------------------------------------------------------------------------------- /src/domain/schemas/shared/person.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared person/contact primitives (names, email, phone, password). 3 | */ 4 | 5 | import { z } from "zod"; 6 | import { primitiveSchemas, refinedSchemas } from "../registry"; 7 | 8 | export const NAME_SCHEMA = z 9 | .string() 10 | .min(1, { error: "Name is required" }) 11 | .max(50, { error: "Name too long" }) 12 | .regex(/^[a-zA-Z\s\-'.]+$/, { 13 | error: "Name can only contain letters, spaces, hyphens, apostrophes, and periods", 14 | }); 15 | 16 | export const EMAIL_SCHEMA = primitiveSchemas.email.max(255); 17 | 18 | export const PHONE_SCHEMA = z 19 | .string() 20 | .regex(/^\+?[\d\s\-()]{10,20}$/, { error: "Please enter a valid phone number" }); 21 | 22 | export const PASSWORD_SCHEMA = refinedSchemas.strongPassword; 23 | 24 | export type PersonName = z.infer; 25 | export type EmailAddress = z.infer; 26 | export type PhoneNumber = z.infer; 27 | -------------------------------------------------------------------------------- /src/app/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Chat route layout implementation. 3 | * 4 | * Implements and exports the ChatLayout component used by the chat route group. 5 | */ 6 | 7 | import "server-only"; 8 | 9 | import type { ReactNode } from "react"; 10 | import { requireUser } from "@/lib/auth/server"; 11 | import { ROUTES } from "@/lib/routes"; 12 | 13 | /** 14 | * Chat route layout that requires an authenticated user. 15 | * 16 | * If the request is unauthenticated, this layout triggers a redirect to the login 17 | * page with `next=/chat` so the user returns here after signing in. 18 | * 19 | * @param props - Layout props. 20 | * @param props.children - Nested route content. 21 | * @returns The nested route content once authentication is verified. 22 | */ 23 | export default async function ChatLayout({ 24 | children, 25 | }: { 26 | children: ReactNode; 27 | }): Promise { 28 | await requireUser({ redirectTo: ROUTES.chat }); 29 | return children; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/providers/telemetry-provider.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview React provider that initializes client-side OpenTelemetry tracing. 3 | * 4 | * This component initializes the WebTracerProvider and FetchInstrumentation 5 | * when mounted in the browser. It renders nothing and is purely for side effects. 6 | */ 7 | 8 | "use client"; 9 | 10 | import { useEffect } from "react"; 11 | import { initTelemetry } from "@/lib/telemetry/client"; 12 | 13 | /** 14 | * TelemetryProvider component. 15 | * 16 | * Initializes client-side OpenTelemetry tracing on mount. Uses useEffect to 17 | * ensure initialization only happens in the browser (not during SSR). 18 | * 19 | * This component renders nothing and is purely for side effects. 20 | * 21 | * @returns null (renders nothing) 22 | */ 23 | export function TelemetryProvider(): null { 24 | useEffect(() => { 25 | // Initialize telemetry only in browser environment 26 | initTelemetry(); 27 | }, []); 28 | 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /src/stores/ui/features.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Feature flags slice for UI store. 3 | */ 4 | 5 | import type { StateCreator } from "zustand"; 6 | import type { FeatureFlags, FeaturesSlice, UiState } from "./types"; 7 | 8 | export const DEFAULT_FEATURES: FeatureFlags = { 9 | enableAnalytics: true, 10 | enableAnimations: true, 11 | enableBetaFeatures: false, 12 | enableHaptics: true, 13 | enableSounds: false, 14 | }; 15 | 16 | export const createFeaturesSlice: StateCreator = ( 17 | set 18 | ) => ({ 19 | features: DEFAULT_FEATURES, 20 | 21 | setFeature: (feature, enabled) => { 22 | set((state) => ({ 23 | features: { 24 | ...state.features, 25 | [feature]: enabled, 26 | }, 27 | })); 28 | }, 29 | 30 | toggleFeature: (feature) => { 31 | set((state) => ({ 32 | features: { 33 | ...state.features, 34 | [feature]: !state.features[feature], 35 | }, 36 | })); 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "incremental": true, 6 | "isolatedModules": true, 7 | "jsx": "react-jsx", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "noEmit": true, 12 | "paths": { 13 | "@/*": ["./src/*"], 14 | "@schemas/*": ["./src/domain/schemas/*"], 15 | "@domain/*": ["./src/domain/*"], 16 | "@ai/*": ["./src/ai/*"] 17 | }, 18 | "typeRoots": ["./@types", "./node_modules/@types"], 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "strict": true, 27 | "target": "ES2017" 28 | }, 29 | "exclude": ["node_modules"], 30 | "include": [ 31 | "next-env.d.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | ".next/types/**/*.ts", 35 | ".next/dev/types/**/*.ts" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/app/api/activities/search/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview POST /api/activities/search route handler. 3 | */ 4 | 5 | import "server-only"; 6 | 7 | import { getActivitiesService } from "@domain/activities/container"; 8 | import { activitySearchParamsSchema } from "@schemas/search"; 9 | import { withApiGuards } from "@/lib/api/factory"; 10 | import { getCurrentUser } from "@/lib/supabase/server"; 11 | 12 | export const POST = withApiGuards({ 13 | auth: false, // Allow anonymous searches 14 | rateLimit: "activities:search", 15 | schema: activitySearchParamsSchema, 16 | telemetry: "activities.search", 17 | })(async (_req, { supabase }, body) => { 18 | const userResult = await getCurrentUser(supabase); 19 | const service = getActivitiesService(); 20 | 21 | const result = await service.search(body, { 22 | userId: userResult.user?.id ?? undefined, 23 | // IP and locale can be extracted from request headers if needed 24 | }); 25 | 26 | return Response.json(result); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/api/rag/index/_handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Pure handler for RAG indexing requests. 3 | * 4 | * Business logic lives here; route.ts should only wrap and delegate. 5 | */ 6 | 7 | import "server-only"; 8 | 9 | import type { RagIndexRequest } from "@schemas/rag"; 10 | import { NextResponse } from "next/server"; 11 | import { indexDocuments } from "@/lib/rag/indexer"; 12 | import type { TypedServerSupabase } from "@/lib/supabase/server"; 13 | 14 | export interface RagIndexDeps { 15 | supabase: TypedServerSupabase; 16 | } 17 | 18 | export async function handleRagIndex( 19 | deps: RagIndexDeps, 20 | body: RagIndexRequest 21 | ): Promise { 22 | const result = await indexDocuments({ 23 | config: { 24 | chunkOverlap: body.chunkOverlap, 25 | chunkSize: body.chunkSize, 26 | namespace: body.namespace, 27 | }, 28 | documents: body.documents, 29 | supabase: deps.supabase, 30 | }); 31 | 32 | return NextResponse.json(result, { status: 200 }); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/features/agent-monitoring/dashboard/agent-status-dashboard-lazy.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | import { LoadingSpinner } from "@/components/ui/loading-spinner"; 5 | 6 | // Dynamically import the AgentStatusDashboard with loading fallback 7 | const AgentStatusDashboard = dynamic( 8 | () => 9 | import("./agent-status-dashboard").then((mod) => ({ 10 | default: mod.AgentStatusDashboard, 11 | })), 12 | { 13 | loading: () => ( 14 |
15 |
16 | 17 |

18 | Loading agent dashboard... 19 |

20 |
21 |
22 | ), 23 | ssr: false, // Disable SSR for this heavy component 24 | } 25 | ); 26 | 27 | export { AgentStatusDashboard }; 28 | export type { AgentStatusDashboardProps } from "./agent-status-dashboard"; 29 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/factories/reset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Centralized factory reset utilities. 3 | * Call resetAllFactories() in beforeEach() to ensure deterministic test data. 4 | */ 5 | 6 | import { resetAuthUserFactory } from "./auth-user-factory"; 7 | import { resetCalendarFactory } from "./calendar-factory"; 8 | import { resetFilterFactory } from "./filter-factory"; 9 | import { resetSearchFactory } from "./search-factory"; 10 | import { resetTripFactory } from "./trip-factory"; 11 | import { resetUserFactory } from "./user-factory"; 12 | 13 | /** 14 | * Resets all factory ID counters to their initial state. 15 | * Call this in beforeEach() to ensure consistent, deterministic IDs across test runs. 16 | * 17 | * @example 18 | * beforeEach(() => { 19 | * resetAllFactories(); 20 | * }); 21 | */ 22 | export const resetAllFactories = (): void => { 23 | resetUserFactory(); 24 | resetAuthUserFactory(); 25 | resetTripFactory(); 26 | resetSearchFactory(); 27 | resetFilterFactory(); 28 | resetCalendarFactory(); 29 | }; 30 | -------------------------------------------------------------------------------- /supabase/migrations/20251220000000_delete_user_memories_rpc.sql: -------------------------------------------------------------------------------- 1 | -- Delete user memories atomically (service_role only). 2 | -- Uses a single transaction via PL/pgSQL function execution. 3 | 4 | CREATE OR REPLACE FUNCTION public.delete_user_memories( 5 | p_user_id UUID 6 | ) RETURNS TABLE ( 7 | deleted_turns BIGINT, 8 | deleted_sessions BIGINT 9 | ) 10 | LANGUAGE plpgsql 11 | SECURITY DEFINER 12 | SET search_path = public 13 | AS $$ 14 | DECLARE 15 | v_deleted_turns BIGINT; 16 | v_deleted_sessions BIGINT; 17 | BEGIN 18 | IF coalesce((current_setting('request.jwt.claims', true)::json->>'role'),'') <> 'service_role' THEN 19 | RAISE EXCEPTION 'Must be called as service role'; 20 | END IF; 21 | 22 | DELETE FROM memories.turns WHERE user_id = p_user_id; 23 | GET DIAGNOSTICS v_deleted_turns = ROW_COUNT; 24 | 25 | DELETE FROM memories.sessions WHERE user_id = p_user_id; 26 | GET DIAGNOSTICS v_deleted_sessions = ROW_COUNT; 27 | 28 | RETURN QUERY SELECT v_deleted_turns, v_deleted_sessions; 29 | END; 30 | $$; 31 | 32 | -------------------------------------------------------------------------------- /src/lib/security/__tests__/internal-key.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { describe, expect, it } from "vitest"; 4 | import { isValidInternalKey } from "@/lib/security/internal-key"; 5 | 6 | describe("isValidInternalKey", () => { 7 | it("returns false when provided key is missing", () => { 8 | expect(isValidInternalKey(null, "expected")).toBe(false); 9 | }); 10 | 11 | it("throws when expected key is empty", () => { 12 | expect(() => isValidInternalKey("provided", "")).toThrow( 13 | "Missing expected internal key: check server configuration" 14 | ); 15 | }); 16 | 17 | it("returns false when keys have different lengths", () => { 18 | expect(isValidInternalKey("short", "much-longer")).toBe(false); 19 | }); 20 | 21 | it("returns true when keys match exactly", () => { 22 | expect(isValidInternalKey("same-key", "same-key")).toBe(true); 23 | }); 24 | 25 | it("returns false when keys differ but have the same length", () => { 26 | expect(isValidInternalKey("abc123", "abd123")).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/lib/qstash/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview QStash configuration constants per ADR-0048. 3 | */ 4 | 5 | import "server-only"; 6 | 7 | /** 8 | * QStash retry configuration for webhook job handlers. 9 | * Max 6 total attempts (1 initial + 5 retries) with exponential backoff. 10 | */ 11 | export const QSTASH_RETRY_CONFIG = { 12 | /** Initial delay before first retry (QStash uses exponential backoff) */ 13 | delay: "10s", 14 | /** Number of retry attempts after initial failure */ 15 | retries: 5, 16 | } as const; 17 | 18 | /** Redis key prefix for dead letter queue entries */ 19 | export const DLQ_KEY_PREFIX = "qstash-dlq" as const; 20 | 21 | /** TTL for DLQ entries in seconds (7 days) */ 22 | export const DLQ_TTL_SECONDS = 60 * 60 * 24 * 7; 23 | 24 | /** Maximum number of DLQ entries to keep per job type */ 25 | export const DLQ_MAX_ENTRIES = 1000; 26 | 27 | /** Threshold for DLQ size alerts */ 28 | export const DLQ_ALERT_THRESHOLD = 100; 29 | 30 | /** QStash signing key header name */ 31 | export const QSTASH_SIGNATURE_HEADER = "upstash-signature" as const; 32 | -------------------------------------------------------------------------------- /src/components/ui/__mocks__/use-toast.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Vitest mocks for the use-toast module. Provides mocked 3 | * implementations for testing toast functionality in isolation. 4 | */ 5 | 6 | import { vi } from "vitest"; 7 | import type { Toast } from "../use-toast"; 8 | 9 | /** 10 | * Mock implementation of the toast function. Creates a mock toast object 11 | * with jest mock functions for testing purposes. 12 | * 13 | * @param _props - Toast properties (ignored in mock). 14 | * @returns A mock toast object with dismiss, id, and update properties. 15 | */ 16 | export const toast = vi.fn((_props: Toast) => ({ 17 | dismiss: vi.fn(), 18 | id: `toast-${Date.now()}`, 19 | update: vi.fn(), 20 | })); 21 | 22 | /** 23 | * Mock implementation of the useToast hook. Returns a mock object 24 | * with the expected hook interface for testing. 25 | * 26 | * @returns A mock useToast hook result with dismiss function, toast function, and empty toasts array. 27 | */ 28 | export const useToast = vi.fn(() => ({ 29 | dismiss: vi.fn(), 30 | toast, 31 | toasts: [], 32 | })); 33 | -------------------------------------------------------------------------------- /src/app/api/keys/_error-mapping.ts: -------------------------------------------------------------------------------- 1 | import { errorResponse } from "@/lib/api/route-helpers"; 2 | 3 | export const PLANNED_ERROR_CODES = { 4 | invalidKey: "INVALID_KEY", 5 | networkError: "NETWORK_ERROR", 6 | vaultUnavailable: "VAULT_UNAVAILABLE", 7 | } as const; 8 | 9 | export type PlannedErrorCode = 10 | (typeof PLANNED_ERROR_CODES)[keyof typeof PLANNED_ERROR_CODES]; 11 | 12 | export function vaultUnavailableResponse(reason: string, err?: unknown): Response { 13 | return errorResponse({ 14 | err, 15 | error: PLANNED_ERROR_CODES.vaultUnavailable, 16 | reason, 17 | status: 500, 18 | }); 19 | } 20 | 21 | export function mapProviderStatusToCode(status: number): PlannedErrorCode { 22 | if (status === 429) return PLANNED_ERROR_CODES.networkError; 23 | if (status >= 500) return PLANNED_ERROR_CODES.networkError; 24 | if (status >= 400) return PLANNED_ERROR_CODES.invalidKey; 25 | return PLANNED_ERROR_CODES.networkError; 26 | } 27 | 28 | export function mapProviderExceptionToCode(_error: unknown): PlannedErrorCode { 29 | return PLANNED_ERROR_CODES.networkError; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/chat/__tests__/page.smoke.test.tsx: -------------------------------------------------------------------------------- 1 | /** @vitest-environment jsdom */ 2 | 3 | import { render, screen } from "@testing-library/react"; 4 | import { describe, expect, it, vi } from "vitest"; 5 | import ChatPage from "../page"; 6 | 7 | // Mock Streamdown-backed Response to avoid rehype/ESM issues in node test runner 8 | vi.mock("@/components/ai-elements/response", () => ({ 9 | Response: ({ children }: { children?: React.ReactNode }) => ( 10 |
{children}
11 | ), 12 | })); 13 | 14 | // Mock the ChatClient component to avoid importing server-side dependencies 15 | // and AI SDK components that are not needed for this smoke test. 16 | // This allows us to test the page structure without full component initialization. 17 | vi.mock("../chat-client", () => ({ 18 | ChatClient: () =>
Chat Client
, 19 | })); 20 | 21 | describe("ChatPage UI smoke", () => { 22 | it("renders the chat client component", () => { 23 | render(); 24 | expect(screen.getByTestId("chat-client")).toBeInTheDocument(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/ai/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared AI agent constants. 3 | */ 4 | 5 | export const CHAT_DEFAULT_SYSTEM_PROMPT = `You are a helpful travel planning assistant with access to many tools. 6 | 7 | When the user asks for something factual, call the matching tool by name: 8 | - Flights: use searchFlights for routes/fares. 9 | - Lodging: use searchAccommodations, getAccommodationDetails, checkAvailability, bookAccommodation (confirm before booking). 10 | - Activities/POIs: use searchActivities / lookupPoiContext; include hours and location. 11 | - Planning: use createTravelPlan / saveTravelPlan to build and store itineraries; confirm before saving. 12 | - Weather: use getCurrentWeather for conditions; include units and location. 13 | - Maps: use geocode for addresses and distanceMatrix for travel times/distances. 14 | - Discovery: use webSearch/webSearchBatch for general research. 15 | - Memory: use searchUserMemories to recall prior trips or preferences. 16 | 17 | Always combine tool outputs into clear recommendations, cite sources when possible, and ask clarifying questions before making bookings or saving plans.`; 18 | -------------------------------------------------------------------------------- /src/test/factories/chat-factory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Factory for creating ChatSession and Message test data. 3 | */ 4 | 5 | import type { ChatSession, Message } from "@schemas/chat"; 6 | 7 | /** 8 | * Create mock chat session. 9 | * 10 | * @param overrides - Partial session to override defaults 11 | * @returns A complete chat session 12 | */ 13 | export function createMockSession(overrides: Partial = {}): ChatSession { 14 | return { 15 | agentId: "agent-1", 16 | createdAt: "2025-01-01T00:00:00Z", 17 | id: "session-1", 18 | messages: [], 19 | title: "Test Conversation", 20 | updatedAt: "2025-01-01T00:00:00Z", 21 | ...overrides, 22 | }; 23 | } 24 | 25 | /** 26 | * Create mock message. 27 | * 28 | * @param overrides - Partial message to override defaults 29 | * @returns A complete message 30 | */ 31 | export function createMockMessage(overrides: Partial = {}): Message { 32 | return { 33 | content: "Test message", 34 | id: "msg-1", 35 | role: "user", 36 | timestamp: "2025-01-01T00:00:00Z", 37 | ...overrides, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/ai/tools/schemas/activities.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Activity tool model output schemas. 3 | */ 4 | 5 | import { z } from "zod"; 6 | 7 | // ===== MODEL OUTPUT SCHEMAS ===== 8 | 9 | /** Activity entry for model consumption. Fields match source activitySchema requirements. */ 10 | const activityEntryModelOutputSchema = z.strictObject({ 11 | duration: z.number(), 12 | id: z.string(), 13 | location: z.string(), 14 | name: z.string(), 15 | price: z.number(), 16 | rating: z.number(), 17 | type: z.string(), 18 | }); 19 | 20 | /** Activity search result metadata for model consumption. */ 21 | const activityMetadataModelOutputSchema = z.strictObject({ 22 | primarySource: z.enum(["googleplaces", "ai_fallback", "mixed"]), 23 | total: z.number().int(), 24 | }); 25 | 26 | /** Activity search result output schema for model consumption. */ 27 | export const activityModelOutputSchema = z.strictObject({ 28 | activities: z.array(activityEntryModelOutputSchema), 29 | metadata: activityMetadataModelOutputSchema, 30 | }); 31 | 32 | export type ActivityModelOutput = z.infer; 33 | -------------------------------------------------------------------------------- /src/app/api/flights/search/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Flight search API route. 3 | * 4 | * POST /api/flights/search 5 | * Searches for flights using Duffel provider. 6 | */ 7 | 8 | import "server-only"; 9 | 10 | import { searchFlightsService } from "@domain/flights/service"; 11 | import { flightSearchRequestSchema } from "@schemas/flights"; 12 | import { withApiGuards } from "@/lib/api/factory"; 13 | 14 | export const POST = withApiGuards({ 15 | auth: false, // Allow anonymous searches 16 | rateLimit: "flights:search", 17 | schema: flightSearchRequestSchema, 18 | telemetry: "flights.search", 19 | })(async (_req, _ctx, body) => { 20 | // Note: The flights service performs its own validation via 21 | // flightSearchRequestSchema.parse(params) because it is invoked from multiple 22 | // entry points (including AI tools) that bypass withApiGuards. It also 23 | // intentionally omits ServiceContext/userId and rate limiting at the service 24 | // layer—unlike accommodations—by API design. 25 | const result = await searchFlightsService(body); 26 | 27 | return Response.json(result); 28 | }); 29 | -------------------------------------------------------------------------------- /src/test/helpers/make-request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared test helper for creating minimal NextRequest mocks. 3 | * 4 | * Use this helper when testing route handlers that only need to access 5 | * request headers. For tests requiring full NextRequest functionality, 6 | * consider a more complete mock or integration tests. 7 | */ 8 | 9 | import type { NextRequest } from "next/server"; 10 | import { unsafeCast } from "./unsafe-cast"; 11 | 12 | /** 13 | * Creates a minimal NextRequest mock with only headers populated. 14 | * 15 | * @param headers - Optional headers to include in the mock request. 16 | * @returns A minimal NextRequest mock suitable for testing header-only access. 17 | * 18 | * @example 19 | * ```typescript 20 | * const req = makeRequest({ "x-real-ip": "192.168.1.1" }); 21 | * expect(getClientIpFromHeaders(req)).toBe("192.168.1.1"); 22 | * ``` 23 | * 24 | * @internal Only use where headers are the sole accessed property. 25 | */ 26 | export function makeRequest(headers: HeadersInit = {}): NextRequest { 27 | return unsafeCast({ headers: new Headers(headers) }); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as SonnerToaster } from "sonner"; 5 | 6 | /** 7 | * Toast notification container using Sonner. 8 | * 9 | * Integrates with next-themes for automatic light/dark theme support. 10 | * Renders at bottom-right on desktop, top on mobile. 11 | */ 12 | export function Toaster() { 13 | const { theme = "system" } = useTheme(); 14 | 15 | return ( 16 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/client/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Client-side session utilities. 3 | * Uses browser storage APIs - must only be imported in client components. 4 | */ 5 | 6 | "use client"; 7 | 8 | import { secureUuid } from "@/lib/security/random"; 9 | 10 | /** 11 | * Get or create a per-session identifier for error tracking and telemetry. 12 | * 13 | * The ID is stored in `sessionStorage` under the key `session_id`. If it does 14 | * not exist, a new ID is generated using `secureUuid()` and persisted. When 15 | * called in environments without access to `sessionStorage` (e.g., server 16 | * rendering, certain privacy contexts), the function returns `undefined`. 17 | * 18 | * @returns A stable session identifier string or `undefined` when unavailable. 19 | */ 20 | export function getSessionId(): string | undefined { 21 | try { 22 | let sessionId = sessionStorage.getItem("session_id"); 23 | if (!sessionId) { 24 | sessionId = `session_${secureUuid()}`; 25 | sessionStorage.setItem("session_id", sessionId); 26 | } 27 | return sessionId; 28 | } catch { 29 | return undefined; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/stores/ui/navigation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Navigation slice for UI store. 3 | */ 4 | 5 | import type { StateCreator } from "zustand"; 6 | import type { NavigationSlice, NavigationState, UiState } from "./types"; 7 | 8 | export const DEFAULT_NAVIGATION_STATE: NavigationState = { 9 | activeRoute: "/", 10 | breadcrumbs: [], 11 | }; 12 | 13 | export const createNavigationSlice: StateCreator = ( 14 | set 15 | ) => ({ 16 | addBreadcrumb: (breadcrumb) => { 17 | set((state) => ({ 18 | navigation: { 19 | ...state.navigation, 20 | breadcrumbs: [...state.navigation.breadcrumbs, breadcrumb], 21 | }, 22 | })); 23 | }, 24 | navigation: DEFAULT_NAVIGATION_STATE, 25 | 26 | setActiveRoute: (route) => { 27 | set((state) => ({ 28 | navigation: { 29 | ...state.navigation, 30 | activeRoute: route, 31 | }, 32 | })); 33 | }, 34 | 35 | setBreadcrumbs: (breadcrumbs) => { 36 | set((state) => ({ 37 | navigation: { 38 | ...state.navigation, 39 | breadcrumbs, 40 | }, 41 | })); 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/lib/__tests__/zod-v4-resolver.dom.test.tsx: -------------------------------------------------------------------------------- 1 | /** @vitest-environment jsdom */ 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { render } from "@testing-library/react"; 5 | import React from "react"; 6 | import { useForm } from "react-hook-form"; 7 | import { describe, expect, it } from "vitest"; 8 | import { z } from "zod"; 9 | 10 | function FormHarness() { 11 | const schema = z.object({ email: z.email() }); 12 | const { handleSubmit, register } = useForm<{ email: string }>({ 13 | resolver: zodResolver(schema as never), 14 | }); 15 | const onSubmit = () => { 16 | // Empty submit handler for test 17 | }; 18 | return ( 19 |
20 | 21 | 22 |
23 | ); 24 | } 25 | 26 | describe("zod v4 resolver interop", () => { 27 | it("mounts a form with zodResolver without runtime errors", () => { 28 | const { getByText } = render(React.createElement(FormHarness)); 29 | expect(getByText("Submit")).toBeInTheDocument(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /docs/architecture/decisions/adr-0033-rag-advanced-v6.md: -------------------------------------------------------------------------------- 1 | # ADR: RAG Advanced with AI SDK v6 (Hybrid + Provider Reranking) 2 | 3 | ## Status 4 | 5 | Proposed 6 | 7 | ## Context 8 | 9 | We need a robust RAG pipeline on Next.js 16 using AI SDK v6. Python RAG remains in FastAPI; we will unify in TypeScript with Supabase pgvector, hybrid retrieval (vector + keyword), and provider reranking (e.g., Cohere). 10 | 11 | ## Decision 12 | 13 | - Adopt hybrid retrieval in TS and apply provider reranking (e.g., cohere.reranking('rerank-v3.5')) for top-k refinement. Provider-dependent feature. 14 | - Cache frequent queries with short TTL via Upstash. 15 | - Ensure v6 UIMessage.parts alignment for returned context and references. 16 | 17 | ## Consequences 18 | 19 | - Improved relevance and maintainability; single Next backend. 20 | - Adds provider dependency; must handle fallback if rerank unavailable. 21 | 22 | ## References 23 | 24 | - v6 Reranking: 25 | - Embeddings: 26 | - Next.js App Router: 27 | -------------------------------------------------------------------------------- /src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Next.js instrumentation hook for OpenTelemetry server-side tracing. 3 | * 4 | * This file is automatically executed by Next.js before the application starts. 5 | * It initializes @vercel/otel to enable automatic instrumentation of Route Handlers, 6 | * Server Components, and Middleware. 7 | */ 8 | 9 | import { registerOTel } from "@vercel/otel"; 10 | import { TELEMETRY_SERVICE_NAME } from "@/lib/telemetry/constants"; 11 | 12 | /** 13 | * Registers OpenTelemetry instrumentation for the Next.js application. 14 | * 15 | * This function is called by Next.js during application startup to enable 16 | * server-side tracing. The @vercel/otel wrapper handles all the complexity 17 | * of setting up NodeSDK, resource detection, and Next.js-specific instrumentation. 18 | */ 19 | export async function register() { 20 | registerOTel({ 21 | serviceName: TELEMETRY_SERVICE_NAME, 22 | }); 23 | 24 | // Initialize security modules on server runtime only 25 | if (process.env.NEXT_RUNTIME === "nodejs") { 26 | const { initMfa } = await import("@/lib/security/mfa"); 27 | initMfa(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Centralized application routes. 3 | * 4 | * Provides a single source of truth for all internal routes to ensure 5 | * consistency across the application. 6 | */ 7 | 8 | export const ROUTES = { 9 | // Demo route 10 | aiDemo: "/ai-demo", 11 | 12 | // Chat 13 | chat: "/chat", 14 | contact: "/contact", 15 | 16 | // Dashboard routes 17 | dashboard: { 18 | agentStatus: "/dashboard/agent-status", 19 | billing: "/dashboard/billing", 20 | calendar: "/dashboard/calendar", 21 | profile: "/dashboard/profile", 22 | root: "/dashboard", 23 | search: "/dashboard/search", 24 | security: "/dashboard/security", 25 | settings: "/dashboard/settings", 26 | settingsApiKeys: "/dashboard/settings/api-keys", 27 | team: "/dashboard/team", 28 | trips: "/dashboard/trips", 29 | }, 30 | 31 | // Root 32 | home: "/", 33 | // Auth routes 34 | login: "/login", 35 | 36 | // Legal routes 37 | privacy: "/privacy", 38 | register: "/register", 39 | 40 | // Search results 41 | searchFlightsResults: "/search/flights/results", 42 | terms: "/terms", 43 | } as const; 44 | -------------------------------------------------------------------------------- /src/styles/streamdown.css: -------------------------------------------------------------------------------- 1 | /* Streamdown chat-specific styling overrides. 2 | * 3 | * Streamdown already ships shadcn/ui-aligned styles. These rules gently 4 | * tighten spacing and borders to match TripSage chat surfaces. 5 | */ 6 | 7 | .streamdown-chat [data-streamdown="code-block"], 8 | .streamdown-chat [data-streamdown="mermaid-block"] { 9 | border: 1px solid hsl(var(--border)); 10 | border-radius: calc(var(--radius) + 2px); 11 | background-color: hsl(var(--muted)); 12 | } 13 | 14 | .streamdown-chat [data-streamdown="inline-code"] { 15 | background-color: hsl(var(--muted)); 16 | border-radius: 0.25rem; 17 | padding: 0.1rem 0.25rem; 18 | } 19 | 20 | .streamdown-chat [data-streamdown="table-wrapper"] { 21 | border: 1px solid hsl(var(--border)); 22 | border-radius: var(--radius); 23 | overflow: hidden; 24 | } 25 | 26 | .streamdown-chat [data-streamdown="blockquote"] { 27 | background-color: hsl(var(--muted)); 28 | border-left: 3px solid hsl(var(--primary)); 29 | border-radius: var(--radius); 30 | padding: 0.75rem 1rem; 31 | } 32 | 33 | .streamdown-chat [data-streamdown^="heading-"] { 34 | scroll-margin-top: 6rem; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/auth/me/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Auth session introspection route. 3 | * 4 | * Returns the current authenticated user in the frontend AuthUser shape using 5 | * Supabase SSR cookies as the session source of truth. 6 | */ 7 | 8 | import "server-only"; 9 | 10 | import type { AuthUser } from "@schemas/stores"; 11 | import type { NextRequest } from "next/server"; 12 | import { NextResponse } from "next/server"; 13 | import { getOptionalUser, mapSupabaseUserToAuthUser } from "@/lib/auth/server"; 14 | 15 | interface MeResponse { 16 | user: AuthUser | null; 17 | } 18 | 19 | /** 20 | * Handles GET /auth/me. 21 | * 22 | * When authenticated, returns `{ user }` with the mapped AuthUser shape. 23 | * When unauthenticated, returns `{ user: null }` with HTTP 401. 24 | */ 25 | export async function GET(_req: NextRequest): Promise> { 26 | const { user } = await getOptionalUser(); 27 | 28 | if (!user) { 29 | return NextResponse.json({ user: null }, { status: 401 }); 30 | } 31 | 32 | const mappedUser = mapSupabaseUserToAuthUser(user); 33 | return NextResponse.json({ user: mappedUser }, { status: 200 }); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Supabase, Inc. and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/geo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Geographic distance calculation utilities. 3 | */ 4 | 5 | /** Earth's radius in kilometers. */ 6 | const EARTH_RADIUS_KM = 6371; 7 | 8 | /** Coordinates interface. */ 9 | export interface Coordinates { 10 | lat: number; 11 | lng: number; 12 | } 13 | 14 | /** 15 | * Calculates the great-circle distance between two points using the Haversine formula. 16 | * 17 | * @param from - Origin coordinates. 18 | * @param to - Destination coordinates. 19 | * @returns Distance in kilometers. 20 | */ 21 | export function calculateDistanceKm(from: Coordinates, to: Coordinates): number { 22 | const lat1Rad = (from.lat * Math.PI) / 180; 23 | const lat2Rad = (to.lat * Math.PI) / 180; 24 | const deltaLat = ((to.lat - from.lat) * Math.PI) / 180; 25 | const deltaLng = ((to.lng - from.lng) * Math.PI) / 180; 26 | 27 | const a = 28 | Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + 29 | Math.cos(lat1Rad) * 30 | Math.cos(lat2Rad) * 31 | Math.sin(deltaLng / 2) * 32 | Math.sin(deltaLng / 2); 33 | 34 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 35 | 36 | return EARTH_RADIUS_KM * c; 37 | } 38 | -------------------------------------------------------------------------------- /src/test/helpers/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Centralized store test helpers. 3 | * 4 | * Provides reusable utilities for testing Zustand stores: 5 | * - Timeout/timer mocking for deterministic async flows 6 | */ 7 | 8 | import { vi } from "vitest"; 9 | 10 | /** 11 | * Mock setTimeout to execute immediately in tests. 12 | * Returns cleanup function. 13 | * 14 | * @returns Object with mockRestore function 15 | */ 16 | export function setupTimeoutMock(): { mockRestore: () => void } { 17 | const originalSetTimeout = globalThis.setTimeout; 18 | 19 | const timeoutSpy = vi 20 | .spyOn(globalThis, "setTimeout") 21 | .mockImplementation((cb: TimerHandler, _ms?: number, ...args: unknown[]) => { 22 | if (typeof cb === "function") { 23 | cb(...(args as never[])); 24 | } 25 | const handle = originalSetTimeout(() => undefined, 0); 26 | if (typeof handle === "object" && handle && "unref" in handle) { 27 | (handle as { unref?: () => void }).unref?.(); 28 | } 29 | return handle; 30 | }); 31 | 32 | return { 33 | mockRestore: () => { 34 | timeoutSpy.mockRestore(); 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/api/__tests__/factory.body-limit.test.ts: -------------------------------------------------------------------------------- 1 | /** @vitest-environment node */ 2 | 3 | import { NextRequest } from "next/server"; 4 | import { describe, expect, it } from "vitest"; 5 | import { z } from "zod"; 6 | import { withApiGuards } from "@/lib/api/factory"; 7 | 8 | describe("withApiGuards body limits", () => { 9 | it("honors maxBodyBytes override for schema parsing", async () => { 10 | const handler = withApiGuards({ 11 | maxBodyBytes: 10, 12 | schema: z.strictObject({ ok: z.boolean() }), 13 | })(async () => new Response("ok", { status: 200 })); 14 | 15 | const stream = new ReadableStream({ 16 | start(controller) { 17 | controller.enqueue(new TextEncoder().encode(JSON.stringify({ ok: true }))); 18 | controller.close(); 19 | }, 20 | }); 21 | 22 | const req = new NextRequest("https://example.com/api/test", { 23 | body: stream, 24 | duplex: "half", 25 | headers: { "content-type": "application/json" }, 26 | method: "POST", 27 | }); 28 | 29 | const res = await handler(req, { params: Promise.resolve({}) }); 30 | expect(res.status).toBe(413); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/api/security/events/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Security events API. Returns recent auth audit events for the current user. 3 | */ 4 | 5 | import "server-only"; 6 | 7 | import { type NextRequest, NextResponse } from "next/server"; 8 | import { withApiGuards } from "@/lib/api/factory"; 9 | import { requireUserId } from "@/lib/api/route-helpers"; 10 | import { getUserSecurityEvents } from "@/lib/security/service"; 11 | import { createAdminSupabase } from "@/lib/supabase/admin"; 12 | 13 | /** 14 | * GET handler for the security events API. 15 | * 16 | * @param _req - The Next.js request object. 17 | * @param user - The authenticated user. 18 | * @returns The security events. 19 | */ 20 | export const GET = withApiGuards({ 21 | auth: true, 22 | rateLimit: "security:events", 23 | telemetry: "security.events", 24 | })(async (_req: NextRequest, { user }) => { 25 | const result = requireUserId(user); 26 | if ("error" in result) return result.error; 27 | const { userId } = result; 28 | 29 | const adminSupabase = createAdminSupabase(); 30 | const events = await getUserSecurityEvents(adminSupabase, userId); 31 | return NextResponse.json(events); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Dashboard loading skeleton. 3 | */ 4 | 5 | "use client"; 6 | 7 | import { CardSkeleton, LoadingContainer } from "@/components/ui/loading"; 8 | 9 | /** 10 | * Dashboard loading component 11 | * Shows skeleton for dashboard layout while content loads 12 | */ 13 | export default function DashboardLoading() { 14 | return ( 15 | 21 | {/* Dashboard grid skeleton */} 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/features/search/cards/rating-stars.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Rating stars component for displaying activity ratings. 3 | */ 4 | 5 | import { StarIcon } from "lucide-react"; 6 | 7 | /** 8 | * Rating stars component for displaying activity ratings. 9 | * 10 | * @param value - The rating value to display. 11 | * @param max - The maximum number of stars to display. 12 | * @returns The rating stars component. 13 | */ 14 | export function RatingStars({ value, max = 5 }: { value: number; max?: number }) { 15 | const roundedValue = Math.round(value); 16 | const stars = Array.from({ length: max }, (_, idx) => ({ 17 | filled: idx < roundedValue, 18 | key: `star-${idx + 1}`, 19 | })); 20 | 21 | return ( 22 |
27 | {stars.map((star) => ( 28 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/security/internal-key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Timing-safe comparison helpers for internal service keys. 3 | */ 4 | 5 | import "server-only"; 6 | 7 | import { timingSafeEqual } from "node:crypto"; 8 | 9 | /** 10 | * Compares a provided internal key against an expected value using 11 | * `timingSafeEqual` (defense-in-depth against subtle timing leaks). 12 | * 13 | * Note: length mismatches return false early (timingSafeEqual requires equal length). 14 | * 15 | * @param provided - Key from request header (may be null). 16 | * @param expected - Key from server env (must be non-empty to be meaningful). 17 | * @returns True when keys are equal, false otherwise. 18 | */ 19 | export function isValidInternalKey(provided: string | null, expected: string): boolean { 20 | if (!provided) return false; 21 | if (!expected) { 22 | throw new Error("Missing expected internal key: check server configuration"); 23 | } 24 | 25 | const providedBuf = Buffer.from(provided, "utf8"); 26 | const expectedBuf = Buffer.from(expected, "utf8"); 27 | if (providedBuf.length !== expectedBuf.length) return false; 28 | 29 | return timingSafeEqual(providedBuf, expectedBuf); 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/url/safe-href.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Safe href sanitizer for AI/tool-derived links. 3 | * 4 | * React does not sanitize URL protocols in href/src attributes. This helper 5 | * enforces a small allow-list for untrusted links before rendering. 6 | */ 7 | 8 | const ALLOWED_PROTOCOLS = new Set(["http:", "https:", "mailto:"]); 9 | 10 | /** 11 | * Returns a safe href string or undefined if unsafe/invalid. 12 | * 13 | * - Allows absolute http/https/mailto URLs. 14 | * - Allows same-origin relative paths that start with `/`. 15 | * - Blocks protocol-relative (`//`) and any other schemes (e.g., javascript:, data:). 16 | */ 17 | export function safeHref(raw?: string | null): string | undefined { 18 | if (!raw) return undefined; 19 | const trimmed = raw.trim(); 20 | if (!trimmed) return undefined; 21 | 22 | if (trimmed.startsWith("//")) return undefined; 23 | if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) 24 | return trimmed; 25 | 26 | try { 27 | const url = new URL(trimmed); 28 | if (ALLOWED_PROTOCOLS.has(url.protocol)) return trimmed; 29 | } catch { 30 | // invalid URL 31 | } 32 | return undefined; 33 | } 34 | -------------------------------------------------------------------------------- /release.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Semantic-release configuration for TripSage. 3 | * 4 | * Temporary rule: breaking changes are treated as minor until we ship 5 | * the next stable major. Remove the breaking->minor rule when ready to 6 | * allow true major bumps. 7 | */ 8 | export default { 9 | branches: ["main"], 10 | plugins: [ 11 | [ 12 | "@semantic-release/commit-analyzer", 13 | { 14 | preset: "conventionalcommits", 15 | releaseRules: [ 16 | { breaking: true, release: "minor" }, 17 | { type: "feat", release: "minor" }, 18 | { type: "fix", release: "patch" }, 19 | { type: "chore", release: false } 20 | ] 21 | } 22 | ], 23 | ["@semantic-release/release-notes-generator", { preset: "conventionalcommits" }], 24 | ["@semantic-release/changelog", { changelogFile: "CHANGELOG.md" }], 25 | ["@semantic-release/npm", { npmPublish: false }], 26 | [ 27 | "@semantic-release/git", 28 | { 29 | assets: ["CHANGELOG.md", "package.json"], 30 | message: "chore(release): ${nextRelease.version} [skip ci]" 31 | } 32 | ], 33 | "@semantic-release/github" 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | 5 | # OpenTelemetry Collector for monitoring 6 | otel-collector: 7 | image: otel/opentelemetry-collector-contrib:latest 8 | container_name: tripsage-otel-collector 9 | command: ["--config=/etc/otelcol/config.yaml"] 10 | volumes: 11 | - ./docker/otel-collector-config.yaml:/etc/otelcol/config.yaml:ro 12 | ports: 13 | - "4317:4317" # OTLP gRPC receiver 14 | - "4318:4318" # OTLP HTTP receiver 15 | - "8888:8888" # Collector self-metrics 16 | restart: unless-stopped 17 | networks: 18 | - tripsage-network 19 | depends_on: 20 | - jaeger 21 | 22 | # Jaeger for distributed tracing 23 | jaeger: 24 | image: jaegertracing/all-in-one:latest 25 | container_name: tripsage-jaeger 26 | environment: 27 | - COLLECTOR_OTLP_ENABLED=true 28 | ports: 29 | - "16686:16686" # Jaeger UI 30 | - "14268:14268" # Jaeger collector 31 | - "14250:14250" # Jaeger gRPC 32 | restart: unless-stopped 33 | networks: 34 | - tripsage-network 35 | 36 | networks: 37 | tripsage-network: 38 | driver: bridge 39 | name: tripsage-infrastructure 40 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Textarea component for form fields. 3 | * Provides a styled textarea field with consistent styling and accessibility features. 4 | */ 5 | import type * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | /** 10 | * Textarea component for form fields. 11 | * 12 | * @param className Optional extra classes. 13 | * @returns A textarea with styling and accessibility features. 14 | */ 15 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 16 | return ( 17 |