diff --git a/app/.dev.vars.example b/app/.dev.vars.example new file mode 100644 index 0000000..4013d6c --- /dev/null +++ b/app/.dev.vars.example @@ -0,0 +1,15 @@ +# Copy to .dev.vars for local development +# These are secrets - never commit .dev.vars + +# JWT secret for session tokens +JWT_SECRET=your-secret-key-here + +# SMS provider (Twilio example) +TWILIO_ACCOUNT_SID=your-account-sid +TWILIO_AUTH_TOKEN=your-auth-token +TWILIO_PHONE_NUMBER=+1234567890 + +# WebAuthn +WEBAUTHN_RP_ID=localhost +WEBAUTHN_RP_NAME=Mini Chat +WEBAUTHN_ORIGIN=http://localhost:5173 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..98f3162 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.wrangler +.dev.vars diff --git a/app/PERMISSIONS.md b/app/PERMISSIONS.md new file mode 100644 index 0000000..d3fdf3a --- /dev/null +++ b/app/PERMISSIONS.md @@ -0,0 +1,229 @@ +# Permissions System + +Rock-solid, fully customizable role-based access control (RBAC) for Mini Chat. + +## Overview + +The system is built on three core concepts: + +1. **Permissions** - Atomic actions (send messages, kick users, edit settings) +2. **Roles** - Named collections of permissions (Owner, Moderator, Member) +3. **Assignments** - Which users have which roles in which rooms + +## Design Principles + +- **Explicit over implicit** - Every permission is clearly named and documented +- **Fully customizable** - Create custom roles with any combination of permissions +- **Room-scoped** - Each room has its own independent role system +- **Default sensible** - Comes with good defaults, but everything can be changed + +## Permission Categories + +### Room Permissions +- `room.delete` - Delete the entire room +- `room.transfer_ownership` - Transfer ownership to another user +- `room.archive` - Archive/unarchive the room + +### Settings Permissions +- `settings.edit_topic` - Change room topic +- `settings.edit_description` - Change room description +- `settings.make_durable` - Make room persistent (won't expire) +- `settings.make_private` - Make room invite-only +- `settings.manage_roles` - Create, edit, delete custom roles + +### Member Permissions +- `members.invite` - Invite users to the room +- `members.kick` - Remove users from the room +- `members.ban` - Ban users permanently or temporarily +- `members.mute` - Mute users (read-only mode) +- `members.assign_roles` - Assign roles to members +- `members.view_list` - View member list + +### Message Permissions +- `messages.send` - Send messages to the room +- `messages.edit_own` - Edit your own messages +- `messages.delete_own` - Delete your own messages +- `messages.edit_any` - Edit any message (moderation) +- `messages.delete_any` - Delete any message (moderation) +- `messages.pin` - Pin important messages +- `messages.search` - Search message history + +## Default Roles + +When a room is created, these roles are automatically set up: + +### Owner (System Role) +- **Cannot be deleted or edited** +- **Color:** `#FFD700` (gold) +- **Permissions:** ALL +- **Assignment:** Automatically assigned to room creator +- **Position:** 1000 (highest) + +### Moderator +- **Color:** `#3B3BBB` (purple-blue) +- **Permissions:** + - All member permissions (invite, kick, ban, mute, assign roles) + - All message moderation (edit any, delete any, pin) + - Some settings (edit topic, edit description) +- **Position:** 900 + +### Member (Default Role) +- **Color:** `#888888` (gray) +- **Permissions:** + - Send messages + - Edit own messages + - Delete own messages + - View member list + - Search messages +- **Position:** 100 +- **Note:** Automatically assigned to new members + +### Everyone (System Role) +- **Cannot be deleted** +- **Color:** `#CCCCCC` (light gray) +- **Permissions:** + - View member list + - Search messages (if room is not private) +- **Position:** 0 (lowest) +- **Note:** Applied to ALL users in the room, regardless of other roles + +## Custom Roles + +Room owners and moderators (with `settings.manage_roles`) can: + +1. **Create new roles** + ``` + POST /api/rooms/{room}/roles + { + "name": "VIP", + "description": "Trusted members", + "color": "#FF69B4", + "permissions": ["messages.send", "messages.pin", "members.invite"] + } + ``` + +2. **Edit existing roles** + - Change name, description, color + - Add/remove permissions + - Cannot edit system roles (Owner, Everyone) + +3. **Delete roles** + - Can delete any custom role + - Cannot delete system roles + - Users with that role lose it automatically + +4. **Set default role** + - Choose which role new members get + - Can only have one default role + +## Permission Hierarchy + +Roles have a `position` field that determines priority: + +- Higher position = more important +- Used for display order (Owner at top, Everyone at bottom) +- Used for "can manage" logic (can only manage roles below you) + +## Permission Checks + +The system checks permissions in this order: + +1. **Is user banned?** → Deny everything +2. **Is user muted?** → Deny `messages.send` (allow read) +3. **Is user owner?** → Allow everything +4. **Check user's roles** → Union of all permissions from all roles +5. **Check "Everyone" role** → Base permissions for all users + +## Database Schema + +### Tables + +**permissions** - All available permissions (seeded) +- `id`, `name`, `description`, `category` + +**roles** - Roles per room +- `id`, `room_id`, `name`, `description`, `color`, `is_default`, `is_system`, `position` + +**role_permissions** - Which permissions each role has +- `role_id`, `permission_id` + +**user_roles** - Which users have which roles +- `user_id`, `room_id`, `role_id`, `assigned_by`, `assigned_at` + +**room_bans** - Banned users +- `room_id`, `user_id`, `banned_by`, `reason`, `expires_at` + +**room_mutes** - Muted users +- `room_id`, `user_id`, `muted_by`, `reason`, `expires_at` + +**room_invites** - Invite codes for private rooms +- `room_id`, `code`, `invited_user_id`, `max_uses`, `expires_at` + +## API Endpoints + +### Role Management +- `GET /api/rooms/{room}/roles` - List all roles +- `POST /api/rooms/{room}/roles` - Create custom role +- `PATCH /api/rooms/{room}/roles/{role}` - Edit role +- `DELETE /api/rooms/{room}/roles/{role}` - Delete role +- `GET /api/rooms/{room}/roles/{role}/permissions` - List role permissions +- `PUT /api/rooms/{room}/roles/{role}/permissions` - Update role permissions + +### Member Management +- `GET /api/rooms/{room}/members` - List members with roles +- `POST /api/rooms/{room}/members/{user}/roles` - Assign role +- `DELETE /api/rooms/{room}/members/{user}/roles/{role}` - Remove role +- `POST /api/rooms/{room}/members/{user}/ban` - Ban user +- `DELETE /api/rooms/{room}/members/{user}/ban` - Unban user +- `POST /api/rooms/{room}/members/{user}/mute` - Mute user +- `DELETE /api/rooms/{room}/members/{user}/mute` - Unmute user + +### Invites (for private rooms) +- `POST /api/rooms/{room}/invites` - Create invite +- `GET /api/rooms/{room}/invites` - List invites +- `DELETE /api/rooms/{room}/invites/{invite}` - Revoke invite +- `POST /api/invites/{code}/accept` - Join via invite code + +### Permission Checks +- `GET /api/rooms/{room}/permissions/me` - Get my permissions +- `GET /api/rooms/{room}/permissions/{user}` - Get user's permissions (if allowed) + +## Usage Examples + +### Check if user can kick someone +```typescript +const canKick = await hasPermission(userId, roomId, 'members.kick'); +if (!canKick) { + throw error(403, 'You do not have permission to kick members'); +} +``` + +### Create a "Read-Only" role +```typescript +await createRole(roomId, { + name: 'Read-Only', + description: 'Can view but not participate', + permissions: ['members.view_list', 'messages.search'], + color: '#AAAAAA' +}); +``` + +### Make user a moderator +```typescript +await assignRole(userId, roomId, moderatorRoleId, assignedBy); +``` + +## Migration Path + +Existing rooms will get: +- Default roles created automatically +- Room creator assigned Owner role +- All other current members assigned Member role + +## Future Enhancements + +Possible additions: +- Permission overrides per user (grant/deny specific permissions) +- Role hierarchies (inherit from parent roles) +- Time-based roles (temporary moderator for 7 days) +- Audit log for permission changes diff --git a/app/QUICKSTART.md b/app/QUICKSTART.md new file mode 100644 index 0000000..1f7b18d --- /dev/null +++ b/app/QUICKSTART.md @@ -0,0 +1,129 @@ +# Quick Start Guide + +Get up and running with Mini Chat v2 in 5 minutes. + +## Option 1: Cloudflare (Full Features) + +### 1. Install Wrangler +```bash +npm install -g wrangler +wrangler login +``` + +### 2. Create D1 Database +```bash +cd app +wrangler d1 create mini-chat +``` + +Copy the `database_id` from the output and update `wrangler.toml`: +```toml +[[d1_databases]] +binding = "DB" +database_name = "mini-chat" +database_id = "YOUR_DATABASE_ID_HERE" # Replace this +``` + +### 3. Initialize Schema +```bash +wrangler d1 execute mini-chat --local --file=schema.sql +``` + +### 4. Set Up Environment +```bash +cp .dev.vars.example .dev.vars +# Edit .dev.vars with your secrets +``` + +### 5. Run Dev Server +```bash +npm install +npm run dev +``` + +Visit http://localhost:5173 + +## Option 2: Local Only (Limited Features) + +For quick testing without Cloudflare setup: + +```bash +cd app +npm install + +# Use Wrangler's local mode (includes local D1) +npx wrangler d1 execute mini-chat --local --file=schema.sql +npm run dev +``` + +Note: This uses Wrangler's local D1 emulation. Some Cloudflare-specific features may not work. + +## What's Working + +✅ SvelteKit app structure +✅ CSS design system ported +✅ Database schema (D1/SQLite) +✅ Storage abstraction layer +✅ API routes for rooms & messages +✅ Durable Object for WebSocket (structure ready) + +## What's Next + +The foundation is solid! Next steps: + +1. **Finish WebSocket UI** - Connect frontend to Durable Objects +2. **Add Auth** - WebAuthn + SMS verification +3. **Build MCP Server** - AI agent integration +4. **Add Cleanup Jobs** - Expire old ephemeral rooms + +## Architecture Highlights + +### Abstraction Layers + +The app uses clean interfaces so you can swap technologies: + +- **Storage**: `StorageAdapter` interface + - Current: D1 (Cloudflare SQLite) + - Easy swap to: PostgreSQL, MySQL, MongoDB + +- **Messaging**: `MessagingAdapter` interface + - Current: Durable Objects + - Easy swap to: Redis, Socket.io, SSE + +See `src/lib/server/README.md` for details. + +### Project Structure + +``` +app/ +├── src/ +│ ├── lib/server/ # Backend abstractions +│ │ ├── storage/ # DB layer (swappable) +│ │ ├── messaging/ # Real-time layer (swappable) +│ │ └── ChatRoom.ts # Durable Object +│ ├── routes/ +│ │ ├── api/ # REST endpoints +│ │ └── +page.svelte # Main UI +│ └── app.css # Polished design system +├── schema.sql # Database schema +└── wrangler.toml # Cloudflare config +``` + +## Troubleshooting + +### "Database not initialized" +- Make sure you've run `wrangler d1 execute mini-chat --local --file=schema.sql` +- Check `wrangler.toml` has correct `database_id` + +### Port already in use +- Change port in `package.json`: `"dev": "vite dev --port 3000"` + +### TypeScript errors +- Run `npm run check` to see detailed errors +- The app is set up for strict TypeScript + +## Next: Try it out! + +The UI is basic but functional. Open http://localhost:5173 and you should see the chat interface with your ported CSS design. + +Ready to add WebSockets, auth, or MCP? Let's keep building! diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..1f88c22 --- /dev/null +++ b/app/README.md @@ -0,0 +1,159 @@ +# Mini Chat v2 + +A modern, minimalist chat application built with SvelteKit and Cloudflare. Features ephemeral rooms, WebAuthn authentication, and AI agent integration via MCP. + +## Features + +- **Ephemeral by default**: Rooms expire after inactivity +- **Optional persistence**: Mark important chats as durable +- **Full-text search**: Search through durable chats +- **Modern auth**: WebAuthn/Passkeys with SMS fallback +- **AI-ready**: MCP server for agent integration +- **Edge deployment**: Runs on Cloudflare Workers + Durable Objects + +## Tech Stack + +- **Frontend**: SvelteKit 2 + Svelte 5 Runes +- **Backend**: Cloudflare Workers +- **Real-time**: Durable Objects (WebSocket) +- **Database**: D1 (distributed SQLite) +- **Auth**: WebAuthn + Twilio SMS +- **Deployment**: Cloudflare Pages + +## Local Development + +### Prerequisites + +- Node.js 18+ +- Wrangler CLI: `npm install -g wrangler` +- Cloudflare account (for D1 database) + +### Setup + +1. **Install dependencies**: + ```bash + cd app + npm install + ``` + +2. **Create D1 database**: + ```bash + wrangler d1 create mini-chat + ``` + + Copy the `database_id` from output and update `wrangler.toml` + +3. **Initialize database schema**: + ```bash + wrangler d1 execute mini-chat --file=schema.sql + ``` + +4. **Set up environment variables**: + ```bash + cp .dev.vars.example .dev.vars + # Edit .dev.vars with your secrets + ``` + +5. **Run dev server**: + ```bash + npm run dev + ``` + + Visit http://localhost:5173 + +## Architecture + +This app uses **abstraction layers** to make it easy to swap technologies: + +### Storage Layer (`src/lib/server/storage/`) +- `StorageAdapter` interface +- Current: `D1StorageAdapter` (Cloudflare D1) +- Easy to swap: PostgreSQL, MySQL, MongoDB + +### Messaging Layer (`src/lib/server/messaging/`) +- `MessagingAdapter` interface +- Current: Durable Objects (WebSocket) +- Easy to swap: Redis, Socket.io, SSE + +See `src/lib/server/README.md` for details on swapping technologies. + +## Deployment + +### Cloudflare + +1. **Create D1 database in production**: + ```bash + wrangler d1 create mini-chat --remote + wrangler d1 execute mini-chat --file=schema.sql --remote + ``` + +2. **Set secrets**: + ```bash + wrangler secret put JWT_SECRET + wrangler secret put TWILIO_AUTH_TOKEN + ``` + +3. **Deploy**: + ```bash + npm run build + npm run deploy + ``` + +## API Endpoints + +- `GET /api/ws?room=NAME&userId=ID&userName=NAME` - WebSocket connection +- `GET /api/rooms/[room]` - Get room info +- `POST /api/rooms/[room]` - Create/update room +- `GET /api/rooms/[room]/messages` - Get messages +- `POST /api/rooms/[room]/messages` - Search messages + +## MCP Integration + +AI agents can connect via Model Context Protocol: + +```json +{ + "mcpServers": { + "mini-chat": { + "url": "https://your-domain.com/mcp" + } + } +} +``` + +Agents can: +- Join/create rooms +- Send/receive messages +- Search chat history +- See public user info (not phone numbers) + +## Project Structure + +``` +app/ +├── src/ +│ ├── lib/ +│ │ └── server/ +│ │ ├── storage/ # Database abstraction +│ │ ├── messaging/ # Real-time messaging abstraction +│ │ ├── ChatRoom.ts # Durable Object for WebSocket +│ │ └── README.md # Architecture docs +│ ├── routes/ +│ │ ├── api/ # API endpoints +│ │ └── +page.svelte # Main chat UI +│ ├── app.css # Global styles (ported from v1) +│ └── app.html # HTML template +├── schema.sql # D1 database schema +├── wrangler.toml # Cloudflare config +└── package.json +``` + +## Original Project + +This is a modernized rewrite of the original Deno-based mini-chat. The original used: +- Deno runtime +- Redis for state + pub/sub +- Custom command protocol +- JWT sessions + +See `../src/` for original implementation. diff --git a/app/TESTING.md b/app/TESTING.md new file mode 100644 index 0000000..0984026 --- /dev/null +++ b/app/TESTING.md @@ -0,0 +1,170 @@ +# Testing Guide + +## Automated Tests + +We have unit tests for critical auth utilities: + +```bash +npm install +npm test +``` + +This tests: +- Phone number hashing +- Verification code generation +- JWT token creation/verification +- Expiration timestamp calculations + +## Manual Testing + +Full integration testing requires a Cloudflare environment because the app uses: +- **D1 Database** (Cloudflare's distributed SQLite) +- **Durable Objects** (for WebSocket coordination) + +### Option 1: Local Testing with Wrangler + +1. **Install dependencies:** + ```bash + cd app + npm install + ``` + +2. **Initialize local D1:** + ```bash + npx wrangler d1 execute mini-chat --local --file=schema.sql + ``` + +3. **Create .dev.vars:** + ```bash + cp .dev.vars.example .dev.vars + # Edit with your values (or use defaults for local) + ``` + +4. **Build the app:** + ```bash + npm run build + ``` + +5. **Run with Wrangler:** + ```bash + npx wrangler pages dev .svelte-kit/cloudflare + ``` + + This starts a local server with Cloudflare bindings emulated. + +6. **Test the flow:** + - Open http://localhost:8788 + - Sign in with phone (in dev mode, SMS code is logged to console) + - Set display name + - Optionally set up passkey (requires HTTPS - use ngrok or similar) + - Join a room + - Open another browser/tab, sign in as different user + - Chat in real-time! + +### Option 2: Deploy to Cloudflare (Recommended) + +The easiest way to test all features is to deploy: + +1. **Create Cloudflare account** (free tier works!) + +2. **Create production D1 database:** + ```bash + npx wrangler d1 create mini-chat --remote + # Update wrangler.toml with the database_id + ``` + +3. **Initialize remote database:** + ```bash + npx wrangler d1 execute mini-chat --remote --file=schema.sql + ``` + +4. **Set production secrets:** + ```bash + npx wrangler secret put JWT_SECRET + # Enter a secure random string + + # Optional: For SMS + npx wrangler secret put TWILIO_ACCOUNT_SID + npx wrangler secret put TWILIO_AUTH_TOKEN + npx wrangler secret put TWILIO_PHONE_NUMBER + ``` + +5. **Deploy:** + ```bash + npm run build + npx wrangler pages deploy .svelte-kit/cloudflare --project-name mini-chat + ``` + +6. **Test on your deployed URL!** + +## What to Test + +### Auth Flow +- [ ] Phone number entry (any format) +- [ ] SMS code reception (check console in dev mode) +- [ ] Code verification (try wrong code, expired code) +- [ ] Display name setup +- [ ] Passkey registration (on HTTPS only) +- [ ] Passkey authentication (on HTTPS only) +- [ ] Session persistence (refresh page, still logged in) + +### Chat Features +- [ ] Room creation via URL `/r/test-room` +- [ ] Real-time message sending/receiving +- [ ] Multiple users in same room +- [ ] Message history loaded on join +- [ ] User join/leave events +- [ ] Auto-reconnection (close network, wait, reconnect) +- [ ] Different rooms isolated from each other + +### Edge Cases +- [ ] Long messages +- [ ] Special characters in messages +- [ ] Rapid message sending +- [ ] Network interruption +- [ ] Multiple tabs/windows for same user +- [ ] Room names with special characters + +## Known Limitations (Local Testing) + +- **Passkeys require HTTPS** - Use ngrok or deploy to test WebAuthn +- **SMS requires Twilio** - Use mock provider (logs to console) for local dev +- **Durable Objects** - Wrangler emulation may differ from production + +## CI/CD Integration + +To add automated testing in CI: + +```yaml +# .github/workflows/test.yml +name: Test + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - run: cd app && npm install + - run: cd app && npm test + - run: cd app && npm run check # Type checking +``` + +## Debugging + +**Check logs:** +```bash +npx wrangler tail # For deployed version +``` + +**Check local D1:** +```bash +npx wrangler d1 execute mini-chat --local --command "SELECT * FROM users" +``` + +**Browser DevTools:** +- Network tab → See WebSocket connection +- Console → See any errors +- Application tab → Check cookies/session storage diff --git a/app/migrations/002_permissions.sql b/app/migrations/002_permissions.sql new file mode 100644 index 0000000..b7a13f8 --- /dev/null +++ b/app/migrations/002_permissions.sql @@ -0,0 +1,152 @@ +-- Permissions and Roles System +-- Flexible RBAC with customizable roles per room + +-- Core permissions that can be granted +-- These are the atomic actions users can perform +CREATE TABLE IF NOT EXISTS permissions ( + id TEXT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + description TEXT NOT NULL, + category TEXT NOT NULL, -- 'room', 'members', 'messages', 'settings' + created_at INTEGER NOT NULL DEFAULT (unixepoch()) +); + +-- Insert default permissions +INSERT OR IGNORE INTO permissions (id, name, description, category) VALUES + -- Room permissions + ('perm_room_delete', 'room.delete', 'Delete the entire room', 'room'), + ('perm_room_transfer', 'room.transfer_ownership', 'Transfer room ownership to another user', 'room'), + ('perm_room_archive', 'room.archive', 'Archive/unarchive the room', 'room'), + + -- Settings permissions + ('perm_settings_topic', 'settings.edit_topic', 'Change room topic', 'settings'), + ('perm_settings_description', 'settings.edit_description', 'Change room description', 'settings'), + ('perm_settings_durable', 'settings.make_durable', 'Make room durable (persistent)', 'settings'), + ('perm_settings_private', 'settings.make_private', 'Make room private (invite-only)', 'settings'), + ('perm_settings_roles', 'settings.manage_roles', 'Create, edit, delete custom roles', 'settings'), + + -- Member permissions + ('perm_members_invite', 'members.invite', 'Invite users to the room', 'members'), + ('perm_members_kick', 'members.kick', 'Remove users from the room', 'members'), + ('perm_members_ban', 'members.ban', 'Ban users from the room', 'members'), + ('perm_members_mute', 'members.mute', 'Mute users (they can read but not send)', 'members'), + ('perm_members_assign_roles', 'members.assign_roles', 'Assign roles to members', 'members'), + ('perm_members_view', 'members.view_list', 'View member list', 'members'), + + -- Message permissions + ('perm_msg_send', 'messages.send', 'Send messages to the room', 'messages'), + ('perm_msg_edit_own', 'messages.edit_own', 'Edit your own messages', 'messages'), + ('perm_msg_delete_own', 'messages.delete_own', 'Delete your own messages', 'messages'), + ('perm_msg_edit_any', 'messages.edit_any', 'Edit any message', 'messages'), + ('perm_msg_delete_any', 'messages.delete_any', 'Delete any message', 'messages'), + ('perm_msg_pin', 'messages.pin', 'Pin important messages', 'messages'), + ('perm_msg_search', 'messages.search', 'Search message history', 'messages'); + +-- Roles: Named collections of permissions +CREATE TABLE IF NOT EXISTS roles ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + color TEXT, -- Hex color for UI display + is_default BOOLEAN NOT NULL DEFAULT 0, -- Default role for new members + is_system BOOLEAN NOT NULL DEFAULT 0, -- System role (owner, everyone) - can't be deleted + position INTEGER NOT NULL DEFAULT 0, -- Higher position = more important (for display order) + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, + UNIQUE(room_id, name) +); + +CREATE INDEX idx_roles_room ON roles(room_id); +CREATE INDEX idx_roles_position ON roles(room_id, position); + +-- Which permissions does each role have +CREATE TABLE IF NOT EXISTS role_permissions ( + role_id TEXT NOT NULL, + permission_id TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE +); + +CREATE INDEX idx_role_perms_role ON role_permissions(role_id); + +-- Which users have which roles in which rooms +CREATE TABLE IF NOT EXISTS user_roles ( + user_id TEXT NOT NULL, + room_id TEXT NOT NULL, + role_id TEXT NOT NULL, + assigned_by TEXT, -- User who assigned this role + assigned_at INTEGER NOT NULL DEFAULT (unixepoch()), + PRIMARY KEY (user_id, room_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE INDEX idx_user_roles_user ON user_roles(user_id, room_id); +CREATE INDEX idx_user_roles_room ON user_roles(room_id); + +-- Bans and mutes +CREATE TABLE IF NOT EXISTS room_bans ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, + banned_by TEXT NOT NULL, + reason TEXT, + expires_at INTEGER, -- NULL = permanent + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (banned_by) REFERENCES users(id) ON DELETE SET NULL, + UNIQUE(room_id, user_id) +); + +CREATE INDEX idx_bans_room ON room_bans(room_id); +CREATE INDEX idx_bans_user ON room_bans(user_id); +CREATE INDEX idx_bans_expires ON room_bans(expires_at); + +CREATE TABLE IF NOT EXISTS room_mutes ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, + muted_by TEXT NOT NULL, + reason TEXT, + expires_at INTEGER, -- NULL = permanent + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (muted_by) REFERENCES users(id) ON DELETE SET NULL, + UNIQUE(room_id, user_id) +); + +CREATE INDEX idx_mutes_room ON room_mutes(room_id); +CREATE INDEX idx_mutes_user ON room_mutes(user_id); +CREATE INDEX idx_mutes_expires ON room_mutes(expires_at); + +-- Add description and private flag to rooms +ALTER TABLE rooms ADD COLUMN description TEXT; +ALTER TABLE rooms ADD COLUMN is_private INTEGER NOT NULL DEFAULT 0; + +-- Invites for private rooms +CREATE TABLE IF NOT EXISTS room_invites ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL, + invited_user_id TEXT, -- NULL = public invite code + invited_by TEXT NOT NULL, + code TEXT UNIQUE, -- For shareable invite links + uses INTEGER NOT NULL DEFAULT 0, + max_uses INTEGER, -- NULL = unlimited + expires_at INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, + FOREIGN KEY (invited_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (invited_by) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE INDEX idx_invites_room ON room_invites(room_id); +CREATE INDEX idx_invites_code ON room_invites(code); +CREATE INDEX idx_invites_user ON room_invites(invited_user_id); diff --git a/app/migrations/003_agents.sql b/app/migrations/003_agents.sql new file mode 100644 index 0000000..70b2247 --- /dev/null +++ b/app/migrations/003_agents.sql @@ -0,0 +1,60 @@ +-- Agent System for Mini Chat +-- Allows external agents to participate in rooms via webhooks + +-- Add user_type to distinguish humans from agents +ALTER TABLE users ADD COLUMN user_type TEXT NOT NULL DEFAULT 'human'; +CREATE INDEX idx_users_type ON users(user_type); + +-- API tokens for agent authentication +CREATE TABLE IF NOT EXISTS api_tokens ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + token_hash TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + last_used_at INTEGER, + expires_at INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE INDEX idx_api_tokens_user ON api_tokens(user_id); +CREATE INDEX idx_api_tokens_hash ON api_tokens(token_hash); +CREATE INDEX idx_api_tokens_expires ON api_tokens(expires_at); + +-- Agent registration in rooms +CREATE TABLE IF NOT EXISTS room_agents ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, -- The agent's user account + webhook_url TEXT NOT NULL, + webhook_secret TEXT, -- Optional secret for webhook signature verification + trigger_config TEXT NOT NULL DEFAULT '{"type":"all"}', -- JSON config: all, mentions, keywords, etc. + is_active INTEGER NOT NULL DEFAULT 1, + created_by TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, + UNIQUE(room_id, user_id) +); + +CREATE INDEX idx_room_agents_room ON room_agents(room_id); +CREATE INDEX idx_room_agents_user ON room_agents(user_id); +CREATE INDEX idx_room_agents_active ON room_agents(room_id, is_active); + +-- Webhook delivery logs (for debugging and monitoring) +CREATE TABLE IF NOT EXISTS webhook_logs ( + id TEXT PRIMARY KEY, + room_agent_id TEXT NOT NULL, + message_id TEXT, + status_code INTEGER, + response_time_ms INTEGER, + error TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (room_agent_id) REFERENCES room_agents(id) ON DELETE CASCADE, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL +); + +CREATE INDEX idx_webhook_logs_agent ON webhook_logs(room_agent_id); +CREATE INDEX idx_webhook_logs_created ON webhook_logs(created_at); diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..3a6ee1f --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,5256 @@ +{ + "name": "mini-chat-v2", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mini-chat-v2", + "version": "0.0.1", + "dependencies": { + "@simplewebauthn/browser": "^11.0.0", + "@simplewebauthn/server": "^11.0.0", + "jose": "^5.9.6" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250110.0", + "@sveltejs/adapter-cloudflare": "^4.7.2", + "@sveltejs/kit": "^2.14.0", + "@sveltejs/vite-plugin-svelte": "^5.0.4", + "svelte": "^5.17.0", + "svelte-check": "^4.1.3", + "typescript": "^5.7.3", + "vite": "^6.0.7", + "vitest": "^2.1.8", + "wrangler": "^3.103.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260210.0.tgz", + "integrity": "sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, + "node_modules/@peculiar/asn1-android": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.6.0.tgz", + "integrity": "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@simplewebauthn/browser": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-11.0.0.tgz", + "integrity": "sha512-KEGCStrl08QC2I561BzxqGiwoknblP6O1YW7jApdXLPtIqZ+vgJYAv8ssLCdm1wD8HGAHd49CJLkUF8X70x/pg==", + "license": "MIT", + "dependencies": { + "@simplewebauthn/types": "^11.0.0" + } + }, + "node_modules/@simplewebauthn/server": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-11.0.0.tgz", + "integrity": "sha512-zu8dxKcPiRUNSN2kmrnNOzNbRI8VaR/rL4ENCHUfC6PEE7SAAdIql9g5GBOd/wOVZolIsaZz3ccFxuGoVP0iaw==", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.3.10", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "@simplewebauthn/types": "^11.0.0", + "cross-fetch": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@simplewebauthn/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-11.0.0.tgz", + "integrity": "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-cloudflare": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-cloudflare/-/adapter-cloudflare-4.9.0.tgz", + "integrity": "sha512-o7o8wXy5zDsEuE9oPWSHO5tAuPEulZZg2QavFdc00fcIHh1dxgCyIRZa5LPjAE8EcdJOh+8SFkhFgVRdCfOBvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/workers-types": "^4.20231121.0", + "esbuild": "^0.24.0", + "worktop": "0.8.0-next.18" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0", + "wrangler": "^3.28.4" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.50.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.2.tgz", + "integrity": "sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.2", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", + "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", + "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "3.20250718.3", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", + "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250718.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/miniflare/node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-inject/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", + "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stacktracey": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/svelte": { + "version": "5.50.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.50.2.tgz", + "integrity": "sha512-WCxzm3BBf+Ase6RwiDPR4G36cM4Kb0NuhmLK6x44I+D6reaxizDDg8kBkk4jT/19+Rgmc44eZkOvMO6daoSFIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.2", + "esm-env": "^1.2.1", + "esrap": "^2.2.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.6.tgz", + "integrity": "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", + "ufo": "^1.5.4" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/worktop": { + "version": "0.8.0-next.18", + "resolved": "https://registry.npmjs.org/worktop/-/worktop-0.8.0-next.18.tgz", + "integrity": "sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mrmime": "^2.0.0", + "regexparam": "^3.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler": { + "version": "3.114.17", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", + "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@cloudflare/unenv-preset": "2.0.2", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.17.19", + "miniflare": "3.20250718.3", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.14", + "workerd": "1.20250718.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250408.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/youch/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..3c838ec --- /dev/null +++ b/app/package.json @@ -0,0 +1,33 @@ +{ + "name": "mini-chat-v2", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest", + "test:ui": "vitest --ui", + "deploy": "wrangler deploy" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250110.0", + "@sveltejs/adapter-cloudflare": "^4.7.2", + "@sveltejs/kit": "^2.14.0", + "@sveltejs/vite-plugin-svelte": "^5.0.4", + "svelte": "^5.17.0", + "svelte-check": "^4.1.3", + "typescript": "^5.7.3", + "vite": "^6.0.7", + "vitest": "^2.1.8", + "wrangler": "^3.103.0" + }, + "dependencies": { + "@simplewebauthn/browser": "^11.0.0", + "@simplewebauthn/server": "^11.0.0", + "jose": "^5.9.6" + } +} diff --git a/app/schema.sql b/app/schema.sql new file mode 100644 index 0000000..8293f29 --- /dev/null +++ b/app/schema.sql @@ -0,0 +1,109 @@ +-- Mini Chat v2 Database Schema +-- Designed for Cloudflare D1 (SQLite) + +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + phone_hash TEXT UNIQUE NOT NULL, + display_name TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()) +); + +CREATE INDEX idx_users_phone_hash ON users(phone_hash); + +-- WebAuthn credentials (passkeys) +CREATE TABLE IF NOT EXISTS credentials ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + credential_id TEXT UNIQUE NOT NULL, + credential_public_key BLOB NOT NULL, + counter INTEGER NOT NULL DEFAULT 0, + device_type TEXT, + backed_up INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + last_used_at INTEGER, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE INDEX idx_credentials_user_id ON credentials(user_id); +CREATE INDEX idx_credentials_credential_id ON credentials(credential_id); + +-- Sessions +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + expires_at INTEGER NOT NULL, + device_info TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE INDEX idx_sessions_user_id ON sessions(user_id); +CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); + +-- Rooms +CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + owner_id TEXT, + topic TEXT, + is_durable INTEGER NOT NULL DEFAULT 0, + soft_expired_at INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE INDEX idx_rooms_name ON rooms(name); +CREATE INDEX idx_rooms_owner_id ON rooms(owner_id); +CREATE INDEX idx_rooms_soft_expired_at ON rooms(soft_expired_at); +CREATE INDEX idx_rooms_is_durable ON rooms(is_durable); + +-- Messages +CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL, + user_id TEXT, + content TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE INDEX idx_messages_room_id ON messages(room_id); +CREATE INDEX idx_messages_user_id ON messages(user_id); +CREATE INDEX idx_messages_created_at ON messages(created_at); +CREATE INDEX idx_messages_room_created ON messages(room_id, created_at); + +-- Full-text search for durable rooms +CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( + content, + content=messages, + content_rowid=rowid +); + +-- Triggers to keep FTS in sync +CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN + INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content); +END; + +CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; +END; + +CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content); +END; + +-- Phone verification codes (temporary) +CREATE TABLE IF NOT EXISTS verification_codes ( + phone_hash TEXT PRIMARY KEY, + code TEXT NOT NULL, + expires_at INTEGER NOT NULL, + attempts INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL DEFAULT (unixepoch()) +); + +CREATE INDEX idx_verification_expires_at ON verification_codes(expires_at); diff --git a/app/src/app.css b/app/src/app.css new file mode 100644 index 0000000..4566d09 --- /dev/null +++ b/app/src/app.css @@ -0,0 +1,497 @@ +/* Ported from original mini-chat CSS - polished for mobile Safari */ + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 13pt; + font-weight: 300; + background: #111; + background-repeat: no-repeat; + color: #eee; + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: -webkit-fill-available; + padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0) 0 env(safe-area-inset-left, 0); + box-sizing: border-box; +} + +a { + color: #fff; + text-decoration-color: #ccc; +} + +a:hover { + opacity: 0.75; +} + +html { + height: -webkit-fill-available; +} + +/* General container for responsive design */ +.container { + display: block; + width: 100%; + margin: 0 auto; + max-width: 960px; + padding: 0 1rem; + box-sizing: border-box; +} + +.container--narrow { + width: max-content; +} + +.form { + margin: 0; +} + +.form__box { + display: flex; +} + +.form__label { + display: block; + font: inherit; + margin-bottom: 1rem; +} + +.form__input { + display: block; + margin: 0 1rem 0 0; + font: inherit; + width: 100%; + outline: none; + background-color: rgba(255,255,255,0.1); + border-radius: 0.5rem; + color: #fff; + border: none; + padding: 1rem; + box-sizing: border-box; +} + +.form__input:focus { + background-color: rgba(255,255,255,0.2); +} + +/* Cool floating button */ +.button { + cursor: pointer; + font: inherit; + background: transparent; + padding: 0.5rem; + color: #fff; + border: none; + border-radius: 0.5rem; + text-shadow: inherit; +} + +.button--filled { + color: #3b3bbb; + background-color: #fff; + padding: 0.5rem 2rem; +} + +.button--right { + float: right; +} + +.button:hover { + opacity: 0.75; +} + +::placeholder { + color: #ccc; + opacity: 1; +} + +:-ms-input-placeholder { + color: #ccc; +} + +::-ms-input-placeholder { + color: #ccc; +} + +/* An individual message */ +.message { + line-height: 1.4; + display: block; + margin: 0.5rem 0; + padding: 0.5rem 1rem; + color: #eee; + word-wrap: break-word; + max-width: 100%; + width: 100%; + box-sizing: border-box; + background: linear-gradient(90deg, #222, #111); + border-left: 1px solid; +} + +.message--chat { + background: transparent; + border: none; + padding: 0; + margin: 1rem 0; +} + +/* General info */ +.message--info { + color: #3bbbbb; +} + +.message--event { + color: #aaa; + font-style: italic; + display: block; + text-align: center; + background: transparent; + border: none; + padding: 0; +} + +.message--event::before { + content: "( "; +} + +.message--event::after { + content: " )"; +} + +/* Error messages */ +.message--error { + color: #bb3b3b; +} + +/* Warning message */ +.message--warning { + color: #bbbb3b; +} + +/* Success message */ +.message--success { + color: #3bbb3b; +} + +.message__bubbles { + width: 100%; + display: flex; + flex-direction: column; +} + +.message__bubbles--self { + align-items: flex-end; +} + +.message__bubble { + color: #fff; + background: #3b3bbb; + background: linear-gradient(90deg, #3b3bbb 0%, #5959b8 100%); + box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.25); + margin: 1px 0; + padding: 0.5rem 1rem; + width: max-content; + word-wrap: break-word; + max-width: 100%; + border-top-right-radius: 1rem; + border-bottom-right-radius: 1rem; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; + box-sizing: border-box; + animation: animateIn 100ms ease; +} + +.message__bubbles--self .message__bubble { + border-top-left-radius: 1rem; + border-bottom-left-radius: 1rem; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.message__bubble:first-child { + border-top-left-radius: 1rem; +} + +.message__bubbles--self .message__bubble:first-child { + border-top-right-radius: 1rem; +} + +.message__bubble:last-child { + border-bottom-left-radius: 1rem; +} + +.message__bubbles--self .message__bubble:last-child { + border-bottom-right-radius: 1rem; +} + +/* Message sender */ +.message__sender { + display: block; + font-size: 0.9rem; + color: #eee; + font-weight: normal; + margin-bottom: 2px; +} + +.message__sender--self { + text-align: right; +} + +.qr-code { + display: block; + max-width: 100%; + padding: 1rem; + margin: auto; + box-sizing: border-box; +} + +.textbox { + position: sticky; + bottom: 0; + display: block; + background: linear-gradient(0deg, #111111, #11111100); + text-shadow: 0 0 20px #000, 0 0 20px #000; + padding-bottom: env(safe-area-inset-bottom, 0); +} + +.textbox .container { + display: flex; + align-items: flex-end; +} + +.textbox__input { + outline: none; + flex: auto; + padding: 1.5rem 0; + word-wrap: break-word; + max-width: auto; + min-width: 0; +} + +.textbox__input:empty::before { + color: #888; + content: attr(placeholder) +} + +.textbox__button { + padding: 1.5rem 0; +} + +.main { + flex: auto; + display: flex; + flex-direction: column; +} + +.chat-screen { + flex: auto; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.header { + padding: 1rem 0 0.5rem 0; + position: sticky; + top: 0; + background: linear-gradient(180deg, #111111, #11111100); + text-shadow: 0 0 20px #000, 0 0 20px #000; + overflow: hidden; +} + +.header__room { + margin: 0; +} + +.header__room:empty { + text-align: center; +} + +.header__room:empty::after { + content: "Welcome!"; +} + +.header__room:not(:empty):before { + content: "/"; + margin-right: 0.5rem; + color: #3b3bbb; +} + +.header__topic { + margin: 0.5rem 0; + font-size: 0.9rem; +} + +.header__topic:empty { + display: none; +} + +.loading-screen { + z-index: 2; + color: #fff; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.loading-screen__text { + font-size: 1.4rem; +} + +.loading-screen__wrapper { + animation: fadeIn 1000ms ease forwards; +} + +.loading-screen__spinner { + margin: 1rem; + display: block; + width: 2rem; + height: 2rem; + border: 5px solid transparent; + border-top: 5px solid #3b3bbb; + border-right: 5px solid #3b3bbb; + border-radius: 2rem; + animation: spin 1000ms infinite linear; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes fadeIn { + from { + visibility: hidden; + opacity: 0; + } + to { + opacity: 1; + visibility: visible; + } +} + +@keyframes fadeOut { + to { + opacity: 0; + visibility: hidden; + } +} + +.name-form-screen { + position: fixed; + z-index: 2; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + background: #3b3bbb; + background-image: linear-gradient(90deg, #3b3bbb 0%, #5959b8 100%); +} + +.name-form__error { + padding: 1rem; + background: linear-gradient(90deg, #222, #111); + border-radius: 0.5rem; +} + +.name-form__error:empty { + display: none; +} + +.name-form__error--error { + color: #bb3b3b; +} + +.name-form__error--warning { + color: #bbbb3b; +} + +.name-form__error--success { + color: #3bbb3b; +} + +.fade-out { + animation: fadeOut 200ms ease forwards; +} + +.fade-in { + animation: fadeIn 200ms ease forwards; +} + +.hidden { + opacity: 0; + visibility: hidden; +} + +.name-form-screen__wrapper { + margin: auto; + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: center; + max-width: 400px; +} + +.name-form-screen__heading { + margin: 0 0 0.5rem 0; + font-size: 2.5rem; +} + +.name-form-screen__description { + margin: 0 0 2rem 0; +} + +@keyframes animateIn { + from { + opacity: 0; + transform: translate(0, 20px) scale(0.9); + } + to { + opacity: 1; + transform: translate(0, 0) scale(1); + } +} + +.command-list { + list-style: none; + margin: 0; + padding: 0; +} + +.command-list li { + margin-bottom: 1rem; +} + +.command-list li:last-child { + margin-bottom: 0; +} + +.command-list__name { + color: #fff; +} + +.command-list__arg { + font-style: italic; +} + +.command-list__arg::before, .command-list__arg::after { + color: #888; +} + +.command-list__description { + margin: 0.25rem 0 0 0; + font-size: 0.9rem; + color: #ccc; +} diff --git a/app/src/app.d.ts b/app/src/app.d.ts new file mode 100644 index 0000000..fb75b50 --- /dev/null +++ b/app/src/app.d.ts @@ -0,0 +1,43 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +import type { StorageAdapter } from '$lib/server/storage/types'; +import type { PermissionsAdapter } from '$lib/server/permissions/types'; + +declare global { + namespace App { + interface Error { + message: string; + code?: string; + } + + interface Locals { + storage: StorageAdapter; + permissions: PermissionsAdapter; + userId?: string; + } + + interface PageData {} + + interface PageState {} + + interface Platform { + env: { + DB: D1Database; + CHATROOM: DurableObjectNamespace; + JWT_SECRET?: string; + TWILIO_ACCOUNT_SID?: string; + TWILIO_AUTH_TOKEN?: string; + TWILIO_PHONE_NUMBER?: string; + WEBAUTHN_RP_ID?: string; + WEBAUTHN_RP_NAME?: string; + WEBAUTHN_ORIGIN?: string; + }; + context: { + waitUntil(promise: Promise): void; + }; + caches: CacheStorage & { default: Cache }; + } + } +} + +export {}; diff --git a/app/src/app.html b/app/src/app.html new file mode 100644 index 0000000..2148f15 --- /dev/null +++ b/app/src/app.html @@ -0,0 +1,12 @@ + + + + + + chat + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/app/src/hooks.server.ts b/app/src/hooks.server.ts new file mode 100644 index 0000000..f642a52 --- /dev/null +++ b/app/src/hooks.server.ts @@ -0,0 +1,61 @@ +import type { Handle } from '@sveltejs/kit'; +import { D1StorageAdapter } from '$lib/server/storage/d1-adapter'; +import type { StorageAdapter } from '$lib/server/storage/types'; +import { D1PermissionsAdapter } from '$lib/server/permissions/d1-permissions'; +import type { PermissionsAdapter } from '$lib/server/permissions/types'; +import { verifySessionToken } from '$lib/server/auth/utils'; +import { validateApiToken } from '$lib/server/auth/tokens'; + +export const handle: Handle = async ({ event, resolve }) => { + // Initialize storage adapter based on environment + let storage: StorageAdapter; + let permissions: PermissionsAdapter; + + if (event.platform?.env?.DB) { + // Production: Use Cloudflare D1 + storage = new D1StorageAdapter(event.platform.env.DB); + permissions = new D1PermissionsAdapter(event.platform.env.DB); + } else { + // Local dev: Use in-memory or file-based SQLite + // For now, throw error with helpful message + throw new Error( + 'Database not initialized. For local dev, run: npm run dev\n' + + 'For Cloudflare, ensure D1 database is set up: wrangler d1 create mini-chat' + ); + } + + // Make storage and permissions available to all routes via locals + event.locals.storage = storage; + event.locals.permissions = permissions; + + // Check for API token (Bearer token) first + const authHeader = event.request.headers.get('Authorization'); + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.slice(7); // Remove 'Bearer ' prefix + try { + const userId = await validateApiToken(event.platform!.env.DB, token); + if (userId) { + event.locals.userId = userId; + } + } catch (err) { + // Invalid API token - ignore and continue + } + } + + // If no API token, check for session token + if (!event.locals.userId) { + const sessionToken = event.cookies.get('session'); + if (sessionToken) { + try { + const jwtSecret = event.platform?.env?.JWT_SECRET || 'dev-secret-key'; + const payload = await verifySessionToken(sessionToken, jwtSecret); + event.locals.userId = payload.userId; + } catch (err) { + // Invalid or expired token - clear cookie + event.cookies.delete('session', { path: '/' }); + } + } + } + + return resolve(event); +}; diff --git a/app/src/lib/components/AuthScreen.svelte b/app/src/lib/components/AuthScreen.svelte new file mode 100644 index 0000000..0aed334 --- /dev/null +++ b/app/src/lib/components/AuthScreen.svelte @@ -0,0 +1,268 @@ + + +
+
+ {#if step === 'phone'} +

Welcome!

+

+ Sign in with your phone number to get started. +

+ + {#if error} +

{error}

+ {/if} + +
{ e.preventDefault(); sendCode(); }}> + +
+ + +
+
+ {:else if step === 'code'} +

Check your phone

+

+ Enter the 6-digit code we sent to {phone} +

+ + {#if error} +

{error}

+ {/if} + +
{ e.preventDefault(); verifyCode(); }}> + +
+ + +
+
+ + + {:else if step === 'name'} +

Almost there!

+

+ Choose a display name for the chat +

+ + {#if error} +

{error}

+ {/if} + +
{ e.preventDefault(); setName(); }}> + +
+ + +
+
+ {:else if step === 'passkey'} +

Set up a passkey?

+

+ Passkeys let you sign in quickly and securely on this device using Face ID, Touch ID, or + your device PIN. +

+ + {#if error} +

{error}

+ {/if} + + + + + {/if} +
+
diff --git a/app/src/lib/server/ChatRoom.ts b/app/src/lib/server/ChatRoom.ts new file mode 100644 index 0000000..498af28 --- /dev/null +++ b/app/src/lib/server/ChatRoom.ts @@ -0,0 +1,299 @@ +/** + * ChatRoom Durable Object + * Handles WebSocket connections and real-time messaging for a single room + */ + +import { getActiveAgents, shouldTriggerAgent, logWebhookDelivery } from './agents/registry'; +import { sendWebhook } from './agents/webhook'; +import type { WebhookPayload } from './agents/types'; + +export interface Env { + DB: D1Database; + CHATROOM: DurableObjectNamespace; +} + +interface Session { + webSocket: WebSocket; + userId: string; + userName: string; + quit?: boolean; +} + +export class ChatRoom implements DurableObject { + private sessions: Set; + private roomName: string; + + constructor(private state: DurableObjectState, private env: Env) { + this.sessions = new Set(); + this.roomName = state.id.name || 'unknown'; + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + + // Handle broadcast endpoint (for HTTP-based message posting) + if (url.pathname === '/broadcast' && request.method === 'POST') { + const message = await request.json(); + this.broadcast(message); + + // Trigger webhooks for agents + await this.triggerAgentWebhooks(message); + + return new Response('OK', { status: 200 }); + } + + // Handle WebSocket upgrade + if (request.headers.get('Upgrade') !== 'websocket') { + return new Response('Expected WebSocket', { status: 426 }); + } + + // Get user info from URL params + const userId = url.searchParams.get('userId'); + const userName = url.searchParams.get('userName'); + + if (!userId || !userName) { + return new Response('Missing userId or userName', { status: 400 }); + } + + // Create WebSocket pair + const pair = new WebSocketPair(); + const [client, server] = Object.values(pair); + + // Accept the WebSocket + server.accept(); + + // Create session + const session: Session = { + webSocket: server, + userId, + userName + }; + + this.sessions.add(session); + + // Set up message handler + server.addEventListener('message', async (event) => { + try { + const data = JSON.parse(event.data as string); + await this.handleMessage(session, data); + } catch (err) { + console.error('Error handling message:', err); + session.webSocket.send(JSON.stringify({ + type: 'error', + message: 'Invalid message format' + })); + } + }); + + // Handle close + server.addEventListener('close', () => { + this.sessions.delete(session); + this.broadcast({ + type: 'event', + content: `${session.userName} left` + }, session); + }); + + // Get room info and recent messages + const room = await this.env.DB.prepare( + `SELECT * FROM rooms WHERE name = ?` + ).bind(this.roomName).first(); + + let messages = []; + if (room) { + const messagesResult = await this.env.DB.prepare(` + SELECT m.*, u.display_name as user_name + FROM messages m + LEFT JOIN users u ON m.user_id = u.id + WHERE m.room_id = ? + ORDER BY m.created_at DESC + LIMIT 50 + `).bind(room.id).all(); + + messages = (messagesResult.results || []).reverse(); + } + + // Send room info with history + server.send(JSON.stringify({ + type: 'room_info', + room: this.roomName, + topic: room?.topic, + users: Array.from(this.sessions).map(s => ({ + id: s.userId, + name: s.userName + })), + messages + })); + + // Send join event to others + this.broadcast({ + type: 'event', + content: `${userName} joined` + }, session); + + return new Response(null, { + status: 101, + webSocket: client + }); + } + + private async handleMessage(session: Session, data: any) { + switch (data.type) { + case 'chat': + // Get room from database + const room = await this.env.DB.prepare( + `SELECT id FROM rooms WHERE name = ?` + ).bind(this.roomName).first(); + + if (!room) { + session.webSocket.send(JSON.stringify({ + type: 'error', + message: 'Room not found' + })); + return; + } + + // Persist message to D1 + const messageId = crypto.randomUUID(); + const timestamp = Math.floor(Date.now() / 1000); + + await this.env.DB.prepare(` + INSERT INTO messages (id, room_id, user_id, content, created_at) + VALUES (?, ?, ?, ?, ?) + `).bind(messageId, room.id, session.userId, data.content, timestamp).run(); + + // Update room timestamp (keep alive) + await this.env.DB.prepare(` + UPDATE rooms SET updated_at = unixepoch() WHERE id = ? + `).bind(room.id).run(); + + // Broadcast to all sessions + const broadcastMessage = { + type: 'chat', + id: messageId, + userId: session.userId, + userName: session.userName, + content: data.content, + timestamp + }; + this.broadcast(broadcastMessage); + + // Trigger webhooks for agents + await this.triggerAgentWebhooks(broadcastMessage); + break; + + case 'ping': + session.webSocket.send(JSON.stringify({ type: 'pong' })); + break; + + default: + session.webSocket.send(JSON.stringify({ + type: 'error', + message: 'Unknown message type' + })); + } + } + + private broadcast(message: any, exclude?: Session) { + const json = JSON.stringify(message); + for (const session of this.sessions) { + if (session !== exclude && !session.quit) { + try { + session.webSocket.send(json); + } catch (err) { + // Mark as quit if send fails + session.quit = true; + } + } + } + } + + async alarm() { + // Periodic cleanup or soft expiration check + // Can be used to implement auto-expiration for ephemeral rooms + } + + private async triggerAgentWebhooks(message: any) { + // Only trigger on chat messages + if (message.type !== 'chat') { + return; + } + + try { + // Get room ID from database + const room = await this.env.DB.prepare('SELECT id FROM rooms WHERE name = ?') + .bind(this.roomName) + .first<{ id: string }>(); + + if (!room) { + return; + } + + // Get active agents for this room + const agents = await getActiveAgents(this.env.DB, room.id); + + if (agents.length === 0) { + return; + } + + // Filter agents based on trigger config + const triggeredAgents = agents.filter((agent) => + shouldTriggerAgent(agent, { + content: message.content, + userId: message.userId + }) + ); + + if (triggeredAgents.length === 0) { + return; + } + + // Prepare webhook payload + const payload: WebhookPayload = { + event: 'message.created', + roomId: room.id, + roomName: this.roomName, + message: { + id: message.id, + content: message.content, + userId: message.userId, + userName: message.userName, + createdAt: message.timestamp + }, + agent: { + id: '', // Will be set per agent + userId: '' // Will be set per agent + } + }; + + // Send webhooks in parallel + const webhookPromises = triggeredAgents.map(async (agent) => { + const agentPayload = { + ...payload, + agent: { + id: agent.id, + userId: agent.userId + } + }; + + const result = await sendWebhook(agent, agentPayload, { timeout: 5000 }); + + // Log webhook delivery + await logWebhookDelivery(this.env.DB, { + roomAgentId: agent.id, + messageId: message.id, + statusCode: result.statusCode, + responseTimeMs: result.responseTimeMs, + error: result.error + }); + + return result; + }); + + await Promise.allSettled(webhookPromises); + } catch (err) { + console.error('Error triggering agent webhooks:', err); + // Don't fail the message delivery if webhooks fail + } + } +} diff --git a/app/src/lib/server/README.md b/app/src/lib/server/README.md new file mode 100644 index 0000000..3db8463 --- /dev/null +++ b/app/src/lib/server/README.md @@ -0,0 +1,69 @@ +# Server Architecture + +This app uses abstraction layers to make it easy to swap technologies. + +## Storage Layer (`storage/`) + +The `StorageAdapter` interface allows swapping database technologies: + +- **Current**: `D1StorageAdapter` (Cloudflare D1 / SQLite) +- **Alternatives**: PostgreSQL, MySQL, MongoDB, etc. + +To swap storage: +1. Implement the `StorageAdapter` interface +2. Update `platform?.env.DB` initialization +3. No other code changes needed + +## Messaging Layer (`messaging/`) + +The `MessagingAdapter` interface allows swapping real-time messaging: + +- **Current**: Durable Objects (Cloudflare Workers) +- **Alternatives**: Redis Pub/Sub, Socket.io, Server-Sent Events, etc. + +To swap messaging: +1. Implement the `MessagingAdapter` interface +2. Update WebSocket handler initialization +3. No other code changes needed + +## Auth Layer (future) + +Plan to add `AuthAdapter` for: +- WebAuthn/Passkeys +- Phone SMS verification +- Magic links +- OAuth providers + +## Local Development + +For local dev without Cloudflare: +- Use `sqlite-storage-adapter.ts` (file-based SQLite) +- Use `in-memory-messaging.ts` (no external dependencies) +- Swap in `hooks.server.ts` based on environment + +## Example: Swapping to PostgreSQL + +```typescript +// Create postgres-adapter.ts +export class PostgresStorageAdapter implements StorageAdapter { + constructor(private pool: Pool) {} + // ... implement all methods +} + +// Update hooks.server.ts +const storage = new PostgresStorageAdapter(pool); +event.locals.storage = storage; +``` + +## Example: Swapping to Redis Messaging + +```typescript +// Create redis-messaging.ts +export class RedisMessagingAdapter implements MessagingAdapter { + constructor(private redis: RedisClient) {} + // ... implement pub/sub +} + +// Update WebSocket handler +const messaging = new RedisMessagingAdapter(redisClient); +``` diff --git a/app/src/lib/server/agents/registry.ts b/app/src/lib/server/agents/registry.ts new file mode 100644 index 0000000..53a37df --- /dev/null +++ b/app/src/lib/server/agents/registry.ts @@ -0,0 +1,314 @@ +/** + * Agent Registry + * Manages agent registration, configuration, and lifecycle + */ + +import type { D1Database } from '@cloudflare/workers-types'; +import type { RoomAgent, TriggerConfig, WebhookLog } from './types'; + +/** + * Register a new agent in a room + */ +export async function registerAgent( + db: D1Database, + params: { + roomId: string; + userId: string; // The agent's user account + webhookUrl: string; + webhookSecret?: string; + triggerConfig?: TriggerConfig; + createdBy: string; + } +): Promise { + const id = crypto.randomUUID(); + const now = Math.floor(Date.now() / 1000); + + const triggerConfig = params.triggerConfig || { type: 'all' }; + const triggerConfigJson = JSON.stringify(triggerConfig); + + await db + .prepare( + `INSERT INTO room_agents + (id, room_id, user_id, webhook_url, webhook_secret, trigger_config, is_active, created_by, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?)` + ) + .bind( + id, + params.roomId, + params.userId, + params.webhookUrl, + params.webhookSecret || null, + triggerConfigJson, + params.createdBy, + now, + now + ) + .run(); + + return { + id, + roomId: params.roomId, + userId: params.userId, + webhookUrl: params.webhookUrl, + webhookSecret: params.webhookSecret || null, + triggerConfig, + isActive: true, + createdBy: params.createdBy, + createdAt: now, + updatedAt: now + }; +} + +/** + * Get all active agents in a room + */ +export async function getActiveAgents(db: D1Database, roomId: string): Promise { + const results = await db + .prepare( + `SELECT id, room_id, user_id, webhook_url, webhook_secret, trigger_config, + is_active, created_by, created_at, updated_at + FROM room_agents + WHERE room_id = ? AND is_active = 1` + ) + .bind(roomId) + .all<{ + id: string; + room_id: string; + user_id: string; + webhook_url: string; + webhook_secret: string | null; + trigger_config: string; + is_active: number; + created_by: string; + created_at: number; + updated_at: number; + }>(); + + return (results.results || []).map((row) => ({ + id: row.id, + roomId: row.room_id, + userId: row.user_id, + webhookUrl: row.webhook_url, + webhookSecret: row.webhook_secret, + triggerConfig: JSON.parse(row.trigger_config), + isActive: row.is_active === 1, + createdBy: row.created_by, + createdAt: row.created_at, + updatedAt: row.updated_at + })); +} + +/** + * Get a specific agent by ID + */ +export async function getAgent(db: D1Database, agentId: string): Promise { + const row = await db + .prepare( + `SELECT id, room_id, user_id, webhook_url, webhook_secret, trigger_config, + is_active, created_by, created_at, updated_at + FROM room_agents + WHERE id = ?` + ) + .bind(agentId) + .first<{ + id: string; + room_id: string; + user_id: string; + webhook_url: string; + webhook_secret: string | null; + trigger_config: string; + is_active: number; + created_by: string; + created_at: number; + updated_at: number; + }>(); + + if (!row) return null; + + return { + id: row.id, + roomId: row.room_id, + userId: row.user_id, + webhookUrl: row.webhook_url, + webhookSecret: row.webhook_secret, + triggerConfig: JSON.parse(row.trigger_config), + isActive: row.is_active === 1, + createdBy: row.created_by, + createdAt: row.created_at, + updatedAt: row.updated_at + }; +} + +/** + * Update agent configuration + */ +export async function updateAgent( + db: D1Database, + agentId: string, + updates: { + webhookUrl?: string; + webhookSecret?: string; + triggerConfig?: TriggerConfig; + isActive?: boolean; + } +): Promise { + const setParts: string[] = []; + const values: unknown[] = []; + + if (updates.webhookUrl !== undefined) { + setParts.push('webhook_url = ?'); + values.push(updates.webhookUrl); + } + + if (updates.webhookSecret !== undefined) { + setParts.push('webhook_secret = ?'); + values.push(updates.webhookSecret); + } + + if (updates.triggerConfig !== undefined) { + setParts.push('trigger_config = ?'); + values.push(JSON.stringify(updates.triggerConfig)); + } + + if (updates.isActive !== undefined) { + setParts.push('is_active = ?'); + values.push(updates.isActive ? 1 : 0); + } + + if (setParts.length === 0) { + return false; + } + + setParts.push('updated_at = ?'); + values.push(Math.floor(Date.now() / 1000)); + + values.push(agentId); + + const sql = `UPDATE room_agents SET ${setParts.join(', ')} WHERE id = ?`; + + const result = await db.prepare(sql).bind(...values).run(); + + return result.meta.changes > 0; +} + +/** + * Remove an agent from a room + */ +export async function removeAgent(db: D1Database, agentId: string): Promise { + const result = await db.prepare('DELETE FROM room_agents WHERE id = ?').bind(agentId).run(); + + return result.meta.changes > 0; +} + +/** + * Log a webhook delivery attempt + */ +export async function logWebhookDelivery( + db: D1Database, + params: { + roomAgentId: string; + messageId?: string; + statusCode?: number; + responseTimeMs: number; + error?: string; + } +): Promise { + const id = crypto.randomUUID(); + const now = Math.floor(Date.now() / 1000); + + await db + .prepare( + `INSERT INTO webhook_logs + (id, room_agent_id, message_id, status_code, response_time_ms, error, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .bind( + id, + params.roomAgentId, + params.messageId || null, + params.statusCode || null, + params.responseTimeMs, + params.error || null, + now + ) + .run(); +} + +/** + * Get recent webhook logs for an agent + */ +export async function getWebhookLogs( + db: D1Database, + agentId: string, + limit: number = 50 +): Promise { + const results = await db + .prepare( + `SELECT id, room_agent_id, message_id, status_code, response_time_ms, error, created_at + FROM webhook_logs + WHERE room_agent_id = ? + ORDER BY created_at DESC + LIMIT ?` + ) + .bind(agentId, limit) + .all<{ + id: string; + room_agent_id: string; + message_id: string | null; + status_code: number | null; + response_time_ms: number; + error: string | null; + created_at: number; + }>(); + + return (results.results || []).map((row) => ({ + id: row.id, + roomAgentId: row.room_agent_id, + messageId: row.message_id, + statusCode: row.status_code, + responseTimeMs: row.response_time_ms, + error: row.error, + createdAt: row.created_at + })); +} + +/** + * Check if a message should trigger an agent based on its trigger config + */ +export function shouldTriggerAgent( + agent: RoomAgent, + message: { + content: string; + userId: string | null; + } +): boolean { + // Don't trigger on agent's own messages unless explicitly configured + if (!agent.triggerConfig.includeOwnMessages && message.userId === agent.userId) { + return false; + } + + switch (agent.triggerConfig.type) { + case 'all': + return true; + + case 'mentions': + // Check if message contains @agentname or similar + // For now, simplified - just check if agent user ID is in message + return message.content.includes(`@${agent.userId}`); + + case 'keywords': + if (!agent.triggerConfig.keywords || agent.triggerConfig.keywords.length === 0) { + return false; + } + const lowerContent = message.content.toLowerCase(); + return agent.triggerConfig.keywords.some((keyword) => + lowerContent.includes(keyword.toLowerCase()) + ); + + case 'none': + return false; + + default: + return false; + } +} diff --git a/app/src/lib/server/agents/types.ts b/app/src/lib/server/agents/types.ts new file mode 100644 index 0000000..84d6630 --- /dev/null +++ b/app/src/lib/server/agents/types.ts @@ -0,0 +1,56 @@ +/** + * Agent system types + */ + +export interface RoomAgent { + id: string; + roomId: string; + userId: string; // The agent's user account + webhookUrl: string; + webhookSecret: string | null; + triggerConfig: TriggerConfig; + isActive: boolean; + createdBy: string; + createdAt: number; + updatedAt: number; +} + +export interface TriggerConfig { + type: 'all' | 'mentions' | 'keywords' | 'none'; + keywords?: string[]; // For keyword triggers + includeOwnMessages?: boolean; // Whether to trigger on agent's own messages +} + +export interface WebhookPayload { + event: 'message.created'; + roomId: string; + roomName: string; + message: { + id: string; + content: string; + userId: string | null; + userName: string | null; + createdAt: number; + }; + agent: { + id: string; + userId: string; + }; +} + +export interface WebhookResponse { + // Agent can optionally respond with a message + reply?: { + content: string; + }; +} + +export interface WebhookLog { + id: string; + roomAgentId: string; + messageId: string | null; + statusCode: number | null; + responseTimeMs: number; + error: string | null; + createdAt: number; +} diff --git a/app/src/lib/server/agents/webhook.ts b/app/src/lib/server/agents/webhook.ts new file mode 100644 index 0000000..6809f01 --- /dev/null +++ b/app/src/lib/server/agents/webhook.ts @@ -0,0 +1,132 @@ +/** + * Webhook Dispatcher + * Handles sending webhooks to agents + */ + +import type { RoomAgent, WebhookPayload, WebhookResponse } from './types'; + +/** + * Sign a webhook payload with HMAC-SHA256 + */ +async function signWebhook(payload: string, secret: string): Promise { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payload)); + const hashArray = Array.from(new Uint8Array(signature)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * Send a webhook to an agent + * Returns the response or null if failed + */ +export async function sendWebhook( + agent: RoomAgent, + payload: WebhookPayload, + options: { + timeout?: number; // milliseconds, default 5000 + } = {} +): Promise<{ + success: boolean; + statusCode?: number; + responseTimeMs: number; + response?: WebhookResponse; + error?: string; +}> { + const startTime = Date.now(); + const timeout = options.timeout || 5000; + + try { + const payloadJson = JSON.stringify(payload); + + // Create headers + const headers: Record = { + 'Content-Type': 'application/json', + 'User-Agent': 'MiniChat-Webhook/1.0' + }; + + // Add signature if secret is configured + if (agent.webhookSecret) { + const signature = await signWebhook(payloadJson, agent.webhookSecret); + headers['X-Webhook-Signature'] = signature; + headers['X-Webhook-Timestamp'] = Date.now().toString(); + } + + // Send webhook with timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(agent.webhookUrl, { + method: 'POST', + headers, + body: payloadJson, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + const responseTimeMs = Date.now() - startTime; + + // Read response body if present + let webhookResponse: WebhookResponse | undefined; + const contentType = response.headers.get('content-type'); + if (contentType?.includes('application/json')) { + try { + webhookResponse = await response.json(); + } catch (e) { + // Invalid JSON response, ignore + } + } + + return { + success: response.ok, + statusCode: response.status, + responseTimeMs, + response: webhookResponse, + error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}` + }; + } catch (error) { + const responseTimeMs = Date.now() - startTime; + + let errorMessage = 'Unknown error'; + if (error instanceof Error) { + errorMessage = error.name === 'AbortError' ? 'Request timeout' : error.message; + } + + return { + success: false, + responseTimeMs, + error: errorMessage + }; + } +} + +/** + * Send webhooks to multiple agents in parallel + */ +export async function broadcastWebhook( + agents: RoomAgent[], + payload: WebhookPayload, + options: { + timeout?: number; + } = {} +): Promise< + Array<{ + agent: RoomAgent; + result: Awaited>; + }> +> { + const promises = agents.map(async (agent) => { + const result = await sendWebhook(agent, payload, options); + return { agent, result }; + }); + + return Promise.all(promises); +} diff --git a/app/src/lib/server/auth/sms.ts b/app/src/lib/server/auth/sms.ts new file mode 100644 index 0000000..0582136 --- /dev/null +++ b/app/src/lib/server/auth/sms.ts @@ -0,0 +1,54 @@ +/** + * SMS provider interface + * Allows swapping Twilio, AWS SNS, etc. + */ + +export interface SmsProvider { + sendVerificationCode(phone: string, code: string): Promise; +} + +/** + * Twilio SMS provider + */ +export class TwilioSmsProvider implements SmsProvider { + constructor( + private accountSid: string, + private authToken: string, + private fromNumber: string + ) {} + + async sendVerificationCode(phone: string, code: string): Promise { + const url = `https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`; + + const body = new URLSearchParams({ + To: phone, + From: this.fromNumber, + Body: `Your Mini Chat verification code is: ${code}. Valid for 10 minutes.` + }); + + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: + 'Basic ' + btoa(`${this.accountSid}:${this.authToken}`), + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: body.toString() + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to send SMS: ${error}`); + } + } +} + +/** + * Mock SMS provider for development + */ +export class MockSmsProvider implements SmsProvider { + async sendVerificationCode(phone: string, code: string): Promise { + console.log(`[MOCK SMS] Sending to ${phone}: Your code is ${code}`); + // In dev, you can store this in memory or log it + } +} diff --git a/app/src/lib/server/auth/tokens.ts b/app/src/lib/server/auth/tokens.ts new file mode 100644 index 0000000..fd4431b --- /dev/null +++ b/app/src/lib/server/auth/tokens.ts @@ -0,0 +1,177 @@ +/** + * API Token System + * For agent authentication via Bearer tokens + */ + +import type { D1Database } from '@cloudflare/workers-types'; + +export interface ApiToken { + id: string; + userId: string; + tokenHash: string; + name: string; + lastUsedAt: number | null; + expiresAt: number | null; + createdAt: number; +} + +/** + * Generate a cryptographically secure API token + * Format: mc_live_<32 random hex characters> + */ +export function generateApiToken(): string { + const randomBytes = new Uint8Array(32); + crypto.getRandomValues(randomBytes); + const hex = Array.from(randomBytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return `mc_live_${hex}`; +} + +/** + * Hash an API token for secure storage + * Uses SHA-256 + */ +export async function hashApiToken(token: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(token); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * Create a new API token for a user + * Returns the plaintext token (only time it's visible) and the stored record + */ +export async function createApiToken( + db: D1Database, + userId: string, + name: string, + expiresIn?: string // e.g., "30d", "1y", null for no expiration +): Promise<{ token: string; record: ApiToken }> { + const token = generateApiToken(); + const tokenHash = await hashApiToken(token); + const id = crypto.randomUUID(); + + let expiresAt: number | null = null; + if (expiresIn) { + expiresAt = getExpirationTimestamp(expiresIn); + } + + const now = Math.floor(Date.now() / 1000); + + await db + .prepare( + `INSERT INTO api_tokens (id, user_id, token_hash, name, expires_at, created_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .bind(id, userId, tokenHash, name, expiresAt, now) + .run(); + + const record: ApiToken = { + id, + userId, + tokenHash, + name, + lastUsedAt: null, + expiresAt, + createdAt: now + }; + + return { token, record }; +} + +/** + * Validate an API token and return the associated user ID + * Also updates last_used_at timestamp + */ +export async function validateApiToken( + db: D1Database, + token: string +): Promise { + const tokenHash = await hashApiToken(token); + const now = Math.floor(Date.now() / 1000); + + // Find token and check expiration + const result = await db + .prepare( + `SELECT id, user_id, expires_at FROM api_tokens + WHERE token_hash = ?` + ) + .bind(tokenHash) + .first<{ id: string; user_id: string; expires_at: number | null }>(); + + if (!result) { + return null; + } + + // Check if expired + if (result.expires_at && result.expires_at < now) { + return null; + } + + // Update last_used_at + await db + .prepare('UPDATE api_tokens SET last_used_at = ? WHERE id = ?') + .bind(now, result.id) + .run(); + + return result.user_id; +} + +/** + * Revoke an API token by ID + */ +export async function revokeApiToken(db: D1Database, tokenId: string): Promise { + const result = await db.prepare('DELETE FROM api_tokens WHERE id = ?').bind(tokenId).run(); + + return result.meta.changes > 0; +} + +/** + * List all API tokens for a user + */ +export async function listApiTokens(db: D1Database, userId: string): Promise { + const results = await db + .prepare( + `SELECT id, user_id, token_hash, name, last_used_at, expires_at, created_at + FROM api_tokens + WHERE user_id = ? + ORDER BY created_at DESC` + ) + .bind(userId) + .all(); + + return results.results || []; +} + +/** + * Get expiration timestamp for a duration string + */ +function getExpirationTimestamp(duration: string): number { + const now = Math.floor(Date.now() / 1000); + const match = duration.match(/^(\d+)([smhdy])$/); + + if (!match) { + throw new Error('Invalid duration format. Use format like: 30d, 1y, 24h'); + } + + const [, value, unit] = match; + const num = parseInt(value); + + switch (unit) { + case 's': + return now + num; + case 'm': + return now + num * 60; + case 'h': + return now + num * 3600; + case 'd': + return now + num * 86400; + case 'y': + return now + num * 31536000; + default: + throw new Error('Invalid duration unit'); + } +} diff --git a/app/src/lib/server/auth/utils.ts b/app/src/lib/server/auth/utils.ts new file mode 100644 index 0000000..70e44e5 --- /dev/null +++ b/app/src/lib/server/auth/utils.ts @@ -0,0 +1,83 @@ +/** + * Auth utilities + * JWT, hashing, and session management + */ + +import { SignJWT, jwtVerify } from 'jose'; + +export interface SessionPayload { + userId: string; + displayName?: string; + iat?: number; + exp?: number; +} + +// Hash phone number for privacy (SHA-256) +export async function hashPhone(phone: string): Promise { + const normalized = phone.replace(/\D/g, ''); // Remove non-digits + const encoder = new TextEncoder(); + const data = encoder.encode(normalized); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +// Generate 6-digit verification code +export function generateVerificationCode(): string { + return Math.floor(100000 + Math.random() * 900000).toString(); +} + +// Create JWT session token +export async function createSessionToken( + payload: SessionPayload, + secret: string, + expiresIn: string = '30d' +): Promise { + const secretKey = new TextEncoder().encode(secret); + + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime(expiresIn) + .sign(secretKey); +} + +// Verify JWT session token +export async function verifySessionToken( + token: string, + secret: string +): Promise { + const secretKey = new TextEncoder().encode(secret); + + const { payload } = await jwtVerify(token, secretKey, { + algorithms: ['HS256'] + }); + + return payload as SessionPayload; +} + +// Get expiration timestamp for a duration +export function getExpirationTimestamp(duration: string): number { + const now = Math.floor(Date.now() / 1000); + const match = duration.match(/^(\d+)([smhd])$/); + + if (!match) { + throw new Error('Invalid duration format'); + } + + const [, value, unit] = match; + const num = parseInt(value); + + switch (unit) { + case 's': + return now + num; + case 'm': + return now + num * 60; + case 'h': + return now + num * 3600; + case 'd': + return now + num * 86400; + default: + throw new Error('Invalid duration unit'); + } +} diff --git a/app/src/lib/server/durable-objects.ts b/app/src/lib/server/durable-objects.ts new file mode 100644 index 0000000..b6783f8 --- /dev/null +++ b/app/src/lib/server/durable-objects.ts @@ -0,0 +1,2 @@ +// Export Durable Objects for Cloudflare Workers +export { ChatRoom } from './ChatRoom'; diff --git a/app/src/lib/server/messaging/in-memory-adapter.ts b/app/src/lib/server/messaging/in-memory-adapter.ts new file mode 100644 index 0000000..e728e32 --- /dev/null +++ b/app/src/lib/server/messaging/in-memory-adapter.ts @@ -0,0 +1,75 @@ +/** + * In-memory messaging adapter for local development + * Single-instance only (not distributed) + */ + +import type { MessagingAdapter, BroadcastMessage } from './types'; + +type Callback = (message: BroadcastMessage) => void; + +export class InMemoryMessagingAdapter implements MessagingAdapter { + private roomSubscriptions = new Map>(); + private userSubscriptions = new Map>(); + + async broadcast(roomName: string, message: BroadcastMessage): Promise { + const callbacks = this.roomSubscriptions.get(roomName); + if (callbacks) { + callbacks.forEach((cb) => { + try { + cb(message); + } catch (err) { + console.error('Error in broadcast callback:', err); + } + }); + } + } + + async sendToUser(userId: string, message: BroadcastMessage): Promise { + const callbacks = this.userSubscriptions.get(userId); + if (callbacks) { + callbacks.forEach((cb) => { + try { + cb(message); + } catch (err) { + console.error('Error in user message callback:', err); + } + }); + } + } + + async subscribe( + roomName: string, + callback: Callback + ): Promise<() => void> { + if (!this.roomSubscriptions.has(roomName)) { + this.roomSubscriptions.set(roomName, new Set()); + } + + const callbacks = this.roomSubscriptions.get(roomName)!; + callbacks.add(callback); + + // Return unsubscribe function + return () => { + callbacks.delete(callback); + if (callbacks.size === 0) { + this.roomSubscriptions.delete(roomName); + } + }; + } + + subscribeToUser(userId: string, callback: Callback): () => void { + if (!this.userSubscriptions.has(userId)) { + this.userSubscriptions.set(userId, new Set()); + } + + const callbacks = this.userSubscriptions.get(userId)!; + callbacks.add(callback); + + return () => { + callbacks.delete(callback); + if (callbacks.size === 0) { + this.userSubscriptions.delete(userId); + } + }; + } +} diff --git a/app/src/lib/server/messaging/types.ts b/app/src/lib/server/messaging/types.ts new file mode 100644 index 0000000..4512dc1 --- /dev/null +++ b/app/src/lib/server/messaging/types.ts @@ -0,0 +1,33 @@ +/** + * Messaging abstraction layer + * Allows swapping Durable Objects, Redis, in-memory, etc. + */ + +export interface BroadcastMessage { + type: 'chat' | 'event' | 'system' | 'error'; + userId?: string; + userName?: string; + content: string; + timestamp?: number; + [key: string]: any; +} + +export interface MessagingAdapter { + /** + * Broadcast a message to all connections in a room + */ + broadcast(roomName: string, message: BroadcastMessage): Promise; + + /** + * Send a direct message to a specific user + */ + sendToUser(userId: string, message: BroadcastMessage): Promise; + + /** + * Subscribe to messages for a room (for server-side listeners) + */ + subscribe( + roomName: string, + callback: (message: BroadcastMessage) => void + ): Promise<() => void>; +} diff --git a/app/src/lib/server/permissions/d1-permissions.ts b/app/src/lib/server/permissions/d1-permissions.ts new file mode 100644 index 0000000..1fd54e1 --- /dev/null +++ b/app/src/lib/server/permissions/d1-permissions.ts @@ -0,0 +1,529 @@ +/** + * D1 Permissions Adapter Implementation + */ + +import type { + PermissionsAdapter, + Permission, + Role, + RoleWithPermissions, + UserRole, + RoomBan, + RoomMute, + RoomInvite +} from './types'; +import { DEFAULT_ROLES } from './types'; + +export class D1PermissionsAdapter implements PermissionsAdapter { + constructor(private db: D1Database) {} + + // Permissions + async getAllPermissions(): Promise { + const result = await this.db.prepare('SELECT * FROM permissions').all(); + return (result.results || []).map(this.mapPermission); + } + + async getPermissionsByCategory(category: string): Promise { + const result = await this.db + .prepare('SELECT * FROM permissions WHERE category = ?') + .bind(category) + .all(); + return (result.results || []).map(this.mapPermission); + } + + // Roles + async getRole(roleId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM roles WHERE id = ?') + .bind(roleId) + .first(); + return result ? this.mapRole(result) : null; + } + + async getRolesByRoom(roomId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM roles WHERE room_id = ? ORDER BY position DESC') + .bind(roomId) + .all(); + return (result.results || []).map(this.mapRole); + } + + async getRoleWithPermissions(roleId: string): Promise { + const role = await this.getRole(roleId); + if (!role) return null; + + const permissions = await this.getRolePermissions(roleId); + return { ...role, permissions }; + } + + async createRole(role: Omit): Promise { + const id = crypto.randomUUID(); + await this.db + .prepare( + `INSERT INTO roles (id, room_id, name, description, color, is_default, is_system, position) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + .bind( + id, + role.roomId, + role.name, + role.description || null, + role.color || null, + role.isDefault ? 1 : 0, + role.isSystem ? 1 : 0, + role.position + ) + .run(); + + return this.getRole(id) as Promise; + } + + async updateRole(roleId: string, updates: Partial): Promise { + const fields: string[] = []; + const values: any[] = []; + + if (updates.name !== undefined) { + fields.push('name = ?'); + values.push(updates.name); + } + if (updates.description !== undefined) { + fields.push('description = ?'); + values.push(updates.description); + } + if (updates.color !== undefined) { + fields.push('color = ?'); + values.push(updates.color); + } + if (updates.isDefault !== undefined) { + fields.push('is_default = ?'); + values.push(updates.isDefault ? 1 : 0); + } + if (updates.position !== undefined) { + fields.push('position = ?'); + values.push(updates.position); + } + + if (fields.length === 0) return; + + fields.push('updated_at = unixepoch()'); + values.push(roleId); + + await this.db + .prepare(`UPDATE roles SET ${fields.join(', ')} WHERE id = ?`) + .bind(...values) + .run(); + } + + async deleteRole(roleId: string): Promise { + await this.db.prepare('DELETE FROM roles WHERE id = ?').bind(roleId).run(); + } + + // Role permissions + async getRolePermissions(roleId: string): Promise { + const result = await this.db + .prepare( + `SELECT p.* FROM permissions p + JOIN role_permissions rp ON p.id = rp.permission_id + WHERE rp.role_id = ?` + ) + .bind(roleId) + .all(); + return (result.results || []).map(this.mapPermission); + } + + async setRolePermissions(roleId: string, permissionIds: string[]): Promise { + // Start transaction manually (D1 doesn't support BEGIN/COMMIT directly via prepared statements) + // Delete existing + await this.db + .prepare('DELETE FROM role_permissions WHERE role_id = ?') + .bind(roleId) + .run(); + + // Insert new + for (const permissionId of permissionIds) { + await this.db + .prepare('INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)') + .bind(roleId, permissionId) + .run(); + } + } + + async addRolePermission(roleId: string, permissionId: string): Promise { + await this.db + .prepare('INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)') + .bind(roleId, permissionId) + .run(); + } + + async removeRolePermission(roleId: string, permissionId: string): Promise { + await this.db + .prepare('DELETE FROM role_permissions WHERE role_id = ? AND permission_id = ?') + .bind(roleId, permissionId) + .run(); + } + + // User roles + async getUserRoles(userId: string, roomId: string): Promise { + const result = await this.db + .prepare( + `SELECT r.* FROM roles r + JOIN user_roles ur ON r.id = ur.role_id + WHERE ur.user_id = ? AND ur.room_id = ? + ORDER BY r.position DESC` + ) + .bind(userId, roomId) + .all(); + return (result.results || []).map(this.mapRole); + } + + async getUsersWithRole(roomId: string, roleId: string): Promise { + const result = await this.db + .prepare('SELECT user_id FROM user_roles WHERE room_id = ? AND role_id = ?') + .bind(roomId, roleId) + .all(); + return (result.results || []).map((r: any) => r.user_id); + } + + async assignRole( + userId: string, + roomId: string, + roleId: string, + assignedBy?: string + ): Promise { + await this.db + .prepare( + 'INSERT OR IGNORE INTO user_roles (user_id, room_id, role_id, assigned_by) VALUES (?, ?, ?, ?)' + ) + .bind(userId, roomId, roleId, assignedBy || null) + .run(); + } + + async removeRole(userId: string, roomId: string, roleId: string): Promise { + await this.db + .prepare('DELETE FROM user_roles WHERE user_id = ? AND room_id = ? AND role_id = ?') + .bind(userId, roomId, roleId) + .run(); + } + + async removeAllRoles(userId: string, roomId: string): Promise { + await this.db + .prepare('DELETE FROM user_roles WHERE user_id = ? AND room_id = ?') + .bind(userId, roomId) + .run(); + } + + // Permission checks + async hasPermission( + userId: string, + roomId: string, + permission: string + ): Promise { + // Check if banned + const ban = await this.getBan(roomId, userId); + if (ban) { + const now = Math.floor(Date.now() / 1000); + if (!ban.expiresAt || ban.expiresAt > now) { + return false; // Banned users have no permissions + } + } + + // Check if owner (owner has all permissions) + const room = await this.db + .prepare('SELECT owner_id FROM rooms WHERE id = ?') + .bind(roomId) + .first(); + if (room && room.owner_id === userId) { + return true; + } + + // Get all user's permissions + const permissions = await this.getUserPermissions(userId, roomId); + return permissions.includes(permission); + } + + async getUserPermissions(userId: string, roomId: string): Promise { + const result = await this.db + .prepare( + `SELECT DISTINCT p.name FROM permissions p + JOIN role_permissions rp ON p.id = rp.permission_id + JOIN user_roles ur ON rp.role_id = ur.role_id + WHERE ur.user_id = ? AND ur.room_id = ?` + ) + .bind(userId, roomId) + .all(); + + return (result.results || []).map((r: any) => r.name); + } + + // Bans + async getBan(roomId: string, userId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM room_bans WHERE room_id = ? AND user_id = ?') + .bind(roomId, userId) + .first(); + return result ? this.mapBan(result) : null; + } + + async getBansByRoom(roomId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM room_bans WHERE room_id = ?') + .bind(roomId) + .all(); + return (result.results || []).map(this.mapBan); + } + + async createBan(ban: Omit): Promise { + const id = crypto.randomUUID(); + await this.db + .prepare( + `INSERT OR REPLACE INTO room_bans (id, room_id, user_id, banned_by, reason, expires_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .bind( + id, + ban.roomId, + ban.userId, + ban.bannedBy, + ban.reason || null, + ban.expiresAt || null + ) + .run(); + + return this.getBan(ban.roomId, ban.userId) as Promise; + } + + async removeBan(roomId: string, userId: string): Promise { + await this.db + .prepare('DELETE FROM room_bans WHERE room_id = ? AND user_id = ?') + .bind(roomId, userId) + .run(); + } + + async cleanExpiredBans(): Promise { + await this.db + .prepare( + 'DELETE FROM room_bans WHERE expires_at IS NOT NULL AND expires_at < unixepoch()' + ) + .run(); + } + + // Mutes + async getMute(roomId: string, userId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM room_mutes WHERE room_id = ? AND user_id = ?') + .bind(roomId, userId) + .first(); + return result ? this.mapMute(result) : null; + } + + async getMutesByRoom(roomId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM room_mutes WHERE room_id = ?') + .bind(roomId) + .all(); + return (result.results || []).map(this.mapMute); + } + + async createMute(mute: Omit): Promise { + const id = crypto.randomUUID(); + await this.db + .prepare( + `INSERT OR REPLACE INTO room_mutes (id, room_id, user_id, muted_by, reason, expires_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .bind( + id, + mute.roomId, + mute.userId, + mute.mutedBy, + mute.reason || null, + mute.expiresAt || null + ) + .run(); + + return this.getMute(mute.roomId, mute.userId) as Promise; + } + + async removeMute(roomId: string, userId: string): Promise { + await this.db + .prepare('DELETE FROM room_mutes WHERE room_id = ? AND user_id = ?') + .bind(roomId, userId) + .run(); + } + + async cleanExpiredMutes(): Promise { + await this.db + .prepare( + 'DELETE FROM room_mutes WHERE expires_at IS NOT NULL AND expires_at < unixepoch()' + ) + .run(); + } + + // Invites + async getInvite(code: string): Promise { + const result = await this.db + .prepare('SELECT * FROM room_invites WHERE code = ?') + .bind(code) + .first(); + return result ? this.mapInvite(result) : null; + } + + async getInvitesByRoom(roomId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM room_invites WHERE room_id = ?') + .bind(roomId) + .all(); + return (result.results || []).map(this.mapInvite); + } + + async createInvite( + invite: Omit + ): Promise { + const id = crypto.randomUUID(); + await this.db + .prepare( + `INSERT INTO room_invites (id, room_id, invited_user_id, invited_by, code, uses, max_uses, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + .bind( + id, + invite.roomId, + invite.invitedUserId || null, + invite.invitedBy, + invite.code || null, + invite.uses, + invite.maxUses || null, + invite.expiresAt || null + ) + .run(); + + return this.getInvite(invite.code!) as Promise; + } + + async useInvite(code: string): Promise { + await this.db + .prepare('UPDATE room_invites SET uses = uses + 1 WHERE code = ?') + .bind(code) + .run(); + } + + async deleteInvite(inviteId: string): Promise { + await this.db + .prepare('DELETE FROM room_invites WHERE id = ?') + .bind(inviteId) + .run(); + } + + async cleanExpiredInvites(): Promise { + await this.db + .prepare( + `DELETE FROM room_invites + WHERE (expires_at IS NOT NULL AND expires_at < unixepoch()) + OR (max_uses IS NOT NULL AND uses >= max_uses)` + ) + .run(); + } + + // Room setup + async initializeRoomRoles(roomId: string, ownerId: string): Promise { + // Get all permissions + const allPermissions = await this.getAllPermissions(); + const permissionMap = new Map(allPermissions.map((p) => [p.name, p.id])); + + // Create default roles + for (const [key, config] of Object.entries(DEFAULT_ROLES)) { + const role = await this.createRole({ + roomId, + name: config.name, + description: config.description, + color: config.color, + isDefault: config.isDefault, + isSystem: config.isSystem, + position: config.position + }); + + // Add permissions + if (config.permissions === '*') { + // Owner gets all permissions + await this.setRolePermissions( + role.id, + allPermissions.map((p) => p.id) + ); + } else { + const permissionIds = config.permissions + .map((name) => permissionMap.get(name)) + .filter((id): id is string => id !== undefined); + await this.setRolePermissions(role.id, permissionIds); + } + + // Assign owner role to creator + if (key === 'OWNER') { + await this.assignRole(ownerId, roomId, role.id); + } + } + } + + // Mappers + private mapPermission(row: any): Permission { + return { + id: row.id, + name: row.name, + description: row.description, + category: row.category, + createdAt: row.created_at + }; + } + + private mapRole(row: any): Role { + return { + id: row.id, + roomId: row.room_id, + name: row.name, + description: row.description, + color: row.color, + isDefault: Boolean(row.is_default), + isSystem: Boolean(row.is_system), + position: row.position, + createdAt: row.created_at, + updatedAt: row.updated_at + }; + } + + private mapBan(row: any): RoomBan { + return { + id: row.id, + roomId: row.room_id, + userId: row.user_id, + bannedBy: row.banned_by, + reason: row.reason, + expiresAt: row.expires_at, + createdAt: row.created_at + }; + } + + private mapMute(row: any): RoomMute { + return { + id: row.id, + roomId: row.room_id, + userId: row.user_id, + mutedBy: row.muted_by, + reason: row.reason, + expiresAt: row.expires_at, + createdAt: row.created_at + }; + } + + private mapInvite(row: any): RoomInvite { + return { + id: row.id, + roomId: row.room_id, + invitedUserId: row.invited_user_id, + invitedBy: row.invited_by, + code: row.code, + uses: row.uses, + maxUses: row.max_uses, + expiresAt: row.expires_at, + createdAt: row.created_at + }; + } +} diff --git a/app/src/lib/server/permissions/types.ts b/app/src/lib/server/permissions/types.ts new file mode 100644 index 0000000..d4cdbe7 --- /dev/null +++ b/app/src/lib/server/permissions/types.ts @@ -0,0 +1,195 @@ +/** + * Permissions and Roles types + */ + +export interface Permission { + id: string; + name: string; + description: string; + category: 'room' | 'members' | 'messages' | 'settings'; + createdAt: number; +} + +export interface Role { + id: string; + roomId: string; + name: string; + description?: string; + color?: string; + isDefault: boolean; + isSystem: boolean; + position: number; + createdAt: number; + updatedAt: number; +} + +export interface RoleWithPermissions extends Role { + permissions: Permission[]; +} + +export interface UserRole { + userId: string; + roomId: string; + roleId: string; + assignedBy?: string; + assignedAt: number; +} + +export interface RoomBan { + id: string; + roomId: string; + userId: string; + bannedBy: string; + reason?: string; + expiresAt?: number; + createdAt: number; +} + +export interface RoomMute { + id: string; + roomId: string; + userId: string; + mutedBy: string; + reason?: string; + expiresAt?: number; + createdAt: number; +} + +export interface RoomInvite { + id: string; + roomId: string; + invitedUserId?: string; + invitedBy: string; + code?: string; + uses: number; + maxUses?: number; + expiresAt?: number; + createdAt: number; +} + +export interface PermissionsAdapter { + // Permissions + getAllPermissions(): Promise; + getPermissionsByCategory(category: string): Promise; + + // Roles + getRole(roleId: string): Promise; + getRolesByRoom(roomId: string): Promise; + getRoleWithPermissions(roleId: string): Promise; + createRole(role: Omit): Promise; + updateRole(roleId: string, updates: Partial): Promise; + deleteRole(roleId: string): Promise; + + // Role permissions + getRolePermissions(roleId: string): Promise; + setRolePermissions(roleId: string, permissionIds: string[]): Promise; + addRolePermission(roleId: string, permissionId: string): Promise; + removeRolePermission(roleId: string, permissionId: string): Promise; + + // User roles + getUserRoles(userId: string, roomId: string): Promise; + getUsersWithRole(roomId: string, roleId: string): Promise; + assignRole( + userId: string, + roomId: string, + roleId: string, + assignedBy?: string + ): Promise; + removeRole(userId: string, roomId: string, roleId: string): Promise; + removeAllRoles(userId: string, roomId: string): Promise; + + // Permission checks + hasPermission( + userId: string, + roomId: string, + permission: string + ): Promise; + getUserPermissions(userId: string, roomId: string): Promise; + + // Bans + getBan(roomId: string, userId: string): Promise; + getBansByRoom(roomId: string): Promise; + createBan(ban: Omit): Promise; + removeBan(roomId: string, userId: string): Promise; + cleanExpiredBans(): Promise; + + // Mutes + getMute(roomId: string, userId: string): Promise; + getMutesByRoom(roomId: string): Promise; + createMute(mute: Omit): Promise; + removeMute(roomId: string, userId: string): Promise; + cleanExpiredMutes(): Promise; + + // Invites + getInvite(code: string): Promise; + getInvitesByRoom(roomId: string): Promise; + createInvite(invite: Omit): Promise; + useInvite(code: string): Promise; + deleteInvite(inviteId: string): Promise; + cleanExpiredInvites(): Promise; + + // Room setup + initializeRoomRoles(roomId: string, ownerId: string): Promise; +} + +// Default role configurations +export const DEFAULT_ROLES = { + OWNER: { + name: 'Owner', + description: 'Room owner with full control', + color: '#FFD700', + isSystem: true, + isDefault: false, + position: 1000, + permissions: '*' // All permissions + }, + MODERATOR: { + name: 'Moderator', + description: 'Trusted member with moderation powers', + color: '#3B3BBB', + isSystem: false, + isDefault: false, + position: 900, + permissions: [ + 'members.invite', + 'members.kick', + 'members.ban', + 'members.mute', + 'members.assign_roles', + 'members.view_list', + 'messages.send', + 'messages.edit_own', + 'messages.delete_own', + 'messages.edit_any', + 'messages.delete_any', + 'messages.pin', + 'messages.search', + 'settings.edit_topic', + 'settings.edit_description' + ] + }, + MEMBER: { + name: 'Member', + description: 'Regular room member', + color: '#888888', + isSystem: false, + isDefault: true, + position: 100, + permissions: [ + 'messages.send', + 'messages.edit_own', + 'messages.delete_own', + 'members.view_list', + 'messages.search' + ] + }, + EVERYONE: { + name: 'Everyone', + description: 'Base permissions for all users', + color: '#CCCCCC', + isSystem: true, + isDefault: false, + position: 0, + permissions: ['members.view_list', 'messages.search'] + } +} as const; diff --git a/app/src/lib/server/storage/d1-adapter.ts b/app/src/lib/server/storage/d1-adapter.ts new file mode 100644 index 0000000..ca87a99 --- /dev/null +++ b/app/src/lib/server/storage/d1-adapter.ts @@ -0,0 +1,421 @@ +/** + * D1 (Cloudflare) storage adapter implementation + */ + +import type { + StorageAdapter, + User, + Room, + Message, + Session, + Credential, + VerificationCode +} from './types'; + +export class D1StorageAdapter implements StorageAdapter { + constructor(private db: D1Database) {} + + // Users + async getUser(id: string): Promise { + const result = await this.db + .prepare('SELECT * FROM users WHERE id = ?') + .bind(id) + .first(); + return result ? this.mapUser(result) : null; + } + + async getUserByPhone(phoneHash: string): Promise { + const result = await this.db + .prepare('SELECT * FROM users WHERE phone_hash = ?') + .bind(phoneHash) + .first(); + return result ? this.mapUser(result) : null; + } + + async createUser(user: Omit): Promise { + await this.db + .prepare( + 'INSERT INTO users (id, phone_hash, display_name) VALUES (?, ?, ?)' + ) + .bind(user.id, user.phoneHash, user.displayName || null) + .run(); + return this.getUser(user.id) as Promise; + } + + async updateUser(id: string, updates: Partial): Promise { + const fields: string[] = []; + const values: any[] = []; + + if (updates.displayName !== undefined) { + fields.push('display_name = ?'); + values.push(updates.displayName); + } + + if (fields.length === 0) return; + + fields.push('updated_at = unixepoch()'); + values.push(id); + + await this.db + .prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`) + .bind(...values) + .run(); + } + + // Rooms + async getRoom(name: string): Promise { + const result = await this.db + .prepare('SELECT * FROM rooms WHERE name = ?') + .bind(name) + .first(); + return result ? this.mapRoom(result) : null; + } + + async createRoom(room: Omit): Promise { + await this.db + .prepare( + 'INSERT INTO rooms (id, name, owner_id, topic, is_durable, soft_expired_at) VALUES (?, ?, ?, ?, ?, ?)' + ) + .bind( + room.id, + room.name, + room.ownerId || null, + room.topic || null, + room.isDurable ? 1 : 0, + room.softExpiredAt || null + ) + .run(); + return this.getRoom(room.name) as Promise; + } + + async updateRoom(name: string, updates: Partial): Promise { + const fields: string[] = []; + const values: any[] = []; + + if (updates.topic !== undefined) { + fields.push('topic = ?'); + values.push(updates.topic); + } + if (updates.isDurable !== undefined) { + fields.push('is_durable = ?'); + values.push(updates.isDurable ? 1 : 0); + } + if (updates.softExpiredAt !== undefined) { + fields.push('soft_expired_at = ?'); + values.push(updates.softExpiredAt); + } + + if (fields.length === 0) return; + + fields.push('updated_at = unixepoch()'); + values.push(name); + + await this.db + .prepare(`UPDATE rooms SET ${fields.join(', ')} WHERE name = ?`) + .bind(...values) + .run(); + } + + async listRooms(filters?: { isDurable?: boolean }): Promise { + let query = 'SELECT * FROM rooms WHERE 1=1'; + const bindings: any[] = []; + + if (filters?.isDurable !== undefined) { + query += ' AND is_durable = ?'; + bindings.push(filters.isDurable ? 1 : 0); + } + + query += ' ORDER BY updated_at DESC'; + + const result = await this.db.prepare(query).bind(...bindings).all(); + return (result.results || []).map(this.mapRoom); + } + + // Messages + async getMessages( + roomId: string, + limit = 50, + before?: number + ): Promise { + let query = ` + SELECT m.*, u.display_name as user_name + FROM messages m + LEFT JOIN users u ON m.user_id = u.id + WHERE m.room_id = ? + `; + const bindings: any[] = [roomId]; + + if (before) { + query += ' AND m.created_at < ?'; + bindings.push(before); + } + + query += ' ORDER BY m.created_at DESC LIMIT ?'; + bindings.push(limit); + + const result = await this.db.prepare(query).bind(...bindings).all(); + return ((result.results || []) as any[]).reverse().map(this.mapMessage); + } + + async createMessage(message: Omit): Promise { + await this.db + .prepare( + 'INSERT INTO messages (id, room_id, user_id, content) VALUES (?, ?, ?, ?)' + ) + .bind(message.id, message.roomId, message.userId || null, message.content) + .run(); + + const result = await this.db + .prepare('SELECT * FROM messages WHERE id = ?') + .bind(message.id) + .first(); + + return this.mapMessage(result!); + } + + async searchMessages(roomId: string, query: string): Promise { + const result = await this.db + .prepare( + ` + SELECT m.*, u.display_name as user_name + FROM messages_fts fts + JOIN messages m ON fts.rowid = m.rowid + LEFT JOIN users u ON m.user_id = u.id + WHERE fts MATCH ? AND m.room_id = ? + ORDER BY m.created_at DESC + LIMIT 50 + ` + ) + .bind(query, roomId) + .all(); + + return (result.results || []).map(this.mapMessage); + } + + // Sessions + async getSession(id: string): Promise { + const result = await this.db + .prepare('SELECT * FROM sessions WHERE id = ?') + .bind(id) + .first(); + return result ? this.mapSession(result) : null; + } + + async createSession( + session: Omit + ): Promise { + await this.db + .prepare( + 'INSERT INTO sessions (id, user_id, expires_at, device_info) VALUES (?, ?, ?, ?)' + ) + .bind( + session.id, + session.userId, + session.expiresAt, + session.deviceInfo || null + ) + .run(); + return this.getSession(session.id) as Promise; + } + + async deleteSession(id: string): Promise { + await this.db.prepare('DELETE FROM sessions WHERE id = ?').bind(id).run(); + } + + async cleanExpiredSessions(): Promise { + await this.db + .prepare('DELETE FROM sessions WHERE expires_at < unixepoch()') + .run(); + } + + // Credentials + async getCredential(credentialId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM credentials WHERE credential_id = ?') + .bind(credentialId) + .first(); + return result ? this.mapCredential(result) : null; + } + + async getCredentialsByUser(userId: string): Promise { + const result = await this.db + .prepare('SELECT * FROM credentials WHERE user_id = ?') + .bind(userId) + .all(); + return (result.results || []).map(this.mapCredential); + } + + async createCredential( + credential: Omit + ): Promise { + // Convert ArrayBuffer to base64 for storage + const publicKeyBase64 = Buffer.from(credential.credentialPublicKey).toString( + 'base64' + ); + + await this.db + .prepare( + `INSERT INTO credentials + (id, user_id, credential_id, credential_public_key, counter, device_type, backed_up) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .bind( + credential.id, + credential.userId, + credential.credentialId, + publicKeyBase64, + credential.counter, + credential.deviceType || null, + credential.backedUp ? 1 : 0 + ) + .run(); + + return this.getCredential(credential.credentialId) as Promise; + } + + async updateCredential( + id: string, + updates: Partial + ): Promise { + const fields: string[] = []; + const values: any[] = []; + + if (updates.counter !== undefined) { + fields.push('counter = ?'); + values.push(updates.counter); + } + if (updates.lastUsedAt !== undefined) { + fields.push('last_used_at = ?'); + values.push(updates.lastUsedAt); + } + + if (fields.length === 0) return; + + values.push(id); + + await this.db + .prepare(`UPDATE credentials SET ${fields.join(', ')} WHERE id = ?`) + .bind(...values) + .run(); + } + + // Verification codes + async getVerificationCode(phoneHash: string): Promise { + const result = await this.db + .prepare('SELECT * FROM verification_codes WHERE phone_hash = ?') + .bind(phoneHash) + .first(); + return result ? this.mapVerificationCode(result) : null; + } + + async createVerificationCode( + code: Omit + ): Promise { + await this.db + .prepare( + `INSERT OR REPLACE INTO verification_codes + (phone_hash, code, expires_at, attempts) + VALUES (?, ?, ?, ?)` + ) + .bind(code.phoneHash, code.code, code.expiresAt, code.attempts) + .run(); + return this.getVerificationCode(code.phoneHash) as Promise; + } + + async deleteVerificationCode(phoneHash: string): Promise { + await this.db + .prepare('DELETE FROM verification_codes WHERE phone_hash = ?') + .bind(phoneHash) + .run(); + } + + async incrementVerificationAttempts(phoneHash: string): Promise { + await this.db + .prepare( + 'UPDATE verification_codes SET attempts = attempts + 1 WHERE phone_hash = ?' + ) + .bind(phoneHash) + .run(); + } + + // Cleanup + async cleanupExpiredRooms(): Promise { + await this.db + .prepare( + `DELETE FROM rooms + WHERE is_durable = 0 + AND soft_expired_at IS NOT NULL + AND soft_expired_at < unixepoch()` + ) + .run(); + } + + // Mappers + private mapUser(row: any): User { + return { + id: row.id, + phoneHash: row.phone_hash, + displayName: row.display_name, + createdAt: row.created_at, + updatedAt: row.updated_at + }; + } + + private mapRoom(row: any): Room { + return { + id: row.id, + name: row.name, + ownerId: row.owner_id, + topic: row.topic, + isDurable: Boolean(row.is_durable), + softExpiredAt: row.soft_expired_at, + createdAt: row.created_at, + updatedAt: row.updated_at + }; + } + + private mapMessage(row: any): Message { + return { + id: row.id, + roomId: row.room_id, + userId: row.user_id, + userName: row.user_name, + content: row.content, + createdAt: row.created_at + }; + } + + private mapSession(row: any): Session { + return { + id: row.id, + userId: row.user_id, + expiresAt: row.expires_at, + deviceInfo: row.device_info, + createdAt: row.created_at + }; + } + + private mapCredential(row: any): Credential { + return { + id: row.id, + userId: row.user_id, + credentialId: row.credential_id, + credentialPublicKey: Buffer.from(row.credential_public_key, 'base64'), + counter: row.counter, + deviceType: row.device_type, + backedUp: Boolean(row.backed_up), + createdAt: row.created_at, + lastUsedAt: row.last_used_at + }; + } + + private mapVerificationCode(row: any): VerificationCode { + return { + phoneHash: row.phone_hash, + code: row.code, + expiresAt: row.expires_at, + attempts: row.attempts, + createdAt: row.created_at + }; + } +} diff --git a/app/src/lib/server/storage/sqlite-adapter.ts b/app/src/lib/server/storage/sqlite-adapter.ts new file mode 100644 index 0000000..7a49832 --- /dev/null +++ b/app/src/lib/server/storage/sqlite-adapter.ts @@ -0,0 +1,99 @@ +/** + * SQLite storage adapter for local development + * Uses better-sqlite3 or similar for file-based SQLite + */ + +import type { StorageAdapter, User, Room, Message, Session, Credential } from './types'; + +// This would use better-sqlite3 or similar in production +// For now, it's a placeholder showing the structure +export class SQLiteStorageAdapter implements StorageAdapter { + // In real implementation, initialize with: new Database('chat.db') + constructor(private dbPath: string) { + // Initialize SQLite connection + // Run schema.sql to create tables + } + + // Implementation would be nearly identical to D1StorageAdapter + // but using better-sqlite3 APIs instead of D1 APIs + + async getUser(id: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async getUserByPhone(phoneHash: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async createUser(user: Omit): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async updateUser(id: string, updates: Partial): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async getRoom(name: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async createRoom(room: Omit): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async updateRoom(name: string, updates: Partial): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async listRooms(filters?: { isDurable?: boolean }): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async getMessages(roomId: string, limit?: number, before?: number): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async createMessage(message: Omit): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async searchMessages(roomId: string, query: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async getSession(id: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async createSession(session: Omit): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async deleteSession(id: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async cleanExpiredSessions(): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async getCredential(credentialId: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async getCredentialsByUser(userId: string): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async createCredential(credential: Omit): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async updateCredential(id: string, updates: Partial): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } + + async cleanupExpiredRooms(): Promise { + throw new Error('SQLite adapter not yet implemented - use D1 adapter'); + } +} diff --git a/app/src/lib/server/storage/types.ts b/app/src/lib/server/storage/types.ts new file mode 100644 index 0000000..6dc7ac6 --- /dev/null +++ b/app/src/lib/server/storage/types.ts @@ -0,0 +1,100 @@ +/** + * Storage abstraction layer + * Allows swapping D1, PostgreSQL, SQLite, etc. + */ + +export interface User { + id: string; + phoneHash: string; + displayName?: string; + createdAt: number; + updatedAt: number; +} + +export interface Room { + id: string; + name: string; + ownerId?: string; + topic?: string; + isDurable: boolean; + softExpiredAt?: number; + createdAt: number; + updatedAt: number; +} + +export interface Message { + id: string; + roomId: string; + userId?: string; + userName?: string; + content: string; + createdAt: number; +} + +export interface Session { + id: string; + userId: string; + expiresAt: number; + deviceInfo?: string; + createdAt: number; +} + +export interface Credential { + id: string; + userId: string; + credentialId: string; + credentialPublicKey: ArrayBuffer; + counter: number; + deviceType?: string; + backedUp: boolean; + createdAt: number; + lastUsedAt?: number; +} + +export interface VerificationCode { + phoneHash: string; + code: string; + expiresAt: number; + attempts: number; + createdAt: number; +} + +export interface StorageAdapter { + // Users + getUser(id: string): Promise; + getUserByPhone(phoneHash: string): Promise; + createUser(user: Omit): Promise; + updateUser(id: string, updates: Partial): Promise; + + // Rooms + getRoom(name: string): Promise; + createRoom(room: Omit): Promise; + updateRoom(name: string, updates: Partial): Promise; + listRooms(filters?: { isDurable?: boolean }): Promise; + + // Messages + getMessages(roomId: string, limit?: number, before?: number): Promise; + createMessage(message: Omit): Promise; + searchMessages(roomId: string, query: string): Promise; + + // Sessions + getSession(id: string): Promise; + createSession(session: Omit): Promise; + deleteSession(id: string): Promise; + cleanExpiredSessions(): Promise; + + // Credentials (WebAuthn) + getCredential(credentialId: string): Promise; + getCredentialsByUser(userId: string): Promise; + createCredential(credential: Omit): Promise; + updateCredential(id: string, updates: Partial): Promise; + + // Verification codes + getVerificationCode(phoneHash: string): Promise; + createVerificationCode(code: Omit): Promise; + deleteVerificationCode(phoneHash: string): Promise; + incrementVerificationAttempts(phoneHash: string): Promise; + + // Cleanup + cleanupExpiredRooms(): Promise; +} diff --git a/app/src/lib/stores/websocket.ts b/app/src/lib/stores/websocket.ts new file mode 100644 index 0000000..928f411 --- /dev/null +++ b/app/src/lib/stores/websocket.ts @@ -0,0 +1,179 @@ +/** + * WebSocket store for real-time chat + */ + +import { writable } from 'svelte/store'; + +export interface ChatMessage { + id: string; + userId: string; + userName: string; + content: string; + timestamp: number; +} + +export interface RoomInfo { + room: string; + topic?: string; + users: Array<{ id: string; name: string }>; + messages: ChatMessage[]; +} + +interface WebSocketStore { + connected: boolean; + room: string | null; + topic: string | null; + users: Array<{ id: string; name: string }>; + messages: ChatMessage[]; + error: string | null; +} + +function createWebSocketStore() { + const { subscribe, set, update } = writable({ + connected: false, + room: null, + topic: null, + users: [], + messages: [], + error: null + }); + + let ws: WebSocket | null = null; + let reconnectTimer: ReturnType | null = null; + let currentRoom: string | null = null; + + function connect(room: string) { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.close(); + } + + currentRoom = room; + + // Build WebSocket URL + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/api/ws?room=${encodeURIComponent(room)}`; + + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + update((state) => ({ ...state, connected: true, error: null, room })); + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + switch (data.type) { + case 'room_info': + update((state) => ({ + ...state, + room: data.room, + topic: data.topic, + users: data.users || [], + messages: data.messages || [] + })); + break; + + case 'chat': + update((state) => ({ + ...state, + messages: [ + ...state.messages, + { + id: data.id, + userId: data.userId, + userName: data.userName, + content: data.content, + timestamp: data.timestamp + } + ] + })); + break; + + case 'event': + // Add system event as special message + update((state) => ({ + ...state, + messages: [ + ...state.messages, + { + id: crypto.randomUUID(), + userId: 'system', + userName: 'System', + content: data.content, + timestamp: Date.now() / 1000 + } + ] + })); + break; + + case 'error': + update((state) => ({ ...state, error: data.message })); + break; + } + } catch (err) { + console.error('Failed to parse WebSocket message:', err); + } + }; + + ws.onerror = (err) => { + console.error('WebSocket error:', err); + update((state) => ({ ...state, error: 'Connection error' })); + }; + + ws.onclose = () => { + update((state) => ({ ...state, connected: false })); + + // Attempt reconnection after 3 seconds + if (currentRoom) { + reconnectTimer = setTimeout(() => { + connect(currentRoom!); + }, 3000); + } + }; + } + + function disconnect() { + currentRoom = null; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + if (ws) { + ws.close(); + ws = null; + } + set({ + connected: false, + room: null, + topic: null, + users: [], + messages: [], + error: null + }); + } + + function sendMessage(content: string) { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: 'chat', + content + }) + ); + } + } + + return { + subscribe, + connect, + disconnect, + sendMessage + }; +} + +export const wsStore = createWebSocketStore(); diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte new file mode 100644 index 0000000..2e511e0 --- /dev/null +++ b/app/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + + diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte new file mode 100644 index 0000000..6a813ea --- /dev/null +++ b/app/src/routes/+page.svelte @@ -0,0 +1,63 @@ + + +{#if loading} +
+
+
+

Loading...

+
+
+{:else if !isAuthenticated} + +{:else} +
+
+

Join a room

+

+ Enter a room name to start chatting +

+ +
{ e.preventDefault(); if (room) window.location.href = `/r/${encodeURIComponent(room)}`; }}> + +
+ + +
+
+ +

+ Or share a room link like:
+ {window.location.origin}/r/your-room-name +

+
+
+{/if} diff --git a/app/src/routes/api/agents/+server.ts b/app/src/routes/api/agents/+server.ts new file mode 100644 index 0000000..5d17b41 --- /dev/null +++ b/app/src/routes/api/agents/+server.ts @@ -0,0 +1,47 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; + +// Create a new agent user account +export const POST: RequestHandler = async ({ locals, platform, request }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const body = await request.json(); + const { displayName } = body; + + if (!displayName || typeof displayName !== 'string' || displayName.trim().length === 0) { + throw error(400, 'Display name is required'); + } + + // Create agent user account + const agentId = crypto.randomUUID(); + const now = Math.floor(Date.now() / 1000); + + // Generate a unique phone_hash for the agent (we use a UUID since agents don't have phones) + const phoneHash = `agent_${agentId}`; + + try { + await platform!.env.DB.prepare( + `INSERT INTO users (id, phone_hash, display_name, user_type, created_at, updated_at) + VALUES (?, ?, ?, 'agent', ?, ?)` + ) + .bind(agentId, phoneHash, displayName.trim(), now, now) + .run(); + + return json( + { + user: { + id: agentId, + displayName: displayName.trim(), + userType: 'agent', + createdAt: now + } + }, + { status: 201 } + ); + } catch (err: any) { + console.error('Failed to create agent user:', err); + throw error(500, 'Failed to create agent user'); + } +}; diff --git a/app/src/routes/api/auth/user/+server.ts b/app/src/routes/api/auth/user/+server.ts new file mode 100644 index 0000000..7500d8e --- /dev/null +++ b/app/src/routes/api/auth/user/+server.ts @@ -0,0 +1,42 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; + +// Update user profile +export const PATCH: RequestHandler = async ({ request, locals }) => { + const userId = locals.userId; + + if (!userId) { + throw error(401, 'Unauthorized'); + } + + const { displayName } = await request.json(); + + if (!displayName || displayName.length < 1 || displayName.length > 50) { + throw error(400, 'Display name must be 1-50 characters'); + } + + await locals.storage.updateUser(userId, { displayName }); + + return json({ success: true }); +}; + +// Get current user +export const GET: RequestHandler = async ({ locals }) => { + const userId = locals.userId; + + if (!userId) { + throw error(401, 'Unauthorized'); + } + + const user = await locals.storage.getUser(userId); + + if (!user) { + throw error(404, 'User not found'); + } + + return json({ + id: user.id, + displayName: user.displayName, + createdAt: user.createdAt + }); +}; diff --git a/app/src/routes/api/auth/verify/check/+server.ts b/app/src/routes/api/auth/verify/check/+server.ts new file mode 100644 index 0000000..e5e5f90 --- /dev/null +++ b/app/src/routes/api/auth/verify/check/+server.ts @@ -0,0 +1,92 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { hashPhone, createSessionToken, getExpirationTimestamp } from '$lib/server/auth/utils'; + +// Verify code and create session +export const POST: RequestHandler = async ({ request, locals, platform, cookies }) => { + const { phone, code } = await request.json(); + + if (!phone || !code) { + throw error(400, 'Missing phone or code'); + } + + const phoneHash = await hashPhone(phone); + + // Get verification code + const verification = await locals.storage.getVerificationCode?.(phoneHash); + + if (!verification) { + throw error(404, 'No verification code found'); + } + + // Check expiration + const now = Math.floor(Date.now() / 1000); + if (verification.expiresAt < now) { + await locals.storage.deleteVerificationCode?.(phoneHash); + throw error(400, 'Verification code expired'); + } + + // Check attempts + if (verification.attempts >= 5) { + throw error(429, 'Too many attempts'); + } + + // Verify code + if (verification.code !== code) { + await locals.storage.incrementVerificationAttempts?.(phoneHash); + throw error(400, 'Invalid verification code'); + } + + // Delete verification code + await locals.storage.deleteVerificationCode?.(phoneHash); + + // Get or create user + let user = await locals.storage.getUserByPhone(phoneHash); + + if (!user) { + user = await locals.storage.createUser({ + id: crypto.randomUUID(), + phoneHash + }); + } + + // Create session + const sessionId = crypto.randomUUID(); + const expiresAt = getExpirationTimestamp('30d'); + + await locals.storage.createSession({ + id: sessionId, + userId: user.id, + expiresAt + }); + + // Create JWT token + const env = platform?.env; + const jwtSecret = env?.JWT_SECRET || 'dev-secret-key'; + + const token = await createSessionToken( + { + userId: user.id, + displayName: user.displayName + }, + jwtSecret + ); + + // Set cookie + cookies.set('session', token, { + path: '/', + httpOnly: true, + secure: true, + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 30 // 30 days + }); + + return json({ + success: true, + user: { + id: user.id, + displayName: user.displayName + }, + token + }); +}; diff --git a/app/src/routes/api/auth/verify/send/+server.ts b/app/src/routes/api/auth/verify/send/+server.ts new file mode 100644 index 0000000..e0b9a10 --- /dev/null +++ b/app/src/routes/api/auth/verify/send/+server.ts @@ -0,0 +1,55 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { hashPhone, generateVerificationCode, getExpirationTimestamp } from '$lib/server/auth/utils'; +import { TwilioSmsProvider, MockSmsProvider } from '$lib/server/auth/sms'; + +// Send verification code +export const POST: RequestHandler = async ({ request, locals, platform }) => { + const { phone } = await request.json(); + + if (!phone || !/^\+?[1-9]\d{1,14}$/.test(phone.replace(/\D/g, ''))) { + throw error(400, 'Invalid phone number'); + } + + const phoneHash = await hashPhone(phone); + const code = generateVerificationCode(); + const expiresAt = getExpirationTimestamp('10m'); + + // Store verification code + const existing = await locals.storage.getVerificationCode?.(phoneHash); + + if (existing && existing.attempts >= 5) { + throw error(429, 'Too many attempts. Please try again later.'); + } + + await locals.storage.createVerificationCode?.({ + phoneHash, + code, + expiresAt, + attempts: 0 + }); + + // Send SMS + const env = platform?.env; + let smsProvider; + + if (env?.TWILIO_ACCOUNT_SID && env?.TWILIO_AUTH_TOKEN && env?.TWILIO_PHONE_NUMBER) { + smsProvider = new TwilioSmsProvider( + env.TWILIO_ACCOUNT_SID, + env.TWILIO_AUTH_TOKEN, + env.TWILIO_PHONE_NUMBER + ); + } else { + // Development mode - mock SMS + smsProvider = new MockSmsProvider(); + } + + try { + await smsProvider.sendVerificationCode(phone, code); + } catch (err) { + console.error('SMS send failed:', err); + throw error(500, 'Failed to send verification code'); + } + + return json({ success: true }); +}; diff --git a/app/src/routes/api/auth/webauthn/authenticate/options/+server.ts b/app/src/routes/api/auth/webauthn/authenticate/options/+server.ts new file mode 100644 index 0000000..48bf4f6 --- /dev/null +++ b/app/src/routes/api/auth/webauthn/authenticate/options/+server.ts @@ -0,0 +1,44 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { + generateAuthenticationOptions, + type PublicKeyCredentialRequestOptionsJSON +} from '@simplewebauthn/server'; + +// Generate WebAuthn authentication options +export const POST: RequestHandler = async ({ request, locals, platform }) => { + const { phoneHash } = await request.json(); + + if (!phoneHash) { + throw error(400, 'Missing phoneHash'); + } + + // Get user by phone + const user = await locals.storage.getUserByPhone(phoneHash); + + if (!user) { + throw error(404, 'User not found'); + } + + // Get credentials for this user + const credentials = await locals.storage.getCredentialsByUser(user.id); + + if (credentials.length === 0) { + throw error(404, 'No credentials registered'); + } + + const env = platform?.env; + const rpID = env?.WEBAUTHN_RP_ID || 'localhost'; + + const options: PublicKeyCredentialRequestOptionsJSON = + await generateAuthenticationOptions({ + rpID, + allowCredentials: credentials.map((cred) => ({ + id: cred.credentialId, + type: 'public-key' + })), + userVerification: 'preferred' + }); + + return json({ options, userId: user.id }); +}; diff --git a/app/src/routes/api/auth/webauthn/authenticate/verify/+server.ts b/app/src/routes/api/auth/webauthn/authenticate/verify/+server.ts new file mode 100644 index 0000000..d1d3986 --- /dev/null +++ b/app/src/routes/api/auth/webauthn/authenticate/verify/+server.ts @@ -0,0 +1,111 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { + verifyAuthenticationResponse, + type AuthenticationResponseJSON +} from '@simplewebauthn/server'; +import { createSessionToken, getExpirationTimestamp } from '$lib/server/auth/utils'; + +// Verify WebAuthn authentication +export const POST: RequestHandler = async ({ + request, + locals, + platform, + cookies +}) => { + const body = await request.json(); + const { response, expectedChallenge, userId } = body as { + response: AuthenticationResponseJSON; + expectedChallenge: string; + userId: string; + }; + + if (!userId) { + throw error(400, 'Missing userId'); + } + + // Get credential + const credentialId = Buffer.from(response.id, 'base64').toString('base64'); + const credential = await locals.storage.getCredential(credentialId); + + if (!credential || credential.userId !== userId) { + throw error(404, 'Credential not found'); + } + + const env = platform?.env; + const rpID = env?.WEBAUTHN_RP_ID || 'localhost'; + const origin = env?.WEBAUTHN_ORIGIN || 'http://localhost:5173'; + + try { + const verification = await verifyAuthenticationResponse({ + response, + expectedChallenge, + expectedOrigin: origin, + expectedRPID: rpID, + credential: { + id: credential.credentialId, + publicKey: new Uint8Array(credential.credentialPublicKey), + counter: credential.counter + } + }); + + if (!verification.verified) { + throw error(400, 'Authentication verification failed'); + } + + // Update credential counter + await locals.storage.updateCredential(credential.id, { + counter: verification.authenticationInfo.newCounter, + lastUsedAt: Math.floor(Date.now() / 1000) + }); + + // Get user + const user = await locals.storage.getUser(userId); + + if (!user) { + throw error(404, 'User not found'); + } + + // Create session + const sessionId = crypto.randomUUID(); + const expiresAt = getExpirationTimestamp('30d'); + + await locals.storage.createSession({ + id: sessionId, + userId: user.id, + expiresAt + }); + + // Create JWT token + const jwtSecret = env?.JWT_SECRET || 'dev-secret-key'; + + const token = await createSessionToken( + { + userId: user.id, + displayName: user.displayName + }, + jwtSecret + ); + + // Set cookie + cookies.set('session', token, { + path: '/', + httpOnly: true, + secure: true, + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 30 // 30 days + }); + + return json({ + verified: true, + user: { + id: user.id, + displayName: user.displayName + }, + token + }); + } catch (err: any) { + console.error('WebAuthn authentication error:', err); + throw error(400, err.message || 'Authentication failed'); + } +}; diff --git a/app/src/routes/api/auth/webauthn/register/options/+server.ts b/app/src/routes/api/auth/webauthn/register/options/+server.ts new file mode 100644 index 0000000..3f8b9f4 --- /dev/null +++ b/app/src/routes/api/auth/webauthn/register/options/+server.ts @@ -0,0 +1,52 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { + generateRegistrationOptions, + type PublicKeyCredentialCreationOptionsJSON +} from '@simplewebauthn/server'; + +// Generate WebAuthn registration options +export const POST: RequestHandler = async ({ locals, platform }) => { + const userId = locals.userId; + + if (!userId) { + throw error(401, 'Unauthorized'); + } + + const user = await locals.storage.getUser(userId); + + if (!user) { + throw error(404, 'User not found'); + } + + const env = platform?.env; + const rpName = env?.WEBAUTHN_RP_NAME || 'Mini Chat'; + const rpID = env?.WEBAUTHN_RP_ID || 'localhost'; + + // Get existing credentials for this user + const existingCredentials = await locals.storage.getCredentialsByUser(userId); + + const options: PublicKeyCredentialCreationOptionsJSON = + await generateRegistrationOptions({ + rpName, + rpID, + userID: new TextEncoder().encode(userId), + userName: user.displayName || userId, + attestationType: 'none', + excludeCredentials: existingCredentials.map((cred) => ({ + id: cred.credentialId, + type: 'public-key' + })), + authenticatorSelection: { + residentKey: 'preferred', + userVerification: 'preferred', + authenticatorAttachment: 'platform' + } + }); + + // Store challenge in session (or platform KV for Cloudflare) + // For simplicity, we'll return it and expect client to send it back + // In production, store in a temporary cache + + return json(options); +}; diff --git a/app/src/routes/api/auth/webauthn/register/verify/+server.ts b/app/src/routes/api/auth/webauthn/register/verify/+server.ts new file mode 100644 index 0000000..e71c873 --- /dev/null +++ b/app/src/routes/api/auth/webauthn/register/verify/+server.ts @@ -0,0 +1,56 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { + verifyRegistrationResponse, + type RegistrationResponseJSON +} from '@simplewebauthn/server'; + +// Verify WebAuthn registration +export const POST: RequestHandler = async ({ request, locals, platform }) => { + const userId = locals.userId; + + if (!userId) { + throw error(401, 'Unauthorized'); + } + + const body = await request.json(); + const { response, expectedChallenge } = body as { + response: RegistrationResponseJSON; + expectedChallenge: string; + }; + + const env = platform?.env; + const rpID = env?.WEBAUTHN_RP_ID || 'localhost'; + const origin = env?.WEBAUTHN_ORIGIN || 'http://localhost:5173'; + + try { + const verification = await verifyRegistrationResponse({ + response, + expectedChallenge, + expectedOrigin: origin, + expectedRPID: rpID + }); + + if (!verification.verified || !verification.registrationInfo) { + throw error(400, 'Registration verification failed'); + } + + const { credential } = verification.registrationInfo; + + // Store credential + await locals.storage.createCredential({ + id: crypto.randomUUID(), + userId, + credentialId: Buffer.from(credential.id).toString('base64'), + credentialPublicKey: credential.publicKey, + counter: credential.counter, + backedUp: credential.backedUp, + deviceType: credential.deviceType + }); + + return json({ verified: true }); + } catch (err: any) { + console.error('WebAuthn registration error:', err); + throw error(400, err.message || 'Registration failed'); + } +}; diff --git a/app/src/routes/api/rooms/[room]/+server.ts b/app/src/routes/api/rooms/[room]/+server.ts new file mode 100644 index 0000000..8705248 --- /dev/null +++ b/app/src/routes/api/rooms/[room]/+server.ts @@ -0,0 +1,44 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; + +// Get room info +export const GET: RequestHandler = async ({ params, locals }) => { + const { room } = params; + const roomData = await locals.storage.getRoom(room); + + if (!roomData) { + throw error(404, 'Room not found'); + } + + return json(roomData); +}; + +// Create or update room +export const POST: RequestHandler = async ({ params, request, locals }) => { + const { room } = params; + const body = await request.json(); + + const { userId, topic, isDurable } = body; + + // Check if room exists + const existing = await locals.storage.getRoom(room); + + if (existing) { + // Update existing room + await locals.storage.updateRoom(room, { + topic, + isDurable: Boolean(isDurable) + }); + } else { + // Create new room + await locals.storage.createRoom({ + id: crypto.randomUUID(), + name: room, + ownerId: userId, + topic, + isDurable: Boolean(isDurable) + }); + } + + return json({ success: true }); +}; diff --git a/app/src/routes/api/rooms/[room]/agents/+server.ts b/app/src/routes/api/rooms/[room]/agents/+server.ts new file mode 100644 index 0000000..a92e43e --- /dev/null +++ b/app/src/routes/api/rooms/[room]/agents/+server.ts @@ -0,0 +1,112 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { registerAgent, getActiveAgents } from '$lib/server/agents/registry'; +import type { TriggerConfig } from '$lib/server/agents/types'; + +// List all agents in a room +export const GET: RequestHandler = async ({ params, locals, platform }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const { room } = params; + + // Get room to verify it exists and get ID + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + // TODO: Check permissions - user should be able to view room members + + const agents = await getActiveAgents(platform!.env.DB, roomData.id); + + return json({ agents }); +}; + +// Register a new agent in the room +export const POST: RequestHandler = async ({ params, locals, platform, request }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const { room } = params; + + // Get room to verify it exists and get ID + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + // TODO: Check permissions - user should have agent management permission + + const body = await request.json(); + const { agentUserId, webhookUrl, webhookSecret, triggerConfig, displayName } = body; + + // Validate required fields + if (!agentUserId || typeof agentUserId !== 'string') { + throw error(400, 'agentUserId is required'); + } + + if (!webhookUrl || typeof webhookUrl !== 'string') { + throw error(400, 'webhookUrl is required'); + } + + // Validate URL + try { + new URL(webhookUrl); + } catch { + throw error(400, 'Invalid webhookUrl'); + } + + // Validate trigger config if provided + let parsedTriggerConfig: TriggerConfig | undefined; + if (triggerConfig) { + if ( + !triggerConfig.type || + !['all', 'mentions', 'keywords', 'none'].includes(triggerConfig.type) + ) { + throw error(400, 'Invalid trigger config type'); + } + parsedTriggerConfig = triggerConfig; + } + + // Verify agent user exists + const agentUser = await platform!.env.DB.prepare('SELECT id, user_type FROM users WHERE id = ?') + .bind(agentUserId) + .first<{ id: string; user_type: string }>(); + + if (!agentUser) { + throw error(404, 'Agent user not found'); + } + + if (agentUser.user_type !== 'agent') { + throw error(400, 'User is not an agent account'); + } + + try { + const agent = await registerAgent(platform!.env.DB, { + roomId: roomData.id, + userId: agentUserId, + webhookUrl, + webhookSecret, + triggerConfig: parsedTriggerConfig, + createdBy: locals.userId + }); + + // If display name provided, update the agent user's display name + if (displayName && typeof displayName === 'string') { + await platform!.env.DB.prepare('UPDATE users SET display_name = ? WHERE id = ?') + .bind(displayName.trim(), agentUserId) + .run(); + } + + return json({ agent }, { status: 201 }); + } catch (err: any) { + if (err.message?.includes('UNIQUE constraint failed')) { + throw error(409, 'Agent already registered in this room'); + } + console.error('Failed to register agent:', err); + throw error(500, 'Failed to register agent'); + } +}; diff --git a/app/src/routes/api/rooms/[room]/agents/[agentId]/+server.ts b/app/src/routes/api/rooms/[room]/agents/[agentId]/+server.ts new file mode 100644 index 0000000..89e9942 --- /dev/null +++ b/app/src/routes/api/rooms/[room]/agents/[agentId]/+server.ts @@ -0,0 +1,147 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { getAgent, updateAgent, removeAgent } from '$lib/server/agents/registry'; +import type { TriggerConfig } from '$lib/server/agents/types'; + +// Get a specific agent +export const GET: RequestHandler = async ({ params, locals, platform }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const { room, agentId } = params; + + // Get room to verify it exists + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + const agent = await getAgent(platform!.env.DB, agentId); + if (!agent) { + throw error(404, 'Agent not found'); + } + + if (agent.roomId !== roomData.id) { + throw error(404, 'Agent not found in this room'); + } + + return json({ agent }); +}; + +// Update agent configuration +export const PATCH: RequestHandler = async ({ params, locals, platform, request }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const { room, agentId } = params; + + // Get room to verify it exists + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + const agent = await getAgent(platform!.env.DB, agentId); + if (!agent) { + throw error(404, 'Agent not found'); + } + + if (agent.roomId !== roomData.id) { + throw error(404, 'Agent not found in this room'); + } + + // TODO: Check permissions - user should have agent management permission + + const body = await request.json(); + const { webhookUrl, webhookSecret, triggerConfig, isActive } = body; + + const updates: { + webhookUrl?: string; + webhookSecret?: string; + triggerConfig?: TriggerConfig; + isActive?: boolean; + } = {}; + + if (webhookUrl !== undefined) { + if (typeof webhookUrl !== 'string') { + throw error(400, 'Invalid webhookUrl'); + } + try { + new URL(webhookUrl); + } catch { + throw error(400, 'Invalid webhookUrl'); + } + updates.webhookUrl = webhookUrl; + } + + if (webhookSecret !== undefined) { + if (webhookSecret !== null && typeof webhookSecret !== 'string') { + throw error(400, 'Invalid webhookSecret'); + } + updates.webhookSecret = webhookSecret; + } + + if (triggerConfig !== undefined) { + if ( + !triggerConfig.type || + !['all', 'mentions', 'keywords', 'none'].includes(triggerConfig.type) + ) { + throw error(400, 'Invalid trigger config type'); + } + updates.triggerConfig = triggerConfig; + } + + if (isActive !== undefined) { + if (typeof isActive !== 'boolean') { + throw error(400, 'Invalid isActive'); + } + updates.isActive = isActive; + } + + const success = await updateAgent(platform!.env.DB, agentId, updates); + + if (!success) { + throw error(500, 'Failed to update agent'); + } + + // Fetch updated agent + const updatedAgent = await getAgent(platform!.env.DB, agentId); + + return json({ agent: updatedAgent }); +}; + +// Remove agent from room +export const DELETE: RequestHandler = async ({ params, locals, platform }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const { room, agentId } = params; + + // Get room to verify it exists + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + const agent = await getAgent(platform!.env.DB, agentId); + if (!agent) { + throw error(404, 'Agent not found'); + } + + if (agent.roomId !== roomData.id) { + throw error(404, 'Agent not found in this room'); + } + + // TODO: Check permissions - user should have agent management permission + + const success = await removeAgent(platform!.env.DB, agentId); + + if (!success) { + throw error(500, 'Failed to remove agent'); + } + + return json({ success: true }); +}; diff --git a/app/src/routes/api/rooms/[room]/messages/+server.ts b/app/src/routes/api/rooms/[room]/messages/+server.ts new file mode 100644 index 0000000..1ffe5fa --- /dev/null +++ b/app/src/routes/api/rooms/[room]/messages/+server.ts @@ -0,0 +1,47 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; + +// Get messages for a room +export const GET: RequestHandler = async ({ params, locals, url }) => { + const { room } = params; + const limit = parseInt(url.searchParams.get('limit') || '50'); + const before = url.searchParams.get('before'); + + // Get room to get ID + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + const messages = await locals.storage.getMessages( + roomData.id, + limit, + before ? parseInt(before) : undefined + ); + + return json({ + messages, + hasMore: messages.length === limit + }); +}; + +// Search messages in a room +export const POST: RequestHandler = async ({ params, request, locals }) => { + const { room } = params; + const body = await request.json(); + const { query } = body; + + if (!query) { + throw error(400, 'Missing query'); + } + + // Get room to get ID + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + const messages = await locals.storage.searchMessages(roomData.id, query); + + return json({ messages }); +}; diff --git a/app/src/routes/api/rooms/[room]/roles/+server.ts b/app/src/routes/api/rooms/[room]/roles/+server.ts new file mode 100644 index 0000000..91018c6 --- /dev/null +++ b/app/src/routes/api/rooms/[room]/roles/+server.ts @@ -0,0 +1,83 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; + +// List all roles in a room +export const GET: RequestHandler = async ({ params, locals }) => { + const { room } = params; + + // Get room + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + // Get roles with permissions + const roles = await locals.permissions.getRolesByRoom(roomData.id); + + // Get permissions for each role + const rolesWithPermissions = await Promise.all( + roles.map(async (role) => ({ + ...role, + permissions: await locals.permissions.getRolePermissions(role.id) + })) + ); + + return json({ roles: rolesWithPermissions }); +}; + +// Create a new role +export const POST: RequestHandler = async ({ params, request, locals }) => { + const userId = locals.userId; + if (!userId) { + throw error(401, 'Unauthorized'); + } + + const { room } = params; + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + // Check permission + const canManage = await locals.permissions.hasPermission( + userId, + roomData.id, + 'settings.manage_roles' + ); + if (!canManage) { + throw error(403, 'You do not have permission to manage roles'); + } + + const body = await request.json(); + const { name, description, color, permissions: permissionNames, position } = body; + + if (!name || !permissionNames) { + throw error(400, 'Missing required fields: name, permissions'); + } + + // Create role + const role = await locals.permissions.createRole({ + roomId: roomData.id, + name, + description, + color: color || '#888888', + isDefault: false, + isSystem: false, + position: position || 500 + }); + + // Get permission IDs from names + const allPermissions = await locals.permissions.getAllPermissions(); + const permissionMap = new Map(allPermissions.map((p) => [p.name, p.id])); + const permissionIds = permissionNames + .map((name: string) => permissionMap.get(name)) + .filter((id: string | undefined): id is string => id !== undefined); + + // Set role permissions + await locals.permissions.setRolePermissions(role.id, permissionIds); + + // Get role with permissions + const roleWithPerms = await locals.permissions.getRoleWithPermissions(role.id); + + return json({ role: roleWithPerms }, { status: 201 }); +}; diff --git a/app/src/routes/api/rooms/[room]/roles/[roleId]/+server.ts b/app/src/routes/api/rooms/[room]/roles/[roleId]/+server.ts new file mode 100644 index 0000000..ea24457 --- /dev/null +++ b/app/src/routes/api/rooms/[room]/roles/[roleId]/+server.ts @@ -0,0 +1,118 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; + +// Get a specific role +export const GET: RequestHandler = async ({ params, locals }) => { + const { roleId } = params; + + const role = await locals.permissions.getRoleWithPermissions(roleId); + if (!role) { + throw error(404, 'Role not found'); + } + + return json({ role }); +}; + +// Update a role +export const PATCH: RequestHandler = async ({ params, request, locals }) => { + const userId = locals.userId; + if (!userId) { + throw error(401, 'Unauthorized'); + } + + const { room, roleId } = params; + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + // Check permission + const canManage = await locals.permissions.hasPermission( + userId, + roomData.id, + 'settings.manage_roles' + ); + if (!canManage) { + throw error(403, 'You do not have permission to manage roles'); + } + + // Get existing role + const existingRole = await locals.permissions.getRole(roleId); + if (!existingRole || existingRole.roomId !== roomData.id) { + throw error(404, 'Role not found'); + } + + // Can't edit system roles + if (existingRole.isSystem) { + throw error(403, 'Cannot edit system roles'); + } + + const body = await request.json(); + const { name, description, color, permissions: permissionNames, position } = body; + + // Update role metadata + const updates: any = {}; + if (name !== undefined) updates.name = name; + if (description !== undefined) updates.description = description; + if (color !== undefined) updates.color = color; + if (position !== undefined) updates.position = position; + + if (Object.keys(updates).length > 0) { + await locals.permissions.updateRole(roleId, updates); + } + + // Update permissions if provided + if (permissionNames) { + const allPermissions = await locals.permissions.getAllPermissions(); + const permissionMap = new Map(allPermissions.map((p) => [p.name, p.id])); + const permissionIds = permissionNames + .map((name: string) => permissionMap.get(name)) + .filter((id: string | undefined): id is string => id !== undefined); + + await locals.permissions.setRolePermissions(roleId, permissionIds); + } + + // Get updated role + const role = await locals.permissions.getRoleWithPermissions(roleId); + + return json({ role }); +}; + +// Delete a role +export const DELETE: RequestHandler = async ({ params, locals }) => { + const userId = locals.userId; + if (!userId) { + throw error(401, 'Unauthorized'); + } + + const { room, roleId } = params; + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + // Check permission + const canManage = await locals.permissions.hasPermission( + userId, + roomData.id, + 'settings.manage_roles' + ); + if (!canManage) { + throw error(403, 'You do not have permission to manage roles'); + } + + // Get existing role + const existingRole = await locals.permissions.getRole(roleId); + if (!existingRole || existingRole.roomId !== roomData.id) { + throw error(404, 'Role not found'); + } + + // Can't delete system roles + if (existingRole.isSystem) { + throw error(403, 'Cannot delete system roles'); + } + + await locals.permissions.deleteRole(roleId); + + return json({ success: true }); +}; diff --git a/app/src/routes/api/rooms/[room]/send/+server.ts b/app/src/routes/api/rooms/[room]/send/+server.ts new file mode 100644 index 0000000..df1c77f --- /dev/null +++ b/app/src/routes/api/rooms/[room]/send/+server.ts @@ -0,0 +1,94 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; + +/** + * POST /api/rooms/:room/send + * Send a message to a room (for agents using API tokens) + * Requires Bearer token authentication + */ +export const POST: RequestHandler = async ({ params, request, locals, platform }) => { + // Must be authenticated with API token + if (!locals.userId) { + throw error(401, 'Authentication required. Use Bearer token.'); + } + + const { room } = params; + + // Get room to verify it exists + const roomData = await locals.storage.getRoom(room); + if (!roomData) { + throw error(404, 'Room not found'); + } + + const body = await request.json(); + const { content } = body; + + if (!content || typeof content !== 'string' || content.trim().length === 0) { + throw error(400, 'Message content is required'); + } + + if (content.length > 4000) { + throw error(400, 'Message content too long (max 4000 characters)'); + } + + // Get user info + const user = await platform!.env.DB.prepare('SELECT display_name FROM users WHERE id = ?') + .bind(locals.userId) + .first<{ display_name: string }>(); + + if (!user) { + throw error(404, 'User not found'); + } + + // Create message in database + const messageId = crypto.randomUUID(); + const timestamp = Math.floor(Date.now() / 1000); + + await platform!.env.DB.prepare( + `INSERT INTO messages (id, room_id, user_id, content, created_at) + VALUES (?, ?, ?, ?, ?)` + ) + .bind(messageId, roomData.id, locals.userId, content.trim(), timestamp) + .run(); + + // Update room timestamp + await platform!.env.DB.prepare('UPDATE rooms SET updated_at = ? WHERE id = ?') + .bind(timestamp, roomData.id) + .run(); + + // Get the Durable Object to broadcast the message + const roomId = platform!.env.CHATROOM.idFromName(room); + const roomStub = platform!.env.CHATROOM.get(roomId); + + // Send broadcast request to the Durable Object + try { + await roomStub.fetch('http://internal/broadcast', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'chat', + id: messageId, + userId: locals.userId, + userName: user.display_name || 'Unknown', + content: content.trim(), + timestamp + }) + }); + } catch (err) { + console.error('Failed to broadcast message to room:', err); + // Message is still persisted, just not broadcast in real-time + } + + return json( + { + message: { + id: messageId, + roomId: roomData.id, + userId: locals.userId, + content: content.trim(), + createdAt: timestamp + } + }, + { status: 201 } + ); +}; diff --git a/app/src/routes/api/tokens/+server.ts b/app/src/routes/api/tokens/+server.ts new file mode 100644 index 0000000..774452d --- /dev/null +++ b/app/src/routes/api/tokens/+server.ts @@ -0,0 +1,65 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { createApiToken, listApiTokens } from '$lib/server/auth/tokens'; + +// List all API tokens for the authenticated user +export const GET: RequestHandler = async ({ locals, platform }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const tokens = await listApiTokens(platform!.env.DB, locals.userId); + + // Don't return the token hashes + const sanitized = tokens.map((t) => ({ + id: t.id, + name: t.name, + lastUsedAt: t.lastUsedAt, + expiresAt: t.expiresAt, + createdAt: t.createdAt + })); + + return json({ tokens: sanitized }); +}; + +// Create a new API token +export const POST: RequestHandler = async ({ locals, platform, request }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const body = await request.json(); + const { name, expiresIn } = body; + + if (!name || typeof name !== 'string' || name.trim().length === 0) { + throw error(400, 'Token name is required'); + } + + if (expiresIn && typeof expiresIn !== 'string') { + throw error(400, 'Invalid expiresIn format. Use format like: 30d, 1y, 24h'); + } + + try { + const { token, record } = await createApiToken( + platform!.env.DB, + locals.userId, + name.trim(), + expiresIn + ); + + return json( + { + token, // Only time this is visible! + id: record.id, + name: record.name, + expiresAt: record.expiresAt, + createdAt: record.createdAt, + warning: 'Save this token now. You will not be able to see it again.' + }, + { status: 201 } + ); + } catch (err) { + console.error('Failed to create API token:', err); + throw error(500, 'Failed to create API token'); + } +}; diff --git a/app/src/routes/api/tokens/[tokenId]/+server.ts b/app/src/routes/api/tokens/[tokenId]/+server.ts new file mode 100644 index 0000000..85461ea --- /dev/null +++ b/app/src/routes/api/tokens/[tokenId]/+server.ts @@ -0,0 +1,35 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { revokeApiToken } from '$lib/server/auth/tokens'; + +// Revoke an API token +export const DELETE: RequestHandler = async ({ params, locals, platform }) => { + if (!locals.userId) { + throw error(401, 'Authentication required'); + } + + const { tokenId } = params; + + // Verify the token belongs to this user + const tokenRecord = await platform!.env.DB.prepare( + 'SELECT user_id FROM api_tokens WHERE id = ?' + ) + .bind(tokenId) + .first<{ user_id: string }>(); + + if (!tokenRecord) { + throw error(404, 'Token not found'); + } + + if (tokenRecord.user_id !== locals.userId) { + throw error(403, 'You do not own this token'); + } + + const success = await revokeApiToken(platform!.env.DB, tokenId); + + if (!success) { + throw error(500, 'Failed to revoke token'); + } + + return json({ success: true }); +}; diff --git a/app/src/routes/api/ws/+server.ts b/app/src/routes/api/ws/+server.ts new file mode 100644 index 0000000..13ac5e4 --- /dev/null +++ b/app/src/routes/api/ws/+server.ts @@ -0,0 +1,58 @@ +import type { RequestHandler } from './$types'; +import type { Env } from '$lib/server/ChatRoom'; +import { error } from '@sveltejs/kit'; + +export const GET: RequestHandler = async ({ request, platform, url, locals }) => { + // Check authentication + const userId = locals.userId; + if (!userId) { + throw error(401, 'Unauthorized'); + } + + // Get user info + const user = await locals.storage.getUser(userId); + if (!user) { + throw error(404, 'User not found'); + } + + // Cloudflare platform object + const env = platform?.env as Env; + + if (!env?.CHATROOM) { + throw error(500, 'Durable Objects not available'); + } + + // Get room name from query params + const roomName = url.searchParams.get('room'); + + if (!roomName) { + throw error(400, 'Missing room parameter'); + } + + // Ensure room exists in database + let room = await locals.storage.getRoom(roomName); + if (!room) { + // Create room + room = await locals.storage.createRoom({ + id: crypto.randomUUID(), + name: roomName, + ownerId: userId, + isDurable: false + }); + } + + // Get or create Durable Object for this room + const durableId = env.CHATROOM.idFromName(roomName); + const durableRoom = env.CHATROOM.get(durableId); + + // Build request URL with authenticated user info + const wsUrl = new URL(request.url); + wsUrl.searchParams.set('userId', userId); + wsUrl.searchParams.set('userName', user.displayName || 'Anonymous'); + + // Create new request with auth info + const authedRequest = new Request(wsUrl.toString(), request); + + // Forward the WebSocket upgrade request to the Durable Object + return durableRoom.fetch(authedRequest); +}; diff --git a/app/src/routes/r/[room]/+page.svelte b/app/src/routes/r/[room]/+page.svelte new file mode 100644 index 0000000..87dedf2 --- /dev/null +++ b/app/src/routes/r/[room]/+page.svelte @@ -0,0 +1,127 @@ + + +
+
+
+

{ws.room || room}

+ {#if ws.topic} +

{ws.topic}

+ {/if} +
+
+ +
+
+ {#if !ws.connected} +
+ Connecting... +
+ {:else if ws.error} +
+ {ws.error} +
+ {/if} + + {#each ws.messages as message (message.id)} + {#if message.userId === 'system'} +
+ {message.content} +
+ {:else} +
+
+ + {message.userName} + +
+ {message.content} +
+
+
+ {/if} + {/each} +
+
+ +
+
+ + +
+
+
diff --git a/app/svelte.config.js b/app/svelte.config.js new file mode 100644 index 0000000..f2824c9 --- /dev/null +++ b/app/svelte.config.js @@ -0,0 +1,17 @@ +import adapter from '@sveltejs/adapter-cloudflare'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + routes: { + include: ['/*'], + exclude: [''] + } + }) + } +}; + +export default config; diff --git a/app/tests/auth-utils.test.ts b/app/tests/auth-utils.test.ts new file mode 100644 index 0000000..c9f7244 --- /dev/null +++ b/app/tests/auth-utils.test.ts @@ -0,0 +1,139 @@ +/** + * Tests for auth utilities + */ + +import { describe, it, expect } from 'vitest'; +import { + hashPhone, + generateVerificationCode, + createSessionToken, + verifySessionToken, + getExpirationTimestamp +} from '../src/lib/server/auth/utils'; + +describe('Auth Utils', () => { + describe('hashPhone', () => { + it('should hash phone numbers consistently', async () => { + const phone1 = '+15551234567'; + const phone2 = '+1 (555) 123-4567'; // Different format, same number + + const hash1 = await hashPhone(phone1); + const hash2 = await hashPhone(phone2); + + // Should produce same hash for same number + expect(hash1).toBe(hash2); + }); + + it('should produce different hashes for different numbers', async () => { + const hash1 = await hashPhone('+15551234567'); + const hash2 = await hashPhone('+15559876543'); + + expect(hash1).not.toBe(hash2); + }); + + it('should strip non-digits', async () => { + const hash = await hashPhone('+1 (555) 123-4567'); + expect(hash).toMatch(/^[a-f0-9]{64}$/); // SHA-256 produces 64 hex chars + }); + }); + + describe('generateVerificationCode', () => { + it('should generate 6-digit codes', () => { + const code = generateVerificationCode(); + expect(code).toMatch(/^\d{6}$/); + expect(parseInt(code)).toBeGreaterThanOrEqual(100000); + expect(parseInt(code)).toBeLessThan(1000000); + }); + + it('should generate different codes', () => { + const code1 = generateVerificationCode(); + const code2 = generateVerificationCode(); + const code3 = generateVerificationCode(); + + // Very unlikely to be all the same + expect( + code1 === code2 && code2 === code3 + ).toBe(false); + }); + }); + + describe('JWT tokens', () => { + const secret = 'test-secret-key'; + + it('should create and verify valid tokens', async () => { + const payload = { + userId: 'user-123', + displayName: 'Test User' + }; + + const token = await createSessionToken(payload, secret); + const verified = await verifySessionToken(token, secret); + + expect(verified.userId).toBe(payload.userId); + expect(verified.displayName).toBe(payload.displayName); + expect(verified.iat).toBeDefined(); + expect(verified.exp).toBeDefined(); + }); + + it('should reject tokens with wrong secret', async () => { + const payload = { userId: 'user-123' }; + const token = await createSessionToken(payload, secret); + + await expect( + verifySessionToken(token, 'wrong-secret') + ).rejects.toThrow(); + }); + + it('should respect custom expiration', async () => { + const payload = { userId: 'user-123' }; + const token = await createSessionToken(payload, secret, '1h'); + const verified = await verifySessionToken(token, secret); + + const now = Math.floor(Date.now() / 1000); + const expectedExp = now + 3600; // 1 hour + + expect(verified.exp).toBeGreaterThan(now); + expect(verified.exp).toBeLessThanOrEqual(expectedExp + 5); // Allow 5s slack + }); + }); + + describe('getExpirationTimestamp', () => { + it('should calculate seconds correctly', () => { + const now = Math.floor(Date.now() / 1000); + const exp = getExpirationTimestamp('30s'); + + expect(exp).toBeGreaterThanOrEqual(now + 30); + expect(exp).toBeLessThanOrEqual(now + 31); + }); + + it('should calculate minutes correctly', () => { + const now = Math.floor(Date.now() / 1000); + const exp = getExpirationTimestamp('10m'); + + expect(exp).toBeGreaterThanOrEqual(now + 600); + expect(exp).toBeLessThanOrEqual(now + 601); + }); + + it('should calculate hours correctly', () => { + const now = Math.floor(Date.now() / 1000); + const exp = getExpirationTimestamp('2h'); + + expect(exp).toBeGreaterThanOrEqual(now + 7200); + expect(exp).toBeLessThanOrEqual(now + 7201); + }); + + it('should calculate days correctly', () => { + const now = Math.floor(Date.now() / 1000); + const exp = getExpirationTimestamp('7d'); + + expect(exp).toBeGreaterThanOrEqual(now + 604800); + expect(exp).toBeLessThanOrEqual(now + 604801); + }); + + it('should throw on invalid format', () => { + expect(() => getExpirationTimestamp('invalid')).toThrow(); + expect(() => getExpirationTimestamp('10')).toThrow(); + expect(() => getExpirationTimestamp('10x')).toThrow(); + }); + }); +}); diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/app/vitest.config.ts b/app/vitest.config.ts new file mode 100644 index 0000000..3eead07 --- /dev/null +++ b/app/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; +import { sveltekit } from '@sveltejs/kit/vite'; + +export default defineConfig({ + plugins: [sveltekit()], + test: { + globals: true, + environment: 'node' + } +}); diff --git a/app/wrangler.toml b/app/wrangler.toml new file mode 100644 index 0000000..542cb64 --- /dev/null +++ b/app/wrangler.toml @@ -0,0 +1,24 @@ +name = "mini-chat-v2" +compatibility_date = "2024-01-01" +compatibility_flags = ["nodejs_compat"] +pages_build_output_dir = ".svelte-kit/cloudflare" + +# D1 Database +[[d1_databases]] +binding = "DB" +database_name = "mini-chat" +database_id = "placeholder" # Will be created with: wrangler d1 create mini-chat + +# Durable Objects +[[durable_objects.bindings]] +name = "CHATROOM" +class_name = "ChatRoom" +script_name = "mini-chat-v2" + +[[migrations]] +tag = "v1" +new_classes = ["ChatRoom"] + +# Environment variables +[vars] +# Add any public env vars here