diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 00000000..81e9e855 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,254 @@ +# Pascal Editor - Setup Guide + +This guide will help you set up the Pascal Editor with authentication and database integration. + +## Prerequisites + +- Node.js 18+ or Bun 1.3+ +- Docker Desktop (for running Supabase locally) + +## Quick Start + +### 1. Install Dependencies + +```bash +bun install +``` + +This installs the Supabase CLI as a dev dependency - no need for global installation! + +### 2. Start Supabase Local Development + +```bash +bun db:start +``` + +This will start a local Supabase instance. You'll see output like: + +``` +API URL: http://127.0.0.1:54321 +DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres +Studio URL: http://127.0.0.1:54323 +Anon key: eyJh... +Service role key: eyJh... +``` + +### 4. Configure Environment Variables + +Create `apps/editor/.env.local` with the following variables: + +```bash +# Database Connection (Supabase local) +POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres + +# Supabase Configuration +NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU + +# Better Auth (generate your own secret with: openssl rand -base64 32) +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 + +# Google Maps (optional, for address search) +NEXT_PUBLIC_GOOGLE_MAPS_API_KEY= +``` + +Generate a secret for `BETTER_AUTH_SECRET`: + +```bash +openssl rand -base64 32 +``` + +### 5. Run Database Migrations + +```bash +bun db:reset +``` + +This will create all necessary tables for authentication and properties. + +### 6. Start the Development Server + +```bash +bun dev +``` + +The editor will be available at http://localhost:3000 + +## Monorepo Structure + +``` +. +├── apps/ +│ └── editor/ # Next.js editor application +│ ├── app/ +│ │ └── api/auth/ # Better Auth API routes +│ ├── components/ # UI components +│ └── features/ +│ └── cloud-sync/ # Cloud sync feature +├── packages/ +│ ├── auth/ # @pascal-app/auth - Authentication package +│ │ ├── src/ +│ │ │ ├── server.ts # Better Auth server config +│ │ │ └── client.ts # Better Auth client +│ │ └── README.md +│ ├── db/ # @pascal-app/db - Database package +│ │ ├── src/ +│ │ │ ├── client.ts # Supabase client (with RLS) +│ │ │ ├── server.ts # Supabase admin client +│ │ │ └── types.ts # Database types +│ │ ├── supabase/ +│ │ │ ├── config.toml +│ │ │ └── migrations/ # SQL migrations +│ │ └── README.md +│ ├── core/ # @pascal-app/core - Core editor logic +│ ├── viewer/ # @pascal-app/viewer - 3D viewer +│ └── ui/ # @repo/ui - Shared UI components +└── turbo.json +``` + +## Database Schema + +### Auth Tables (Better Auth) + +- **users** - User accounts with email and profile +- **sessions** - Active authentication sessions +- **accounts** - OAuth provider accounts (for future use) +- **verification_tokens** - Magic link tokens + +### Application Tables + +- **properties** - User properties + - `id`: Property ID + - `name`: Property name + - `owner_id`: User ID (foreign key to users) + +- **properties_addresses** - Property addresses with Google Maps data + - `id`: Address ID + - `property_id`: Property ID (foreign key) + - `formatted_address`: Full address + - `latitude`, `longitude`: GPS coordinates + - Plus detailed address components (street, city, state, etc.) + +- **properties_models** - Scene graph models (versions) + - `id`: Model ID + - `property_id`: Property ID (foreign key) + - `name`: Model name + - `version`: Version number + - `draft`: Draft status + - `scene_graph`: JSONB scene graph data + +## Features + +### Authentication + +- **Magic Link Sign-In**: Passwordless authentication via email +- **Session Management**: 7-day sessions with automatic refresh +- **Cookie-based**: Secure httpOnly cookies + +### Property Management + +- **Create Properties**: Add properties with real-world addresses +- **Google Maps Integration**: Address autocomplete and geocoding +- **Switch Properties**: Seamlessly switch between properties + +### Scene Management + +- **Auto-Save**: Changes saved every 2 seconds +- **Scene Loading**: Automatic scene loading when switching properties +- **Version Control**: Models are versioned for future rollback support + +## Development Workflow + +### Making Database Changes + +1. Create a new migration: + ```bash + cd packages/db + supabase migration new your_migration_name + ``` + +2. Edit the migration file in `supabase/migrations/` + +3. Apply the migration: + ```bash + supabase db reset + ``` + +### Updating Database Types + +After changing the database schema, regenerate TypeScript types: + +1. Update `packages/db/src/types.ts` to match your new schema +2. Run `bun install` to update type checking + +### Testing Authentication + +1. Start the editor: `bun dev` +2. Click "Save to cloud" button +3. Enter your email +4. Check console for magic link (not sent via email in development) +5. Click the link to authenticate + +## Supabase Studio + +Access the local Supabase Studio at: http://127.0.0.1:54323 + +Use this to: +- Browse and edit tables +- Run SQL queries +- View logs +- Manage RLS policies +- Test database functions + +## Production Deployment + +For production deployment: + +1. Create a Supabase project at https://supabase.com +2. Get your production database connection string +3. Update environment variables in your hosting platform +4. Link and push migrations: + ```bash + cd packages/db + bunx supabase link --project-ref your-project-ref + bunx supabase db push + ``` +5. Configure email provider in `packages/auth/src/server.ts` + +## Troubleshooting + +### "Missing POSTGRES_URL" error + +Make sure you've set `POSTGRES_URL` in `apps/editor/.env.local` to your Supabase connection string. + +### Supabase not starting + +Try stopping and restarting: +```bash +bun db:stop +bun db:start +``` + +### Migration errors + +Reset the database: +```bash +cd packages/db +supabase db reset +``` + +### Auth not working + +1. Check that Better Auth API route exists at `apps/editor/app/api/auth/[...all]/route.ts` +2. Verify `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are set +3. Check console for magic link URLs in development + +## Next Steps + +- Configure email provider for magic links +- Add OAuth providers (Google, GitHub, etc.) +- Set up production Supabase project +- Configure RLS policies for additional security +- Add more property features (sharing, collaboration, etc.) diff --git a/apps/editor/app/api/auth/[...all]/route.ts b/apps/editor/app/api/auth/[...all]/route.ts new file mode 100644 index 00000000..a2cd0932 --- /dev/null +++ b/apps/editor/app/api/auth/[...all]/route.ts @@ -0,0 +1,11 @@ +/** + * Better Auth API route handler + * Handles all /api/auth/* routes for authentication + */ + +import { toNextJsHandler } from 'better-auth/next-js' +import { auth } from '@/lib/auth' + +const { GET, POST } = toNextJsHandler(auth) + +export { GET, POST } diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index ce675101..07bc825c 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -4,9 +4,11 @@ import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-a import { Viewer } from '@pascal-app/viewer' import { useKeyboard } from '@/hooks/use-keyboard' import useEditor from '@/store/use-editor' +import { usePropertyScene } from '@/features/community/lib/models/hooks' import { ZoneSystem } from '../systems/zone/zone-system' import { ToolManager } from '../tools/tool-manager' import { ActionMenu } from '../ui/action-menu' +import { CloudSaveButton } from '@/features/community/components/cloud-save-button' import { PanelManager } from '../ui/panels/panel-manager' import { SidebarProvider } from '../ui/primitives/sidebar' import { AppSidebar } from '../ui/sidebar/app-sidebar' @@ -15,8 +17,8 @@ import { ExportManager } from './export-manager' import { Grid } from './grid' import { SelectionManager } from './selection-manager' +// Load default scene initially (will be replaced when property loads) useScene.getState().loadScene() -console.log('Loaded scene in editor') initSpatialGridSync() initSpaceDetectionSync(useScene, useEditor) @@ -27,6 +29,7 @@ export default function Editor() {
+ diff --git a/apps/editor/features/community/README.md b/apps/editor/features/community/README.md new file mode 100644 index 00000000..4de77f66 --- /dev/null +++ b/apps/editor/features/community/README.md @@ -0,0 +1,198 @@ +# Community Feature + +This directory contains the **optional** community features (cloud synchronization and authentication) for the Pascal Editor. This feature is specific to the Pascal platform and can be safely removed if you're using the editor standalone. + +## What This Does + +The community feature provides: + +- **Authentication** - Sign in with magic link via Better Auth +- **Property Management** - Create and manage properties with Google Maps address search +- **Scene Loading** - Automatically load property scenes from the database when a property is selected +- **Auto-Save** - Automatically save scene changes to the database (2-second debounce) +- **Database Sync** - Save and load editor state from a PostgreSQL database via Supabase + +## Architecture + +``` +features/community/ +├── lib/ +│ ├── auth/ +│ │ ├── client.ts # Re-exports from @pascal-app/auth +│ │ ├── server.ts # Server-side session handling +│ │ └── hooks.ts # useAuth React hook +│ ├── properties/ +│ │ ├── actions.ts # Server actions for CRUD operations +│ │ ├── types.ts # TypeScript types for properties +│ │ ├── hooks.ts # Property React hooks +│ │ └── store.ts # Zustand store for property state +│ ├── models/ +│ │ ├── actions.ts # Scene model CRUD operations +│ │ └── hooks.ts # Scene loading and auto-save hooks +│ ├── database/ +│ │ └── server.ts # Re-exports from @pascal-app/db +│ └── utils/ +│ └── id-generator.ts # nanoid-based ID generation +├── components/ +│ ├── cloud-save-button.tsx # Main UI entry point (top-right button) +│ ├── sign-in-dialog.tsx # Magic link sign-in dialog +│ ├── profile-dropdown.tsx # User profile menu +│ ├── property-dropdown.tsx # Property selector dropdown +│ ├── new-property-dialog.tsx # Create new property dialog +│ └── google-address-search.tsx # Google Maps autocomplete +└── README.md # This file +``` + +## How It Works + +### Authentication Flow +1. User clicks "Save to cloud" button +2. Signs in with magic link (email-based, no password) +3. Better Auth session is stored in cookies +4. Server actions validate session using Better Auth API + +### Property Management +1. User creates a property with a real-world address (Google Maps) +2. Address and property are saved to PostgreSQL via Supabase +3. Properties are associated with the authenticated user +4. User can switch between properties + +### Scene Management +1. When a property is selected, its scene is loaded from `properties_models` table +2. If no scene exists, loads default empty scene +3. Scene changes are auto-saved every 2 seconds (debounced) +4. Updates existing model (highest version) instead of creating new ones +5. Scene graph includes all nodes and hierarchy + +### Database Integration +- Uses Supabase (PostgreSQL) for database access +- Better Auth manages authentication tables directly +- Server actions use service role key to bypass RLS +- Permissions enforced by filtering on `owner_id` +- Tables: `users`, `sessions`, `properties`, `properties_addresses`, `properties_models` + +## Required Environment Variables + +```bash +# Database Connection +DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres + +# Supabase Configuration +NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key_here +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here + +# Better Auth +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 + +# Google Maps API Key (for address search) +NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_google_maps_key_here +``` + +Generate `BETTER_AUTH_SECRET`: +```bash +openssl rand -base64 32 +``` + +## Dependencies + +The community feature requires these packages: + +```json +{ + "better-auth": "^1.4.18", + "@supabase/supabase-js": "^2.95.3", + "@react-google-maps/api": "^2.20.8", + "nanoid": "^5.1.6" +} +``` + +## How to Remove (For Open Source Users) + +If you want to use the editor without community features: + +### 1. Delete this directory +```bash +rm -rf features/community +``` + +### 2. Remove the CloudSaveButton from the editor +Edit `components/editor/index.tsx`: +```diff +- import { CloudSaveButton } from '@/features/community/components/cloud-save-button' + + export default function Editor() { + return ( +
+ + +- +``` + +### 3. Remove dependencies (optional) +Edit `package.json`: +```diff +- "better-auth": "^1.4.18", +- "@supabase/supabase-js": "^2.95.3", +- "@react-google-maps/api": "^2.20.8", +- "nanoid": "^5.1.6" +``` + +### 4. Remove environment variables +Delete from `.env.local` and `.env.example`: +```diff +- NEXT_PUBLIC_API_URL=... +- NEXT_PUBLIC_SUPABASE_URL=... +- NEXT_PUBLIC_SUPABASE_ANON_KEY=... +- SUPABASE_SERVICE_ROLE_KEY=... +- NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=... +``` + +That's it! The editor will work as a standalone application without any cloud features. + +## Backend Requirements + +This feature requires: +- Supabase local development instance +- PostgreSQL database with the following tables: + - `users` - User accounts (Better Auth) + - `sessions` - Authentication sessions (Better Auth) + - `verification_tokens` - Magic link tokens (Better Auth) + - `properties` - Property records + - `properties_addresses` - Property addresses + - `properties_models` - Scene graph models +- Database migrations are managed in `packages/db/supabase/migrations/` + +## Development + +To work on this feature: + +1. Install dependencies: + ```bash + bun install + ``` +2. Start Supabase local development: + ```bash + bun db:start + ``` +3. Run database migrations: + ```bash + bun db:reset + ``` +4. Configure all environment variables in `apps/editor/.env.local` +5. Run the editor: `bun dev` + +The editor will be available at `http://localhost:3000`. + +For detailed setup instructions, see [SETUP.md](../../../SETUP.md) in the root directory. + +## Notes + +- This feature uses **server actions** (Next.js App Router) for all database operations +- Authentication is handled by **Better Auth** with magic link support +- Better Auth server is configured in `packages/auth` and mounted at `/api/auth/*` +- The editor queries the database directly using Supabase with service role key +- IDs are generated using nanoid with custom alphabet +- Scene state is managed with a **Zustand store** for reliable property switching +- Scene changes are auto-saved with 2-second debouncing to the currently selected property diff --git a/apps/editor/features/community/components/cloud-save-button.tsx b/apps/editor/features/community/components/cloud-save-button.tsx new file mode 100644 index 00000000..b61f17fb --- /dev/null +++ b/apps/editor/features/community/components/cloud-save-button.tsx @@ -0,0 +1,64 @@ +'use client' + +import { Cloud } from 'lucide-react' +import { useEffect, useState } from 'react' +import { useAuth } from '../lib/auth/hooks' +import { usePropertyStore } from '../lib/properties/store' +import { ProfileDropdown } from './profile-dropdown' +import { PropertyDropdown } from './property-dropdown' +import { SignInDialog } from './sign-in-dialog' + +/** + * CloudSaveButton - Shows authentication state and property management + * + * Not authenticated: Shows "Save to cloud" button + * Authenticated: Shows PropertyDropdown and ProfileDropdown + */ +export function CloudSaveButton() { + const { isAuthenticated, isLoading } = useAuth() + const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false) + const initialize = usePropertyStore(state => state.initialize) + + // Initialize property store when authenticated + useEffect(() => { + if (isAuthenticated) { + initialize() + } + }, [isAuthenticated, initialize]) + + if (isLoading) { + return ( +
+
+
+
+
+ ) + } + + if (!isAuthenticated) { + return ( + <> +
+ +
+ + + ) + } + + return ( +
+
+ + +
+
+ ) +} diff --git a/apps/editor/features/community/components/google-address-search.tsx b/apps/editor/features/community/components/google-address-search.tsx new file mode 100644 index 00000000..1f08107c --- /dev/null +++ b/apps/editor/features/community/components/google-address-search.tsx @@ -0,0 +1,113 @@ +'use client' + +import { Autocomplete, LoadScript } from '@react-google-maps/api' +import { MapPin } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' + +const libraries: ('places')[] = ['places'] + +interface AddressComponents { + streetNumber?: string + route?: string + city?: string + state?: string + postalCode?: string + country?: string + center: [number, number] + formattedAddress: string +} + +interface GoogleAddressSearchProps { + onAddressSelect: (address: AddressComponents) => void + disabled?: boolean +} + +export function GoogleAddressSearch({ onAddressSelect, disabled }: GoogleAddressSearchProps) { + const [autocomplete, setAutocomplete] = useState(null) + const inputRef = useRef(null) + + const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY + + // Fix Google Maps autocomplete dropdown z-index and pointer events to work with dialog + useEffect(() => { + const style = document.createElement('style') + style.textContent = ` + .pac-container { + z-index: 9999 !important; + pointer-events: auto !important; + } + ` + document.head.appendChild(style) + return () => { + document.head.removeChild(style) + } + }, []) + + if (!apiKey) { + return ( +
+

Google Maps API Key Missing

+

+ Add NEXT_PUBLIC_GOOGLE_MAPS_API_KEY to your .env.local file +

+
+ ) + } + + const onLoad = (autocompleteInstance: google.maps.places.Autocomplete) => { + setAutocomplete(autocompleteInstance) + } + + const onPlaceChanged = () => { + if (!autocomplete) return + + const place = autocomplete.getPlace() + if (!place.geometry?.location || !place.address_components) return + + const components: AddressComponents = { + center: [place.geometry.location.lng(), place.geometry.location.lat()], + formattedAddress: place.formatted_address || '', + } + + // Parse address components + for (const component of place.address_components) { + const types = component.types + + if (types.includes('street_number')) { + components.streetNumber = component.long_name + } else if (types.includes('route')) { + components.route = component.long_name + } else if (types.includes('locality')) { + components.city = component.long_name + } else if (types.includes('administrative_area_level_1')) { + components.state = component.short_name + } else if (types.includes('postal_code')) { + components.postalCode = component.long_name + } else if (types.includes('country')) { + components.country = component.short_name + } + } + + onAddressSelect(components) + } + + return ( + +
+ + + + +
+
+ ) +} diff --git a/apps/editor/features/community/components/new-property-dialog.tsx b/apps/editor/features/community/components/new-property-dialog.tsx new file mode 100644 index 00000000..2e73e123 --- /dev/null +++ b/apps/editor/features/community/components/new-property-dialog.tsx @@ -0,0 +1,142 @@ +'use client' + +import { X } from 'lucide-react' +import { useState } from 'react' +import { createProperty } from '../lib/properties/actions' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog' +import { GoogleAddressSearch } from './google-address-search' + +interface NewPropertyDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSuccess?: (propertyId: string) => void +} + +interface AddressData { + streetNumber?: string + route?: string + city?: string + state?: string + postalCode?: string + country?: string + center: [number, number] + formattedAddress: string +} + +/** + * NewPropertyDialog - Dialog for creating a new property with Google Maps address search + */ +export function NewPropertyDialog({ open, onOpenChange, onSuccess }: NewPropertyDialogProps) { + const [address, setAddress] = useState(null) + const [isCreating, setIsCreating] = useState(false) + const [error, setError] = useState(null) + + const handleAddressSelect = (addressData: AddressData) => { + setAddress(addressData) + setError(null) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + + if (!address) { + setError('Please select an address') + return + } + + setIsCreating(true) + + try { + // Use formatted address as property name (like monorepo) + const result = await createProperty({ + name: address.formattedAddress, + center: address.center, + streetNumber: address.streetNumber, + route: address.route, + city: address.city, + state: address.state, + postalCode: address.postalCode, + country: address.country || 'US', + }) + + if (result.success && result.data) { + onOpenChange(false) + setAddress(null) + onSuccess?.(result.data.id) + } else { + setError(result.error || 'Failed to create property') + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + } finally { + setIsCreating(false) + } + } + + const handleClose = () => { + if (!isCreating) { + onOpenChange(false) + setAddress(null) + setError(null) + } + } + + return ( + + e.preventDefault()} + > + + Add New Property + + + +
+ {/* Google Maps Address Search */} + + + {/* Show selected address */} + {address && ( +
+

Selected Address:

+

{address.formattedAddress}

+
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ + +
+ +
+
+ ) +} diff --git a/apps/editor/features/community/components/profile-dropdown.tsx b/apps/editor/features/community/components/profile-dropdown.tsx new file mode 100644 index 00000000..6071b2e2 --- /dev/null +++ b/apps/editor/features/community/components/profile-dropdown.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useAuth } from '../lib/auth/hooks' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/primitives/dropdown-menu' + +function getInitials(name: string): string { + return name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) +} + +/** + * ProfileDropdown - User profile menu with sign out + */ +export function ProfileDropdown() { + const { user, signOut } = useAuth() + + const handleSignOut = async () => { + await signOut() + // TODO: Show sign-in dialog or redirect + console.log('Signed out') + } + + const initials = user?.name ? getInitials(user.name) : user?.email?.[0]?.toUpperCase() || 'U' + + return ( + + + + + + {user?.name && ( +
+
{user.name}
+ {user.email &&
{user.email}
} +
+ )} + {user?.name && } + + Sign out + +
+
+ ) +} diff --git a/apps/editor/features/community/components/property-dropdown.tsx b/apps/editor/features/community/components/property-dropdown.tsx new file mode 100644 index 00000000..095cdee8 --- /dev/null +++ b/apps/editor/features/community/components/property-dropdown.tsx @@ -0,0 +1,109 @@ +'use client' + +import { Check, ChevronDown, Home, Plus } from 'lucide-react' +import { useState } from 'react' +import { usePropertyStore } from '../lib/properties/store' +import { cn } from '@/lib/utils' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/primitives/dropdown-menu' +import { NewPropertyDialog } from './new-property-dialog' +import { usePropertyScene } from '../lib/models/hooks' + +/** + * PropertyDropdown - Shows active property and allows switching between properties + */ +export function PropertyDropdown() { + usePropertyScene() // Load and auto-save property scenes + + // Use property store + const properties = usePropertyStore(state => state.properties) + const activeProperty = usePropertyStore(state => state.activeProperty) + const isLoading = usePropertyStore(state => state.isLoading) + const setActiveProperty = usePropertyStore(state => state.setActiveProperty) + const fetchProperties = usePropertyStore(state => state.fetchProperties) + + const [isNewPropertyDialogOpen, setIsNewPropertyDialogOpen] = useState(false) + + const handlePropertySelect = async (propertyId: string) => { + await setActiveProperty(propertyId) + } + + const handleAddNew = () => { + setIsNewPropertyDialogOpen(true) + } + + const handlePropertyCreated = async (propertyId: string) => { + // Set the newly created property as active (this will also fetch properties) + await setActiveProperty(propertyId) + } + + return ( + <> + + + + + + {/* Property list */} + {properties.length > 0 ? ( +
+ {properties.map((property) => ( + + activeProperty?.id === property.id ? null : handlePropertySelect(property.id) + } + > +
+
{property.name}
+ {activeProperty?.id === property.id && ( + + )} +
+
+ ))} +
+ ) : ( +
+ No properties yet +
+ )} + + {/* Add new property option */} + + + Add new property + +
+
+ + + + ) +} diff --git a/apps/editor/features/community/components/sign-in-dialog.tsx b/apps/editor/features/community/components/sign-in-dialog.tsx new file mode 100644 index 00000000..efb6bc05 --- /dev/null +++ b/apps/editor/features/community/components/sign-in-dialog.tsx @@ -0,0 +1,148 @@ +'use client' + +import { Mail, X } from 'lucide-react' +import { useState } from 'react' +import { authClient } from '../lib/auth/client' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog' + +interface SignInDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +/** + * SignInDialog - Magic link authentication dialog + */ +export function SignInDialog({ open, onOpenChange }: SignInDialogProps) { + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setIsLoading(true) + + try { + // Use better-auth's magic link sign in + const result = await authClient.signIn.magicLink({ + email, + callbackURL: window.location.origin, + }) + + if (result.error) { + setError(result.error.message || 'Failed to send magic link') + } else { + setSuccess(true) + setEmail('') + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + } finally { + setIsLoading(false) + } + } + + const handleClose = () => { + if (!isLoading) { + onOpenChange(false) + // Reset state after a short delay to avoid flash + setTimeout(() => { + setEmail('') + setError(null) + setSuccess(false) + }, 200) + } + } + + return ( + + + + Sign in to Pascal + + + + {success ? ( +
+
+
+ +
+
+

Check your email

+

+ We've sent a magic link to {email} +

+

+ Click the link in the email to sign in to your account. +

+
+
+ +
+ ) : ( +
+
+ + setEmail(e.target.value)} + /> +
+ + {error && ( +
+ {error} +
+ )} + + + +

+ We'll send you a magic link to sign in without a password. +

+
+ )} +
+
+ ) +} diff --git a/apps/editor/features/community/lib/auth/client.ts b/apps/editor/features/community/lib/auth/client.ts new file mode 100644 index 00000000..47907379 --- /dev/null +++ b/apps/editor/features/community/lib/auth/client.ts @@ -0,0 +1,7 @@ +/** + * Auth client for the editor using better-auth + * Re-exports from @pascal-app/auth package + */ + +export { authClient } from '@pascal-app/auth/client' +export type { AuthState, User, Session } from '@pascal-app/auth/client' diff --git a/apps/editor/features/community/lib/auth/hooks.ts b/apps/editor/features/community/lib/auth/hooks.ts new file mode 100644 index 00000000..c17e6375 --- /dev/null +++ b/apps/editor/features/community/lib/auth/hooks.ts @@ -0,0 +1,20 @@ +'use client' + +import { authClient } from './client' + +/** + * Hook to access authentication state using better-auth + * @returns Current auth state including user, session, and loading status + */ +export function useAuth() { + const session = authClient.useSession() + + return { + user: session.data?.user ?? null, + session: session.data?.session ?? null, + isAuthenticated: !!session.data?.user && !!session.data?.session, + isLoading: session.isPending, + signOut: () => authClient.signOut(), + signIn: authClient.signIn, + } +} diff --git a/apps/editor/features/community/lib/auth/server.ts b/apps/editor/features/community/lib/auth/server.ts new file mode 100644 index 00000000..301b4835 --- /dev/null +++ b/apps/editor/features/community/lib/auth/server.ts @@ -0,0 +1,48 @@ +import { headers as nextHeaders } from 'next/headers' + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000' + +/** + * Get the current session from Better Auth backend (server-side) + */ +export async function getSession() { + try { + const headersList = await nextHeaders() + + // Make authenticated request to the auth backend to get session + const response = await fetch(`${API_URL}/api/auth/get-session`, { + headers: { + cookie: headersList.get('cookie') || '', + }, + credentials: 'include', + cache: 'no-store', + }) + + if (!response.ok) { + return null + } + + const data = await response.json() + + // Better Auth returns the session data directly + if (data?.user && data?.session) { + return { + user: data.user, + session: data.session, + } + } + + return null + } catch (error) { + console.error('Failed to get session:', error) + return null + } +} + +/** + * Get the current user from the session + */ +export async function getUser() { + const session = await getSession() + return session?.user ?? null +} diff --git a/apps/editor/features/community/lib/database/server.ts b/apps/editor/features/community/lib/database/server.ts new file mode 100644 index 00000000..6d853650 --- /dev/null +++ b/apps/editor/features/community/lib/database/server.ts @@ -0,0 +1,14 @@ +/** + * Supabase server client for database access + */ + +import { supabaseAdmin } from '@/lib/supabase/server' + +/** + * Create a Supabase client for server-side use with service role key + * This bypasses RLS and allows server actions to query the database directly + * Authentication is handled by Better Auth, permissions enforced by filtering on user_id + */ +export async function createServerSupabaseClient() { + return supabaseAdmin +} diff --git a/apps/editor/features/community/lib/models/actions.ts b/apps/editor/features/community/lib/models/actions.ts new file mode 100644 index 00000000..1d0b5bcc --- /dev/null +++ b/apps/editor/features/community/lib/models/actions.ts @@ -0,0 +1,242 @@ +/** + * Property model actions - Server actions for scene loading/saving + * Manages 3D models (scene graphs) stored in properties_models table + */ + +'use server' + +import type { AnyNode, AnyNodeId } from '@pascal-app/core' +import { createServerSupabaseClient } from '../database/server' +import { getSession } from '../auth/server' +import { createId } from '../utils/id-generator' +import type { ActionResult } from '../properties/actions' + +export interface SceneGraph { + nodes: Record + rootNodeIds: AnyNodeId[] +} + +export interface PropertyModel { + id: string + name: string + version: number + draft: boolean + property_id: string + scene_graph: SceneGraph | null + created_at: string + updated_at: string +} + +/** + * Get the latest model for a property (highest version) + */ +export async function getPropertyModel(propertyId: string): Promise> { + try { + const session = await getSession() + + if (!session?.user) { + return { + success: false, + error: 'Not authenticated', + data: null, + } + } + + const supabase = await createServerSupabaseClient() + + // Get the property to verify ownership + const { data: property, error: propertyError } = await supabase + .from('properties') + .select('id, owner_id') + .eq('id', propertyId) + .single<{ id: string; owner_id: string }>() + + if (propertyError || !property) { + return { + success: false, + error: 'Property not found', + data: null, + } + } + + // Verify ownership + if (property.owner_id !== session.user.id) { + return { + success: false, + error: 'Unauthorized', + data: null, + } + } + + // Get the latest model (highest version, then most recent) + const { data: model, error: modelError } = await supabase + .from('properties_models') + .select('*') + .eq('property_id', propertyId) + .is('deleted_at', null) + .order('version', { ascending: false }) + .order('created_at', { ascending: false }) + .limit(1) + .single() + + console.log('[getPropertyModel] Query result:', { + propertyId, + hasModel: !!model, + modelError: modelError?.message, + errorCode: modelError?.code, + modelKeys: model ? Object.keys(model) : [], + }) + + if (modelError) { + // No model found is not an error - just return null + if (modelError.code === 'PGRST116') { + console.log('[getPropertyModel] No model found (PGRST116), returning null') + return { + success: true, + data: null, + } + } + + console.log('[getPropertyModel] Database error:', modelError) + return { + success: false, + error: modelError.message, + data: null, + } + } + + console.log('[getPropertyModel] Model found:', { + id: model.id, + version: model.version, + hasSceneGraph: !!model.scene_graph, + }) + + return { + success: true, + data: model as PropertyModel, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch property model', + data: null, + } + } +} + +/** + * Save or update a property model's scene graph + * If a model exists, updates it. Otherwise creates a new one. + */ +export async function savePropertyModel( + propertyId: string, + sceneGraph: SceneGraph, +): Promise> { + try { + const session = await getSession() + + if (!session?.user) { + return { + success: false, + error: 'Not authenticated', + } + } + + const supabase = await createServerSupabaseClient() + + // Get the property to verify ownership + const { data: property, error: propertyError } = await supabase + .from('properties') + .select('id, owner_id, name') + .eq('id', propertyId) + .single<{ id: string; owner_id: string; name: string }>() + + if (propertyError || !property) { + return { + success: false, + error: 'Property not found', + } + } + + // Verify ownership + if (property.owner_id !== session.user.id) { + return { + success: false, + error: 'Unauthorized', + } + } + + // Check if a model already exists + const { data: existingModel } = await supabase + .from('properties_models') + .select('id, version') + .eq('property_id', propertyId) + .is('deleted_at', null) + .order('version', { ascending: false }) + .order('created_at', { ascending: false }) + .limit(1) + .single<{ id: string; version: number }>() + + if (existingModel) { + // Update existing model + const updateData = { + scene_graph: sceneGraph, + updated_at: new Date().toISOString(), + } + const { data: updatedModel, error: updateError } = (await (supabase + .from('properties_models') as any) + .update(updateData) + .eq('id', existingModel.id) + .select() + .single()) as { data: PropertyModel | null; error: any } + + if (updateError) { + return { + success: false, + error: updateError.message, + } + } + + return { + success: true, + data: updatedModel as PropertyModel, + message: 'Model updated successfully', + } + } else { + // Create new model + const modelId = createId('model') + + const insertData = { + id: modelId, + property_id: propertyId, + name: `${property.name} - Editor`, + version: 1, + draft: true, + scene_graph: sceneGraph, + } + const { data: newModel, error: createError } = (await (supabase + .from('properties_models') as any) + .insert(insertData) + .select() + .single()) as { data: PropertyModel | null; error: any } + + if (createError) { + return { + success: false, + error: createError.message, + } + } + + return { + success: true, + data: newModel as PropertyModel, + message: 'Model created successfully', + } + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to save property model', + } + } +} diff --git a/apps/editor/features/community/lib/models/hooks.ts b/apps/editor/features/community/lib/models/hooks.ts new file mode 100644 index 00000000..3109aeeb --- /dev/null +++ b/apps/editor/features/community/lib/models/hooks.ts @@ -0,0 +1,142 @@ +/** + * Hooks for property model (scene) loading and auto-saving + */ + +'use client' + +import { useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useEffect, useRef } from 'react' +import useEditor from '@/store/use-editor' +import { usePropertyStore } from '../properties/store' +import { getPropertyModel, savePropertyModel } from './actions' + +/** + * Load the scene when a property becomes active + * Saves changes automatically with debouncing + */ +export function usePropertyScene() { + // Subscribe to property store + const activeProperty = usePropertyStore((state) => state.activeProperty) + const isLoadingProperty = usePropertyStore((state) => state.isLoading) + + const lastPropertyIdRef = useRef(null) + const saveTimeoutRef = useRef(undefined) + const isSavingRef = useRef(false) + const currentPropertyIdRef = useRef(null) + + // Extract property ID for dependency tracking + const propertyId = activeProperty?.id ?? null + const propertyName = activeProperty?.name ?? null + + // Load scene when active property changes + useEffect(() => { + if (isLoadingProperty) { + return + } + + if (!propertyId) { + return + } + + // Skip if same property + if (lastPropertyIdRef.current === propertyId) { + return + } + + lastPropertyIdRef.current = propertyId + + // Load the property's scene + async function loadScene() { + try { + const result = await getPropertyModel(propertyId || '') + + if (result.success && result.data?.scene_graph) { + // Load the scene graph into the store + const { nodes, rootNodeIds } = result.data.scene_graph + useScene.getState().setScene(nodes, rootNodeIds) + } else { + // No scene found - clear the scene + useScene.getState().clearScene() + } + } catch (error) { + // Fall back to clear scene + useScene.getState().clearScene() + } + + // Reset editor state after loading/clearing scene + useEditor.getState().setPhase('site') + useViewer.getState().setSelection({ + buildingId: null, + levelId: null, + selectedIds: [], + zoneId: null, + }) + } + + loadScene() + }, [propertyId, isLoadingProperty]) + + // Auto-save scene changes with debouncing + useEffect(() => { + if (!propertyId) { + currentPropertyIdRef.current = null + return + } + + currentPropertyIdRef.current = propertyId + + // Subscribe to any scene changes + // Use JSON stringification to detect any node changes, not just count + let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes) + + const unsubscribe = useScene.subscribe((state) => { + const currentNodesSnapshot = JSON.stringify(state.nodes) + + // Only trigger save if nodes actually changed + if (currentNodesSnapshot === lastNodesSnapshot) { + return + } + + lastNodesSnapshot = currentNodesSnapshot + const nodes = state.nodes + + // Skip if currently saving + if (isSavingRef.current) { + return + } + + // Clear existing timeout + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current) + } + + // Debounce save by 2 seconds + saveTimeoutRef.current = setTimeout(async () => { + // Get the current property ID at save time (not the captured value) + const currentPropertyId = currentPropertyIdRef.current + if (!currentPropertyId) { + return + } + + const rootNodeIds = useScene.getState().rootNodeIds + const sceneGraph = { nodes, rootNodeIds } + + isSavingRef.current = true + + try { + await savePropertyModel(currentPropertyId, sceneGraph) + } finally { + isSavingRef.current = false + } + }, 2000) + }) + + return () => { + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current) + } + unsubscribe() + } + }, [propertyId]) +} diff --git a/apps/editor/features/community/lib/properties/actions.ts b/apps/editor/features/community/lib/properties/actions.ts new file mode 100644 index 00000000..81f3c5f0 --- /dev/null +++ b/apps/editor/features/community/lib/properties/actions.ts @@ -0,0 +1,328 @@ +/** + * Property actions - Server actions for property management + * Uses Better Auth session + Supabase to query the same database as the monorepo + */ + +'use server' + +import { createServerSupabaseClient } from '../database/server' +import { getSession } from '../auth/server' +import { createId } from '../utils/id-generator' +import type { CreatePropertyParams, Property } from './types' + +export type ActionResult = { + success: boolean + data?: T + error?: string + message?: string +} + +/** + * Fetch all properties for the current user + */ +export async function getUserProperties(): Promise> { + try { + const session = await getSession() + + if (!session?.user) { + return { + success: false, + error: 'Not authenticated', + data: [], + } + } + + const supabase = await createServerSupabaseClient() + + // Query properties table with address relation + const { data, error } = await supabase + .from('properties') + .select(` + *, + address:properties_addresses(*) + `) + .eq('owner_id', session.user.id) + + if (error) { + return { + success: false, + error: error.message, + data: [], + } + } + + return { + success: true, + data: data as Property[], + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch properties', + data: [], + } + } +} + +/** + * Get the active property for the current session + */ +export async function getActiveProperty(): Promise> { + try { + const session = await getSession() + + if (!session?.user) { + return { + success: false, + error: 'Not authenticated', + data: null, + } + } + + const supabase = await createServerSupabaseClient() + + // Get session's active_property_id from sessions table + const { data: sessionData, error: sessionError } = await supabase + .from('auth_sessions') + .select('active_property_id') + .eq('user_id', session.user.id) + .single<{ active_property_id: string | null }>() + + if (sessionError || !sessionData?.active_property_id) { + return { + success: true, + data: null, + } + } + + // Get the property with address + const { data, error } = await supabase + .from('properties') + .select(` + *, + address:properties_addresses(*) + `) + .eq('id', sessionData.active_property_id) + .single() + + if (error) { + return { + success: false, + error: error.message, + data: null, + } + } + + return { + success: true, + data: data as Property, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch active property', + data: null, + } + } +} + +/** + * Set the active property for the current session + */ +export async function setActiveProperty(propertyId: string | null): Promise { + try { + const session = await getSession() + + if (!session?.user) { + return { + success: false, + error: 'Not authenticated', + } + } + + const supabase = await createServerSupabaseClient() + + // Update session's active_property_id + const { error } = await (supabase + .from('auth_sessions') as any) + .update({ active_property_id: propertyId }) + .eq('user_id', session.user.id) + + if (error) { + return { + success: false, + error: error.message, + } + } + + return { + success: true, + message: propertyId ? 'Active property updated' : 'Active property cleared', + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to set active property', + } + } +} + +/** + * Create a new property + */ +export async function createProperty(params: CreatePropertyParams): Promise> { + try { + const session = await getSession() + + if (!session?.user) { + return { + success: false, + error: 'Not authenticated', + } + } + + const supabase = await createServerSupabaseClient() + + // Generate IDs for address and property + const addressId = createId('address') + const propertyId = createId('property') + + // First, create the address + const addressData = { + id: addressId, + street_number: params.streetNumber, + route: params.route, + city: params.city || '', + state: params.state || '', + postal_code: params.postalCode || '', + country: params.country || 'US', + latitude: params.center[1].toString(), + longitude: params.center[0].toString(), + } + const { data: address, error: addressError } = (await (supabase + .from('properties_addresses') as any) + .insert(addressData) + .select() + .single()) as { data: Property['address'] | null; error: any } + + if (addressError || !address) { + return { + success: false, + error: addressError?.message || 'Failed to create address', + } + } + + // Create the property + const propertyData = { + id: propertyId, + name: params.name, + address_id: address.id, + owner_id: session.user.id, + details_json: { + coordinates: params.center, + createdFrom: 'editor-app', + }, + } + const { data, error } = (await (supabase + .from('properties') as any) + .insert(propertyData) + .select(` + *, + address:properties_addresses(*) + `) + .single()) as { data: Property | null; error: any } + + if (error) { + return { + success: false, + error: error.message, + } + } + + return { + success: true, + data: data as Property, + message: 'Property created successfully', + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create property', + } + } +} + +/** + * Check if a property with the given address already exists + */ +export async function checkPropertyDuplicate(params: { + streetNumber?: string + route?: string + city?: string + state?: string + postalCode?: string +}): Promise< + ActionResult<{ + isDuplicate: boolean + isUserProperty?: boolean + existingProperty?: Property + }> +> { + try { + const session = await getSession() + + if (!session?.user) { + return { + success: false, + error: 'Not authenticated', + } + } + + const supabase = await createServerSupabaseClient() + + // Query for existing property with matching address + const { data, error } = await supabase + .from('properties') + .select(` + *, + address:properties_addresses!inner(*) + `) + .eq('address.street_number', params.streetNumber || '') + .eq('address.route', params.route || '') + .eq('address.city', params.city || '') + .eq('address.state', params.state || '') + .eq('address.postal_code', params.postalCode || '') + .limit(1) + + if (error) { + return { + success: false, + error: error.message, + } + } + + if (data && data.length > 0) { + const existingProperty = data[0] as unknown as Property + return { + success: true, + data: { + isDuplicate: true, + isUserProperty: existingProperty.owner_id === session.user.id, + existingProperty, + }, + } + } + + return { + success: true, + data: { + isDuplicate: false, + }, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to check for duplicates', + } + } +} diff --git a/apps/editor/features/community/lib/properties/hooks.ts b/apps/editor/features/community/lib/properties/hooks.ts new file mode 100644 index 00000000..edc73204 --- /dev/null +++ b/apps/editor/features/community/lib/properties/hooks.ts @@ -0,0 +1,234 @@ +'use client' + +import { useCallback, useEffect, useRef, useState } from 'react' +import { + getActiveProperty, + getUserProperties, + setActiveProperty as setActivePropertyAction, +} from './actions' +import type { Property } from './types' + +interface UsePropertiesReturn { + properties: Property[] + isLoading: boolean + error: string | null + refetch: () => Promise +} + +/** + * Hook to fetch and manage user properties + */ +export function useProperties(): UsePropertiesReturn { + const [properties, setProperties] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchProperties = useCallback(async () => { + try { + setIsLoading(true) + setError(null) + + const result = await getUserProperties() + + if (result.success) { + setProperties(result.data || []) + } else { + setError(result.error || 'Failed to fetch properties') + setProperties([]) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + setProperties([]) + } finally { + setIsLoading(false) + } + }, []) + + useEffect(() => { + fetchProperties() + }, [fetchProperties]) + + return { + properties, + isLoading, + error, + refetch: fetchProperties, + } +} + +/** + * Hook to fetch a single property by ID + */ +export function useProperty(propertyId: string | undefined) { + const [property, setProperty] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + if (!propertyId) { + setProperty(null) + setIsLoading(false) + return + } + + const fetchProperty = async () => { + try { + setIsLoading(true) + setError(null) + + const result = await getUserProperties() + + if (result.success) { + const found = result.data?.find((p) => p.id === propertyId) + if (found) { + setProperty(found) + } else { + setError('Property not found') + setProperty(null) + } + } else { + setError(result.error || 'Failed to fetch property') + setProperty(null) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + setProperty(null) + } finally { + setIsLoading(false) + } + } + + fetchProperty() + }, [propertyId]) + + return { + property, + isLoading, + error, + } +} + +/** + * Hook to manage the active property for the current session + */ +export function useActiveProperty() { + const [activeProperty, setActivePropertyState] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [isPending, setIsPending] = useState(false) + const isInitialFetchRef = useRef(true) + + const fetchActiveProperty = useCallback(async (allowAutoSelect = false) => { + try { + setIsLoading(true) + setError(null) + + const result = await getActiveProperty() + + if (result.success) { + setActivePropertyState(result.data || null) + + // If no active property is set, automatically set the first property as active + // Only do this on initial mount to avoid interfering with property selection/creation + if (!result.data && allowAutoSelect) { + console.log('[useActiveProperty] No active property, checking if we should auto-select') + const propertiesResult = await getUserProperties() + + if ( + propertiesResult.success && + propertiesResult.data && + propertiesResult.data.length > 0 + ) { + console.log('[useActiveProperty] Found properties, auto-selecting first one') + const firstProperty = propertiesResult.data[0] + if (firstProperty) { + const setActiveResult = await setActivePropertyAction(firstProperty.id) + + if (setActiveResult.success) { + console.log('[useActiveProperty] Auto-selected property:', firstProperty.name) + setActivePropertyState(firstProperty) + } + } + } + } else if (!result.data && !allowAutoSelect) { + console.log('[useActiveProperty] No active property but auto-select is disabled') + } + } else { + setError(result.error || 'Failed to fetch active property') + setActivePropertyState(null) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + setActivePropertyState(null) + } finally { + setIsLoading(false) + } + }, []) + + const changeActiveProperty = useCallback( + async (propertyId: string | null) => { + try { + setIsPending(true) + setIsLoading(true) + const result = await setActivePropertyAction(propertyId) + + if (result.success) { + if (propertyId) { + // Fetch the property data and set it immediately + console.log('[useActiveProperty] Fetching property data for ID:', propertyId) + const propertiesResult = await getUserProperties() + + if (propertiesResult.success && propertiesResult.data) { + const selectedProperty = propertiesResult.data.find(p => p.id === propertyId) + if (selectedProperty) { + console.log('[useActiveProperty] Found property, setting as active:', selectedProperty.name) + console.log('[useActiveProperty] Current isLoading state:', isLoading) + setActivePropertyState(selectedProperty) + setIsLoading(false) + console.log('[useActiveProperty] Set isLoading to false') + } else { + console.error('[useActiveProperty] Property not found in user properties') + // Fall back to refetch + await fetchActiveProperty(false) + } + } else { + console.error('[useActiveProperty] Failed to fetch properties') + // Fall back to refetch + await fetchActiveProperty(false) + } + } else { + setActivePropertyState(null) + setIsLoading(false) + } + } else { + console.error(result.error || 'Failed to update active property') + setIsLoading(false) + } + } catch (err) { + console.error(err instanceof Error ? err.message : 'An unexpected error occurred') + setIsLoading(false) + } finally { + setIsPending(false) + } + }, + [fetchActiveProperty], + ) + + useEffect(() => { + // Only allow auto-select on the initial mount + const allowAutoSelect = isInitialFetchRef.current + if (isInitialFetchRef.current) { + isInitialFetchRef.current = false + } + fetchActiveProperty(allowAutoSelect) + }, [fetchActiveProperty]) + + return { + activeProperty, + isLoading, + error, + setActiveProperty: changeActiveProperty, + isPending, + refetch: fetchActiveProperty, + } +} diff --git a/apps/editor/features/community/lib/properties/store.ts b/apps/editor/features/community/lib/properties/store.ts new file mode 100644 index 00000000..aef9ab69 --- /dev/null +++ b/apps/editor/features/community/lib/properties/store.ts @@ -0,0 +1,126 @@ +/** + * Property store - Zustand store for property state management + */ + +import { create } from 'zustand' +import type { Property } from './types' +import { + getActiveProperty, + getUserProperties, + setActiveProperty as setActivePropertyAction, +} from './actions' + +interface PropertyStore { + // State + activeProperty: Property | null + properties: Property[] + isLoading: boolean + error: string | null + + // Actions + fetchProperties: () => Promise + fetchActiveProperty: () => Promise + setActiveProperty: (propertyId: string) => Promise + initialize: () => Promise +} + +export const usePropertyStore = create((set, get) => ({ + // Initial state + activeProperty: null, + properties: [], + isLoading: true, + error: null, + + // Fetch all properties + fetchProperties: async () => { + const result = await getUserProperties() + + if (result.success) { + set({ properties: result.data || [], error: null }) + } else { + set({ error: result.error || 'Failed to fetch properties', properties: [] }) + } + }, + + // Fetch the active property from database + fetchActiveProperty: async () => { + set({ isLoading: true }) + + const result = await getActiveProperty() + + if (result.success) { + set({ + activeProperty: result.data || null, + isLoading: false, + error: null + }) + + // If no active property, auto-select the first one + if (!result.data) { + const propertiesResult = await getUserProperties() + if (propertiesResult.success && propertiesResult.data && propertiesResult.data.length > 0) { + const firstProperty = propertiesResult.data[0] + if (firstProperty) { + await get().setActiveProperty(firstProperty.id) + } + } + } + } else { + set({ + error: result.error || 'Failed to fetch active property', + activeProperty: null, + isLoading: false + }) + } + }, + + // Set active property + setActiveProperty: async (propertyId: string) => { + set({ isLoading: true }) + + // Update database + const result = await setActivePropertyAction(propertyId) + + if (result.success) { + // Fetch properties to get the full property object + const propertiesResult = await getUserProperties() + + if (propertiesResult.success && propertiesResult.data) { + const selectedProperty = propertiesResult.data.find(p => p.id === propertyId) + + if (selectedProperty) { + set({ + activeProperty: selectedProperty, + properties: propertiesResult.data, + isLoading: false, + error: null + }) + } else { + set({ + isLoading: false, + error: 'Property not found' + }) + } + } else { + set({ + isLoading: false, + error: 'Failed to fetch properties' + }) + } + } else { + set({ + isLoading: false, + error: result.error || 'Failed to set active property' + }) + } + }, + + // Initialize - fetch both properties and active property + initialize: async () => { + set({ isLoading: true }) + await Promise.all([ + get().fetchProperties(), + get().fetchActiveProperty(), + ]) + }, +})) diff --git a/apps/editor/features/community/lib/properties/types.ts b/apps/editor/features/community/lib/properties/types.ts new file mode 100644 index 00000000..393f583a --- /dev/null +++ b/apps/editor/features/community/lib/properties/types.ts @@ -0,0 +1,43 @@ +/** + * Property-related type definitions + * Isolated from monorepo database schema + */ + +export type Property = { + id: string + name: string + owner_id: string + organization_id: string | null + address_id: string + created_at: string + updated_at: string + address: { + id: string + street_number?: string + route?: string + city?: string + state?: string + postal_code?: string + country?: string + latitude?: string + longitude?: string + } +} + +export type CreatePropertyParams = { + name: string + center: [number, number] + streetNumber?: string + route?: string + routeShort?: string + neighborhood?: string + city?: string + county?: string + state?: string + stateLong?: string + postalCode?: string + postalCodeSuffix?: string + country?: string + countryLong?: string + rawJson?: Record +} diff --git a/apps/editor/features/community/lib/utils/id-generator.ts b/apps/editor/features/community/lib/utils/id-generator.ts new file mode 100644 index 00000000..f22ad1f0 --- /dev/null +++ b/apps/editor/features/community/lib/utils/id-generator.ts @@ -0,0 +1,13 @@ +import { customAlphabet } from 'nanoid' + +const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +const nanoid = customAlphabet(alphabet, 16) + +/** + * Generate a unique ID with optional prefix (matches monorepo implementation) + * @example createId('user') => 'user_Abc123...' + */ +export const createId = (prefix?: string) => { + const id = nanoid() + return prefix ? `${prefix}_${id}` : id +} diff --git a/apps/editor/lib/auth.ts b/apps/editor/lib/auth.ts new file mode 100644 index 00000000..3160d53b --- /dev/null +++ b/apps/editor/lib/auth.ts @@ -0,0 +1,44 @@ +import { createAuth } from '@pascal-app/auth/server' +import { db } from '@pascal-app/db' +import { Resend } from 'resend' + +const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null + +export const auth = createAuth({ + db, + appName: 'Pascal Editor', + baseURL: process.env.BETTER_AUTH_URL!, + secret: process.env.BETTER_AUTH_SECRET!, + sendMagicLink: async ({ email, url }) => { + if (!resend) { + console.log(`[DEV] Magic link for ${email}: ${url}`) + return + } + + try { + await resend.emails.send({ + from: 'Pascal ', + to: email, + subject: 'Sign in to Pascal Editor', + html: ` +
+

Sign in to Pascal Editor

+

Click the button below to sign in to your account:

+ + Sign In + +

This link will expire in 5 minutes.

+

If you didn't request this email, you can safely ignore it.

+
+ `, + }) + console.log(`✓ Magic link email sent to ${email}`) + } catch (error) { + console.error('Failed to send magic link email:', error) + throw error + } + }, +}) + +export type Session = typeof auth.$Infer.Session +export type User = typeof auth.$Infer.Session.user diff --git a/apps/editor/lib/supabase/client.ts b/apps/editor/lib/supabase/client.ts new file mode 100644 index 00000000..1a53b8fd --- /dev/null +++ b/apps/editor/lib/supabase/client.ts @@ -0,0 +1,11 @@ +import { createClient } from '@supabase/supabase-js' +import type { SupabaseDatabase } from '@pascal-app/db' + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + +/** + * Supabase client for client-side use with anon key + * Uses Row Level Security (RLS) policies + */ +export const supabase = createClient(supabaseUrl, supabaseAnonKey) diff --git a/apps/editor/lib/supabase/server.ts b/apps/editor/lib/supabase/server.ts new file mode 100644 index 00000000..a059c430 --- /dev/null +++ b/apps/editor/lib/supabase/server.ts @@ -0,0 +1,21 @@ +import { createClient } from '@supabase/supabase-js' +import type { SupabaseDatabase } from '@pascal-app/db' + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! +const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY! + +/** + * Supabase client for server-side use with service role key + * Bypasses Row Level Security (RLS) - use with caution + * Always filter by user_id to enforce permissions + */ +export const supabaseAdmin = createClient( + supabaseUrl, + supabaseServiceRoleKey, + { + auth: { + persistSession: false, + autoRefreshToken: false, + }, + }, +) diff --git a/apps/editor/package.json b/apps/editor/package.json index 922ff855..41132f8c 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -11,7 +11,9 @@ "check-types": "next typegen && tsc --noEmit" }, "dependencies": { + "@pascal-app/auth": "*", "@pascal-app/core": "*", + "@pascal-app/db": "*", "@pascal-app/viewer": "*", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16", @@ -25,14 +27,18 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@react-google-maps/api": "^2.20.8", "@react-three/uikit-lucide": "^1.0.60", "@repo/ui": "*", + "@supabase/supabase-js": "^2.95.3", "@tailwindcss/postcss": "^4.1.18", "@types/three": "^0.182.0", + "better-auth": "^1.4.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", "motion": "^12.26.2", + "nanoid": "^5.1.6", "next": "16.1.0", "postcss": "^8.5.6", "react": "^19.2.0", diff --git a/apps/editor/public/audios/radios/classic/Ballroom in Miniature.mp3 b/apps/editor/public/audios/radios/classic/Ballroom in Miniature.mp3 new file mode 100644 index 00000000..ebcf4dd7 Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Ballroom in Miniature.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Blueprints in Springtime.mp3 b/apps/editor/public/audios/radios/classic/Blueprints in Springtime.mp3 new file mode 100644 index 00000000..973955ba Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Blueprints in Springtime.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Clockwork Tea Party (Alternate).mp3 b/apps/editor/public/audios/radios/classic/Clockwork Tea Party (Alternate).mp3 new file mode 100644 index 00000000..d4ec67d7 Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Clockwork Tea Party (Alternate).mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Clockwork Tea Party.mp3 b/apps/editor/public/audios/radios/classic/Clockwork Tea Party.mp3 new file mode 100644 index 00000000..88915bad Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Clockwork Tea Party.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Clockwork Teacups.mp3 b/apps/editor/public/audios/radios/classic/Clockwork Teacups.mp3 new file mode 100644 index 00000000..a8967b20 Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Clockwork Teacups.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Evening in the Parlor.mp3 b/apps/editor/public/audios/radios/classic/Evening in the Parlor.mp3 new file mode 100644 index 00000000..ed7b590f Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Evening in the Parlor.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Glass Atrium.mp3 b/apps/editor/public/audios/radios/classic/Glass Atrium.mp3 new file mode 100644 index 00000000..460f72f9 Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Glass Atrium.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Moonlight On The Drafting Table.mp3 b/apps/editor/public/audios/radios/classic/Moonlight On The Drafting Table.mp3 new file mode 100644 index 00000000..9b4e5275 Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Moonlight On The Drafting Table.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Sunlit Garden Reverie.mp3 b/apps/editor/public/audios/radios/classic/Sunlit Garden Reverie.mp3 new file mode 100644 index 00000000..120d20ab Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Sunlit Garden Reverie.mp3 differ diff --git a/apps/editor/public/audios/radios/classic/Sunlit Waltz in Pastel Hues.mp3 b/apps/editor/public/audios/radios/classic/Sunlit Waltz in Pastel Hues.mp3 new file mode 100644 index 00000000..037cd889 Binary files /dev/null and b/apps/editor/public/audios/radios/classic/Sunlit Waltz in Pastel Hues.mp3 differ diff --git a/apps/editor/public/audios/sfx/grid_snap.mp3 b/apps/editor/public/audios/sfx/grid_snap.mp3 new file mode 100644 index 00000000..f3c853c0 Binary files /dev/null and b/apps/editor/public/audios/sfx/grid_snap.mp3 differ diff --git a/apps/editor/public/audios/sfx/item_delete.mp3 b/apps/editor/public/audios/sfx/item_delete.mp3 new file mode 100644 index 00000000..06f759d1 Binary files /dev/null and b/apps/editor/public/audios/sfx/item_delete.mp3 differ diff --git a/apps/editor/public/audios/sfx/item_pick.mp3 b/apps/editor/public/audios/sfx/item_pick.mp3 new file mode 100644 index 00000000..f65770ca Binary files /dev/null and b/apps/editor/public/audios/sfx/item_pick.mp3 differ diff --git a/apps/editor/public/audios/sfx/item_place.mp3 b/apps/editor/public/audios/sfx/item_place.mp3 new file mode 100644 index 00000000..5a176fe9 Binary files /dev/null and b/apps/editor/public/audios/sfx/item_place.mp3 differ diff --git a/apps/editor/public/audios/sfx/structure_build.mp3 b/apps/editor/public/audios/sfx/structure_build.mp3 new file mode 100644 index 00000000..028b3f2e Binary files /dev/null and b/apps/editor/public/audios/sfx/structure_build.mp3 differ diff --git a/apps/editor/public/audios/sfx/structure_delete.mp3 b/apps/editor/public/audios/sfx/structure_delete.mp3 new file mode 100644 index 00000000..6895e26a Binary files /dev/null and b/apps/editor/public/audios/sfx/structure_delete.mp3 differ diff --git a/bun.lock b/bun.lock index 5ed59f8c..4d6eb984 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "editor", @@ -13,6 +14,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.3.13", + "supabase": "2.75.3", "turbo": "^2.8.1", "typescript": "5.9.2", "ultracite": "^7.1.1", @@ -22,7 +24,9 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@pascal-app/auth": "*", "@pascal-app/core": "*", + "@pascal-app/db": "*", "@pascal-app/viewer": "*", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16", @@ -36,14 +40,18 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@react-google-maps/api": "^2.20.8", "@react-three/uikit-lucide": "^1.0.60", "@repo/ui": "*", + "@supabase/supabase-js": "^2.95.3", "@tailwindcss/postcss": "^4.1.18", "@types/three": "^0.182.0", + "better-auth": "^1.4.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", "motion": "^12.26.2", + "nanoid": "^5.1.6", "next": "16.1.0", "postcss": "^8.5.6", "react": "^19.2.0", @@ -60,6 +68,19 @@ "typescript": "5.9.2", }, }, + "packages/auth": { + "name": "@pascal-app/auth", + "version": "0.0.0", + "dependencies": { + "@pascal-app/db": "*", + "better-auth": "^1.4.18", + "resend": "^4.0.1", + }, + "devDependencies": { + "@repo/typescript-config": "*", + "typescript": "5.9.2", + }, + }, "packages/core": { "name": "@pascal-app/core", "version": "0.1.11", @@ -87,6 +108,22 @@ "three": "^0.182", }, }, + "packages/db": { + "name": "@pascal-app/db", + "version": "0.0.0", + "dependencies": { + "@supabase/supabase-js": "^2.95.3", + "drizzle-orm": "^0.39.0", + "drizzle-zod": "^0.5.1", + "nanoid": "^5.0.9", + "postgres": "^3.4.5", + }, + "devDependencies": { + "@repo/typescript-config": "*", + "drizzle-kit": "^0.30.0", + "typescript": "5.9.2", + }, + }, "packages/eslint-config": { "name": "@repo/eslint-config", "version": "0.0.0", @@ -151,6 +188,14 @@ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.18", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.18" } }, "sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ=="], + + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="], @@ -175,8 +220,60 @@ "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -203,6 +300,10 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@googlemaps/js-api-loader": ["@googlemaps/js-api-loader@1.16.8", "", {}, "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ=="], + + "@googlemaps/markerclusterer": ["@googlemaps/markerclusterer@2.5.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "supercluster": "^8.0.1" } }, "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -265,6 +366,8 @@ "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -299,16 +402,26 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Tr0j94MphimCCks+1rtYPzQFK+faJuhHWCegU9S9gDlgyOk8Y3kPmO64UcjyzZAlligeBtYZ/2bEyrKq0d2wqQ=="], + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@pascal-app/auth": ["@pascal-app/auth@workspace:packages/auth"], + "@pascal-app/core": ["@pascal-app/core@workspace:packages/core"], + "@pascal-app/db": ["@pascal-app/db@workspace:packages/db"], + "@pascal-app/viewer": ["@pascal-app/viewer@workspace:packages/viewer"], + "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], + "@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.61", "", {}, "sha512-Nxk+3dAdsXFdx5pq1IHNe1j4zxogkAfhmBKUIHS+aJUs45q/XMtlADVrHLooiQ/Amlp9tXAvbvFYUOGd897POw=="], "@pmndrs/uikit": ["@pmndrs/uikit@1.0.61", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.61", "@pmndrs/uikit-pub-sub": "^1.0.61", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-xoNp2jKRoHa7INC3QaAL2ddpGmKy+eORMh4zoNds17EndqyBrqYzfWJK+PVmei/5MQNBTi0KrY2qxBGj6hB0cg=="], @@ -397,6 +510,14 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="], + + "@react-google-maps/api": ["@react-google-maps/api@2.20.8", "", { "dependencies": { "@googlemaps/js-api-loader": "1.16.8", "@googlemaps/markerclusterer": "2.5.3", "@react-google-maps/infobox": "2.20.0", "@react-google-maps/marker-clusterer": "2.20.0", "@types/google.maps": "3.58.1", "invariant": "2.2.4" }, "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19", "react-dom": "^16.8 || ^17 || ^18 || ^19" } }, "sha512-wtLYFtCGXK3qbIz1H5to3JxbosPnKsvjDKhqGylXUb859EskhzR7OpuNt0LqdLarXUtZCJTKzPn3BNaekNIahg=="], + + "@react-google-maps/infobox": ["@react-google-maps/infobox@2.20.0", "", {}, "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ=="], + + "@react-google-maps/marker-clusterer": ["@react-google-maps/marker-clusterer@2.20.0", "", {}, "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw=="], + "@react-three/drei": ["@react-three/drei@10.7.7", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ=="], "@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="], @@ -411,6 +532,22 @@ "@repo/ui": ["@repo/ui@workspace:packages/ui"], + "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@supabase/auth-js": ["@supabase/auth-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg=="], + + "@supabase/functions-js": ["@supabase/functions-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig=="], + + "@supabase/postgrest-js": ["@supabase/postgrest-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg=="], + + "@supabase/realtime-js": ["@supabase/realtime-js@2.95.3", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw=="], + + "@supabase/storage-js": ["@supabase/storage-js@2.95.3", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A=="], + + "@supabase/supabase-js": ["@supabase/supabase-js@2.95.3", "", { "dependencies": { "@supabase/auth-js": "2.95.3", "@supabase/functions-js": "2.95.3", "@supabase/postgrest-js": "2.95.3", "@supabase/realtime-js": "2.95.3", "@supabase/storage-js": "2.95.3" } }, "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], @@ -449,12 +586,16 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/google.maps": ["@types/google.maps@3.58.1", "", {}, "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], + "@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], @@ -467,6 +608,8 @@ "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="], @@ -499,6 +642,8 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -531,14 +676,22 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], + "better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="], + + "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -553,6 +706,8 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -561,6 +716,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -577,6 +734,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], @@ -595,6 +754,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "detect-gpu": ["detect-gpu@5.0.70", "", { "dependencies": { "webgl-constants": "^1.1.1" } }, "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -603,14 +764,32 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dotenv": ["dotenv@16.0.3", "", {}, "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="], "draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="], + "drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="], + + "drizzle-orm": ["drizzle-orm@0.39.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw=="], + + "drizzle-zod": ["drizzle-zod@0.5.1", "", { "peerDependencies": { "drizzle-orm": ">=0.23.13", "zod": "*" } }, "sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -627,6 +806,10 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], @@ -667,6 +850,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -681,6 +866,8 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "framer-motion": ["framer-motion@12.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -689,6 +876,8 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="], + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -699,6 +888,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -729,6 +920,14 @@ "hls.js": ["hls.js@1.6.15", "", {}, "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA=="], + "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="], + + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="], + "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -743,6 +942,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], @@ -797,7 +998,7 @@ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], @@ -805,6 +1006,8 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -819,8 +1022,14 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + + "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], @@ -877,6 +1086,8 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "motion": ["motion@12.26.2", "", { "dependencies": { "framer-motion": "^12.26.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw=="], @@ -889,10 +1100,18 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "next": ["next@16.1.0", "", { "dependencies": { "@next/env": "16.1.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.0", "@next/swc-darwin-x64": "16.1.0", "@next/swc-linux-arm64-gnu": "16.1.0", "@next/swc-linux-arm64-musl": "16.1.0", "@next/swc-linux-x64-gnu": "16.1.0", "@next/swc-linux-x64-musl": "16.1.0", "@next/swc-win32-arm64-msvc": "16.1.0", "@next/swc-win32-x64-msvc": "16.1.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Y+KbmDbefYtHDDQKLNrmzE/YYzG2msqo2VXhzh5yrJ54tx/6TmGdkR5+kP9ma7i7LwZpZMfoY3m/AoPPPKxtVw=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="], + "nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -919,6 +1138,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -929,6 +1150,24 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], + + "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="], + + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="], + + "pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -937,10 +1176,24 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "potpack": ["potpack@1.0.2", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + "promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], @@ -955,6 +1208,8 @@ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -963,18 +1218,26 @@ "react-use-measure": ["react-use-measure@2.1.7", "", { "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" }, "optionalPeers": ["react-dom"] }, "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg=="], + "read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="], + "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], @@ -985,8 +1248,12 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -999,6 +1266,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -1007,10 +1276,18 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="], "stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="], @@ -1031,6 +1308,10 @@ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "supabase": ["supabase@2.75.3", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.7" }, "bin": { "supabase": "bin/supabase" } }, "sha512-j2uHJfK8TE7SxL6cd+kQixwkl8H5ASBkQfOEVjf3d3XS1Wixy4UO8WudDYNVriu1KuKzsNnkkSC/jTyHNXNrcQ=="], + + "supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -1043,6 +1324,8 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="], + "three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="], "three-bvh-csg": ["three-bvh-csg@0.0.17", "", { "peerDependencies": { "three": ">=0.151.0", "three-mesh-bvh": ">=0.6.6" } }, "sha512-iEkHDF8GRfGM6593Cuw8SnF1vfENCp46gIAtRzuL4nGXGWPcR1sbTBwM9ptDONb5twqlZp5WkAKya5aBKe2qcA=="], @@ -1117,11 +1400,13 @@ "web": ["web@workspace:apps/editor"], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="], "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -1133,6 +1418,14 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "write-file-atomic": ["write-file-atomic@7.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], @@ -1143,6 +1436,8 @@ "zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -1187,14 +1482,20 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "gel/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="], + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="], @@ -1207,8 +1508,54 @@ "web/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], } } diff --git a/package.json b/package.json index fbc5bb3d..276a913f 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "format:check": "biome format", "check": "biome check", "check:fix": "biome check --write", - "check-types": "turbo run check-types" + "check-types": "turbo run check-types", + "db:start": "cd packages/db && supabase start", + "db:stop": "cd packages/db && supabase stop", + "db:reset": "cd packages/db && supabase db reset", + "db:status": "cd packages/db && supabase status" }, "dependencies": { "@react-three/drei": "^10.7.7", @@ -22,6 +26,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.3.13", + "supabase": "2.75.3", "turbo": "^2.8.1", "typescript": "5.9.2", "ultracite": "^7.1.1" diff --git a/packages/auth/README.md b/packages/auth/README.md new file mode 100644 index 00000000..bc7ffd7c --- /dev/null +++ b/packages/auth/README.md @@ -0,0 +1,142 @@ +# @pascal-app/auth + +Authentication package for Pascal Editor using Better Auth. + +## Features + +- **Magic Link Authentication** - Passwordless email-based authentication +- **Session Management** - Secure cookie-based sessions +- **Supabase Integration** - Uses Supabase as the database adapter +- **Type-safe** - Full TypeScript support with type inference + +## Setup + +### 1. Configure environment variables + +Add these to `apps/editor/.env.local`: + +```bash +# Better Auth +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 +``` + +Generate a secret for `BETTER_AUTH_SECRET`: + +```bash +openssl rand -base64 32 +``` + +### 2. Ensure database is running + +Make sure you have Supabase running with the auth tables created. See `@pascal-app/db` package for setup. + +## Usage + +### Server-side (API routes, server actions) + +```typescript +import { auth } from '@pascal-app/auth/server' + +// Get session in server component or action +const session = await auth.api.getSession({ headers: request.headers }) + +if (!session) { + return { error: 'Unauthorized' } +} + +// Access user data +const userId = session.user.id +const email = session.user.email +``` + +### Client-side (React components) + +```typescript +'use client' + +import { authClient } from '@pascal-app/auth/client' + +function SignInButton() { + const { signIn } = authClient + + const handleSignIn = async (email: string) => { + await signIn.magicLink({ + email, + callbackURL: '/dashboard', + }) + } + + return +} +``` + +### Using the auth hook + +```typescript +'use client' + +import { authClient } from '@pascal-app/auth/client' + +function Profile() { + const { data: session, isPending } = authClient.useSession() + + if (isPending) return
Loading...
+ if (!session) return
Not signed in
+ + return
Signed in as {session.user.email}
+} +``` + +## API Routes + +The auth package requires an API route handler in your Next.js app: + +```typescript +// app/api/auth/[...all]/route.ts +import { auth } from '@pascal-app/auth/server' +import { toNextJsHandler } from 'better-auth/next-js' + +export const { GET, POST } = toNextJsHandler(auth) +``` + +This handles all Better Auth endpoints: +- `/api/auth/sign-in/magic-link` - Send magic link +- `/api/auth/sign-in/magic-link/verify` - Verify magic link +- `/api/auth/sign-out` - Sign out +- `/api/auth/session` - Get session +- And more... + +## Email Configuration + +By default, magic links are logged to the console. To send actual emails, you'll need to configure an email provider in `packages/auth/src/server.ts`: + +```typescript +magicLink({ + sendMagicLink: async ({ email, url }) => { + // Use Resend, SendGrid, or your preferred email service + await sendEmail({ + to: email, + subject: 'Sign in to Pascal Editor', + html: `Click here to sign in: ${url}`, + }) + }, +}) +``` + +## Database Schema + +The auth package requires these tables (created by `@pascal-app/db` migrations): + +- `users` - User accounts +- `sessions` - Active sessions +- `accounts` - OAuth provider accounts (for future use) +- `verification_tokens` - Magic link tokens + +## Security + +- Session cookies are httpOnly and secure (in production) +- Sessions expire after 7 days +- Session cache is enabled for 5 minutes to reduce database queries +- Magic link tokens expire after 15 minutes +- All sensitive operations require valid session tokens diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 00000000..c354edfc --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,24 @@ +{ + "name": "@pascal-app/auth", + "version": "0.0.0", + "type": "module", + "exports": { + "./server": { + "types": "./src/server.ts", + "default": "./src/server.ts" + }, + "./client": { + "types": "./src/client.ts", + "default": "./src/client.ts" + } + }, + "dependencies": { + "@pascal-app/db": "*", + "better-auth": "^1.4.18", + "resend": "^4.0.1" + }, + "devDependencies": { + "@repo/typescript-config": "*", + "typescript": "5.9.2" + } +} diff --git a/packages/auth/src/client.ts b/packages/auth/src/client.ts new file mode 100644 index 00000000..7994f42d --- /dev/null +++ b/packages/auth/src/client.ts @@ -0,0 +1,37 @@ +import { magicLinkClient } from 'better-auth/client/plugins' +import { createAuthClient } from 'better-auth/react' + +/** + * Get the auth base URL + * In development: use the editor URL (localhost:3000) + * In production: use the same origin + */ +function getAuthURL(): string { + if (typeof window !== 'undefined') { + return window.location.origin + } + + // SSR fallback + return process.env.BETTER_AUTH_URL || 'http://localhost:3000' +} + +/** + * Auth client instance + * Configured for magic link authentication + */ +export const authClient = createAuthClient({ + baseURL: getAuthURL(), + plugins: [magicLinkClient()], +}) + +/** + * Export types for use in components + */ +export type AuthState = { + user: (typeof authClient)['$Infer']['Session']['user'] | null + session: (typeof authClient)['$Infer']['Session']['session'] | null + isLoading: boolean +} + +export type User = NonNullable +export type Session = NonNullable diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts new file mode 100644 index 00000000..0929c1c0 --- /dev/null +++ b/packages/auth/src/server.ts @@ -0,0 +1,76 @@ +import type { Database } from '@pascal-app/db' +import { schema } from '@pascal-app/db' +import type { BetterAuthOptions } from 'better-auth' +import { betterAuth } from 'better-auth' +import { drizzleAdapter } from 'better-auth/adapters/drizzle' +import { magicLink } from 'better-auth/plugins' + +export interface SendMagicLinkParams { + email: string + url: string + token: string +} + +export interface AuthConfig { + db: Database + appName: string + baseURL: string + secret: string + /** Callback to send magic link emails */ + sendMagicLink?: (params: SendMagicLinkParams) => Promise + /** Additional plugins to add (e.g., nextCookies for web) */ + additionalPlugins?: BetterAuthOptions['plugins'] +} + +/** + * Creates a Better Auth instance with full configuration including: + * - Magic link authentication + * - Custom session with activePropertyId + * - Session cookie caching + */ +export function createAuth(config: AuthConfig): ReturnType { + return betterAuth({ + appName: config.appName, + baseURL: config.baseURL, + secret: config.secret, + basePath: '/api/auth', + database: drizzleAdapter(config.db, { + provider: 'pg', + usePlural: true, + schema, + }), + advanced: { + database: { + generateId: false, // Use our prefixed nanoid IDs from schema + }, + }, + session: { + // Session caching to reduce database queries + cookieCache: { + enabled: true, + maxAge: 5 * 60, // Cache duration in seconds (5 minutes) + }, + additionalFields: { + // Additional fields for the session table + activePropertyId: { + type: 'string', + }, + }, + }, + plugins: [ + ...(config.additionalPlugins ?? []), + // Magic link authentication + ...(config.sendMagicLink + ? [ + magicLink({ + sendMagicLink: config.sendMagicLink, + expiresIn: 300, // 5 minutes + disableSignUp: false, // Allow new users to sign up via magic link + }), + ] + : []), + ], + }) +} + +export type Auth = ReturnType diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 00000000..c6daff18 --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/db/README.md b/packages/db/README.md new file mode 100644 index 00000000..34bad2f9 --- /dev/null +++ b/packages/db/README.md @@ -0,0 +1,127 @@ +# @pascal-app/db + +Database package for Pascal Editor with Supabase. + +## Setup + +### 1. Install Dependencies + +From the monorepo root: + +```bash +bun install +``` + +This installs Supabase CLI as a dev dependency. + +### 2. Start Supabase locally + +From the monorepo root: + +```bash +bun db:start +``` + +This will start a local Supabase instance with PostgreSQL, PostgREST, and Studio. + +### 3. Check Supabase status + +```bash +bun db:status +``` + +You'll see output like: + +``` +API URL: http://127.0.0.1:54321 +DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres +Studio URL: http://127.0.0.1:54323 +Anon key: eyJh... +Service role key: eyJh... +``` + +### 4. Configure environment variables + +Add these to `apps/editor/.env.local`: + +```bash +# Supabase +NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 +NEXT_PUBLIC_SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= + +# Better Auth +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 +``` + +Generate a secret for `BETTER_AUTH_SECRET`: + +```bash +openssl rand -base64 32 +``` + +## Migrations + +Migrations are located in `supabase/migrations/`. + +### Apply migrations + +```bash +bun db:reset # Resets and applies all migrations +``` + +### Create a new migration + +```bash +cd packages/db +bunx supabase migration new +``` + +## Database Schema + +### Auth Tables (Better Auth) + +- `users` - User accounts +- `sessions` - Active sessions +- `accounts` - OAuth provider accounts +- `verification_tokens` - Magic link tokens + +### Application Tables + +- `properties` - User properties +- `properties_addresses` - Property addresses with Google Maps data +- `properties_models` - Scene graph models (versions) + +## Usage + +### Client-side (with RLS) + +```typescript +import { supabase } from '@pascal-app/db/client' + +// RLS policies automatically filter by authenticated user +const { data } = await supabase.from('properties').select('*') +``` + +### Server-side (service role) + +```typescript +import { supabaseAdmin } from '@pascal-app/db/server' + +// Bypasses RLS - you must manually filter by user_id +const { data } = await supabaseAdmin + .from('properties') + .select('*') + .eq('owner_id', userId) +``` + +## Supabase Studio + +Access the local Supabase Studio at: http://127.0.0.1:54323 + +Use this to: +- Browse tables and data +- Run SQL queries +- View logs +- Manage RLS policies diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 00000000..5ccd5316 --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,27 @@ +{ + "name": "@pascal-app/db", + "version": "0.0.0", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./types": { + "types": "./src/types.ts", + "default": "./src/types.ts" + } + }, + "dependencies": { + "@supabase/supabase-js": "^2.95.3", + "drizzle-orm": "^0.39.0", + "drizzle-zod": "^0.5.1", + "nanoid": "^5.0.9", + "postgres": "^3.4.5" + }, + "devDependencies": { + "@repo/typescript-config": "*", + "drizzle-kit": "^0.30.0", + "typescript": "5.9.2" + } +} diff --git a/packages/db/src/drizzle.ts b/packages/db/src/drizzle.ts new file mode 100644 index 00000000..f86e112e --- /dev/null +++ b/packages/db/src/drizzle.ts @@ -0,0 +1,11 @@ +import { drizzle } from 'drizzle-orm/postgres-js' +import postgres from 'postgres' +import * as schema from './schema' + +const connectionString = process.env.POSTGRES_URL! + +const client = postgres(connectionString, { prepare: false }) + +export const db = drizzle({ client, schema }) + +export type Database = typeof db diff --git a/packages/db/src/helpers.ts b/packages/db/src/helpers.ts new file mode 100644 index 00000000..58967300 --- /dev/null +++ b/packages/db/src/helpers.ts @@ -0,0 +1,56 @@ +import { type AnyColumn, type SQL, sql } from 'drizzle-orm' +import { text, timestamp } from 'drizzle-orm/pg-core' +import { customAlphabet } from 'nanoid' + +const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +const nanoid = customAlphabet(alphabet, 16) + +/** + * Generate a unique ID with optional prefix + * @example createId('user') => 'user_Abc123...' + */ +export const createId = (prefix?: string) => { + const id = nanoid() + return prefix ? `${prefix}_${id}` : id +} + +/** + * Primary key column with auto-generated prefixed ID + * @example id('user') => text('id').notNull().primaryKey().$defaultFn(() => createId('user')) + */ +export const id = (prefix?: string) => + text('id') + .notNull() + .primaryKey() + .$defaultFn(() => createId(prefix)) + .$type() + +export const createdAt = timestamp('created_at', { withTimezone: true }).notNull().defaultNow() + +export const updatedAt = timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()) + +export const deletedAt = timestamp('deleted_at', { withTimezone: true }) + +/** + * Standard timestamp columns for created_at and updated_at + */ +export const timestamps = { + createdAt, + updatedAt, +} + +// Alias for backwards compatibility with existing code +export const timestampsColumns = timestamps + +export const timestampsColumnsSoftDelete = { + ...timestampsColumns, + deletedAt, +} + +/** + * SQL helper for case-insensitive comparison + */ +export const lower = (column: AnyColumn): SQL => sql`lower(${column})` diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts new file mode 100644 index 00000000..d3123e43 --- /dev/null +++ b/packages/db/src/index.ts @@ -0,0 +1,23 @@ +/** + * Database package + * Exports Drizzle ORM and types + * Note: Supabase clients are kept in the app's lib directory to avoid build-time initialization + */ + +export type { Database as SupabaseDatabase } from './types' + +// Drizzle exports +export { type Database, db } from './drizzle' +export * from './schema' + +import * as dbSchema from './schema' +export const schema = dbSchema +export { + createId, + deletedAt, + id, + lower, + timestamps, + timestampsColumns, + timestampsColumnsSoftDelete, +} from './helpers' diff --git a/packages/db/src/schema/auth/accounts.ts b/packages/db/src/schema/auth/accounts.ts new file mode 100644 index 00000000..c3650555 --- /dev/null +++ b/packages/db/src/schema/auth/accounts.ts @@ -0,0 +1,31 @@ +import { pgTable } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { id, timestamps } from '../../helpers' +import { users } from './users' + +export const accounts = pgTable('auth_accounts', (t) => ({ + id: id('account'), + userId: t + .text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + providerId: t.text('provider_id').notNull(), + accountId: t.text('account_id').notNull(), + password: t.text('password'), + accessToken: t.text('access_token'), + refreshToken: t.text('refresh_token'), + idToken: t.text('id_token'), + accessTokenExpiresAt: t.timestamp('access_token_expires_at', { + withTimezone: true, + }), + refreshTokenExpiresAt: t.timestamp('refresh_token_expires_at', { + withTimezone: true, + }), + scope: t.text('scope'), + ...timestamps, +})).enableRLS() + +export type Account = typeof accounts.$inferSelect +export type NewAccount = typeof accounts.$inferInsert +export const insertAccountSchema = createInsertSchema(accounts) +export const selectAccountSchema = createSelectSchema(accounts) diff --git a/packages/db/src/schema/auth/jwks.ts b/packages/db/src/schema/auth/jwks.ts new file mode 100644 index 00000000..9f5cb2a0 --- /dev/null +++ b/packages/db/src/schema/auth/jwks.ts @@ -0,0 +1,15 @@ +import { pgTable } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { createdAt, id } from '../../helpers' + +export const jwks = pgTable('auth_jwks', (t) => ({ + id: id('jwks'), + publicKey: t.text('public_key').notNull(), + privateKey: t.text('private_key').notNull(), + createdAt, +})).enableRLS() + +export type Jwks = typeof jwks.$inferSelect +export type NewJwks = typeof jwks.$inferInsert +export const insertJwksSchema = createInsertSchema(jwks) +export const selectJwksSchema = createSelectSchema(jwks) diff --git a/packages/db/src/schema/auth/sessions.ts b/packages/db/src/schema/auth/sessions.ts new file mode 100644 index 00000000..bfd83511 --- /dev/null +++ b/packages/db/src/schema/auth/sessions.ts @@ -0,0 +1,28 @@ +import { pgTable } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { id, timestamps } from '../../helpers' +import { users } from './users' + +export const sessions = pgTable('auth_sessions', (t) => ({ + id: id('session'), + userId: t + .text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + expiresAt: t.timestamp('expires_at', { withTimezone: true }), + token: t.text('token').notNull(), + ipAddress: t.text('ip_address'), + userAgent: t.text('user_agent'), + // Custom: active property for the session context + activePropertyId: t.text('active_property_id'), + // Admin plugin support: tracks who is impersonating this session + impersonatedBy: t + .text('impersonated_by') + .references(() => users.id, { onDelete: 'set null' }), + ...timestamps, +})).enableRLS() + +export type Session = typeof sessions.$inferSelect +export type NewSession = typeof sessions.$inferInsert +export const insertSessionSchema = createInsertSchema(sessions) +export const selectSessionSchema = createSelectSchema(sessions) diff --git a/packages/db/src/schema/auth/users.ts b/packages/db/src/schema/auth/users.ts new file mode 100644 index 00000000..e01799f9 --- /dev/null +++ b/packages/db/src/schema/auth/users.ts @@ -0,0 +1,28 @@ +import { pgEnum, pgTable, uniqueIndex } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { id, lower, timestampsColumns } from '../../helpers' + +export const USER_ROLES = ['user', 'admin'] as const +export const userRoles = pgEnum('auth_user_roles', USER_ROLES) + +export const users = pgTable( + 'auth_users', + (t) => ({ + id: id('user'), + email: t.text('email').notNull(), + emailVerified: t.boolean('email_verified').notNull().default(false), + name: t.text('name').notNull(), + image: t.text('image'), + role: userRoles('role').notNull().default('user'), + banned: t.boolean('banned').notNull().default(false), + banReason: t.text('ban_reason'), + banExpires: t.timestamp('ban_expires', { withTimezone: true }), + ...timestampsColumns, + }), + (t) => [uniqueIndex('email_unique_index').on(lower(t.email))], +).enableRLS() + +export type User = typeof users.$inferSelect +export type NewUser = typeof users.$inferInsert +export const insertUserSchema = createInsertSchema(users) +export const selectUserSchema = createSelectSchema(users) diff --git a/packages/db/src/schema/auth/verifications.ts b/packages/db/src/schema/auth/verifications.ts new file mode 100644 index 00000000..a360f355 --- /dev/null +++ b/packages/db/src/schema/auth/verifications.ts @@ -0,0 +1,22 @@ +import { index, pgTable } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { id, timestamps } from '../../helpers' + +export const verifications = pgTable( + 'auth_verifications', + (t) => ({ + id: id('verification'), + value: t.text('value').notNull(), + identifier: t.text('identifier').notNull(), + expiresAt: t.timestamp('expires_at', { + withTimezone: true, + }), + ...timestamps, + }), + (t) => [index('verification_identifier_index').on(t.identifier)], +).enableRLS() + +export type Verification = typeof verifications.$inferSelect +export type NewVerification = typeof verifications.$inferInsert +export const insertVerificationSchema = createInsertSchema(verifications) +export const selectVerificationSchema = createSelectSchema(verifications) diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts new file mode 100644 index 00000000..656cb454 --- /dev/null +++ b/packages/db/src/schema/index.ts @@ -0,0 +1,11 @@ +// Auth tables +export * from './auth/accounts' +export * from './auth/jwks' +export * from './auth/sessions' +export * from './auth/users' +export * from './auth/verifications' + +// Property tables +export * from './properties/addresses' +export * from './properties/models' +export * from './properties/properties' diff --git a/packages/db/src/schema/properties/addresses.ts b/packages/db/src/schema/properties/addresses.ts new file mode 100644 index 00000000..ea4925f3 --- /dev/null +++ b/packages/db/src/schema/properties/addresses.ts @@ -0,0 +1,53 @@ +import { pgTable, unique } from 'drizzle-orm/pg-core' +import { z } from 'zod' +import { id, timestampsColumns } from '../../helpers' + +export const addresses = pgTable( + 'properties_addresses', + (t) => ({ + id: id('address'), + streetNumber: t.text('street_number'), + route: t.text('route'), + routeShort: t.text('route_short'), + neighborhood: t.text('neighborhood'), + city: t.text('city'), + county: t.text('county'), + state: t.text('state'), + stateLong: t.text('state_long'), + postalCode: t.text('postal_code'), + postalCodeSuffix: t.text('postal_code_suffix'), + country: t.text('country'), + countryLong: t.text('country_long'), + latitude: t.numeric('latitude'), + longitude: t.numeric('longitude'), + rawJson: t.jsonb('raw_json'), + ...timestampsColumns, + }), + (t) => [ + // Unique constraint on core address components to prevent duplicates + unique('address_components_unique').on(t.streetNumber, t.route, t.city, t.state, t.postalCode), + ], +).enableRLS() + +// Create address schema manually to avoid issues with generated columns +export const addressSchema = z.object({ + streetNumber: z.string().optional(), + route: z.string().optional(), + routeShort: z.string().optional(), + neighborhood: z.string().optional(), + city: z.string().optional(), + county: z.string().optional(), + state: z.string().optional(), + stateLong: z.string().optional(), + postalCode: z.string().optional(), + postalCodeSuffix: z.string().optional(), + country: z.string().default('US'), + countryLong: z.string().optional(), + latitude: z.string().optional(), + longitude: z.string().optional(), + rawJson: z.record(z.string(), z.unknown()).optional(), +}) + +export type AddressSchema = z.infer +export type Address = typeof addresses.$inferSelect +export type NewAddress = typeof addresses.$inferInsert diff --git a/packages/db/src/schema/properties/models.ts b/packages/db/src/schema/properties/models.ts new file mode 100644 index 00000000..2f6a51ab --- /dev/null +++ b/packages/db/src/schema/properties/models.ts @@ -0,0 +1,31 @@ +import { relations } from 'drizzle-orm' +import { pgTable } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { id, timestampsColumnsSoftDelete } from '../../helpers' +import { properties } from './properties' + +export const models = pgTable('properties_models', (t) => ({ + id: id('model'), + name: t.text('name'), + version: t.integer('version').default(1), + description: t.text('description'), + draft: t.boolean('draft').default(true), + propertyId: t + .text('property_id') + .references(() => properties.id, { onDelete: 'set null' }), + sceneGraph: t.jsonb('scene_graph'), + metadata: t.jsonb('metadata'), + ...timestampsColumnsSoftDelete, +})).enableRLS() + +export const modelsRelations = relations(models, ({ one }) => ({ + property: one(properties, { + fields: [models.propertyId], + references: [properties.id], + }), +})) + +export type Model = typeof models.$inferSelect +export type NewModel = typeof models.$inferInsert +export const insertModelSchema = createInsertSchema(models) +export const selectModelSchema = createSelectSchema(models) diff --git a/packages/db/src/schema/properties/properties.ts b/packages/db/src/schema/properties/properties.ts new file mode 100644 index 00000000..5e0dc416 --- /dev/null +++ b/packages/db/src/schema/properties/properties.ts @@ -0,0 +1,44 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { id, timestampsColumns } from '../../helpers' +import { users } from '../auth/users' +import { addresses } from './addresses' + +export const properties = pgTable( + 'properties', + (t) => ({ + id: id('property'), + name: t.text('name'), + addressId: t + .text('address_id') + .references(() => addresses.id, { onDelete: 'set null' }) + .unique(), + ownerId: t + .text('owner_id') + .references(() => users.id, { onDelete: 'set null' }), + detailsJson: t.jsonb('details_json'), + metadata: t.jsonb('metadata'), + ...timestampsColumns, + }), + (t) => [ + index('property_address_idx').on(t.addressId), + index('property_owner_idx').on(t.ownerId), + ], +).enableRLS() + +export const propertiesRelations = relations(properties, ({ one }) => ({ + address: one(addresses, { + fields: [properties.addressId], + references: [addresses.id], + }), + owner: one(users, { + fields: [properties.ownerId], + references: [users.id], + }), +})) + +export type Property = typeof properties.$inferSelect +export type NewProperty = typeof properties.$inferInsert +export const insertPropertySchema = createInsertSchema(properties) +export const selectPropertySchema = createSelectSchema(properties) diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts new file mode 100644 index 00000000..7e246c17 --- /dev/null +++ b/packages/db/src/types.ts @@ -0,0 +1,124 @@ +/** + * Database types for Supabase + * Generated from database schema + */ + +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] + +export interface Database { + public: { + Tables: { + properties: { + Row: { + id: string + name: string + owner_id: string + created_at: string + updated_at: string + } + Insert: { + id?: string + name: string + owner_id: string + created_at?: string + updated_at?: string + } + Update: { + id?: string + name?: string + owner_id?: string + created_at?: string + updated_at?: string + } + } + properties_addresses: { + Row: { + id: string + property_id: string + formatted_address: string + street_number: string | null + route: string | null + locality: string | null + administrative_area_level_1: string | null + administrative_area_level_2: string | null + country: string | null + postal_code: string | null + latitude: number | null + longitude: number | null + place_id: string | null + created_at: string + updated_at: string + } + Insert: { + id?: string + property_id: string + formatted_address: string + street_number?: string | null + route?: string | null + locality?: string | null + administrative_area_level_1?: string | null + administrative_area_level_2?: string | null + country?: string | null + postal_code?: string | null + latitude?: number | null + longitude?: number | null + place_id?: string | null + created_at?: string + updated_at?: string + } + Update: { + id?: string + property_id?: string + formatted_address?: string + street_number?: string | null + route?: string | null + locality?: string | null + administrative_area_level_1?: string | null + administrative_area_level_2?: string | null + country?: string | null + postal_code?: string | null + latitude?: number | null + longitude?: number | null + place_id?: string | null + created_at?: string + updated_at?: string + } + } + properties_models: { + Row: { + id: string + property_id: string + name: string + version: number + draft: boolean + scene_graph: Json | null + created_at: string + updated_at: string + } + Insert: { + id?: string + property_id: string + name: string + version?: number + draft?: boolean + scene_graph?: Json | null + created_at?: string + updated_at?: string + } + Update: { + id?: string + property_id?: string + name?: string + version?: number + draft?: boolean + scene_graph?: Json | null + created_at?: string + updated_at?: string + } + } + } + Views: {} + Functions: {} + Enums: {} + } +} diff --git a/packages/db/supabase/.branches/_current_branch b/packages/db/supabase/.branches/_current_branch new file mode 100644 index 00000000..88d050b1 --- /dev/null +++ b/packages/db/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/packages/db/supabase/.temp/cli-latest b/packages/db/supabase/.temp/cli-latest new file mode 100644 index 00000000..1dd61787 --- /dev/null +++ b/packages/db/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v2.75.0 \ No newline at end of file diff --git a/packages/db/supabase/config.toml b/packages/db/supabase/config.toml new file mode 100644 index 00000000..6a42a735 --- /dev/null +++ b/packages/db/supabase/config.toml @@ -0,0 +1,38 @@ +# Supabase local development configuration +# This file is used by `supabase start` for local development + +project_id = "pascal-editor" + +[api] +enabled = true +port = 54321 +schemas = ["public"] +extra_search_path = ["public"] +max_rows = 1000 + +[db] +port = 54322 +shadow_port = 54320 +major_version = 17 + +[studio] +enabled = true +port = 54323 + +[inbucket] +enabled = true +port = 54324 +smtp_port = 54325 +pop3_port = 54326 + +[auth] +enabled = true +site_url = "http://localhost:3000" +additional_redirect_urls = ["http://localhost:3000"] +jwt_expiry = 3600 +enable_signup = true + +[auth.email] +enable_signup = true +double_confirm_changes = false +enable_confirmations = false diff --git a/packages/db/supabase/migrations/20240211000001_create_auth_tables.sql b/packages/db/supabase/migrations/20240211000001_create_auth_tables.sql new file mode 100644 index 00000000..efb99103 --- /dev/null +++ b/packages/db/supabase/migrations/20240211000001_create_auth_tables.sql @@ -0,0 +1,80 @@ +-- Better Auth tables (copied from working monorepo) +-- Using auth_ prefix and snake_case columns + +CREATE TYPE "public"."auth_user_roles" AS ENUM('user', 'admin'); + +CREATE TABLE "auth_accounts" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "provider_id" text NOT NULL, + "account_id" text NOT NULL, + "password" text, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp with time zone, + "refresh_token_expires_at" timestamp with time zone, + "scope" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); + +ALTER TABLE "auth_accounts" ENABLE ROW LEVEL SECURITY; + +CREATE TABLE "auth_jwks" ( + "id" text PRIMARY KEY NOT NULL, + "public_key" text NOT NULL, + "private_key" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); + +ALTER TABLE "auth_jwks" ENABLE ROW LEVEL SECURITY; + +CREATE TABLE "auth_sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "expires_at" timestamp with time zone, + "token" text NOT NULL, + "ip_address" text, + "user_agent" text, + "impersonated_by" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); + +ALTER TABLE "auth_sessions" ENABLE ROW LEVEL SECURITY; + +CREATE TABLE "auth_users" ( + "id" text PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "name" text NOT NULL, + "image" text, + "role" "auth_user_roles" DEFAULT 'user' NOT NULL, + "banned" boolean DEFAULT false NOT NULL, + "ban_reason" text, + "ban_expires" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); + +ALTER TABLE "auth_users" ENABLE ROW LEVEL SECURITY; + +CREATE TABLE "auth_verifications" ( + "id" text PRIMARY KEY NOT NULL, + "value" text NOT NULL, + "identifier" text NOT NULL, + "expires_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); + +ALTER TABLE "auth_verifications" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "auth_accounts" ADD CONSTRAINT "auth_accounts_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action; + +ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action; + +ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_impersonated_by_auth_users_id_fk" FOREIGN KEY ("impersonated_by") REFERENCES "public"."auth_users"("id") ON DELETE set null ON UPDATE no action; + +CREATE UNIQUE INDEX "email_unique_index" ON "auth_users" USING btree (lower("email")); diff --git a/packages/db/supabase/migrations/20240211000002_create_properties_tables.sql b/packages/db/supabase/migrations/20240211000002_create_properties_tables.sql new file mode 100644 index 00000000..aa17fbdd --- /dev/null +++ b/packages/db/supabase/migrations/20240211000002_create_properties_tables.sql @@ -0,0 +1,165 @@ +-- Properties tables +-- Simple property management without organizations + +-- Properties addresses table (must be created first due to foreign key) +CREATE TABLE IF NOT EXISTS properties_addresses ( + id TEXT PRIMARY KEY, + street_number TEXT, + route TEXT, + route_short TEXT, + neighborhood TEXT, + city TEXT, + county TEXT, + state TEXT, + state_long TEXT, + postal_code TEXT, + postal_code_suffix TEXT, + country TEXT, + country_long TEXT, + latitude NUMERIC, + longitude NUMERIC, + raw_json JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(street_number, route, city, state, postal_code) +); + +CREATE INDEX IF NOT EXISTS idx_properties_addresses_city_state ON properties_addresses(city, state); + +-- Properties table +CREATE TABLE IF NOT EXISTS properties ( + id TEXT PRIMARY KEY, + name TEXT, + address_id TEXT UNIQUE REFERENCES properties_addresses(id) ON DELETE SET NULL, + owner_id TEXT REFERENCES auth_users(id) ON DELETE SET NULL, + details_json JSONB, + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_properties_owner_id ON properties(owner_id); +CREATE INDEX IF NOT EXISTS idx_properties_address_id ON properties(address_id); + +-- Properties models table (scene graphs) +CREATE TABLE IF NOT EXISTS properties_models ( + id TEXT PRIMARY KEY, + name TEXT, + version INTEGER DEFAULT 1, + description TEXT, + draft BOOLEAN DEFAULT true, + property_id TEXT REFERENCES properties(id) ON DELETE SET NULL, + scene_graph JSONB, + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_properties_models_property_id ON properties_models(property_id); +CREATE INDEX IF NOT EXISTS idx_properties_models_version ON properties_models(property_id, version DESC); + +-- Enable Row Level Security (RLS) +ALTER TABLE properties ENABLE ROW LEVEL SECURITY; +ALTER TABLE properties_addresses ENABLE ROW LEVEL SECURITY; +ALTER TABLE properties_models ENABLE ROW LEVEL SECURITY; + +-- RLS Policies for properties table +CREATE POLICY "Users can view their own properties" + ON properties FOR SELECT + USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL); + +CREATE POLICY "Users can insert their own properties" + ON properties FOR INSERT + WITH CHECK (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL); + +CREATE POLICY "Users can update their own properties" + ON properties FOR UPDATE + USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL); + +CREATE POLICY "Users can delete their own properties" + ON properties FOR DELETE + USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL); + +-- RLS Policies for properties_addresses table +-- Addresses table doesn't have property_id, so we'll allow all authenticated users +CREATE POLICY "Authenticated users can view all addresses" + ON properties_addresses FOR SELECT + USING (true); + +CREATE POLICY "Authenticated users can insert addresses" + ON properties_addresses FOR INSERT + WITH CHECK (true); + +CREATE POLICY "Authenticated users can update addresses" + ON properties_addresses FOR UPDATE + USING (true); + +CREATE POLICY "Authenticated users can delete addresses" + ON properties_addresses FOR DELETE + USING (true); + +-- RLS Policies for properties_models table +CREATE POLICY "Users can view models of their own properties" + ON properties_models FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM properties + WHERE properties.id = properties_models.property_id + AND properties.owner_id = current_setting('app.user_id', true)::TEXT + ) + ); + +CREATE POLICY "Users can insert models for their own properties" + ON properties_models FOR INSERT + WITH CHECK ( + EXISTS ( + SELECT 1 FROM properties + WHERE properties.id = properties_models.property_id + AND properties.owner_id = current_setting('app.user_id', true)::TEXT + ) + ); + +CREATE POLICY "Users can update models of their own properties" + ON properties_models FOR UPDATE + USING ( + EXISTS ( + SELECT 1 FROM properties + WHERE properties.id = properties_models.property_id + AND properties.owner_id = current_setting('app.user_id', true)::TEXT + ) + ); + +CREATE POLICY "Users can delete models of their own properties" + ON properties_models FOR DELETE + USING ( + EXISTS ( + SELECT 1 FROM properties + WHERE properties.id = properties_models.property_id + AND properties.owner_id = current_setting('app.user_id', true)::TEXT + ) + ); + +-- Trigger to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_properties_updated_at + BEFORE UPDATE ON properties + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_properties_addresses_updated_at + BEFORE UPDATE ON properties_addresses + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_properties_models_updated_at + BEFORE UPDATE ON properties_models + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/packages/db/supabase/migrations/20240211000003_add_active_property_to_sessions.sql b/packages/db/supabase/migrations/20240211000003_add_active_property_to_sessions.sql new file mode 100644 index 00000000..645cbaf3 --- /dev/null +++ b/packages/db/supabase/migrations/20240211000003_add_active_property_to_sessions.sql @@ -0,0 +1,2 @@ +-- Add active_property_id to sessions table +ALTER TABLE auth_sessions ADD COLUMN IF NOT EXISTS active_property_id TEXT; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 00000000..c6daff18 --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +}