Podcasts hosted on ATProto
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Bind server to 0.0.0.0 for Railway deployment

Lee Ingram 98c0a0e3

+11360
+13
.env.example
··· 1 + # Server Configuration 2 + PORT=3000 3 + 4 + # AT Protocol Configuration 5 + ATPROTO_SERVICE=https://bsky.social 6 + ATPROTO_IDENTIFIER=your-handle.bsky.social 7 + ATPROTO_PASSWORD=your-app-password 8 + 9 + # Podcast Feed Configuration 10 + PODCAST_TITLE=My AT Protocol Podcast 11 + PODCAST_DESCRIPTION=A podcast hosted on AT Protocol 12 + PODCAST_LINK=https://example.com 13 + PODCAST_LANGUAGE=en
+35
.gitignore
··· 1 + # Dependencies 2 + node_modules/ 3 + 4 + # Environment variables 5 + .env 6 + .env.local 7 + .env.production 8 + 9 + # Uploads 10 + public/uploads/* 11 + !public/uploads/.gitkeep 12 + 13 + # Metadata storage 14 + podcast-metadata.json 15 + 16 + # Build output 17 + client/dist/ 18 + client/build/ 19 + 20 + # Logs 21 + *.log 22 + npm-debug.log* 23 + 24 + # OS files 25 + .DS_Store 26 + Thumbs.db 27 + 28 + # IDE 29 + .vscode/ 30 + .idea/ 31 + *.swp 32 + *.swo 33 + 34 + # Railway 35 + .railway/
+295
DEPLOYMENT.md
··· 1 + # Deployment Guide 2 + 3 + This guide covers deploying the AT Protocol Podcast Host to Railway (or any Node.js hosting platform). 4 + 5 + ## Overview 6 + 7 + The application is structured as: 8 + - **Backend**: Express API server (Node.js) 9 + - **Frontend**: React app built with Vite 10 + - **Deployment**: Single server serves both API and built React app 11 + 12 + ## Quick Deploy to Railway 13 + 14 + ### Prerequisites 15 + 16 + 1. [Railway account](https://railway.app) 17 + 2. Git repository with your code 18 + 3. AT Protocol credentials (Bluesky account + app password) 19 + 20 + ### Step-by-Step Deployment 21 + 22 + #### 1. Prepare Your Repository 23 + 24 + Make sure all code is committed: 25 + 26 + ```bash 27 + git add . 28 + git commit -m "Prepare for Railway deployment" 29 + git push origin main 30 + ``` 31 + 32 + #### 2. Create Railway Project 33 + 34 + 1. Go to [Railway](https://railway.app) 35 + 2. Click "New Project" 36 + 3. Select "Deploy from GitHub repo" 37 + 4. Choose your repository 38 + 5. Railway will automatically detect it's a Node.js project 39 + 40 + #### 3. Configure Environment Variables 41 + 42 + In Railway dashboard, add these variables: 43 + 44 + ```env 45 + PORT=5000 46 + ATPROTO_SERVICE=https://bsky.social 47 + ATPROTO_IDENTIFIER=your-handle.bsky.social 48 + ATPROTO_PASSWORD=your-app-password 49 + NODE_ENV=production 50 + ``` 51 + 52 + **Important:** Use an **app password**, not your main password! 53 + Get it from: Bluesky App → Settings → App Passwords 54 + 55 + #### 4. Configure Build & Start Commands 56 + 57 + Railway should auto-detect these, but verify: 58 + 59 + **Build Command:** 60 + ```bash 61 + npm run build 62 + ``` 63 + 64 + **Start Command:** 65 + ```bash 66 + npm start 67 + ``` 68 + 69 + #### 5. Deploy 70 + 71 + Railway will automatically: 72 + 1. Install dependencies (`npm install`) 73 + 2. Install client dependencies (`cd client && npm install`) 74 + 3. Build React app (`npm run build`) 75 + 4. Start the server (`npm start`) 76 + 77 + Your app will be live at: `https://your-app.railway.app` 78 + 79 + ## Local Production Testing 80 + 81 + Test the production build locally before deploying: 82 + 83 + ```bash 84 + # Build the React app 85 + npm run build 86 + 87 + # Start the server (will serve built React app) 88 + npm start 89 + ``` 90 + 91 + Visit `http://localhost:5000` - you should see the React app served by the backend. 92 + 93 + ## Environment Variables Reference 94 + 95 + | Variable | Description | Required | Example | 96 + |----------|-------------|----------|---------| 97 + | `PORT` | Server port | No (defaults to 5000) | `5000` | 98 + | `ATPROTO_SERVICE` | AT Protocol service URL | Yes | `https://bsky.social` | 99 + | `ATPROTO_IDENTIFIER` | Your handle or DID | Yes | `yourname.bsky.social` | 100 + | `ATPROTO_PASSWORD` | App password | Yes | `xxxx-xxxx-xxxx-xxxx` | 101 + | `NODE_ENV` | Environment | No | `production` | 102 + 103 + ## How It Works 104 + 105 + ### Development Mode 106 + 107 + ```bash 108 + npm run dev:all 109 + ``` 110 + 111 + - Backend runs on `http://localhost:5000` 112 + - React dev server on `http://localhost:3000` 113 + - React proxies API calls to backend 114 + 115 + ### Production Mode 116 + 117 + ```bash 118 + npm run build 119 + npm start 120 + ``` 121 + 122 + - Single server on port 5000 123 + - Serves built React app from `client/dist/` 124 + - API routes at `/api/*` 125 + - React app handles all other routes 126 + 127 + ### Build Process 128 + 129 + 1. `npm install` - Install root dependencies 130 + 2. `npm run build` - Runs `client:build` 131 + 3. `client:build` - Runs `cd client && npm run build` 132 + 4. Vite builds React app to `client/dist/` 133 + 5. Express serves static files from `client/dist/` 134 + 135 + ## File Structure 136 + 137 + ``` 138 + atproto-podcast/ 139 + ├── client/ # React frontend 140 + │ ├── dist/ # Built React app (generated) 141 + │ ├── src/ # React source 142 + │ ├── package.json 143 + │ └── vite.config.js 144 + ├── src/ # Express backend 145 + │ ├── index.js # Main server (serves React + API) 146 + │ └── routes/ 147 + ├── package.json # Root package.json 148 + ├── railway.json # Railway config 149 + └── nixpacks.toml # Build config 150 + ``` 151 + 152 + ## Deployment Architecture 153 + 154 + ``` 155 + ┌─────────────────────────────────────┐ 156 + │ Railway Server │ 157 + │ │ 158 + │ ┌──────────────────────────────┐ │ 159 + │ │ Express Server (Port 5000) │ │ 160 + │ │ │ │ 161 + │ │ ┌────────────┐ ┌──────────┐│ │ 162 + │ │ │ React App │ │ API ││ │ 163 + │ │ │ (Static) │ │ Routes ││ │ 164 + │ │ │ / │ │ /api/* ││ │ 165 + │ │ └────────────┘ └──────────┘│ │ 166 + │ └──────────────────────────────┘ │ 167 + └─────────────────────────────────────┘ 168 + ``` 169 + 170 + ## Troubleshooting 171 + 172 + ### Build Fails on Railway 173 + 174 + **Problem:** Build fails with "Cannot find module" 175 + 176 + **Solution:** Ensure `postinstall` script in root `package.json`: 177 + ```json 178 + "postinstall": "cd client && npm install" 179 + ``` 180 + 181 + ### App Shows API JSON Instead of React 182 + 183 + **Problem:** Visiting root shows `{"name": "AT Protocol Podcast Host"}` 184 + 185 + **Solution:** Build the React app first: 186 + ```bash 187 + npm run build 188 + ``` 189 + 190 + ### Environment Variables Not Working 191 + 192 + **Problem:** Server can't connect to AT Protocol 193 + 194 + **Solution:** 195 + 1. Check Railway environment variables are set 196 + 2. Verify `ATPROTO_PASSWORD` is an **app password** 197 + 3. Check Railway logs for connection errors 198 + 199 + ### Port Binding Issues 200 + 201 + **Problem:** Railway shows "Address already in use" 202 + 203 + **Solution:** Ensure server uses `process.env.PORT`: 204 + ```javascript 205 + const PORT = process.env.PORT || 5000; 206 + ``` 207 + 208 + ## Alternative Deployment Platforms 209 + 210 + ### Render 211 + 212 + 1. Create new Web Service 213 + 2. Build Command: `npm run build` 214 + 3. Start Command: `npm start` 215 + 4. Add environment variables 216 + 217 + ### Heroku 218 + 219 + ```bash 220 + heroku create your-app-name 221 + heroku config:set ATPROTO_SERVICE=https://bsky.social 222 + heroku config:set ATPROTO_IDENTIFIER=your-handle 223 + heroku config:set ATPROTO_PASSWORD=your-password 224 + git push heroku main 225 + ``` 226 + 227 + ### DigitalOcean App Platform 228 + 229 + 1. Create new app from GitHub 230 + 2. Set build command: `npm run build` 231 + 3. Set run command: `npm start` 232 + 4. Configure environment variables 233 + 234 + ## Production Checklist 235 + 236 + - [ ] Environment variables configured 237 + - [ ] Using app password (not main password) 238 + - [ ] Build succeeds locally 239 + - [ ] API endpoints accessible 240 + - [ ] React app loads correctly 241 + - [ ] File uploads work 242 + - [ ] RSS feed generates 243 + - [ ] AT Protocol publishing works 244 + 245 + ## Monitoring & Logs 246 + 247 + ### Railway Logs 248 + 249 + View real-time logs in Railway dashboard: 250 + ``` 251 + Settings → Deployments → View Logs 252 + ``` 253 + 254 + ### Health Check Endpoint 255 + 256 + Add to your backend: 257 + ```javascript 258 + app.get('/health', (req, res) => { 259 + res.json({ status: 'ok', timestamp: new Date() }); 260 + }); 261 + ``` 262 + 263 + ## Updating Your Deployment 264 + 265 + ```bash 266 + git add . 267 + git commit -m "Update podcast app" 268 + git push origin main 269 + ``` 270 + 271 + Railway will automatically rebuild and redeploy. 272 + 273 + ## Custom Domain 274 + 275 + 1. Railway → Settings → Networking 276 + 2. Add custom domain 277 + 3. Update DNS records as shown 278 + 4. Wait for SSL certificate 279 + 280 + ## Cost Estimation 281 + 282 + Railway pricing (as of 2025): 283 + - **Hobby Plan**: $5/month 284 + - **Pro Plan**: $20/month + usage 285 + - Free tier available with limitations 286 + 287 + Storage considerations: 288 + - Podcast files stored in AT Protocol (blob storage) 289 + - Minimal server storage needed 290 + 291 + ## Support 292 + 293 + - Railway Docs: https://docs.railway.app 294 + - AT Protocol Docs: https://atproto.com/docs 295 + - Project Issues: https://github.com/your-repo/issues
+251
QUICKSTART.md
··· 1 + # Quick Start Guide 2 + 3 + Get your AT Protocol podcast hosting running in minutes! 4 + 5 + ## Local Development 6 + 7 + ### 1. Install Dependencies 8 + 9 + ```bash 10 + npm install 11 + ``` 12 + 13 + This automatically installs both backend and frontend dependencies. 14 + 15 + ### 2. Configure Environment 16 + 17 + Copy the example environment file: 18 + 19 + ```bash 20 + cp .env.example .env 21 + ``` 22 + 23 + Edit `.env` with your AT Protocol credentials: 24 + 25 + ```env 26 + PORT=5000 27 + ATPROTO_SERVICE=https://bsky.social 28 + ATPROTO_IDENTIFIER=your-handle.bsky.social 29 + ATPROTO_PASSWORD=your-app-password 30 + ``` 31 + 32 + **Get an app password:** 33 + 1. Open Bluesky app 34 + 2. Go to Settings → App Passwords 35 + 3. Create new app password 36 + 4. Copy and paste into `.env` 37 + 38 + ### 3. Run Development Servers 39 + 40 + **Option A: Both servers at once (recommended)** 41 + 42 + ```bash 43 + npm run dev:all 44 + ``` 45 + 46 + - Backend API: http://localhost:5000 47 + - React frontend: http://localhost:3000 48 + 49 + **Option B: Run separately** 50 + 51 + ```bash 52 + # Terminal 1 - Backend 53 + npm run dev 54 + 55 + # Terminal 2 - Frontend 56 + npm run client 57 + ``` 58 + 59 + ### 4. Access the App 60 + 61 + Open http://localhost:3000 in your browser. 62 + 63 + ## Production Build 64 + 65 + ### Build the App 66 + 67 + ```bash 68 + npm run build 69 + ``` 70 + 71 + This builds the React app to `client/dist/` 72 + 73 + ### Start Production Server 74 + 75 + ```bash 76 + npm start 77 + ``` 78 + 79 + Access at http://localhost:5000 (both React app + API) 80 + 81 + ## Deploy to Railway 82 + 83 + ### 1. Push to GitHub 84 + 85 + ```bash 86 + git init 87 + git add . 88 + git commit -m "Initial commit" 89 + git remote add origin https://github.com/yourusername/your-repo.git 90 + git push -u origin main 91 + ``` 92 + 93 + ### 2. Deploy on Railway 94 + 95 + 1. Go to https://railway.app 96 + 2. Click "New Project" 97 + 3. Select "Deploy from GitHub repo" 98 + 4. Choose your repository 99 + 5. Add environment variables: 100 + - `ATPROTO_SERVICE` 101 + - `ATPROTO_IDENTIFIER` 102 + - `ATPROTO_PASSWORD` 103 + 6. Deploy! 104 + 105 + Railway will automatically: 106 + - Install dependencies 107 + - Build the React app 108 + - Start the server 109 + 110 + Your app will be live at `https://your-app.railway.app` 111 + 112 + ## First Steps After Deployment 113 + 114 + ### 1. Configure Your Podcast 115 + 116 + 1. Go to Settings page 117 + 2. Fill in podcast details: 118 + - Title 119 + - Description 120 + - Website 121 + - Language 122 + 123 + 3. Click "Save Settings" 124 + 125 + ### 2. Upload Your First Episode 126 + 127 + 1. Click "Create New" button (or go to Episodes page) 128 + 2. Select audio file (MP3, M4A, WAV, etc.) 129 + 3. Enter episode title and description 130 + 4. Click "Upload Episode" 131 + 132 + ### 3. Publish RSS Feed 133 + 134 + 1. Go to Settings page 135 + 2. Scroll to "RSS Feed Distribution" 136 + 3. Click "Publish to AT Protocol" 137 + 4. Copy your `at://` URI 138 + 139 + Your podcast is now decentralized! 🎉 140 + 141 + ## Next Steps 142 + 143 + ### Share Your Podcast 144 + 145 + **For AT Protocol apps:** 146 + Share your `at://` URI: 147 + ``` 148 + at://did:plc:your-did/app.podcast.feed/self 149 + ``` 150 + 151 + **For traditional podcast apps:** 152 + Share the HTTP proxy URL: 153 + ``` 154 + https://your-app.railway.app/api/feed/at-rss 155 + ``` 156 + 157 + ### Add to Podcast Directories 158 + 159 + Submit your HTTP proxy URL to: 160 + - Apple Podcasts 161 + - Spotify 162 + - Google Podcasts 163 + - Pocket Casts 164 + - etc. 165 + 166 + ## Troubleshooting 167 + 168 + ### Can't Connect to AT Protocol 169 + 170 + **Error:** "Authentication failed" 171 + 172 + **Solution:** 173 + - Verify your handle is correct (e.g., `yourname.bsky.social`) 174 + - Ensure you're using an **app password**, not your main password 175 + - Check that `ATPROTO_SERVICE` is `https://bsky.social` 176 + 177 + ### Upload Fails 178 + 179 + **Error:** "File too large" 180 + 181 + **Solution:** 182 + - Check file format (MP3, M4A, WAV supported) 183 + - AT Protocol blob limit is ~1GB per file 184 + - Try compressing the audio file 185 + 186 + ### RSS Feed Not Publishing 187 + 188 + **Error:** "Failed to publish RSS feed" 189 + 190 + **Solution:** 191 + - Upload at least one episode first 192 + - Configure podcast metadata 193 + - Check AT Protocol connection 194 + 195 + ### Port Already in Use 196 + 197 + **Error:** "EADDRINUSE: address already in use" 198 + 199 + **Solution:** 200 + ```bash 201 + # Kill the process on port 5000 202 + npx kill-port 5000 203 + 204 + # Or change PORT in .env 205 + PORT=3001 206 + ``` 207 + 208 + ## Commands Reference 209 + 210 + | Command | Description | 211 + |---------|-------------| 212 + | `npm install` | Install all dependencies | 213 + | `npm run dev` | Start backend only | 214 + | `npm run client` | Start React dev server | 215 + | `npm run dev:all` | Start both servers | 216 + | `npm run build` | Build React for production | 217 + | `npm start` | Start production server | 218 + 219 + ## File Structure 220 + 221 + ``` 222 + atproto-podcast/ 223 + ├── client/ # React frontend 224 + │ ├── src/ 225 + │ │ ├── components/ # UI components 226 + │ │ ├── pages/ # Page views 227 + │ │ └── services/ # API calls 228 + │ └── dist/ # Built app (production) 229 + ├── src/ # Express backend 230 + │ ├── index.js # Server entry point 231 + │ └── routes/ # API routes 232 + ├── .env # Environment config 233 + └── package.json # Dependencies & scripts 234 + ``` 235 + 236 + ## Getting Help 237 + 238 + - **Deployment Issues:** See [DEPLOYMENT.md](DEPLOYMENT.md) 239 + - **RSS Feed Details:** See [RSS_AT_PROTOCOL.md](RSS_AT_PROTOCOL.md) 240 + - **API Documentation:** See [README.md](README.md#api-endpoints) 241 + 242 + ## Resources 243 + 244 + - [AT Protocol Docs](https://atproto.com/docs) 245 + - [Bluesky](https://bsky.app) 246 + - [Railway Docs](https://docs.railway.app) 247 + - [Vite Docs](https://vitejs.dev) 248 + 249 + --- 250 + 251 + **Ready to podcast on the decentralized web!** 🎙️
+273
README.md
··· 1 + # AT Protocol Podcast Host 2 + 3 + A podcast hosting platform that stores media files and RSS feeds using AT Protocol blob storage. Stream your podcasts directly from `at://` URIs! 4 + 5 + ## Features 6 + 7 + - 🎙️ Upload podcast episodes to AT Protocol blob storage 8 + - 📡 Generate RSS feeds from AT Protocol-stored content 9 + - 🎵 Stream media files from `at://` URIs 10 + - 🔐 Secure authentication with Bluesky/AT Protocol 11 + - 📱 RESTful API for podcast management 12 + - 🌐 CORS-enabled for web player integration 13 + 14 + ## Prerequisites 15 + 16 + - Node.js 18+ 17 + - An AT Protocol account (e.g., Bluesky account) 18 + - App password for your AT Protocol account 19 + 20 + ## Setup 21 + 22 + ### 1. Clone and Install 23 + 24 + ```bash 25 + cd atproto-podcast 26 + npm install 27 + ``` 28 + 29 + ### 2. Configure Environment 30 + 31 + Copy the example environment file: 32 + 33 + ```bash 34 + cp .env.example .env 35 + ``` 36 + 37 + Edit `.env` with your AT Protocol credentials: 38 + 39 + ```env 40 + PORT=3000 41 + ATPROTO_SERVICE=https://bsky.social 42 + ATPROTO_IDENTIFIER=your-handle.bsky.social 43 + ATPROTO_PASSWORD=your-app-password 44 + ``` 45 + 46 + **Note:** Get an app password from your Bluesky account settings, not your main password. 47 + 48 + ### 3. Run the Application 49 + 50 + #### Development Mode (React + API) 51 + 52 + Run both the React frontend and backend API concurrently: 53 + ```bash 54 + npm run dev:all 55 + ``` 56 + 57 + This starts: 58 + - Backend API on `http://localhost:5000` 59 + - React frontend on `http://localhost:3000` (with proxy to API) 60 + 61 + Or run them separately: 62 + ```bash 63 + # Terminal 1 - Backend API 64 + npm run dev 65 + 66 + # Terminal 2 - React Frontend 67 + npm run client 68 + ``` 69 + 70 + #### Production Mode 71 + 72 + First, build the React application: 73 + ```bash 74 + npm run build 75 + ``` 76 + 77 + Then start the server: 78 + ```bash 79 + npm start 80 + ``` 81 + 82 + The server will serve the built React app and API on `http://localhost:5000` 83 + 84 + ## API Endpoints 85 + 86 + ### Upload Episode 87 + ``` 88 + POST /api/upload/episode 89 + Content-Type: multipart/form-data 90 + 91 + Fields: 92 + - audio: Audio file (required) 93 + - title: Episode title (required) 94 + - description: Episode description (optional) 95 + - pubDate: Publication date (optional, defaults to now) 96 + ``` 97 + 98 + ### Get RSS Feed 99 + ``` 100 + GET /api/feed/rss 101 + 102 + Returns: RSS 2.0 XML feed with podcast episodes 103 + ``` 104 + 105 + ### Update Feed Metadata 106 + ``` 107 + POST /api/feed/metadata 108 + Content-Type: application/json 109 + 110 + { 111 + "title": "Your Podcast Name", 112 + "description": "Podcast description", 113 + "link": "https://yoursite.com", 114 + "language": "en" 115 + } 116 + ``` 117 + 118 + ### Get Feed Metadata 119 + ``` 120 + GET /api/feed/metadata 121 + 122 + Returns: Current feed metadata 123 + ``` 124 + 125 + ### Stream Media 126 + ``` 127 + GET /api/media/stream/:did/:cid 128 + 129 + Parameters: 130 + - did: Decentralized identifier 131 + - cid: Content identifier (from AT Protocol blob) 132 + ``` 133 + 134 + ### Get All Episodes 135 + ``` 136 + GET /api/media/episodes 137 + 138 + Returns: List of all episodes with streaming URLs 139 + ``` 140 + 141 + ### Get Episode by ID 142 + ``` 143 + GET /api/media/episodes/:id 144 + 145 + Returns: Single episode details 146 + ``` 147 + 148 + ## Usage Example 149 + 150 + ### Upload an Episode 151 + 152 + ```bash 153 + curl -X POST http://localhost:3000/api/upload/episode \ 154 + -F "audio=@/path/to/episode.mp3" \ 155 + -F "title=My First Episode" \ 156 + -F "description=This is my first podcast episode!" 157 + ``` 158 + 159 + ### Get RSS Feed 160 + 161 + ```bash 162 + curl http://localhost:3000/api/feed/rss 163 + ``` 164 + 165 + ### Stream an Episode 166 + 167 + ```bash 168 + curl http://localhost:3000/api/media/stream/{did}/{cid} --output episode.mp3 169 + ``` 170 + 171 + ## Project Structure 172 + 173 + ``` 174 + atproto-podcast/ 175 + ├── client/ # React Frontend (Vite) 176 + │ ├── src/ 177 + │ │ ├── components/ # React components (Header, Sidebar, etc.) 178 + │ │ ├── pages/ # Page components (Dashboard, Episodes, Settings) 179 + │ │ ├── services/ # API service layer 180 + │ │ ├── App.jsx # Main App component 181 + │ │ └── main.jsx # Entry point 182 + │ ├── public/ # Static assets 183 + │ ├── package.json 184 + │ └── vite.config.js # Vite configuration 185 + ├── src/ # Backend API (Express) 186 + │ ├── index.js # Main server 187 + │ ├── atproto-client.js # AT Protocol client 188 + │ ├── routes/ 189 + │ │ ├── upload.js # Upload endpoints 190 + │ │ ├── feed.js # RSS feed endpoints 191 + │ │ └── media.js # Media streaming endpoints 192 + │ └── storage/ 193 + │ └── metadata.js # Episode metadata storage 194 + ├── public/ # Legacy vanilla JS (deprecated) 195 + │ ├── index.html 196 + │ ├── app.js 197 + │ └── styles.css 198 + ├── .env.example # Environment template 199 + ├── .gitignore 200 + ├── package.json 201 + └── README.md 202 + ``` 203 + 204 + ## How It Works 205 + 206 + 1. **Upload**: Audio files are uploaded via multipart form data 207 + 2. **Store**: Files are uploaded to AT Protocol as blobs, receiving a CID (Content Identifier) 208 + 3. **Metadata**: Episode metadata (title, description, CID) is stored locally 209 + 4. **RSS**: RSS feed is dynamically generated from stored metadata 210 + 5. **Stream**: Episodes are streamed directly from AT Protocol blob storage using `at://` URIs 211 + 212 + ## AT Protocol Integration 213 + 214 + This project uses AT Protocol's blob storage to host podcast files: 215 + 216 + - Audio files are stored as blobs in your AT Protocol repository 217 + - Each blob gets a unique CID (Content Identifier) 218 + - Files are accessible via `at://{did}/{cid}` URIs 219 + - The server acts as a bridge between HTTP and AT Protocol 220 + 221 + ## Development 222 + 223 + The project uses ES modules and includes: 224 + 225 + - Express.js for the web server 226 + - React + Vite for the frontend 227 + - @atproto/api for AT Protocol integration 228 + - Multer for file uploads 229 + - RSS package for feed generation 230 + 231 + ## Deployment 232 + 233 + ### Quick Deploy to Railway 234 + 235 + 1. Push your code to GitHub 236 + 2. Connect to Railway: https://railway.app 237 + 3. Set environment variables (see [DEPLOYMENT.md](DEPLOYMENT.md)) 238 + 4. Railway will automatically build and deploy 239 + 240 + **One Command Production Build:** 241 + ```bash 242 + npm run build # Builds React app 243 + npm start # Serves production app on port 5000 244 + ``` 245 + 246 + ### Deployment Platforms 247 + 248 + This app works on: 249 + - ✅ Railway (recommended) 250 + - ✅ Render 251 + - ✅ Heroku 252 + - ✅ DigitalOcean App Platform 253 + - ✅ Any Node.js hosting 254 + 255 + See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed instructions. 256 + 257 + ## RSS Feed on AT Protocol 258 + 259 + Your podcast RSS feed can be published as a decentralized AT Protocol record: 260 + 261 + - Get an `at://` URI for your feed 262 + - Access it from any AT Protocol node 263 + - Maintain full ownership and portability 264 + 265 + See [RSS_AT_PROTOCOL.md](RSS_AT_PROTOCOL.md) for details. 266 + 267 + ## License 268 + 269 + MIT 270 + 271 + ## Contributing 272 + 273 + Contributions welcome! Please feel free to submit a Pull Request.
+205
RSS_AT_PROTOCOL.md
··· 1 + # RSS Feed on AT Protocol 2 + 3 + This document explains how podcast RSS feeds are stored and distributed using AT Protocol's decentralized infrastructure. 4 + 5 + ## Overview 6 + 7 + Your podcast RSS feed can be published as a custom AT Protocol record, making it: 8 + - **Decentralized**: Not dependent on any single server 9 + - **Portable**: Accessible via `at://` URIs from any AT Protocol client 10 + - **Verifiable**: Cryptographically linked to your DID (Decentralized Identifier) 11 + - **Updatable**: Can be republished when episodes change 12 + 13 + ## How It Works 14 + 15 + ### 1. RSS Feed Generation 16 + 17 + The RSS feed is generated dynamically from: 18 + - Podcast metadata (title, description, language, etc.) 19 + - Episode records stored in AT Protocol 20 + - Media files stored as AT Protocol blobs 21 + 22 + ### 2. Publishing to AT Protocol 23 + 24 + When you click "Publish to AT Protocol" in settings: 25 + 26 + 1. The RSS XML is generated with AT Protocol-specific metadata 27 + 2. A custom record is created with collection type `app.podcast.feed` 28 + 3. The record is stored at `at://[YOUR_DID]/app.podcast.feed/self` 29 + 4. The record contains: 30 + - RSS XML content 31 + - Creation timestamp 32 + - Feed metadata 33 + 34 + ```javascript 35 + { 36 + $type: 'app.podcast.feed', 37 + content: '<rss>...</rss>', 38 + createdAt: '2025-10-22T02:30:00.000Z', 39 + metadata: { /* podcast info */ } 40 + } 41 + ``` 42 + 43 + ### 3. Accessing the Feed 44 + 45 + **AT Protocol URI (Decentralized):** 46 + ``` 47 + at://did:plc:your-did-here/app.podcast.feed/self 48 + ``` 49 + 50 + **HTTP Proxy (For Compatibility):** 51 + ``` 52 + http://localhost:5000/api/feed/at-rss 53 + ``` 54 + 55 + ## API Endpoints 56 + 57 + ### Publish Feed to AT Protocol 58 + 59 + ```bash 60 + POST /api/feed/publish 61 + ``` 62 + 63 + Creates or updates the RSS feed record on AT Protocol. 64 + 65 + **Response:** 66 + ```json 67 + { 68 + "success": true, 69 + "uri": "at://did:plc:xyz123/app.podcast.feed/self", 70 + "cid": "bafyreiabc123...", 71 + "message": "RSS feed published to AT Protocol" 72 + } 73 + ``` 74 + 75 + ### Get Feed URI Information 76 + 77 + ```bash 78 + GET /api/feed/uri 79 + ``` 80 + 81 + Returns information about the published feed. 82 + 83 + **Response:** 84 + ```json 85 + { 86 + "atUri": "at://did:plc:xyz123/app.podcast.feed/self", 87 + "httpProxy": "http://localhost:5000/api/feed/at-rss", 88 + "did": "did:plc:xyz123", 89 + "published": true 90 + } 91 + ``` 92 + 93 + ### Fetch Feed from AT Protocol 94 + 95 + ```bash 96 + GET /api/feed/at-rss 97 + ``` 98 + 99 + Retrieves the RSS feed from the AT Protocol record. 100 + 101 + **Response:** RSS XML (application/rss+xml) 102 + 103 + ### Legacy HTTP Endpoint 104 + 105 + ```bash 106 + GET /api/feed/rss 107 + ``` 108 + 109 + Generates RSS feed on-the-fly (does not require publishing). 110 + 111 + ## AT Protocol RSS Extensions 112 + 113 + The RSS feed includes custom AT Protocol elements: 114 + 115 + ```xml 116 + <rss xmlns:atproto="https://atproto.com/ns/1.0"> 117 + <channel> 118 + <link>at://did:plc:xyz123/app.podcast.feed/self</link> 119 + <item> 120 + <title>Episode 1</title> 121 + <atproto:cid>bafyreiabc123...</atproto:cid> 122 + <atproto:uri>at://did:plc:xyz123/app.podcast.episode/bafyreiabc123</atproto:uri> 123 + <atproto:did>did:plc:xyz123</atproto:did> 124 + </item> 125 + </channel> 126 + </rss> 127 + ``` 128 + 129 + ## Benefits 130 + 131 + ### For Podcasters 132 + 133 + 1. **Ownership**: Your feed is tied to your DID, not a hosting service 134 + 2. **Portability**: Move your podcast without changing feed URLs 135 + 3. **Resilience**: Feed can be accessed from any AT Protocol node 136 + 4. **Transparency**: All changes are recorded on the protocol 137 + 138 + ### For Listeners 139 + 140 + 1. **Trust**: Verify feed authenticity via cryptographic signatures 141 + 2. **Availability**: Access from multiple sources/mirrors 142 + 3. **Privacy**: Can access via AT Protocol without centralized tracking 143 + 144 + ## Usage in Podcast Apps 145 + 146 + ### AT Protocol-Native Apps 147 + 148 + Apps that understand `at://` URIs can directly fetch the feed: 149 + 150 + ```javascript 151 + const agent = new BskyAgent({ service: 'https://bsky.social' }); 152 + const record = await agent.com.atproto.repo.getRecord({ 153 + repo: 'did:plc:xyz123', 154 + collection: 'app.podcast.feed', 155 + rkey: 'self' 156 + }); 157 + const rssXml = record.value.content; 158 + ``` 159 + 160 + ### Legacy Podcast Apps 161 + 162 + Use the HTTP proxy URL which fetches from AT Protocol: 163 + ``` 164 + http://localhost:5000/api/feed/at-rss 165 + ``` 166 + 167 + ## Technical Details 168 + 169 + ### Record Structure 170 + 171 + - **Collection**: `app.podcast.feed` 172 + - **Record Key**: `self` (fixed, only one feed per user) 173 + - **Type**: Custom record type (not an official lexicon yet) 174 + - **Content**: RSS XML as string 175 + - **Metadata**: Podcast information object 176 + 177 + ### CID (Content Identifier) 178 + 179 + Each time you publish/update the feed: 180 + - A new CID is generated 181 + - The CID uniquely identifies this version of the feed 182 + - Old versions remain accessible if CID is known 183 + 184 + ### DID (Decentralized Identifier) 185 + 186 + Your DID is your permanent identity on AT Protocol: 187 + - Example: `did:plc:xyz123abc456` 188 + - Portable across different services 189 + - Cryptographically verifiable 190 + 191 + ## Future Enhancements 192 + 193 + - [ ] Subscribe to feed updates via AT Protocol subscriptions 194 + - [ ] Episode-level records (each episode as separate record) 195 + - [ ] Podcast discovery via AT Protocol feeds 196 + - [ ] Analytics without centralized tracking 197 + - [ ] Support for podcast:* namespace tags 198 + - [ ] Automatic re-publishing on episode upload 199 + 200 + ## Learn More 201 + 202 + - [AT Protocol Docs](https://atproto.com/docs) 203 + - [AT Protocol Repository Specification](https://atproto.com/specs/repository) 204 + - [RSS 2.0 Specification](https://www.rssboard.org/rss-specification) 205 + - [Podcast Namespace](https://github.com/Podcastindex-org/podcast-namespace)
+91
WARP.md
··· 1 + # WARP.md 2 + 3 + This file provides guidance to WARP (warp.dev) when working with code in this repository. 4 + 5 + ## Project Overview 6 + 7 + AT Protocol Podcast Host is a podcast hosting platform that stores media files and RSS feeds using AT Protocol blob storage. Audio files are uploaded as blobs to AT Protocol, generating unique CIDs (Content Identifiers) accessible via `at://` URIs. The server acts as a bridge between HTTP clients and AT Protocol storage. 8 + 9 + ## Common Development Commands 10 + 11 + ### Running the Server 12 + - **Development mode** (with auto-reload): `npm run dev` 13 + - **Production mode**: `npm start` 14 + - Server runs on `http://localhost:3000` by default 15 + 16 + ### Installation 17 + - Install dependencies: `npm install` 18 + - Copy environment template: `cp .env.example .env` 19 + 20 + ### Testing API Endpoints 21 + - **Upload episode**: 22 + ```powershell 23 + curl -X POST http://localhost:3000/api/upload/episode ` 24 + -F "audio=@/path/to/episode.mp3" ` 25 + -F "title=Episode Title" ` 26 + -F "description=Episode description" 27 + ``` 28 + - **Get RSS feed**: `curl http://localhost:3000/api/feed/rss` 29 + - **Stream episode**: `curl http://localhost:3000/api/media/stream/{did}/{cid} --output episode.mp3` 30 + 31 + ## Architecture 32 + 33 + ### Core Components 34 + 35 + **AT Protocol Integration** (`src/atproto-client.js`): 36 + - Uses `@atproto/api` BskyAgent for authentication and blob operations 37 + - Manages session state with `isAuthenticated` flag 38 + - Key methods: `getAgent()`, `uploadBlob()`, `getBlob()` 39 + - Authenticates once per server lifecycle using credentials from `.env` 40 + 41 + **Storage Layer** (`src/storage/metadata.js`): 42 + - File-based metadata storage in `podcast-metadata.json` at project root 43 + - Stores episode metadata (title, description, CID, pubDate) and feed configuration 44 + - Episodes sorted by pubDate in descending order 45 + - No database required; metadata persists locally 46 + 47 + **Route Structure** (`src/routes/`): 48 + - **upload.js**: Handles multipart file uploads via Multer, uploads blobs to AT Protocol, stores metadata, cleans up temp files 49 + - **feed.js**: Generates RSS 2.0 XML feeds dynamically from stored metadata, includes custom `atproto:cid` elements 50 + - **media.js**: Streams blobs from AT Protocol using DID/CID pairs, provides episode listing endpoints 51 + 52 + ### Key Architectural Patterns 53 + 54 + 1. **Blob Storage Flow**: Client uploads → Multer temp storage → AT Protocol blob upload → CID generation → Local metadata storage → Temp file cleanup 55 + 2. **Streaming Flow**: Client requests DID/CID → Server fetches from AT Protocol XRPC endpoint → Proxies binary data to client 56 + 3. **Authentication**: Single agent instance shared across all modules, lazy authentication on first API call 57 + 4. **URL Construction**: RSS feed and API responses construct full URLs using `req.protocol` and `req.get('host')` for proper streaming links 58 + 59 + ## Environment Configuration 60 + 61 + Required environment variables in `.env`: 62 + - `PORT`: Server port (default: 3000) 63 + - `ATPROTO_SERVICE`: AT Protocol service URL (usually `https://bsky.social`) 64 + - `ATPROTO_IDENTIFIER`: Your Bluesky/AT Protocol handle 65 + - `ATPROTO_PASSWORD`: App password (NOT main account password) from Bluesky settings 66 + 67 + The server requires valid AT Protocol credentials to start. Obtain an app password from Bluesky account settings for secure authentication. 68 + 69 + ## Project Structure 70 + 71 + ``` 72 + src/ 73 + ├── index.js # Express server initialization, route registration 74 + ├── atproto-client.js # AT Protocol authentication and blob operations 75 + ├── routes/ 76 + │ ├── upload.js # Episode upload endpoint 77 + │ ├── feed.js # RSS feed generation and metadata management 78 + │ └── media.js # Blob streaming and episode retrieval 79 + └── storage/ 80 + └── metadata.js # JSON-based metadata persistence 81 + ``` 82 + 83 + ## Important Notes 84 + 85 + - Project uses ES modules (`"type": "module"` in package.json) 86 + - No test framework is currently configured 87 + - No linter or type checker configured 88 + - Temporary uploads go to `public/uploads/` (cleaned up after blob upload) 89 + - Metadata file `podcast-metadata.json` is created automatically if missing 90 + - AT Protocol blobs are immutable; reuploading creates new CIDs 91 + - DID (Decentralized Identifier) comes from authenticated agent session
+24
client/.gitignore
··· 1 + # Logs 2 + logs 3 + *.log 4 + npm-debug.log* 5 + yarn-debug.log* 6 + yarn-error.log* 7 + pnpm-debug.log* 8 + lerna-debug.log* 9 + 10 + node_modules 11 + dist 12 + dist-ssr 13 + *.local 14 + 15 + # Editor directories and files 16 + .vscode/* 17 + !.vscode/extensions.json 18 + .idea 19 + .DS_Store 20 + *.suo 21 + *.ntvs* 22 + *.njsproj 23 + *.sln 24 + *.sw?
+16
client/README.md
··· 1 + # React + Vite 2 + 3 + This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 + 5 + Currently, two official plugins are available: 6 + 7 + - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh 8 + - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 + 10 + ## React Compiler 11 + 12 + The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). 13 + 14 + ## Expanding the ESLint configuration 15 + 16 + If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+29
client/eslint.config.js
··· 1 + import js from '@eslint/js' 2 + import globals from 'globals' 3 + import reactHooks from 'eslint-plugin-react-hooks' 4 + import reactRefresh from 'eslint-plugin-react-refresh' 5 + import { defineConfig, globalIgnores } from 'eslint/config' 6 + 7 + export default defineConfig([ 8 + globalIgnores(['dist']), 9 + { 10 + files: ['**/*.{js,jsx}'], 11 + extends: [ 12 + js.configs.recommended, 13 + reactHooks.configs['recommended-latest'], 14 + reactRefresh.configs.vite, 15 + ], 16 + languageOptions: { 17 + ecmaVersion: 2020, 18 + globals: globals.browser, 19 + parserOptions: { 20 + ecmaVersion: 'latest', 21 + ecmaFeatures: { jsx: true }, 22 + sourceType: 'module', 23 + }, 24 + }, 25 + rules: { 26 + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 + }, 28 + }, 29 + ])
+13
client/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>client</title> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="/src/main.jsx"></script> 12 + </body> 13 + </html>
+2879
client/package-lock.json
··· 1 + { 2 + "name": "client", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "client", 9 + "version": "0.0.0", 10 + "dependencies": { 11 + "react": "^19.1.1", 12 + "react-dom": "^19.1.1", 13 + "react-router-dom": "^7.9.4" 14 + }, 15 + "devDependencies": { 16 + "@eslint/js": "^9.36.0", 17 + "@types/react": "^19.1.16", 18 + "@types/react-dom": "^19.1.9", 19 + "@vitejs/plugin-react": "^5.0.4", 20 + "eslint": "^9.36.0", 21 + "eslint-plugin-react-hooks": "^5.2.0", 22 + "eslint-plugin-react-refresh": "^0.4.22", 23 + "globals": "^16.4.0", 24 + "vite": "^7.1.7" 25 + } 26 + }, 27 + "node_modules/@babel/code-frame": { 28 + "version": "7.27.1", 29 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", 30 + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", 31 + "dev": true, 32 + "license": "MIT", 33 + "dependencies": { 34 + "@babel/helper-validator-identifier": "^7.27.1", 35 + "js-tokens": "^4.0.0", 36 + "picocolors": "^1.1.1" 37 + }, 38 + "engines": { 39 + "node": ">=6.9.0" 40 + } 41 + }, 42 + "node_modules/@babel/compat-data": { 43 + "version": "7.28.4", 44 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", 45 + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", 46 + "dev": true, 47 + "license": "MIT", 48 + "engines": { 49 + "node": ">=6.9.0" 50 + } 51 + }, 52 + "node_modules/@babel/core": { 53 + "version": "7.28.4", 54 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", 55 + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", 56 + "dev": true, 57 + "license": "MIT", 58 + "dependencies": { 59 + "@babel/code-frame": "^7.27.1", 60 + "@babel/generator": "^7.28.3", 61 + "@babel/helper-compilation-targets": "^7.27.2", 62 + "@babel/helper-module-transforms": "^7.28.3", 63 + "@babel/helpers": "^7.28.4", 64 + "@babel/parser": "^7.28.4", 65 + "@babel/template": "^7.27.2", 66 + "@babel/traverse": "^7.28.4", 67 + "@babel/types": "^7.28.4", 68 + "@jridgewell/remapping": "^2.3.5", 69 + "convert-source-map": "^2.0.0", 70 + "debug": "^4.1.0", 71 + "gensync": "^1.0.0-beta.2", 72 + "json5": "^2.2.3", 73 + "semver": "^6.3.1" 74 + }, 75 + "engines": { 76 + "node": ">=6.9.0" 77 + }, 78 + "funding": { 79 + "type": "opencollective", 80 + "url": "https://opencollective.com/babel" 81 + } 82 + }, 83 + "node_modules/@babel/generator": { 84 + "version": "7.28.3", 85 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", 86 + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", 87 + "dev": true, 88 + "license": "MIT", 89 + "dependencies": { 90 + "@babel/parser": "^7.28.3", 91 + "@babel/types": "^7.28.2", 92 + "@jridgewell/gen-mapping": "^0.3.12", 93 + "@jridgewell/trace-mapping": "^0.3.28", 94 + "jsesc": "^3.0.2" 95 + }, 96 + "engines": { 97 + "node": ">=6.9.0" 98 + } 99 + }, 100 + "node_modules/@babel/helper-compilation-targets": { 101 + "version": "7.27.2", 102 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", 103 + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", 104 + "dev": true, 105 + "license": "MIT", 106 + "dependencies": { 107 + "@babel/compat-data": "^7.27.2", 108 + "@babel/helper-validator-option": "^7.27.1", 109 + "browserslist": "^4.24.0", 110 + "lru-cache": "^5.1.1", 111 + "semver": "^6.3.1" 112 + }, 113 + "engines": { 114 + "node": ">=6.9.0" 115 + } 116 + }, 117 + "node_modules/@babel/helper-globals": { 118 + "version": "7.28.0", 119 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", 120 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", 121 + "dev": true, 122 + "license": "MIT", 123 + "engines": { 124 + "node": ">=6.9.0" 125 + } 126 + }, 127 + "node_modules/@babel/helper-module-imports": { 128 + "version": "7.27.1", 129 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", 130 + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", 131 + "dev": true, 132 + "license": "MIT", 133 + "dependencies": { 134 + "@babel/traverse": "^7.27.1", 135 + "@babel/types": "^7.27.1" 136 + }, 137 + "engines": { 138 + "node": ">=6.9.0" 139 + } 140 + }, 141 + "node_modules/@babel/helper-module-transforms": { 142 + "version": "7.28.3", 143 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", 144 + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", 145 + "dev": true, 146 + "license": "MIT", 147 + "dependencies": { 148 + "@babel/helper-module-imports": "^7.27.1", 149 + "@babel/helper-validator-identifier": "^7.27.1", 150 + "@babel/traverse": "^7.28.3" 151 + }, 152 + "engines": { 153 + "node": ">=6.9.0" 154 + }, 155 + "peerDependencies": { 156 + "@babel/core": "^7.0.0" 157 + } 158 + }, 159 + "node_modules/@babel/helper-plugin-utils": { 160 + "version": "7.27.1", 161 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", 162 + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", 163 + "dev": true, 164 + "license": "MIT", 165 + "engines": { 166 + "node": ">=6.9.0" 167 + } 168 + }, 169 + "node_modules/@babel/helper-string-parser": { 170 + "version": "7.27.1", 171 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 172 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 173 + "dev": true, 174 + "license": "MIT", 175 + "engines": { 176 + "node": ">=6.9.0" 177 + } 178 + }, 179 + "node_modules/@babel/helper-validator-identifier": { 180 + "version": "7.27.1", 181 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", 182 + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", 183 + "dev": true, 184 + "license": "MIT", 185 + "engines": { 186 + "node": ">=6.9.0" 187 + } 188 + }, 189 + "node_modules/@babel/helper-validator-option": { 190 + "version": "7.27.1", 191 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", 192 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", 193 + "dev": true, 194 + "license": "MIT", 195 + "engines": { 196 + "node": ">=6.9.0" 197 + } 198 + }, 199 + "node_modules/@babel/helpers": { 200 + "version": "7.28.4", 201 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", 202 + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", 203 + "dev": true, 204 + "license": "MIT", 205 + "dependencies": { 206 + "@babel/template": "^7.27.2", 207 + "@babel/types": "^7.28.4" 208 + }, 209 + "engines": { 210 + "node": ">=6.9.0" 211 + } 212 + }, 213 + "node_modules/@babel/parser": { 214 + "version": "7.28.4", 215 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", 216 + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", 217 + "dev": true, 218 + "license": "MIT", 219 + "dependencies": { 220 + "@babel/types": "^7.28.4" 221 + }, 222 + "bin": { 223 + "parser": "bin/babel-parser.js" 224 + }, 225 + "engines": { 226 + "node": ">=6.0.0" 227 + } 228 + }, 229 + "node_modules/@babel/plugin-transform-react-jsx-self": { 230 + "version": "7.27.1", 231 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", 232 + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", 233 + "dev": true, 234 + "license": "MIT", 235 + "dependencies": { 236 + "@babel/helper-plugin-utils": "^7.27.1" 237 + }, 238 + "engines": { 239 + "node": ">=6.9.0" 240 + }, 241 + "peerDependencies": { 242 + "@babel/core": "^7.0.0-0" 243 + } 244 + }, 245 + "node_modules/@babel/plugin-transform-react-jsx-source": { 246 + "version": "7.27.1", 247 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", 248 + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", 249 + "dev": true, 250 + "license": "MIT", 251 + "dependencies": { 252 + "@babel/helper-plugin-utils": "^7.27.1" 253 + }, 254 + "engines": { 255 + "node": ">=6.9.0" 256 + }, 257 + "peerDependencies": { 258 + "@babel/core": "^7.0.0-0" 259 + } 260 + }, 261 + "node_modules/@babel/template": { 262 + "version": "7.27.2", 263 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", 264 + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", 265 + "dev": true, 266 + "license": "MIT", 267 + "dependencies": { 268 + "@babel/code-frame": "^7.27.1", 269 + "@babel/parser": "^7.27.2", 270 + "@babel/types": "^7.27.1" 271 + }, 272 + "engines": { 273 + "node": ">=6.9.0" 274 + } 275 + }, 276 + "node_modules/@babel/traverse": { 277 + "version": "7.28.4", 278 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", 279 + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", 280 + "dev": true, 281 + "license": "MIT", 282 + "dependencies": { 283 + "@babel/code-frame": "^7.27.1", 284 + "@babel/generator": "^7.28.3", 285 + "@babel/helper-globals": "^7.28.0", 286 + "@babel/parser": "^7.28.4", 287 + "@babel/template": "^7.27.2", 288 + "@babel/types": "^7.28.4", 289 + "debug": "^4.3.1" 290 + }, 291 + "engines": { 292 + "node": ">=6.9.0" 293 + } 294 + }, 295 + "node_modules/@babel/types": { 296 + "version": "7.28.4", 297 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", 298 + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", 299 + "dev": true, 300 + "license": "MIT", 301 + "dependencies": { 302 + "@babel/helper-string-parser": "^7.27.1", 303 + "@babel/helper-validator-identifier": "^7.27.1" 304 + }, 305 + "engines": { 306 + "node": ">=6.9.0" 307 + } 308 + }, 309 + "node_modules/@esbuild/aix-ppc64": { 310 + "version": "0.25.11", 311 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", 312 + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", 313 + "cpu": [ 314 + "ppc64" 315 + ], 316 + "dev": true, 317 + "license": "MIT", 318 + "optional": true, 319 + "os": [ 320 + "aix" 321 + ], 322 + "engines": { 323 + "node": ">=18" 324 + } 325 + }, 326 + "node_modules/@esbuild/android-arm": { 327 + "version": "0.25.11", 328 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", 329 + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", 330 + "cpu": [ 331 + "arm" 332 + ], 333 + "dev": true, 334 + "license": "MIT", 335 + "optional": true, 336 + "os": [ 337 + "android" 338 + ], 339 + "engines": { 340 + "node": ">=18" 341 + } 342 + }, 343 + "node_modules/@esbuild/android-arm64": { 344 + "version": "0.25.11", 345 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", 346 + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", 347 + "cpu": [ 348 + "arm64" 349 + ], 350 + "dev": true, 351 + "license": "MIT", 352 + "optional": true, 353 + "os": [ 354 + "android" 355 + ], 356 + "engines": { 357 + "node": ">=18" 358 + } 359 + }, 360 + "node_modules/@esbuild/android-x64": { 361 + "version": "0.25.11", 362 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", 363 + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", 364 + "cpu": [ 365 + "x64" 366 + ], 367 + "dev": true, 368 + "license": "MIT", 369 + "optional": true, 370 + "os": [ 371 + "android" 372 + ], 373 + "engines": { 374 + "node": ">=18" 375 + } 376 + }, 377 + "node_modules/@esbuild/darwin-arm64": { 378 + "version": "0.25.11", 379 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", 380 + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", 381 + "cpu": [ 382 + "arm64" 383 + ], 384 + "dev": true, 385 + "license": "MIT", 386 + "optional": true, 387 + "os": [ 388 + "darwin" 389 + ], 390 + "engines": { 391 + "node": ">=18" 392 + } 393 + }, 394 + "node_modules/@esbuild/darwin-x64": { 395 + "version": "0.25.11", 396 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", 397 + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", 398 + "cpu": [ 399 + "x64" 400 + ], 401 + "dev": true, 402 + "license": "MIT", 403 + "optional": true, 404 + "os": [ 405 + "darwin" 406 + ], 407 + "engines": { 408 + "node": ">=18" 409 + } 410 + }, 411 + "node_modules/@esbuild/freebsd-arm64": { 412 + "version": "0.25.11", 413 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", 414 + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", 415 + "cpu": [ 416 + "arm64" 417 + ], 418 + "dev": true, 419 + "license": "MIT", 420 + "optional": true, 421 + "os": [ 422 + "freebsd" 423 + ], 424 + "engines": { 425 + "node": ">=18" 426 + } 427 + }, 428 + "node_modules/@esbuild/freebsd-x64": { 429 + "version": "0.25.11", 430 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", 431 + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", 432 + "cpu": [ 433 + "x64" 434 + ], 435 + "dev": true, 436 + "license": "MIT", 437 + "optional": true, 438 + "os": [ 439 + "freebsd" 440 + ], 441 + "engines": { 442 + "node": ">=18" 443 + } 444 + }, 445 + "node_modules/@esbuild/linux-arm": { 446 + "version": "0.25.11", 447 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", 448 + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", 449 + "cpu": [ 450 + "arm" 451 + ], 452 + "dev": true, 453 + "license": "MIT", 454 + "optional": true, 455 + "os": [ 456 + "linux" 457 + ], 458 + "engines": { 459 + "node": ">=18" 460 + } 461 + }, 462 + "node_modules/@esbuild/linux-arm64": { 463 + "version": "0.25.11", 464 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", 465 + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", 466 + "cpu": [ 467 + "arm64" 468 + ], 469 + "dev": true, 470 + "license": "MIT", 471 + "optional": true, 472 + "os": [ 473 + "linux" 474 + ], 475 + "engines": { 476 + "node": ">=18" 477 + } 478 + }, 479 + "node_modules/@esbuild/linux-ia32": { 480 + "version": "0.25.11", 481 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", 482 + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", 483 + "cpu": [ 484 + "ia32" 485 + ], 486 + "dev": true, 487 + "license": "MIT", 488 + "optional": true, 489 + "os": [ 490 + "linux" 491 + ], 492 + "engines": { 493 + "node": ">=18" 494 + } 495 + }, 496 + "node_modules/@esbuild/linux-loong64": { 497 + "version": "0.25.11", 498 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", 499 + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", 500 + "cpu": [ 501 + "loong64" 502 + ], 503 + "dev": true, 504 + "license": "MIT", 505 + "optional": true, 506 + "os": [ 507 + "linux" 508 + ], 509 + "engines": { 510 + "node": ">=18" 511 + } 512 + }, 513 + "node_modules/@esbuild/linux-mips64el": { 514 + "version": "0.25.11", 515 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", 516 + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", 517 + "cpu": [ 518 + "mips64el" 519 + ], 520 + "dev": true, 521 + "license": "MIT", 522 + "optional": true, 523 + "os": [ 524 + "linux" 525 + ], 526 + "engines": { 527 + "node": ">=18" 528 + } 529 + }, 530 + "node_modules/@esbuild/linux-ppc64": { 531 + "version": "0.25.11", 532 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", 533 + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", 534 + "cpu": [ 535 + "ppc64" 536 + ], 537 + "dev": true, 538 + "license": "MIT", 539 + "optional": true, 540 + "os": [ 541 + "linux" 542 + ], 543 + "engines": { 544 + "node": ">=18" 545 + } 546 + }, 547 + "node_modules/@esbuild/linux-riscv64": { 548 + "version": "0.25.11", 549 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", 550 + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", 551 + "cpu": [ 552 + "riscv64" 553 + ], 554 + "dev": true, 555 + "license": "MIT", 556 + "optional": true, 557 + "os": [ 558 + "linux" 559 + ], 560 + "engines": { 561 + "node": ">=18" 562 + } 563 + }, 564 + "node_modules/@esbuild/linux-s390x": { 565 + "version": "0.25.11", 566 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", 567 + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", 568 + "cpu": [ 569 + "s390x" 570 + ], 571 + "dev": true, 572 + "license": "MIT", 573 + "optional": true, 574 + "os": [ 575 + "linux" 576 + ], 577 + "engines": { 578 + "node": ">=18" 579 + } 580 + }, 581 + "node_modules/@esbuild/linux-x64": { 582 + "version": "0.25.11", 583 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", 584 + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", 585 + "cpu": [ 586 + "x64" 587 + ], 588 + "dev": true, 589 + "license": "MIT", 590 + "optional": true, 591 + "os": [ 592 + "linux" 593 + ], 594 + "engines": { 595 + "node": ">=18" 596 + } 597 + }, 598 + "node_modules/@esbuild/netbsd-arm64": { 599 + "version": "0.25.11", 600 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", 601 + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", 602 + "cpu": [ 603 + "arm64" 604 + ], 605 + "dev": true, 606 + "license": "MIT", 607 + "optional": true, 608 + "os": [ 609 + "netbsd" 610 + ], 611 + "engines": { 612 + "node": ">=18" 613 + } 614 + }, 615 + "node_modules/@esbuild/netbsd-x64": { 616 + "version": "0.25.11", 617 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", 618 + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", 619 + "cpu": [ 620 + "x64" 621 + ], 622 + "dev": true, 623 + "license": "MIT", 624 + "optional": true, 625 + "os": [ 626 + "netbsd" 627 + ], 628 + "engines": { 629 + "node": ">=18" 630 + } 631 + }, 632 + "node_modules/@esbuild/openbsd-arm64": { 633 + "version": "0.25.11", 634 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", 635 + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", 636 + "cpu": [ 637 + "arm64" 638 + ], 639 + "dev": true, 640 + "license": "MIT", 641 + "optional": true, 642 + "os": [ 643 + "openbsd" 644 + ], 645 + "engines": { 646 + "node": ">=18" 647 + } 648 + }, 649 + "node_modules/@esbuild/openbsd-x64": { 650 + "version": "0.25.11", 651 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", 652 + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", 653 + "cpu": [ 654 + "x64" 655 + ], 656 + "dev": true, 657 + "license": "MIT", 658 + "optional": true, 659 + "os": [ 660 + "openbsd" 661 + ], 662 + "engines": { 663 + "node": ">=18" 664 + } 665 + }, 666 + "node_modules/@esbuild/openharmony-arm64": { 667 + "version": "0.25.11", 668 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", 669 + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", 670 + "cpu": [ 671 + "arm64" 672 + ], 673 + "dev": true, 674 + "license": "MIT", 675 + "optional": true, 676 + "os": [ 677 + "openharmony" 678 + ], 679 + "engines": { 680 + "node": ">=18" 681 + } 682 + }, 683 + "node_modules/@esbuild/sunos-x64": { 684 + "version": "0.25.11", 685 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", 686 + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", 687 + "cpu": [ 688 + "x64" 689 + ], 690 + "dev": true, 691 + "license": "MIT", 692 + "optional": true, 693 + "os": [ 694 + "sunos" 695 + ], 696 + "engines": { 697 + "node": ">=18" 698 + } 699 + }, 700 + "node_modules/@esbuild/win32-arm64": { 701 + "version": "0.25.11", 702 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", 703 + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", 704 + "cpu": [ 705 + "arm64" 706 + ], 707 + "dev": true, 708 + "license": "MIT", 709 + "optional": true, 710 + "os": [ 711 + "win32" 712 + ], 713 + "engines": { 714 + "node": ">=18" 715 + } 716 + }, 717 + "node_modules/@esbuild/win32-ia32": { 718 + "version": "0.25.11", 719 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", 720 + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", 721 + "cpu": [ 722 + "ia32" 723 + ], 724 + "dev": true, 725 + "license": "MIT", 726 + "optional": true, 727 + "os": [ 728 + "win32" 729 + ], 730 + "engines": { 731 + "node": ">=18" 732 + } 733 + }, 734 + "node_modules/@esbuild/win32-x64": { 735 + "version": "0.25.11", 736 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", 737 + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", 738 + "cpu": [ 739 + "x64" 740 + ], 741 + "dev": true, 742 + "license": "MIT", 743 + "optional": true, 744 + "os": [ 745 + "win32" 746 + ], 747 + "engines": { 748 + "node": ">=18" 749 + } 750 + }, 751 + "node_modules/@eslint-community/eslint-utils": { 752 + "version": "4.9.0", 753 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", 754 + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", 755 + "dev": true, 756 + "license": "MIT", 757 + "dependencies": { 758 + "eslint-visitor-keys": "^3.4.3" 759 + }, 760 + "engines": { 761 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 762 + }, 763 + "funding": { 764 + "url": "https://opencollective.com/eslint" 765 + }, 766 + "peerDependencies": { 767 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 768 + } 769 + }, 770 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 771 + "version": "3.4.3", 772 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 773 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 774 + "dev": true, 775 + "license": "Apache-2.0", 776 + "engines": { 777 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 778 + }, 779 + "funding": { 780 + "url": "https://opencollective.com/eslint" 781 + } 782 + }, 783 + "node_modules/@eslint-community/regexpp": { 784 + "version": "4.12.1", 785 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", 786 + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", 787 + "dev": true, 788 + "license": "MIT", 789 + "engines": { 790 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 791 + } 792 + }, 793 + "node_modules/@eslint/config-array": { 794 + "version": "0.21.1", 795 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", 796 + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", 797 + "dev": true, 798 + "license": "Apache-2.0", 799 + "dependencies": { 800 + "@eslint/object-schema": "^2.1.7", 801 + "debug": "^4.3.1", 802 + "minimatch": "^3.1.2" 803 + }, 804 + "engines": { 805 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 806 + } 807 + }, 808 + "node_modules/@eslint/config-helpers": { 809 + "version": "0.4.1", 810 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", 811 + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", 812 + "dev": true, 813 + "license": "Apache-2.0", 814 + "dependencies": { 815 + "@eslint/core": "^0.16.0" 816 + }, 817 + "engines": { 818 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 819 + } 820 + }, 821 + "node_modules/@eslint/core": { 822 + "version": "0.16.0", 823 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", 824 + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", 825 + "dev": true, 826 + "license": "Apache-2.0", 827 + "dependencies": { 828 + "@types/json-schema": "^7.0.15" 829 + }, 830 + "engines": { 831 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 832 + } 833 + }, 834 + "node_modules/@eslint/eslintrc": { 835 + "version": "3.3.1", 836 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", 837 + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", 838 + "dev": true, 839 + "license": "MIT", 840 + "dependencies": { 841 + "ajv": "^6.12.4", 842 + "debug": "^4.3.2", 843 + "espree": "^10.0.1", 844 + "globals": "^14.0.0", 845 + "ignore": "^5.2.0", 846 + "import-fresh": "^3.2.1", 847 + "js-yaml": "^4.1.0", 848 + "minimatch": "^3.1.2", 849 + "strip-json-comments": "^3.1.1" 850 + }, 851 + "engines": { 852 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 853 + }, 854 + "funding": { 855 + "url": "https://opencollective.com/eslint" 856 + } 857 + }, 858 + "node_modules/@eslint/eslintrc/node_modules/globals": { 859 + "version": "14.0.0", 860 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 861 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 862 + "dev": true, 863 + "license": "MIT", 864 + "engines": { 865 + "node": ">=18" 866 + }, 867 + "funding": { 868 + "url": "https://github.com/sponsors/sindresorhus" 869 + } 870 + }, 871 + "node_modules/@eslint/js": { 872 + "version": "9.38.0", 873 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", 874 + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", 875 + "dev": true, 876 + "license": "MIT", 877 + "engines": { 878 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 879 + }, 880 + "funding": { 881 + "url": "https://eslint.org/donate" 882 + } 883 + }, 884 + "node_modules/@eslint/object-schema": { 885 + "version": "2.1.7", 886 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 887 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 888 + "dev": true, 889 + "license": "Apache-2.0", 890 + "engines": { 891 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 892 + } 893 + }, 894 + "node_modules/@eslint/plugin-kit": { 895 + "version": "0.4.0", 896 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", 897 + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", 898 + "dev": true, 899 + "license": "Apache-2.0", 900 + "dependencies": { 901 + "@eslint/core": "^0.16.0", 902 + "levn": "^0.4.1" 903 + }, 904 + "engines": { 905 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 906 + } 907 + }, 908 + "node_modules/@humanfs/core": { 909 + "version": "0.19.1", 910 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 911 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 912 + "dev": true, 913 + "license": "Apache-2.0", 914 + "engines": { 915 + "node": ">=18.18.0" 916 + } 917 + }, 918 + "node_modules/@humanfs/node": { 919 + "version": "0.16.7", 920 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 921 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 922 + "dev": true, 923 + "license": "Apache-2.0", 924 + "dependencies": { 925 + "@humanfs/core": "^0.19.1", 926 + "@humanwhocodes/retry": "^0.4.0" 927 + }, 928 + "engines": { 929 + "node": ">=18.18.0" 930 + } 931 + }, 932 + "node_modules/@humanwhocodes/module-importer": { 933 + "version": "1.0.1", 934 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 935 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 936 + "dev": true, 937 + "license": "Apache-2.0", 938 + "engines": { 939 + "node": ">=12.22" 940 + }, 941 + "funding": { 942 + "type": "github", 943 + "url": "https://github.com/sponsors/nzakas" 944 + } 945 + }, 946 + "node_modules/@humanwhocodes/retry": { 947 + "version": "0.4.3", 948 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 949 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 950 + "dev": true, 951 + "license": "Apache-2.0", 952 + "engines": { 953 + "node": ">=18.18" 954 + }, 955 + "funding": { 956 + "type": "github", 957 + "url": "https://github.com/sponsors/nzakas" 958 + } 959 + }, 960 + "node_modules/@jridgewell/gen-mapping": { 961 + "version": "0.3.13", 962 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 963 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 964 + "dev": true, 965 + "license": "MIT", 966 + "dependencies": { 967 + "@jridgewell/sourcemap-codec": "^1.5.0", 968 + "@jridgewell/trace-mapping": "^0.3.24" 969 + } 970 + }, 971 + "node_modules/@jridgewell/remapping": { 972 + "version": "2.3.5", 973 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 974 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 975 + "dev": true, 976 + "license": "MIT", 977 + "dependencies": { 978 + "@jridgewell/gen-mapping": "^0.3.5", 979 + "@jridgewell/trace-mapping": "^0.3.24" 980 + } 981 + }, 982 + "node_modules/@jridgewell/resolve-uri": { 983 + "version": "3.1.2", 984 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 985 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 986 + "dev": true, 987 + "license": "MIT", 988 + "engines": { 989 + "node": ">=6.0.0" 990 + } 991 + }, 992 + "node_modules/@jridgewell/sourcemap-codec": { 993 + "version": "1.5.5", 994 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 995 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 996 + "dev": true, 997 + "license": "MIT" 998 + }, 999 + "node_modules/@jridgewell/trace-mapping": { 1000 + "version": "0.3.31", 1001 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 1002 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 1003 + "dev": true, 1004 + "license": "MIT", 1005 + "dependencies": { 1006 + "@jridgewell/resolve-uri": "^3.1.0", 1007 + "@jridgewell/sourcemap-codec": "^1.4.14" 1008 + } 1009 + }, 1010 + "node_modules/@rolldown/pluginutils": { 1011 + "version": "1.0.0-beta.38", 1012 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", 1013 + "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", 1014 + "dev": true, 1015 + "license": "MIT" 1016 + }, 1017 + "node_modules/@rollup/rollup-android-arm-eabi": { 1018 + "version": "4.52.5", 1019 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", 1020 + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", 1021 + "cpu": [ 1022 + "arm" 1023 + ], 1024 + "dev": true, 1025 + "license": "MIT", 1026 + "optional": true, 1027 + "os": [ 1028 + "android" 1029 + ] 1030 + }, 1031 + "node_modules/@rollup/rollup-android-arm64": { 1032 + "version": "4.52.5", 1033 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", 1034 + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", 1035 + "cpu": [ 1036 + "arm64" 1037 + ], 1038 + "dev": true, 1039 + "license": "MIT", 1040 + "optional": true, 1041 + "os": [ 1042 + "android" 1043 + ] 1044 + }, 1045 + "node_modules/@rollup/rollup-darwin-arm64": { 1046 + "version": "4.52.5", 1047 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", 1048 + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", 1049 + "cpu": [ 1050 + "arm64" 1051 + ], 1052 + "dev": true, 1053 + "license": "MIT", 1054 + "optional": true, 1055 + "os": [ 1056 + "darwin" 1057 + ] 1058 + }, 1059 + "node_modules/@rollup/rollup-darwin-x64": { 1060 + "version": "4.52.5", 1061 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", 1062 + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", 1063 + "cpu": [ 1064 + "x64" 1065 + ], 1066 + "dev": true, 1067 + "license": "MIT", 1068 + "optional": true, 1069 + "os": [ 1070 + "darwin" 1071 + ] 1072 + }, 1073 + "node_modules/@rollup/rollup-freebsd-arm64": { 1074 + "version": "4.52.5", 1075 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", 1076 + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", 1077 + "cpu": [ 1078 + "arm64" 1079 + ], 1080 + "dev": true, 1081 + "license": "MIT", 1082 + "optional": true, 1083 + "os": [ 1084 + "freebsd" 1085 + ] 1086 + }, 1087 + "node_modules/@rollup/rollup-freebsd-x64": { 1088 + "version": "4.52.5", 1089 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", 1090 + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", 1091 + "cpu": [ 1092 + "x64" 1093 + ], 1094 + "dev": true, 1095 + "license": "MIT", 1096 + "optional": true, 1097 + "os": [ 1098 + "freebsd" 1099 + ] 1100 + }, 1101 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1102 + "version": "4.52.5", 1103 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", 1104 + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", 1105 + "cpu": [ 1106 + "arm" 1107 + ], 1108 + "dev": true, 1109 + "license": "MIT", 1110 + "optional": true, 1111 + "os": [ 1112 + "linux" 1113 + ] 1114 + }, 1115 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1116 + "version": "4.52.5", 1117 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", 1118 + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", 1119 + "cpu": [ 1120 + "arm" 1121 + ], 1122 + "dev": true, 1123 + "license": "MIT", 1124 + "optional": true, 1125 + "os": [ 1126 + "linux" 1127 + ] 1128 + }, 1129 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1130 + "version": "4.52.5", 1131 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", 1132 + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", 1133 + "cpu": [ 1134 + "arm64" 1135 + ], 1136 + "dev": true, 1137 + "license": "MIT", 1138 + "optional": true, 1139 + "os": [ 1140 + "linux" 1141 + ] 1142 + }, 1143 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1144 + "version": "4.52.5", 1145 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", 1146 + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", 1147 + "cpu": [ 1148 + "arm64" 1149 + ], 1150 + "dev": true, 1151 + "license": "MIT", 1152 + "optional": true, 1153 + "os": [ 1154 + "linux" 1155 + ] 1156 + }, 1157 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 1158 + "version": "4.52.5", 1159 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", 1160 + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", 1161 + "cpu": [ 1162 + "loong64" 1163 + ], 1164 + "dev": true, 1165 + "license": "MIT", 1166 + "optional": true, 1167 + "os": [ 1168 + "linux" 1169 + ] 1170 + }, 1171 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 1172 + "version": "4.52.5", 1173 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", 1174 + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", 1175 + "cpu": [ 1176 + "ppc64" 1177 + ], 1178 + "dev": true, 1179 + "license": "MIT", 1180 + "optional": true, 1181 + "os": [ 1182 + "linux" 1183 + ] 1184 + }, 1185 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1186 + "version": "4.52.5", 1187 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", 1188 + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", 1189 + "cpu": [ 1190 + "riscv64" 1191 + ], 1192 + "dev": true, 1193 + "license": "MIT", 1194 + "optional": true, 1195 + "os": [ 1196 + "linux" 1197 + ] 1198 + }, 1199 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1200 + "version": "4.52.5", 1201 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", 1202 + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", 1203 + "cpu": [ 1204 + "riscv64" 1205 + ], 1206 + "dev": true, 1207 + "license": "MIT", 1208 + "optional": true, 1209 + "os": [ 1210 + "linux" 1211 + ] 1212 + }, 1213 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1214 + "version": "4.52.5", 1215 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", 1216 + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", 1217 + "cpu": [ 1218 + "s390x" 1219 + ], 1220 + "dev": true, 1221 + "license": "MIT", 1222 + "optional": true, 1223 + "os": [ 1224 + "linux" 1225 + ] 1226 + }, 1227 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1228 + "version": "4.52.5", 1229 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", 1230 + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", 1231 + "cpu": [ 1232 + "x64" 1233 + ], 1234 + "dev": true, 1235 + "license": "MIT", 1236 + "optional": true, 1237 + "os": [ 1238 + "linux" 1239 + ] 1240 + }, 1241 + "node_modules/@rollup/rollup-linux-x64-musl": { 1242 + "version": "4.52.5", 1243 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", 1244 + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", 1245 + "cpu": [ 1246 + "x64" 1247 + ], 1248 + "dev": true, 1249 + "license": "MIT", 1250 + "optional": true, 1251 + "os": [ 1252 + "linux" 1253 + ] 1254 + }, 1255 + "node_modules/@rollup/rollup-openharmony-arm64": { 1256 + "version": "4.52.5", 1257 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", 1258 + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", 1259 + "cpu": [ 1260 + "arm64" 1261 + ], 1262 + "dev": true, 1263 + "license": "MIT", 1264 + "optional": true, 1265 + "os": [ 1266 + "openharmony" 1267 + ] 1268 + }, 1269 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1270 + "version": "4.52.5", 1271 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", 1272 + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", 1273 + "cpu": [ 1274 + "arm64" 1275 + ], 1276 + "dev": true, 1277 + "license": "MIT", 1278 + "optional": true, 1279 + "os": [ 1280 + "win32" 1281 + ] 1282 + }, 1283 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1284 + "version": "4.52.5", 1285 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", 1286 + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", 1287 + "cpu": [ 1288 + "ia32" 1289 + ], 1290 + "dev": true, 1291 + "license": "MIT", 1292 + "optional": true, 1293 + "os": [ 1294 + "win32" 1295 + ] 1296 + }, 1297 + "node_modules/@rollup/rollup-win32-x64-gnu": { 1298 + "version": "4.52.5", 1299 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", 1300 + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", 1301 + "cpu": [ 1302 + "x64" 1303 + ], 1304 + "dev": true, 1305 + "license": "MIT", 1306 + "optional": true, 1307 + "os": [ 1308 + "win32" 1309 + ] 1310 + }, 1311 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1312 + "version": "4.52.5", 1313 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", 1314 + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", 1315 + "cpu": [ 1316 + "x64" 1317 + ], 1318 + "dev": true, 1319 + "license": "MIT", 1320 + "optional": true, 1321 + "os": [ 1322 + "win32" 1323 + ] 1324 + }, 1325 + "node_modules/@types/babel__core": { 1326 + "version": "7.20.5", 1327 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", 1328 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", 1329 + "dev": true, 1330 + "license": "MIT", 1331 + "dependencies": { 1332 + "@babel/parser": "^7.20.7", 1333 + "@babel/types": "^7.20.7", 1334 + "@types/babel__generator": "*", 1335 + "@types/babel__template": "*", 1336 + "@types/babel__traverse": "*" 1337 + } 1338 + }, 1339 + "node_modules/@types/babel__generator": { 1340 + "version": "7.27.0", 1341 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", 1342 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", 1343 + "dev": true, 1344 + "license": "MIT", 1345 + "dependencies": { 1346 + "@babel/types": "^7.0.0" 1347 + } 1348 + }, 1349 + "node_modules/@types/babel__template": { 1350 + "version": "7.4.4", 1351 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", 1352 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", 1353 + "dev": true, 1354 + "license": "MIT", 1355 + "dependencies": { 1356 + "@babel/parser": "^7.1.0", 1357 + "@babel/types": "^7.0.0" 1358 + } 1359 + }, 1360 + "node_modules/@types/babel__traverse": { 1361 + "version": "7.28.0", 1362 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", 1363 + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", 1364 + "dev": true, 1365 + "license": "MIT", 1366 + "dependencies": { 1367 + "@babel/types": "^7.28.2" 1368 + } 1369 + }, 1370 + "node_modules/@types/estree": { 1371 + "version": "1.0.8", 1372 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1373 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1374 + "dev": true, 1375 + "license": "MIT" 1376 + }, 1377 + "node_modules/@types/json-schema": { 1378 + "version": "7.0.15", 1379 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 1380 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 1381 + "dev": true, 1382 + "license": "MIT" 1383 + }, 1384 + "node_modules/@types/react": { 1385 + "version": "19.2.2", 1386 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", 1387 + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", 1388 + "dev": true, 1389 + "license": "MIT", 1390 + "dependencies": { 1391 + "csstype": "^3.0.2" 1392 + } 1393 + }, 1394 + "node_modules/@types/react-dom": { 1395 + "version": "19.2.2", 1396 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", 1397 + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", 1398 + "dev": true, 1399 + "license": "MIT", 1400 + "peerDependencies": { 1401 + "@types/react": "^19.2.0" 1402 + } 1403 + }, 1404 + "node_modules/@vitejs/plugin-react": { 1405 + "version": "5.0.4", 1406 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", 1407 + "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", 1408 + "dev": true, 1409 + "license": "MIT", 1410 + "dependencies": { 1411 + "@babel/core": "^7.28.4", 1412 + "@babel/plugin-transform-react-jsx-self": "^7.27.1", 1413 + "@babel/plugin-transform-react-jsx-source": "^7.27.1", 1414 + "@rolldown/pluginutils": "1.0.0-beta.38", 1415 + "@types/babel__core": "^7.20.5", 1416 + "react-refresh": "^0.17.0" 1417 + }, 1418 + "engines": { 1419 + "node": "^20.19.0 || >=22.12.0" 1420 + }, 1421 + "peerDependencies": { 1422 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" 1423 + } 1424 + }, 1425 + "node_modules/acorn": { 1426 + "version": "8.15.0", 1427 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 1428 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 1429 + "dev": true, 1430 + "license": "MIT", 1431 + "bin": { 1432 + "acorn": "bin/acorn" 1433 + }, 1434 + "engines": { 1435 + "node": ">=0.4.0" 1436 + } 1437 + }, 1438 + "node_modules/acorn-jsx": { 1439 + "version": "5.3.2", 1440 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 1441 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 1442 + "dev": true, 1443 + "license": "MIT", 1444 + "peerDependencies": { 1445 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 1446 + } 1447 + }, 1448 + "node_modules/ajv": { 1449 + "version": "6.12.6", 1450 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1451 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1452 + "dev": true, 1453 + "license": "MIT", 1454 + "dependencies": { 1455 + "fast-deep-equal": "^3.1.1", 1456 + "fast-json-stable-stringify": "^2.0.0", 1457 + "json-schema-traverse": "^0.4.1", 1458 + "uri-js": "^4.2.2" 1459 + }, 1460 + "funding": { 1461 + "type": "github", 1462 + "url": "https://github.com/sponsors/epoberezkin" 1463 + } 1464 + }, 1465 + "node_modules/ansi-styles": { 1466 + "version": "4.3.0", 1467 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1468 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1469 + "dev": true, 1470 + "license": "MIT", 1471 + "dependencies": { 1472 + "color-convert": "^2.0.1" 1473 + }, 1474 + "engines": { 1475 + "node": ">=8" 1476 + }, 1477 + "funding": { 1478 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1479 + } 1480 + }, 1481 + "node_modules/argparse": { 1482 + "version": "2.0.1", 1483 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1484 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1485 + "dev": true, 1486 + "license": "Python-2.0" 1487 + }, 1488 + "node_modules/balanced-match": { 1489 + "version": "1.0.2", 1490 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1491 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1492 + "dev": true, 1493 + "license": "MIT" 1494 + }, 1495 + "node_modules/baseline-browser-mapping": { 1496 + "version": "2.8.19", 1497 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz", 1498 + "integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==", 1499 + "dev": true, 1500 + "license": "Apache-2.0", 1501 + "bin": { 1502 + "baseline-browser-mapping": "dist/cli.js" 1503 + } 1504 + }, 1505 + "node_modules/brace-expansion": { 1506 + "version": "1.1.12", 1507 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 1508 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 1509 + "dev": true, 1510 + "license": "MIT", 1511 + "dependencies": { 1512 + "balanced-match": "^1.0.0", 1513 + "concat-map": "0.0.1" 1514 + } 1515 + }, 1516 + "node_modules/browserslist": { 1517 + "version": "4.26.3", 1518 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", 1519 + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", 1520 + "dev": true, 1521 + "funding": [ 1522 + { 1523 + "type": "opencollective", 1524 + "url": "https://opencollective.com/browserslist" 1525 + }, 1526 + { 1527 + "type": "tidelift", 1528 + "url": "https://tidelift.com/funding/github/npm/browserslist" 1529 + }, 1530 + { 1531 + "type": "github", 1532 + "url": "https://github.com/sponsors/ai" 1533 + } 1534 + ], 1535 + "license": "MIT", 1536 + "dependencies": { 1537 + "baseline-browser-mapping": "^2.8.9", 1538 + "caniuse-lite": "^1.0.30001746", 1539 + "electron-to-chromium": "^1.5.227", 1540 + "node-releases": "^2.0.21", 1541 + "update-browserslist-db": "^1.1.3" 1542 + }, 1543 + "bin": { 1544 + "browserslist": "cli.js" 1545 + }, 1546 + "engines": { 1547 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 1548 + } 1549 + }, 1550 + "node_modules/callsites": { 1551 + "version": "3.1.0", 1552 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 1553 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 1554 + "dev": true, 1555 + "license": "MIT", 1556 + "engines": { 1557 + "node": ">=6" 1558 + } 1559 + }, 1560 + "node_modules/caniuse-lite": { 1561 + "version": "1.0.30001751", 1562 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", 1563 + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", 1564 + "dev": true, 1565 + "funding": [ 1566 + { 1567 + "type": "opencollective", 1568 + "url": "https://opencollective.com/browserslist" 1569 + }, 1570 + { 1571 + "type": "tidelift", 1572 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 1573 + }, 1574 + { 1575 + "type": "github", 1576 + "url": "https://github.com/sponsors/ai" 1577 + } 1578 + ], 1579 + "license": "CC-BY-4.0" 1580 + }, 1581 + "node_modules/chalk": { 1582 + "version": "4.1.2", 1583 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1584 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1585 + "dev": true, 1586 + "license": "MIT", 1587 + "dependencies": { 1588 + "ansi-styles": "^4.1.0", 1589 + "supports-color": "^7.1.0" 1590 + }, 1591 + "engines": { 1592 + "node": ">=10" 1593 + }, 1594 + "funding": { 1595 + "url": "https://github.com/chalk/chalk?sponsor=1" 1596 + } 1597 + }, 1598 + "node_modules/color-convert": { 1599 + "version": "2.0.1", 1600 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1601 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1602 + "dev": true, 1603 + "license": "MIT", 1604 + "dependencies": { 1605 + "color-name": "~1.1.4" 1606 + }, 1607 + "engines": { 1608 + "node": ">=7.0.0" 1609 + } 1610 + }, 1611 + "node_modules/color-name": { 1612 + "version": "1.1.4", 1613 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1614 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1615 + "dev": true, 1616 + "license": "MIT" 1617 + }, 1618 + "node_modules/concat-map": { 1619 + "version": "0.0.1", 1620 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1621 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1622 + "dev": true, 1623 + "license": "MIT" 1624 + }, 1625 + "node_modules/convert-source-map": { 1626 + "version": "2.0.0", 1627 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 1628 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 1629 + "dev": true, 1630 + "license": "MIT" 1631 + }, 1632 + "node_modules/cookie": { 1633 + "version": "1.0.2", 1634 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", 1635 + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", 1636 + "license": "MIT", 1637 + "engines": { 1638 + "node": ">=18" 1639 + } 1640 + }, 1641 + "node_modules/cross-spawn": { 1642 + "version": "7.0.6", 1643 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 1644 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 1645 + "dev": true, 1646 + "license": "MIT", 1647 + "dependencies": { 1648 + "path-key": "^3.1.0", 1649 + "shebang-command": "^2.0.0", 1650 + "which": "^2.0.1" 1651 + }, 1652 + "engines": { 1653 + "node": ">= 8" 1654 + } 1655 + }, 1656 + "node_modules/csstype": { 1657 + "version": "3.1.3", 1658 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 1659 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 1660 + "dev": true, 1661 + "license": "MIT" 1662 + }, 1663 + "node_modules/debug": { 1664 + "version": "4.4.3", 1665 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1666 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1667 + "dev": true, 1668 + "license": "MIT", 1669 + "dependencies": { 1670 + "ms": "^2.1.3" 1671 + }, 1672 + "engines": { 1673 + "node": ">=6.0" 1674 + }, 1675 + "peerDependenciesMeta": { 1676 + "supports-color": { 1677 + "optional": true 1678 + } 1679 + } 1680 + }, 1681 + "node_modules/deep-is": { 1682 + "version": "0.1.4", 1683 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 1684 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 1685 + "dev": true, 1686 + "license": "MIT" 1687 + }, 1688 + "node_modules/electron-to-chromium": { 1689 + "version": "1.5.238", 1690 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.238.tgz", 1691 + "integrity": "sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==", 1692 + "dev": true, 1693 + "license": "ISC" 1694 + }, 1695 + "node_modules/esbuild": { 1696 + "version": "0.25.11", 1697 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", 1698 + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", 1699 + "dev": true, 1700 + "hasInstallScript": true, 1701 + "license": "MIT", 1702 + "bin": { 1703 + "esbuild": "bin/esbuild" 1704 + }, 1705 + "engines": { 1706 + "node": ">=18" 1707 + }, 1708 + "optionalDependencies": { 1709 + "@esbuild/aix-ppc64": "0.25.11", 1710 + "@esbuild/android-arm": "0.25.11", 1711 + "@esbuild/android-arm64": "0.25.11", 1712 + "@esbuild/android-x64": "0.25.11", 1713 + "@esbuild/darwin-arm64": "0.25.11", 1714 + "@esbuild/darwin-x64": "0.25.11", 1715 + "@esbuild/freebsd-arm64": "0.25.11", 1716 + "@esbuild/freebsd-x64": "0.25.11", 1717 + "@esbuild/linux-arm": "0.25.11", 1718 + "@esbuild/linux-arm64": "0.25.11", 1719 + "@esbuild/linux-ia32": "0.25.11", 1720 + "@esbuild/linux-loong64": "0.25.11", 1721 + "@esbuild/linux-mips64el": "0.25.11", 1722 + "@esbuild/linux-ppc64": "0.25.11", 1723 + "@esbuild/linux-riscv64": "0.25.11", 1724 + "@esbuild/linux-s390x": "0.25.11", 1725 + "@esbuild/linux-x64": "0.25.11", 1726 + "@esbuild/netbsd-arm64": "0.25.11", 1727 + "@esbuild/netbsd-x64": "0.25.11", 1728 + "@esbuild/openbsd-arm64": "0.25.11", 1729 + "@esbuild/openbsd-x64": "0.25.11", 1730 + "@esbuild/openharmony-arm64": "0.25.11", 1731 + "@esbuild/sunos-x64": "0.25.11", 1732 + "@esbuild/win32-arm64": "0.25.11", 1733 + "@esbuild/win32-ia32": "0.25.11", 1734 + "@esbuild/win32-x64": "0.25.11" 1735 + } 1736 + }, 1737 + "node_modules/escalade": { 1738 + "version": "3.2.0", 1739 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 1740 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 1741 + "dev": true, 1742 + "license": "MIT", 1743 + "engines": { 1744 + "node": ">=6" 1745 + } 1746 + }, 1747 + "node_modules/escape-string-regexp": { 1748 + "version": "4.0.0", 1749 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1750 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1751 + "dev": true, 1752 + "license": "MIT", 1753 + "engines": { 1754 + "node": ">=10" 1755 + }, 1756 + "funding": { 1757 + "url": "https://github.com/sponsors/sindresorhus" 1758 + } 1759 + }, 1760 + "node_modules/eslint": { 1761 + "version": "9.38.0", 1762 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", 1763 + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", 1764 + "dev": true, 1765 + "license": "MIT", 1766 + "dependencies": { 1767 + "@eslint-community/eslint-utils": "^4.8.0", 1768 + "@eslint-community/regexpp": "^4.12.1", 1769 + "@eslint/config-array": "^0.21.1", 1770 + "@eslint/config-helpers": "^0.4.1", 1771 + "@eslint/core": "^0.16.0", 1772 + "@eslint/eslintrc": "^3.3.1", 1773 + "@eslint/js": "9.38.0", 1774 + "@eslint/plugin-kit": "^0.4.0", 1775 + "@humanfs/node": "^0.16.6", 1776 + "@humanwhocodes/module-importer": "^1.0.1", 1777 + "@humanwhocodes/retry": "^0.4.2", 1778 + "@types/estree": "^1.0.6", 1779 + "ajv": "^6.12.4", 1780 + "chalk": "^4.0.0", 1781 + "cross-spawn": "^7.0.6", 1782 + "debug": "^4.3.2", 1783 + "escape-string-regexp": "^4.0.0", 1784 + "eslint-scope": "^8.4.0", 1785 + "eslint-visitor-keys": "^4.2.1", 1786 + "espree": "^10.4.0", 1787 + "esquery": "^1.5.0", 1788 + "esutils": "^2.0.2", 1789 + "fast-deep-equal": "^3.1.3", 1790 + "file-entry-cache": "^8.0.0", 1791 + "find-up": "^5.0.0", 1792 + "glob-parent": "^6.0.2", 1793 + "ignore": "^5.2.0", 1794 + "imurmurhash": "^0.1.4", 1795 + "is-glob": "^4.0.0", 1796 + "json-stable-stringify-without-jsonify": "^1.0.1", 1797 + "lodash.merge": "^4.6.2", 1798 + "minimatch": "^3.1.2", 1799 + "natural-compare": "^1.4.0", 1800 + "optionator": "^0.9.3" 1801 + }, 1802 + "bin": { 1803 + "eslint": "bin/eslint.js" 1804 + }, 1805 + "engines": { 1806 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1807 + }, 1808 + "funding": { 1809 + "url": "https://eslint.org/donate" 1810 + }, 1811 + "peerDependencies": { 1812 + "jiti": "*" 1813 + }, 1814 + "peerDependenciesMeta": { 1815 + "jiti": { 1816 + "optional": true 1817 + } 1818 + } 1819 + }, 1820 + "node_modules/eslint-plugin-react-hooks": { 1821 + "version": "5.2.0", 1822 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", 1823 + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", 1824 + "dev": true, 1825 + "license": "MIT", 1826 + "engines": { 1827 + "node": ">=10" 1828 + }, 1829 + "peerDependencies": { 1830 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 1831 + } 1832 + }, 1833 + "node_modules/eslint-plugin-react-refresh": { 1834 + "version": "0.4.24", 1835 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", 1836 + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", 1837 + "dev": true, 1838 + "license": "MIT", 1839 + "peerDependencies": { 1840 + "eslint": ">=8.40" 1841 + } 1842 + }, 1843 + "node_modules/eslint-scope": { 1844 + "version": "8.4.0", 1845 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 1846 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 1847 + "dev": true, 1848 + "license": "BSD-2-Clause", 1849 + "dependencies": { 1850 + "esrecurse": "^4.3.0", 1851 + "estraverse": "^5.2.0" 1852 + }, 1853 + "engines": { 1854 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1855 + }, 1856 + "funding": { 1857 + "url": "https://opencollective.com/eslint" 1858 + } 1859 + }, 1860 + "node_modules/eslint-visitor-keys": { 1861 + "version": "4.2.1", 1862 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 1863 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 1864 + "dev": true, 1865 + "license": "Apache-2.0", 1866 + "engines": { 1867 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1868 + }, 1869 + "funding": { 1870 + "url": "https://opencollective.com/eslint" 1871 + } 1872 + }, 1873 + "node_modules/espree": { 1874 + "version": "10.4.0", 1875 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 1876 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 1877 + "dev": true, 1878 + "license": "BSD-2-Clause", 1879 + "dependencies": { 1880 + "acorn": "^8.15.0", 1881 + "acorn-jsx": "^5.3.2", 1882 + "eslint-visitor-keys": "^4.2.1" 1883 + }, 1884 + "engines": { 1885 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1886 + }, 1887 + "funding": { 1888 + "url": "https://opencollective.com/eslint" 1889 + } 1890 + }, 1891 + "node_modules/esquery": { 1892 + "version": "1.6.0", 1893 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", 1894 + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", 1895 + "dev": true, 1896 + "license": "BSD-3-Clause", 1897 + "dependencies": { 1898 + "estraverse": "^5.1.0" 1899 + }, 1900 + "engines": { 1901 + "node": ">=0.10" 1902 + } 1903 + }, 1904 + "node_modules/esrecurse": { 1905 + "version": "4.3.0", 1906 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 1907 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 1908 + "dev": true, 1909 + "license": "BSD-2-Clause", 1910 + "dependencies": { 1911 + "estraverse": "^5.2.0" 1912 + }, 1913 + "engines": { 1914 + "node": ">=4.0" 1915 + } 1916 + }, 1917 + "node_modules/estraverse": { 1918 + "version": "5.3.0", 1919 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 1920 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 1921 + "dev": true, 1922 + "license": "BSD-2-Clause", 1923 + "engines": { 1924 + "node": ">=4.0" 1925 + } 1926 + }, 1927 + "node_modules/esutils": { 1928 + "version": "2.0.3", 1929 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 1930 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 1931 + "dev": true, 1932 + "license": "BSD-2-Clause", 1933 + "engines": { 1934 + "node": ">=0.10.0" 1935 + } 1936 + }, 1937 + "node_modules/fast-deep-equal": { 1938 + "version": "3.1.3", 1939 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1940 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 1941 + "dev": true, 1942 + "license": "MIT" 1943 + }, 1944 + "node_modules/fast-json-stable-stringify": { 1945 + "version": "2.1.0", 1946 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1947 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 1948 + "dev": true, 1949 + "license": "MIT" 1950 + }, 1951 + "node_modules/fast-levenshtein": { 1952 + "version": "2.0.6", 1953 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 1954 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 1955 + "dev": true, 1956 + "license": "MIT" 1957 + }, 1958 + "node_modules/fdir": { 1959 + "version": "6.5.0", 1960 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 1961 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 1962 + "dev": true, 1963 + "license": "MIT", 1964 + "engines": { 1965 + "node": ">=12.0.0" 1966 + }, 1967 + "peerDependencies": { 1968 + "picomatch": "^3 || ^4" 1969 + }, 1970 + "peerDependenciesMeta": { 1971 + "picomatch": { 1972 + "optional": true 1973 + } 1974 + } 1975 + }, 1976 + "node_modules/file-entry-cache": { 1977 + "version": "8.0.0", 1978 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 1979 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 1980 + "dev": true, 1981 + "license": "MIT", 1982 + "dependencies": { 1983 + "flat-cache": "^4.0.0" 1984 + }, 1985 + "engines": { 1986 + "node": ">=16.0.0" 1987 + } 1988 + }, 1989 + "node_modules/find-up": { 1990 + "version": "5.0.0", 1991 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 1992 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 1993 + "dev": true, 1994 + "license": "MIT", 1995 + "dependencies": { 1996 + "locate-path": "^6.0.0", 1997 + "path-exists": "^4.0.0" 1998 + }, 1999 + "engines": { 2000 + "node": ">=10" 2001 + }, 2002 + "funding": { 2003 + "url": "https://github.com/sponsors/sindresorhus" 2004 + } 2005 + }, 2006 + "node_modules/flat-cache": { 2007 + "version": "4.0.1", 2008 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 2009 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 2010 + "dev": true, 2011 + "license": "MIT", 2012 + "dependencies": { 2013 + "flatted": "^3.2.9", 2014 + "keyv": "^4.5.4" 2015 + }, 2016 + "engines": { 2017 + "node": ">=16" 2018 + } 2019 + }, 2020 + "node_modules/flatted": { 2021 + "version": "3.3.3", 2022 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 2023 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 2024 + "dev": true, 2025 + "license": "ISC" 2026 + }, 2027 + "node_modules/fsevents": { 2028 + "version": "2.3.3", 2029 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 2030 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 2031 + "dev": true, 2032 + "hasInstallScript": true, 2033 + "license": "MIT", 2034 + "optional": true, 2035 + "os": [ 2036 + "darwin" 2037 + ], 2038 + "engines": { 2039 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 2040 + } 2041 + }, 2042 + "node_modules/gensync": { 2043 + "version": "1.0.0-beta.2", 2044 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 2045 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 2046 + "dev": true, 2047 + "license": "MIT", 2048 + "engines": { 2049 + "node": ">=6.9.0" 2050 + } 2051 + }, 2052 + "node_modules/glob-parent": { 2053 + "version": "6.0.2", 2054 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 2055 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 2056 + "dev": true, 2057 + "license": "ISC", 2058 + "dependencies": { 2059 + "is-glob": "^4.0.3" 2060 + }, 2061 + "engines": { 2062 + "node": ">=10.13.0" 2063 + } 2064 + }, 2065 + "node_modules/globals": { 2066 + "version": "16.4.0", 2067 + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", 2068 + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", 2069 + "dev": true, 2070 + "license": "MIT", 2071 + "engines": { 2072 + "node": ">=18" 2073 + }, 2074 + "funding": { 2075 + "url": "https://github.com/sponsors/sindresorhus" 2076 + } 2077 + }, 2078 + "node_modules/has-flag": { 2079 + "version": "4.0.0", 2080 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2081 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2082 + "dev": true, 2083 + "license": "MIT", 2084 + "engines": { 2085 + "node": ">=8" 2086 + } 2087 + }, 2088 + "node_modules/ignore": { 2089 + "version": "5.3.2", 2090 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 2091 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 2092 + "dev": true, 2093 + "license": "MIT", 2094 + "engines": { 2095 + "node": ">= 4" 2096 + } 2097 + }, 2098 + "node_modules/import-fresh": { 2099 + "version": "3.3.1", 2100 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 2101 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 2102 + "dev": true, 2103 + "license": "MIT", 2104 + "dependencies": { 2105 + "parent-module": "^1.0.0", 2106 + "resolve-from": "^4.0.0" 2107 + }, 2108 + "engines": { 2109 + "node": ">=6" 2110 + }, 2111 + "funding": { 2112 + "url": "https://github.com/sponsors/sindresorhus" 2113 + } 2114 + }, 2115 + "node_modules/imurmurhash": { 2116 + "version": "0.1.4", 2117 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 2118 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 2119 + "dev": true, 2120 + "license": "MIT", 2121 + "engines": { 2122 + "node": ">=0.8.19" 2123 + } 2124 + }, 2125 + "node_modules/is-extglob": { 2126 + "version": "2.1.1", 2127 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 2128 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 2129 + "dev": true, 2130 + "license": "MIT", 2131 + "engines": { 2132 + "node": ">=0.10.0" 2133 + } 2134 + }, 2135 + "node_modules/is-glob": { 2136 + "version": "4.0.3", 2137 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 2138 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 2139 + "dev": true, 2140 + "license": "MIT", 2141 + "dependencies": { 2142 + "is-extglob": "^2.1.1" 2143 + }, 2144 + "engines": { 2145 + "node": ">=0.10.0" 2146 + } 2147 + }, 2148 + "node_modules/isexe": { 2149 + "version": "2.0.0", 2150 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 2151 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 2152 + "dev": true, 2153 + "license": "ISC" 2154 + }, 2155 + "node_modules/js-tokens": { 2156 + "version": "4.0.0", 2157 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 2158 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 2159 + "dev": true, 2160 + "license": "MIT" 2161 + }, 2162 + "node_modules/js-yaml": { 2163 + "version": "4.1.0", 2164 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 2165 + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 2166 + "dev": true, 2167 + "license": "MIT", 2168 + "dependencies": { 2169 + "argparse": "^2.0.1" 2170 + }, 2171 + "bin": { 2172 + "js-yaml": "bin/js-yaml.js" 2173 + } 2174 + }, 2175 + "node_modules/jsesc": { 2176 + "version": "3.1.0", 2177 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 2178 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 2179 + "dev": true, 2180 + "license": "MIT", 2181 + "bin": { 2182 + "jsesc": "bin/jsesc" 2183 + }, 2184 + "engines": { 2185 + "node": ">=6" 2186 + } 2187 + }, 2188 + "node_modules/json-buffer": { 2189 + "version": "3.0.1", 2190 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 2191 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 2192 + "dev": true, 2193 + "license": "MIT" 2194 + }, 2195 + "node_modules/json-schema-traverse": { 2196 + "version": "0.4.1", 2197 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 2198 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 2199 + "dev": true, 2200 + "license": "MIT" 2201 + }, 2202 + "node_modules/json-stable-stringify-without-jsonify": { 2203 + "version": "1.0.1", 2204 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 2205 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 2206 + "dev": true, 2207 + "license": "MIT" 2208 + }, 2209 + "node_modules/json5": { 2210 + "version": "2.2.3", 2211 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 2212 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 2213 + "dev": true, 2214 + "license": "MIT", 2215 + "bin": { 2216 + "json5": "lib/cli.js" 2217 + }, 2218 + "engines": { 2219 + "node": ">=6" 2220 + } 2221 + }, 2222 + "node_modules/keyv": { 2223 + "version": "4.5.4", 2224 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 2225 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 2226 + "dev": true, 2227 + "license": "MIT", 2228 + "dependencies": { 2229 + "json-buffer": "3.0.1" 2230 + } 2231 + }, 2232 + "node_modules/levn": { 2233 + "version": "0.4.1", 2234 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 2235 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 2236 + "dev": true, 2237 + "license": "MIT", 2238 + "dependencies": { 2239 + "prelude-ls": "^1.2.1", 2240 + "type-check": "~0.4.0" 2241 + }, 2242 + "engines": { 2243 + "node": ">= 0.8.0" 2244 + } 2245 + }, 2246 + "node_modules/locate-path": { 2247 + "version": "6.0.0", 2248 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 2249 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 2250 + "dev": true, 2251 + "license": "MIT", 2252 + "dependencies": { 2253 + "p-locate": "^5.0.0" 2254 + }, 2255 + "engines": { 2256 + "node": ">=10" 2257 + }, 2258 + "funding": { 2259 + "url": "https://github.com/sponsors/sindresorhus" 2260 + } 2261 + }, 2262 + "node_modules/lodash.merge": { 2263 + "version": "4.6.2", 2264 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 2265 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 2266 + "dev": true, 2267 + "license": "MIT" 2268 + }, 2269 + "node_modules/lru-cache": { 2270 + "version": "5.1.1", 2271 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 2272 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 2273 + "dev": true, 2274 + "license": "ISC", 2275 + "dependencies": { 2276 + "yallist": "^3.0.2" 2277 + } 2278 + }, 2279 + "node_modules/minimatch": { 2280 + "version": "3.1.2", 2281 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 2282 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 2283 + "dev": true, 2284 + "license": "ISC", 2285 + "dependencies": { 2286 + "brace-expansion": "^1.1.7" 2287 + }, 2288 + "engines": { 2289 + "node": "*" 2290 + } 2291 + }, 2292 + "node_modules/ms": { 2293 + "version": "2.1.3", 2294 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 2295 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 2296 + "dev": true, 2297 + "license": "MIT" 2298 + }, 2299 + "node_modules/nanoid": { 2300 + "version": "3.3.11", 2301 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 2302 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 2303 + "dev": true, 2304 + "funding": [ 2305 + { 2306 + "type": "github", 2307 + "url": "https://github.com/sponsors/ai" 2308 + } 2309 + ], 2310 + "license": "MIT", 2311 + "bin": { 2312 + "nanoid": "bin/nanoid.cjs" 2313 + }, 2314 + "engines": { 2315 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2316 + } 2317 + }, 2318 + "node_modules/natural-compare": { 2319 + "version": "1.4.0", 2320 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 2321 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 2322 + "dev": true, 2323 + "license": "MIT" 2324 + }, 2325 + "node_modules/node-releases": { 2326 + "version": "2.0.26", 2327 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", 2328 + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", 2329 + "dev": true, 2330 + "license": "MIT" 2331 + }, 2332 + "node_modules/optionator": { 2333 + "version": "0.9.4", 2334 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 2335 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 2336 + "dev": true, 2337 + "license": "MIT", 2338 + "dependencies": { 2339 + "deep-is": "^0.1.3", 2340 + "fast-levenshtein": "^2.0.6", 2341 + "levn": "^0.4.1", 2342 + "prelude-ls": "^1.2.1", 2343 + "type-check": "^0.4.0", 2344 + "word-wrap": "^1.2.5" 2345 + }, 2346 + "engines": { 2347 + "node": ">= 0.8.0" 2348 + } 2349 + }, 2350 + "node_modules/p-limit": { 2351 + "version": "3.1.0", 2352 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 2353 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 2354 + "dev": true, 2355 + "license": "MIT", 2356 + "dependencies": { 2357 + "yocto-queue": "^0.1.0" 2358 + }, 2359 + "engines": { 2360 + "node": ">=10" 2361 + }, 2362 + "funding": { 2363 + "url": "https://github.com/sponsors/sindresorhus" 2364 + } 2365 + }, 2366 + "node_modules/p-locate": { 2367 + "version": "5.0.0", 2368 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 2369 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 2370 + "dev": true, 2371 + "license": "MIT", 2372 + "dependencies": { 2373 + "p-limit": "^3.0.2" 2374 + }, 2375 + "engines": { 2376 + "node": ">=10" 2377 + }, 2378 + "funding": { 2379 + "url": "https://github.com/sponsors/sindresorhus" 2380 + } 2381 + }, 2382 + "node_modules/parent-module": { 2383 + "version": "1.0.1", 2384 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 2385 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 2386 + "dev": true, 2387 + "license": "MIT", 2388 + "dependencies": { 2389 + "callsites": "^3.0.0" 2390 + }, 2391 + "engines": { 2392 + "node": ">=6" 2393 + } 2394 + }, 2395 + "node_modules/path-exists": { 2396 + "version": "4.0.0", 2397 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 2398 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 2399 + "dev": true, 2400 + "license": "MIT", 2401 + "engines": { 2402 + "node": ">=8" 2403 + } 2404 + }, 2405 + "node_modules/path-key": { 2406 + "version": "3.1.1", 2407 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 2408 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 2409 + "dev": true, 2410 + "license": "MIT", 2411 + "engines": { 2412 + "node": ">=8" 2413 + } 2414 + }, 2415 + "node_modules/picocolors": { 2416 + "version": "1.1.1", 2417 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 2418 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 2419 + "dev": true, 2420 + "license": "ISC" 2421 + }, 2422 + "node_modules/picomatch": { 2423 + "version": "4.0.3", 2424 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 2425 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 2426 + "dev": true, 2427 + "license": "MIT", 2428 + "engines": { 2429 + "node": ">=12" 2430 + }, 2431 + "funding": { 2432 + "url": "https://github.com/sponsors/jonschlinkert" 2433 + } 2434 + }, 2435 + "node_modules/postcss": { 2436 + "version": "8.5.6", 2437 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 2438 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 2439 + "dev": true, 2440 + "funding": [ 2441 + { 2442 + "type": "opencollective", 2443 + "url": "https://opencollective.com/postcss/" 2444 + }, 2445 + { 2446 + "type": "tidelift", 2447 + "url": "https://tidelift.com/funding/github/npm/postcss" 2448 + }, 2449 + { 2450 + "type": "github", 2451 + "url": "https://github.com/sponsors/ai" 2452 + } 2453 + ], 2454 + "license": "MIT", 2455 + "dependencies": { 2456 + "nanoid": "^3.3.11", 2457 + "picocolors": "^1.1.1", 2458 + "source-map-js": "^1.2.1" 2459 + }, 2460 + "engines": { 2461 + "node": "^10 || ^12 || >=14" 2462 + } 2463 + }, 2464 + "node_modules/prelude-ls": { 2465 + "version": "1.2.1", 2466 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 2467 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 2468 + "dev": true, 2469 + "license": "MIT", 2470 + "engines": { 2471 + "node": ">= 0.8.0" 2472 + } 2473 + }, 2474 + "node_modules/punycode": { 2475 + "version": "2.3.1", 2476 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 2477 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 2478 + "dev": true, 2479 + "license": "MIT", 2480 + "engines": { 2481 + "node": ">=6" 2482 + } 2483 + }, 2484 + "node_modules/react": { 2485 + "version": "19.2.0", 2486 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", 2487 + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", 2488 + "license": "MIT", 2489 + "engines": { 2490 + "node": ">=0.10.0" 2491 + } 2492 + }, 2493 + "node_modules/react-dom": { 2494 + "version": "19.2.0", 2495 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", 2496 + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", 2497 + "license": "MIT", 2498 + "dependencies": { 2499 + "scheduler": "^0.27.0" 2500 + }, 2501 + "peerDependencies": { 2502 + "react": "^19.2.0" 2503 + } 2504 + }, 2505 + "node_modules/react-refresh": { 2506 + "version": "0.17.0", 2507 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", 2508 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", 2509 + "dev": true, 2510 + "license": "MIT", 2511 + "engines": { 2512 + "node": ">=0.10.0" 2513 + } 2514 + }, 2515 + "node_modules/react-router": { 2516 + "version": "7.9.4", 2517 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", 2518 + "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", 2519 + "license": "MIT", 2520 + "dependencies": { 2521 + "cookie": "^1.0.1", 2522 + "set-cookie-parser": "^2.6.0" 2523 + }, 2524 + "engines": { 2525 + "node": ">=20.0.0" 2526 + }, 2527 + "peerDependencies": { 2528 + "react": ">=18", 2529 + "react-dom": ">=18" 2530 + }, 2531 + "peerDependenciesMeta": { 2532 + "react-dom": { 2533 + "optional": true 2534 + } 2535 + } 2536 + }, 2537 + "node_modules/react-router-dom": { 2538 + "version": "7.9.4", 2539 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", 2540 + "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", 2541 + "license": "MIT", 2542 + "dependencies": { 2543 + "react-router": "7.9.4" 2544 + }, 2545 + "engines": { 2546 + "node": ">=20.0.0" 2547 + }, 2548 + "peerDependencies": { 2549 + "react": ">=18", 2550 + "react-dom": ">=18" 2551 + } 2552 + }, 2553 + "node_modules/resolve-from": { 2554 + "version": "4.0.0", 2555 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 2556 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 2557 + "dev": true, 2558 + "license": "MIT", 2559 + "engines": { 2560 + "node": ">=4" 2561 + } 2562 + }, 2563 + "node_modules/rollup": { 2564 + "version": "4.52.5", 2565 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", 2566 + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", 2567 + "dev": true, 2568 + "license": "MIT", 2569 + "dependencies": { 2570 + "@types/estree": "1.0.8" 2571 + }, 2572 + "bin": { 2573 + "rollup": "dist/bin/rollup" 2574 + }, 2575 + "engines": { 2576 + "node": ">=18.0.0", 2577 + "npm": ">=8.0.0" 2578 + }, 2579 + "optionalDependencies": { 2580 + "@rollup/rollup-android-arm-eabi": "4.52.5", 2581 + "@rollup/rollup-android-arm64": "4.52.5", 2582 + "@rollup/rollup-darwin-arm64": "4.52.5", 2583 + "@rollup/rollup-darwin-x64": "4.52.5", 2584 + "@rollup/rollup-freebsd-arm64": "4.52.5", 2585 + "@rollup/rollup-freebsd-x64": "4.52.5", 2586 + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", 2587 + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", 2588 + "@rollup/rollup-linux-arm64-gnu": "4.52.5", 2589 + "@rollup/rollup-linux-arm64-musl": "4.52.5", 2590 + "@rollup/rollup-linux-loong64-gnu": "4.52.5", 2591 + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", 2592 + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", 2593 + "@rollup/rollup-linux-riscv64-musl": "4.52.5", 2594 + "@rollup/rollup-linux-s390x-gnu": "4.52.5", 2595 + "@rollup/rollup-linux-x64-gnu": "4.52.5", 2596 + "@rollup/rollup-linux-x64-musl": "4.52.5", 2597 + "@rollup/rollup-openharmony-arm64": "4.52.5", 2598 + "@rollup/rollup-win32-arm64-msvc": "4.52.5", 2599 + "@rollup/rollup-win32-ia32-msvc": "4.52.5", 2600 + "@rollup/rollup-win32-x64-gnu": "4.52.5", 2601 + "@rollup/rollup-win32-x64-msvc": "4.52.5", 2602 + "fsevents": "~2.3.2" 2603 + } 2604 + }, 2605 + "node_modules/scheduler": { 2606 + "version": "0.27.0", 2607 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", 2608 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", 2609 + "license": "MIT" 2610 + }, 2611 + "node_modules/semver": { 2612 + "version": "6.3.1", 2613 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 2614 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 2615 + "dev": true, 2616 + "license": "ISC", 2617 + "bin": { 2618 + "semver": "bin/semver.js" 2619 + } 2620 + }, 2621 + "node_modules/set-cookie-parser": { 2622 + "version": "2.7.1", 2623 + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", 2624 + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", 2625 + "license": "MIT" 2626 + }, 2627 + "node_modules/shebang-command": { 2628 + "version": "2.0.0", 2629 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 2630 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 2631 + "dev": true, 2632 + "license": "MIT", 2633 + "dependencies": { 2634 + "shebang-regex": "^3.0.0" 2635 + }, 2636 + "engines": { 2637 + "node": ">=8" 2638 + } 2639 + }, 2640 + "node_modules/shebang-regex": { 2641 + "version": "3.0.0", 2642 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 2643 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 2644 + "dev": true, 2645 + "license": "MIT", 2646 + "engines": { 2647 + "node": ">=8" 2648 + } 2649 + }, 2650 + "node_modules/source-map-js": { 2651 + "version": "1.2.1", 2652 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2653 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2654 + "dev": true, 2655 + "license": "BSD-3-Clause", 2656 + "engines": { 2657 + "node": ">=0.10.0" 2658 + } 2659 + }, 2660 + "node_modules/strip-json-comments": { 2661 + "version": "3.1.1", 2662 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 2663 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 2664 + "dev": true, 2665 + "license": "MIT", 2666 + "engines": { 2667 + "node": ">=8" 2668 + }, 2669 + "funding": { 2670 + "url": "https://github.com/sponsors/sindresorhus" 2671 + } 2672 + }, 2673 + "node_modules/supports-color": { 2674 + "version": "7.2.0", 2675 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 2676 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 2677 + "dev": true, 2678 + "license": "MIT", 2679 + "dependencies": { 2680 + "has-flag": "^4.0.0" 2681 + }, 2682 + "engines": { 2683 + "node": ">=8" 2684 + } 2685 + }, 2686 + "node_modules/tinyglobby": { 2687 + "version": "0.2.15", 2688 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 2689 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 2690 + "dev": true, 2691 + "license": "MIT", 2692 + "dependencies": { 2693 + "fdir": "^6.5.0", 2694 + "picomatch": "^4.0.3" 2695 + }, 2696 + "engines": { 2697 + "node": ">=12.0.0" 2698 + }, 2699 + "funding": { 2700 + "url": "https://github.com/sponsors/SuperchupuDev" 2701 + } 2702 + }, 2703 + "node_modules/type-check": { 2704 + "version": "0.4.0", 2705 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 2706 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 2707 + "dev": true, 2708 + "license": "MIT", 2709 + "dependencies": { 2710 + "prelude-ls": "^1.2.1" 2711 + }, 2712 + "engines": { 2713 + "node": ">= 0.8.0" 2714 + } 2715 + }, 2716 + "node_modules/update-browserslist-db": { 2717 + "version": "1.1.3", 2718 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", 2719 + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", 2720 + "dev": true, 2721 + "funding": [ 2722 + { 2723 + "type": "opencollective", 2724 + "url": "https://opencollective.com/browserslist" 2725 + }, 2726 + { 2727 + "type": "tidelift", 2728 + "url": "https://tidelift.com/funding/github/npm/browserslist" 2729 + }, 2730 + { 2731 + "type": "github", 2732 + "url": "https://github.com/sponsors/ai" 2733 + } 2734 + ], 2735 + "license": "MIT", 2736 + "dependencies": { 2737 + "escalade": "^3.2.0", 2738 + "picocolors": "^1.1.1" 2739 + }, 2740 + "bin": { 2741 + "update-browserslist-db": "cli.js" 2742 + }, 2743 + "peerDependencies": { 2744 + "browserslist": ">= 4.21.0" 2745 + } 2746 + }, 2747 + "node_modules/uri-js": { 2748 + "version": "4.4.1", 2749 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 2750 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 2751 + "dev": true, 2752 + "license": "BSD-2-Clause", 2753 + "dependencies": { 2754 + "punycode": "^2.1.0" 2755 + } 2756 + }, 2757 + "node_modules/vite": { 2758 + "version": "7.1.11", 2759 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", 2760 + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", 2761 + "dev": true, 2762 + "license": "MIT", 2763 + "dependencies": { 2764 + "esbuild": "^0.25.0", 2765 + "fdir": "^6.5.0", 2766 + "picomatch": "^4.0.3", 2767 + "postcss": "^8.5.6", 2768 + "rollup": "^4.43.0", 2769 + "tinyglobby": "^0.2.15" 2770 + }, 2771 + "bin": { 2772 + "vite": "bin/vite.js" 2773 + }, 2774 + "engines": { 2775 + "node": "^20.19.0 || >=22.12.0" 2776 + }, 2777 + "funding": { 2778 + "url": "https://github.com/vitejs/vite?sponsor=1" 2779 + }, 2780 + "optionalDependencies": { 2781 + "fsevents": "~2.3.3" 2782 + }, 2783 + "peerDependencies": { 2784 + "@types/node": "^20.19.0 || >=22.12.0", 2785 + "jiti": ">=1.21.0", 2786 + "less": "^4.0.0", 2787 + "lightningcss": "^1.21.0", 2788 + "sass": "^1.70.0", 2789 + "sass-embedded": "^1.70.0", 2790 + "stylus": ">=0.54.8", 2791 + "sugarss": "^5.0.0", 2792 + "terser": "^5.16.0", 2793 + "tsx": "^4.8.1", 2794 + "yaml": "^2.4.2" 2795 + }, 2796 + "peerDependenciesMeta": { 2797 + "@types/node": { 2798 + "optional": true 2799 + }, 2800 + "jiti": { 2801 + "optional": true 2802 + }, 2803 + "less": { 2804 + "optional": true 2805 + }, 2806 + "lightningcss": { 2807 + "optional": true 2808 + }, 2809 + "sass": { 2810 + "optional": true 2811 + }, 2812 + "sass-embedded": { 2813 + "optional": true 2814 + }, 2815 + "stylus": { 2816 + "optional": true 2817 + }, 2818 + "sugarss": { 2819 + "optional": true 2820 + }, 2821 + "terser": { 2822 + "optional": true 2823 + }, 2824 + "tsx": { 2825 + "optional": true 2826 + }, 2827 + "yaml": { 2828 + "optional": true 2829 + } 2830 + } 2831 + }, 2832 + "node_modules/which": { 2833 + "version": "2.0.2", 2834 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 2835 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 2836 + "dev": true, 2837 + "license": "ISC", 2838 + "dependencies": { 2839 + "isexe": "^2.0.0" 2840 + }, 2841 + "bin": { 2842 + "node-which": "bin/node-which" 2843 + }, 2844 + "engines": { 2845 + "node": ">= 8" 2846 + } 2847 + }, 2848 + "node_modules/word-wrap": { 2849 + "version": "1.2.5", 2850 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 2851 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 2852 + "dev": true, 2853 + "license": "MIT", 2854 + "engines": { 2855 + "node": ">=0.10.0" 2856 + } 2857 + }, 2858 + "node_modules/yallist": { 2859 + "version": "3.1.1", 2860 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 2861 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 2862 + "dev": true, 2863 + "license": "ISC" 2864 + }, 2865 + "node_modules/yocto-queue": { 2866 + "version": "0.1.0", 2867 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 2868 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 2869 + "dev": true, 2870 + "license": "MIT", 2871 + "engines": { 2872 + "node": ">=10" 2873 + }, 2874 + "funding": { 2875 + "url": "https://github.com/sponsors/sindresorhus" 2876 + } 2877 + } 2878 + } 2879 + }
+28
client/package.json
··· 1 + { 2 + "name": "client", 3 + "private": true, 4 + "version": "0.0.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "vite build", 9 + "lint": "eslint .", 10 + "preview": "vite preview" 11 + }, 12 + "dependencies": { 13 + "react": "^19.1.1", 14 + "react-dom": "^19.1.1", 15 + "react-router-dom": "^7.9.4" 16 + }, 17 + "devDependencies": { 18 + "@eslint/js": "^9.36.0", 19 + "@types/react": "^19.1.16", 20 + "@types/react-dom": "^19.1.9", 21 + "@vitejs/plugin-react": "^5.0.4", 22 + "eslint": "^9.36.0", 23 + "eslint-plugin-react-hooks": "^5.2.0", 24 + "eslint-plugin-react-refresh": "^0.4.22", 25 + "globals": "^16.4.0", 26 + "vite": "^7.1.7" 27 + } 28 + }
+1
client/public/vite.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
+60
client/src/App.css
··· 1 + * { 2 + margin: 0; 3 + padding: 0; 4 + box-sizing: border-box; 5 + } 6 + 7 + :root { 8 + --primary-color: #1185FE; 9 + --primary-hover: #0F6FD7; 10 + --primary-light: #E8F3FF; 11 + --sidebar-bg: #FAFAFA; 12 + --text-primary: #2D2D2D; 13 + --text-secondary: #6B6B6B; 14 + --text-light: #9B9B9B; 15 + --border-color: #E8E8E8; 16 + --white: #FFFFFF; 17 + --hover-bg: #F5F5F5; 18 + --bluesky-blue: #1185FE; 19 + } 20 + 21 + body { 22 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 23 + background: #F7F7F7; 24 + color: var(--text-primary); 25 + font-size: 14px; 26 + line-height: 1.5; 27 + } 28 + 29 + #root { 30 + margin: 0; 31 + padding: 0; 32 + text-align: left; 33 + } 34 + 35 + /* Spinner animation */ 36 + @keyframes spin { 37 + to { transform: rotate(360deg); } 38 + } 39 + 40 + .spinner { 41 + display: inline-block; 42 + width: 16px; 43 + height: 16px; 44 + border: 2px solid rgba(255, 255, 255, 0.3); 45 + border-radius: 50%; 46 + border-top-color: white; 47 + animation: spin 0.8s linear infinite; 48 + } 49 + 50 + /* Fade in animation */ 51 + @keyframes fadeIn { 52 + from { 53 + opacity: 0; 54 + transform: translateY(10px); 55 + } 56 + to { 57 + opacity: 1; 58 + transform: translateY(0); 59 + } 60 + }
+25
client/src/App.jsx
··· 1 + import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; 2 + import Layout from './components/Layout'; 3 + import Dashboard from './pages/Dashboard'; 4 + import MyPodcasts from './pages/MyPodcasts'; 5 + import Episodes from './pages/Episodes'; 6 + import Settings from './pages/Settings'; 7 + import './App.css'; 8 + 9 + function App() { 10 + return ( 11 + <Router> 12 + <Layout> 13 + <Routes> 14 + <Route path="/" element={<Dashboard />} /> 15 + <Route path="/podcasts" element={<MyPodcasts />} /> 16 + <Route path="/episodes" element={<Episodes />} /> 17 + <Route path="/settings" element={<Settings />} /> 18 + <Route path="*" element={<Navigate to="/" replace />} /> 19 + </Routes> 20 + </Layout> 21 + </Router> 22 + ); 23 + } 24 + 25 + export default App;
+1
client/src/assets/react.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
+51
client/src/components/EpisodeCard.css
··· 1 + .episode-item { 2 + background: white; 3 + padding: 1.5rem; 4 + border-radius: 8px; 5 + border: 1px solid var(--border-color); 6 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 7 + transition: all 0.2s; 8 + } 9 + 10 + .episode-item:hover { 11 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 12 + } 13 + 14 + .episode-header { 15 + display: flex; 16 + justify-content: space-between; 17 + align-items: start; 18 + margin-bottom: 0.75rem; 19 + } 20 + 21 + .episode-title { 22 + font-size: 18px; 23 + font-weight: 600; 24 + color: var(--text-primary); 25 + margin-bottom: 0.25rem; 26 + } 27 + 28 + .episode-date { 29 + color: var(--text-light); 30 + font-size: 13px; 31 + } 32 + 33 + .episode-description { 34 + color: var(--text-secondary); 35 + margin-bottom: 1rem; 36 + line-height: 1.6; 37 + } 38 + 39 + .episode-meta { 40 + display: flex; 41 + gap: 1.5rem; 42 + font-size: 13px; 43 + color: var(--text-light); 44 + margin-bottom: 1rem; 45 + } 46 + 47 + .audio-player { 48 + width: 100%; 49 + margin-top: 1rem; 50 + border-radius: 6px; 51 + }
+31
client/src/components/EpisodeCard.jsx
··· 1 + import { formatDate, formatFileSize } from '../services/api'; 2 + import './EpisodeCard.css'; 3 + 4 + const EpisodeCard = ({ episode }) => { 5 + const date = formatDate(episode.pubDate); 6 + const size = formatFileSize(episode.blob.size); 7 + 8 + return ( 9 + <div className="episode-item"> 10 + <div className="episode-header"> 11 + <div> 12 + <h3 className="episode-title">{episode.title}</h3> 13 + <div className="episode-date">{date}</div> 14 + </div> 15 + </div> 16 + {episode.description && ( 17 + <p className="episode-description">{episode.description}</p> 18 + )} 19 + <div className="episode-meta"> 20 + <span>📦 {size}</span> 21 + <span>🎵 {episode.blob.mimeType}</span> 22 + </div> 23 + <audio className="audio-player" controls preload="metadata"> 24 + <source src={episode.streamUrl} type={episode.blob.mimeType} /> 25 + Your browser does not support the audio element. 26 + </audio> 27 + </div> 28 + ); 29 + }; 30 + 31 + export default EpisodeCard;
+127
client/src/components/Header.css
··· 1 + .top-header { 2 + background: #1E2240; 3 + color: white; 4 + padding: 0 1.5rem; 5 + height: 56px; 6 + display: flex; 7 + align-items: center; 8 + justify-content: space-between; 9 + position: fixed; 10 + top: 0; 11 + left: 0; 12 + right: 0; 13 + z-index: 100; 14 + border-bottom: 1px solid rgba(255, 255, 255, 0.1); 15 + } 16 + 17 + .header-left { 18 + display: flex; 19 + align-items: center; 20 + gap: 2rem; 21 + } 22 + 23 + .logo { 24 + display: flex; 25 + align-items: center; 26 + gap: 0.75rem; 27 + font-weight: 600; 28 + font-size: 16px; 29 + } 30 + 31 + .logo-text { 32 + color: white; 33 + } 34 + 35 + .header-right { 36 + display: flex; 37 + align-items: center; 38 + gap: 1rem; 39 + } 40 + 41 + .search-container { 42 + position: relative; 43 + width: 300px; 44 + } 45 + 46 + .search-input { 47 + width: 100%; 48 + padding: 0.5rem 2.5rem 0.5rem 1rem; 49 + border: 1px solid rgba(255, 255, 255, 0.2); 50 + border-radius: 6px; 51 + background: rgba(255, 255, 255, 0.1); 52 + color: white; 53 + font-size: 14px; 54 + transition: all 0.2s; 55 + } 56 + 57 + .search-input::placeholder { 58 + color: rgba(255, 255, 255, 0.5); 59 + } 60 + 61 + .search-input:focus { 62 + outline: none; 63 + background: rgba(255, 255, 255, 0.15); 64 + border-color: rgba(255, 255, 255, 0.3); 65 + } 66 + 67 + .search-shortcut { 68 + position: absolute; 69 + right: 0.75rem; 70 + top: 50%; 71 + transform: translateY(-50%); 72 + color: rgba(255, 255, 255, 0.5); 73 + font-size: 12px; 74 + } 75 + 76 + .header-btn { 77 + display: flex; 78 + align-items: center; 79 + gap: 0.5rem; 80 + padding: 0.5rem 1rem; 81 + background: transparent; 82 + border: 1px solid rgba(255, 255, 255, 0.2); 83 + border-radius: 6px; 84 + color: white; 85 + cursor: pointer; 86 + font-size: 14px; 87 + transition: all 0.2s; 88 + } 89 + 90 + .header-btn:hover { 91 + background: rgba(255, 255, 255, 0.1); 92 + } 93 + 94 + .user-avatar { 95 + width: 32px; 96 + height: 32px; 97 + border-radius: 50%; 98 + background: var(--primary-color); 99 + color: white; 100 + border: none; 101 + cursor: pointer; 102 + font-weight: 600; 103 + display: flex; 104 + align-items: center; 105 + justify-content: center; 106 + } 107 + 108 + @media (max-width: 1024px) { 109 + .search-container { 110 + width: 200px; 111 + } 112 + } 113 + 114 + @media (max-width: 768px) { 115 + .search-container { 116 + display: none; 117 + } 118 + 119 + .header-btn { 120 + font-size: 0; 121 + padding: 0.5rem; 122 + } 123 + 124 + .header-btn .icon { 125 + font-size: 18px; 126 + } 127 + }
+30
client/src/components/Header.jsx
··· 1 + import './Header.css'; 2 + 3 + const Header = () => { 4 + return ( 5 + <header className="top-header"> 6 + <div className="header-left"> 7 + <div className="logo"> 8 + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 9 + <circle cx="12" cy="12" r="10" fill="#1185FE"/> 10 + <path d="M12 6v12M8 10l4-4 4 4" stroke="white" strokeWidth="2" strokeLinecap="round"/> 11 + </svg> 12 + <span className="logo-text">AT Protocol</span> 13 + </div> 14 + </div> 15 + <div className="header-right"> 16 + <div className="search-container"> 17 + <input type="text" className="search-input" placeholder="Search for anything..." /> 18 + <span className="search-shortcut">⌘K</span> 19 + </div> 20 + <button className="header-btn"> 21 + <span className="icon">🎙️</span> 22 + My Podcasts 23 + </button> 24 + <button className="user-avatar">A</button> 25 + </div> 26 + </header> 27 + ); 28 + }; 29 + 30 + export default Header;
+26
client/src/components/Layout.css
··· 1 + .app-container { 2 + display: flex; 3 + margin-top: 56px; 4 + min-height: calc(100vh - 56px); 5 + } 6 + 7 + .main-content { 8 + flex: 1; 9 + margin-left: 240px; 10 + padding: 2rem; 11 + background: #F7F7F7; 12 + min-width: 0; 13 + } 14 + 15 + @media (max-width: 1024px) { 16 + .main-content { 17 + margin-left: 200px; 18 + } 19 + } 20 + 21 + @media (max-width: 768px) { 22 + .main-content { 23 + margin-left: 0; 24 + padding: 1rem; 25 + } 26 + }
+59
client/src/components/Layout.jsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import Header from './Header'; 3 + import Sidebar from './Sidebar'; 4 + import PodcastHeader from './PodcastHeader'; 5 + import UploadModal from './UploadModal'; 6 + import { api } from '../services/api'; 7 + import './Layout.css'; 8 + 9 + const Layout = ({ children }) => { 10 + const [podcastTitle, setPodcastTitle] = useState('My Podcast'); 11 + const [showUploadModal, setShowUploadModal] = useState(false); 12 + const [refreshKey, setRefreshKey] = useState(0); 13 + 14 + useEffect(() => { 15 + loadPodcastMetadata(); 16 + }, []); 17 + 18 + const loadPodcastMetadata = async () => { 19 + try { 20 + const data = await api.getFeedMetadata(); 21 + if (data.title) { 22 + setPodcastTitle(data.title); 23 + } 24 + } catch (error) { 25 + console.error('Error loading podcast metadata:', error); 26 + } 27 + }; 28 + 29 + const handleUploadSuccess = () => { 30 + setShowUploadModal(false); 31 + setRefreshKey(prev => prev + 1); 32 + }; 33 + 34 + return ( 35 + <> 36 + <Header /> 37 + <div className="app-container"> 38 + <Sidebar /> 39 + <main className="main-content"> 40 + <PodcastHeader 41 + title={podcastTitle} 42 + onCreateNew={() => setShowUploadModal(true)} 43 + /> 44 + <div key={refreshKey}> 45 + {children} 46 + </div> 47 + </main> 48 + </div> 49 + {showUploadModal && ( 50 + <UploadModal 51 + onClose={() => setShowUploadModal(false)} 52 + onSuccess={handleUploadSuccess} 53 + /> 54 + )} 55 + </> 56 + ); 57 + }; 58 + 59 + export default Layout;
+85
client/src/components/PodcastHeader.css
··· 1 + .podcast-header { 2 + background: white; 3 + padding: 1.5rem 2rem; 4 + border-radius: 8px; 5 + margin-bottom: 2rem; 6 + display: flex; 7 + align-items: center; 8 + justify-content: space-between; 9 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 10 + } 11 + 12 + .podcast-info { 13 + display: flex; 14 + align-items: center; 15 + gap: 1rem; 16 + } 17 + 18 + .podcast-avatar { 19 + width: 56px; 20 + height: 56px; 21 + border-radius: 8px; 22 + background: var(--primary-light); 23 + display: flex; 24 + align-items: center; 25 + justify-content: center; 26 + } 27 + 28 + .podcast-title { 29 + font-size: 24px; 30 + font-weight: 600; 31 + color: var(--text-primary); 32 + margin-bottom: 0.25rem; 33 + } 34 + 35 + .podcast-meta { 36 + display: flex; 37 + align-items: center; 38 + gap: 1rem; 39 + font-size: 13px; 40 + } 41 + 42 + .podcast-link { 43 + color: #5B8DEE; 44 + text-decoration: none; 45 + } 46 + 47 + .podcast-link:hover { 48 + text-decoration: underline; 49 + } 50 + 51 + .podcast-badge { 52 + padding: 0.25rem 0.5rem; 53 + background: #F0F0F0; 54 + border-radius: 4px; 55 + color: var(--text-secondary); 56 + font-size: 12px; 57 + } 58 + 59 + .btn-create { 60 + display: flex; 61 + align-items: center; 62 + gap: 0.5rem; 63 + padding: 0.75rem 1.5rem; 64 + background: var(--primary-color); 65 + color: white; 66 + border: none; 67 + border-radius: 6px; 68 + font-size: 14px; 69 + font-weight: 600; 70 + cursor: pointer; 71 + transition: all 0.2s; 72 + } 73 + 74 + .btn-create:hover { 75 + background: var(--primary-hover); 76 + transform: translateY(-1px); 77 + } 78 + 79 + @media (max-width: 768px) { 80 + .podcast-header { 81 + flex-direction: column; 82 + align-items: flex-start; 83 + gap: 1rem; 84 + } 85 + }
+31
client/src/components/PodcastHeader.jsx
··· 1 + import './PodcastHeader.css'; 2 + 3 + const PodcastHeader = ({ title, onCreateNew }) => { 4 + return ( 5 + <div className="podcast-header"> 6 + <div className="podcast-info"> 7 + <div className="podcast-avatar"> 8 + <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> 9 + <rect width="48" height="48" rx="8" fill="#1185FE" opacity="0.2"/> 10 + <circle cx="24" cy="24" r="12" fill="#1185FE"/> 11 + </svg> 12 + </div> 13 + <div> 14 + <h1 className="podcast-title">{title}</h1> 15 + <div className="podcast-meta"> 16 + <a href="#" className="podcast-link">Podcast Settings</a> 17 + <span className="podcast-badge">Private Podcast</span> 18 + </div> 19 + </div> 20 + </div> 21 + <button className="btn-create" onClick={onCreateNew}> 22 + <span>Create New</span> 23 + <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"> 24 + <path d="M4 6l4 4 4-4"></path> 25 + </svg> 26 + </button> 27 + </div> 28 + ); 29 + }; 30 + 31 + export default PodcastHeader;
+71
client/src/components/Sidebar.css
··· 1 + .sidebar { 2 + width: 240px; 3 + background: var(--sidebar-bg); 4 + border-right: 1px solid var(--border-color); 5 + padding: 1.5rem 0; 6 + position: fixed; 7 + left: 0; 8 + top: 56px; 9 + bottom: 0; 10 + overflow-y: auto; 11 + } 12 + 13 + .nav-section { 14 + margin-bottom: 2rem; 15 + } 16 + 17 + .nav-section-title { 18 + padding: 0 1.5rem; 19 + margin-bottom: 0.5rem; 20 + font-size: 11px; 21 + font-weight: 600; 22 + text-transform: uppercase; 23 + color: var(--text-light); 24 + letter-spacing: 0.5px; 25 + } 26 + 27 + .nav-item { 28 + display: flex; 29 + align-items: center; 30 + gap: 0.75rem; 31 + padding: 0.625rem 1.5rem; 32 + color: var(--text-secondary); 33 + text-decoration: none; 34 + font-size: 14px; 35 + transition: all 0.2s; 36 + cursor: pointer; 37 + } 38 + 39 + .nav-item:hover { 40 + background: var(--hover-bg); 41 + color: var(--text-primary); 42 + } 43 + 44 + .nav-item.active { 45 + background: var(--primary-light); 46 + color: var(--primary-color); 47 + font-weight: 600; 48 + } 49 + 50 + .nav-icon { 51 + width: 18px; 52 + height: 18px; 53 + flex-shrink: 0; 54 + } 55 + 56 + @media (max-width: 1024px) { 57 + .sidebar { 58 + width: 200px; 59 + } 60 + } 61 + 62 + @media (max-width: 768px) { 63 + .sidebar { 64 + transform: translateX(-100%); 65 + transition: transform 0.3s; 66 + } 67 + 68 + .sidebar.open { 69 + transform: translateX(0); 70 + } 71 + }
+61
client/src/components/Sidebar.jsx
··· 1 + import { NavLink } from 'react-router-dom'; 2 + import './Sidebar.css'; 3 + 4 + const Sidebar = () => { 5 + return ( 6 + <aside className="sidebar"> 7 + <nav> 8 + <div className="nav-section"> 9 + <div className="nav-section-title">My Captivate</div> 10 + <NavLink to="/" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} end> 11 + <svg className="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 12 + <rect x="3" y="3" width="7" height="7"></rect> 13 + <rect x="14" y="3" width="7" height="7"></rect> 14 + <rect x="14" y="14" width="7" height="7"></rect> 15 + <rect x="3" y="14" width="7" height="7"></rect> 16 + </svg> 17 + <span>Dashboard</span> 18 + </NavLink> 19 + <NavLink to="/podcasts" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}> 20 + <svg className="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 21 + <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path> 22 + </svg> 23 + <span>My Podcasts</span> 24 + </NavLink> 25 + </div> 26 + 27 + <div className="nav-section"> 28 + <div className="nav-section-title">Content Management</div> 29 + <NavLink to="/episodes" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}> 30 + <svg className="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 31 + <polygon points="5 3 19 12 5 21 5 3"></polygon> 32 + </svg> 33 + <span>Episodes</span> 34 + </NavLink> 35 + <a href="#" className="nav-item"> 36 + <svg className="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 37 + <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> 38 + <circle cx="9" cy="7" r="4"></circle> 39 + <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> 40 + <path d="M16 3.13a4 4 0 0 1 0 7.75"></path> 41 + </svg> 42 + <span>Subscribers</span> 43 + </a> 44 + </div> 45 + 46 + <div className="nav-section"> 47 + <div className="nav-section-title">Settings</div> 48 + <NavLink to="/settings" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}> 49 + <svg className="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 50 + <circle cx="12" cy="12" r="3"></circle> 51 + <path d="M12 1v6m0 6v6M1 12h6m6 0h6"></path> 52 + </svg> 53 + <span>Podcast Settings</span> 54 + </NavLink> 55 + </div> 56 + </nav> 57 + </aside> 58 + ); 59 + }; 60 + 61 + export default Sidebar;
+169
client/src/components/UploadModal.css
··· 1 + .modal { 2 + position: fixed; 3 + top: 0; 4 + left: 0; 5 + right: 0; 6 + bottom: 0; 7 + background: rgba(0, 0, 0, 0.5); 8 + display: flex; 9 + align-items: center; 10 + justify-content: center; 11 + z-index: 1000; 12 + animation: fadeIn 0.2s ease-in; 13 + } 14 + 15 + .modal-content { 16 + background: white; 17 + border-radius: 8px; 18 + width: 90%; 19 + max-width: 600px; 20 + max-height: 90vh; 21 + overflow-y: auto; 22 + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); 23 + } 24 + 25 + .modal-header { 26 + display: flex; 27 + align-items: center; 28 + justify-content: space-between; 29 + padding: 1.5rem 2rem; 30 + border-bottom: 1px solid var(--border-color); 31 + } 32 + 33 + .modal-header h2 { 34 + font-size: 20px; 35 + font-weight: 600; 36 + color: var(--text-primary); 37 + } 38 + 39 + .modal-close { 40 + background: transparent; 41 + border: none; 42 + font-size: 28px; 43 + color: var(--text-light); 44 + cursor: pointer; 45 + width: 32px; 46 + height: 32px; 47 + display: flex; 48 + align-items: center; 49 + justify-content: center; 50 + border-radius: 4px; 51 + transition: all 0.2s; 52 + } 53 + 54 + .modal-close:hover { 55 + background: var(--hover-bg); 56 + color: var(--text-primary); 57 + } 58 + 59 + .modal-content form { 60 + padding: 2rem; 61 + } 62 + 63 + .modal-footer { 64 + display: flex; 65 + align-items: center; 66 + justify-content: flex-end; 67 + gap: 1rem; 68 + padding-top: 1.5rem; 69 + border-top: 1px solid var(--border-color); 70 + margin-top: 1.5rem; 71 + } 72 + 73 + .form-group { 74 + margin-bottom: 1.5rem; 75 + } 76 + 77 + .form-group label { 78 + display: block; 79 + margin-bottom: 0.5rem; 80 + font-weight: 500; 81 + color: var(--text-primary); 82 + font-size: 14px; 83 + } 84 + 85 + .form-group input[type="text"], 86 + .form-group input[type="file"], 87 + .form-group textarea { 88 + width: 100%; 89 + padding: 0.75rem; 90 + border: 1px solid var(--border-color); 91 + border-radius: 6px; 92 + font-size: 14px; 93 + font-family: inherit; 94 + transition: all 0.2s; 95 + background: white; 96 + } 97 + 98 + .form-group input:focus, 99 + .form-group textarea:focus { 100 + outline: none; 101 + border-color: var(--primary-color); 102 + box-shadow: 0 0 0 3px rgba(199, 91, 155, 0.1); 103 + } 104 + 105 + .form-group small { 106 + display: block; 107 + margin-top: 0.5rem; 108 + color: var(--text-light); 109 + font-size: 13px; 110 + } 111 + 112 + .btn-primary { 113 + padding: 0.75rem 1.5rem; 114 + background: var(--primary-color); 115 + color: white; 116 + border: none; 117 + border-radius: 6px; 118 + font-size: 14px; 119 + font-weight: 600; 120 + cursor: pointer; 121 + transition: all 0.2s; 122 + display: inline-flex; 123 + align-items: center; 124 + gap: 0.5rem; 125 + } 126 + 127 + .btn-primary:hover { 128 + background: var(--primary-hover); 129 + } 130 + 131 + .btn-primary:disabled { 132 + opacity: 0.6; 133 + cursor: not-allowed; 134 + } 135 + 136 + .btn-secondary { 137 + padding: 0.75rem 1.5rem; 138 + background: #F0F0F0; 139 + color: var(--text-primary); 140 + border: none; 141 + border-radius: 6px; 142 + font-size: 14px; 143 + font-weight: 600; 144 + cursor: pointer; 145 + transition: all 0.2s; 146 + } 147 + 148 + .btn-secondary:hover { 149 + background: #E0E0E0; 150 + } 151 + 152 + .status-message { 153 + margin-top: 1rem; 154 + padding: 1rem; 155 + border-radius: 6px; 156 + font-size: 14px; 157 + } 158 + 159 + .status-message.success { 160 + background: #D4EDDA; 161 + color: #155724; 162 + border: 1px solid #C3E6CB; 163 + } 164 + 165 + .status-message.error { 166 + background: #F8D7DA; 167 + color: #721C24; 168 + border: 1px solid #F5C6CB; 169 + }
+110
client/src/components/UploadModal.jsx
··· 1 + import { useState } from 'react'; 2 + import { api } from '../services/api'; 3 + import './UploadModal.css'; 4 + 5 + const UploadModal = ({ onClose, onSuccess }) => { 6 + const [file, setFile] = useState(null); 7 + const [title, setTitle] = useState(''); 8 + const [description, setDescription] = useState(''); 9 + const [loading, setLoading] = useState(false); 10 + const [error, setError] = useState(''); 11 + const [success, setSuccess] = useState(''); 12 + 13 + const handleSubmit = async (e) => { 14 + e.preventDefault(); 15 + 16 + if (!file) { 17 + setError('Please select an audio file'); 18 + return; 19 + } 20 + 21 + setLoading(true); 22 + setError(''); 23 + setSuccess(''); 24 + 25 + const formData = new FormData(); 26 + formData.append('audio', file); 27 + formData.append('title', title); 28 + formData.append('description', description); 29 + 30 + try { 31 + await api.uploadEpisode(formData); 32 + setSuccess(`Episode "${title}" uploaded successfully!`); 33 + setTimeout(() => { 34 + onSuccess(); 35 + }, 1500); 36 + } catch (err) { 37 + setError(err.message); 38 + } finally { 39 + setLoading(false); 40 + } 41 + }; 42 + 43 + return ( 44 + <div className="modal" onClick={onClose}> 45 + <div className="modal-content" onClick={(e) => e.stopPropagation()}> 46 + <div className="modal-header"> 47 + <h2>Upload Episode</h2> 48 + <button className="modal-close" onClick={onClose}>&times;</button> 49 + </div> 50 + <form onSubmit={handleSubmit}> 51 + <div className="form-group"> 52 + <label htmlFor="audioFile">Audio File *</label> 53 + <input 54 + type="file" 55 + id="audioFile" 56 + accept="audio/*" 57 + onChange={(e) => setFile(e.target.files[0])} 58 + required 59 + /> 60 + <small>Supported formats: MP3, M4A, WAV, etc.</small> 61 + </div> 62 + 63 + <div className="form-group"> 64 + <label htmlFor="title">Episode Title *</label> 65 + <input 66 + type="text" 67 + id="title" 68 + placeholder="My Awesome Episode" 69 + value={title} 70 + onChange={(e) => setTitle(e.target.value)} 71 + required 72 + /> 73 + </div> 74 + 75 + <div className="form-group"> 76 + <label htmlFor="description">Description</label> 77 + <textarea 78 + id="description" 79 + rows="4" 80 + placeholder="Episode description..." 81 + value={description} 82 + onChange={(e) => setDescription(e.target.value)} 83 + ></textarea> 84 + </div> 85 + 86 + {error && <div className="status-message error">{error}</div>} 87 + {success && <div className="status-message success">{success}</div>} 88 + 89 + <div className="modal-footer"> 90 + <button type="button" className="btn-secondary" onClick={onClose}> 91 + Cancel 92 + </button> 93 + <button type="submit" className="btn-primary" disabled={loading}> 94 + {loading ? ( 95 + <> 96 + <span className="spinner"></span> 97 + Uploading... 98 + </> 99 + ) : ( 100 + 'Upload Episode' 101 + )} 102 + </button> 103 + </div> 104 + </form> 105 + </div> 106 + </div> 107 + ); 108 + }; 109 + 110 + export default UploadModal;
+68
client/src/index.css
··· 1 + :root { 2 + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 + line-height: 1.5; 4 + font-weight: 400; 5 + 6 + color-scheme: light dark; 7 + color: rgba(255, 255, 255, 0.87); 8 + background-color: #242424; 9 + 10 + font-synthesis: none; 11 + text-rendering: optimizeLegibility; 12 + -webkit-font-smoothing: antialiased; 13 + -moz-osx-font-smoothing: grayscale; 14 + } 15 + 16 + a { 17 + font-weight: 500; 18 + color: #646cff; 19 + text-decoration: inherit; 20 + } 21 + a:hover { 22 + color: #535bf2; 23 + } 24 + 25 + body { 26 + margin: 0; 27 + display: flex; 28 + place-items: center; 29 + min-width: 320px; 30 + min-height: 100vh; 31 + } 32 + 33 + h1 { 34 + font-size: 3.2em; 35 + line-height: 1.1; 36 + } 37 + 38 + button { 39 + border-radius: 8px; 40 + border: 1px solid transparent; 41 + padding: 0.6em 1.2em; 42 + font-size: 1em; 43 + font-weight: 500; 44 + font-family: inherit; 45 + background-color: #1a1a1a; 46 + cursor: pointer; 47 + transition: border-color 0.25s; 48 + } 49 + button:hover { 50 + border-color: #646cff; 51 + } 52 + button:focus, 53 + button:focus-visible { 54 + outline: 4px auto -webkit-focus-ring-color; 55 + } 56 + 57 + @media (prefers-color-scheme: light) { 58 + :root { 59 + color: #213547; 60 + background-color: #ffffff; 61 + } 62 + a:hover { 63 + color: #747bff; 64 + } 65 + button { 66 + background-color: #f9f9f9; 67 + } 68 + }
+10
client/src/main.jsx
··· 1 + import { StrictMode } from 'react' 2 + import { createRoot } from 'react-dom/client' 3 + import './index.css' 4 + import App from './App.jsx' 5 + 6 + createRoot(document.getElementById('root')).render( 7 + <StrictMode> 8 + <App /> 9 + </StrictMode>, 10 + )
+150
client/src/pages/Dashboard.css
··· 1 + .view-content { 2 + animation: fadeIn 0.3s ease-in; 3 + width: 100%; 4 + } 5 + 6 + .section-title { 7 + font-size: 28px; 8 + font-weight: 600; 9 + color: var(--text-primary); 10 + margin-bottom: 2rem; 11 + } 12 + 13 + .section-subtitle { 14 + display: flex; 15 + align-items: center; 16 + gap: 0.5rem; 17 + font-size: 16px; 18 + font-weight: 600; 19 + color: var(--text-primary); 20 + margin-bottom: 1rem; 21 + } 22 + 23 + /* Analytics Summary */ 24 + .analytics-summary { 25 + background: white; 26 + padding: 2rem; 27 + border-radius: 8px; 28 + margin-bottom: 2rem; 29 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 30 + } 31 + 32 + .metrics-label { 33 + font-size: 14px; 34 + font-weight: 600; 35 + color: var(--text-primary); 36 + margin-bottom: 1rem; 37 + } 38 + 39 + .metrics-grid { 40 + display: grid; 41 + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 42 + gap: 1rem; 43 + margin-bottom: 1.5rem; 44 + } 45 + 46 + .metric-card { 47 + padding: 1rem; 48 + border: 1px solid var(--border-color); 49 + border-radius: 6px; 50 + background: white; 51 + } 52 + 53 + .metric-label { 54 + font-size: 13px; 55 + color: var(--text-secondary); 56 + margin-bottom: 0.5rem; 57 + } 58 + 59 + .metric-value { 60 + font-size: 28px; 61 + font-weight: 600; 62 + color: var(--primary-color); 63 + } 64 + 65 + .analytics-footer { 66 + display: flex; 67 + align-items: center; 68 + justify-content: space-between; 69 + padding-top: 1rem; 70 + border-top: 1px solid var(--border-color); 71 + } 72 + 73 + .update-time { 74 + font-size: 13px; 75 + color: var(--text-light); 76 + } 77 + 78 + .analytics-link { 79 + font-size: 14px; 80 + color: #5B8DEE; 81 + text-decoration: none; 82 + font-weight: 500; 83 + } 84 + 85 + .analytics-link:hover { 86 + text-decoration: underline; 87 + } 88 + 89 + /* Latest Section */ 90 + .latest-section { 91 + background: white; 92 + padding: 2rem; 93 + border-radius: 8px; 94 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 95 + } 96 + 97 + .empty-state-banner { 98 + background: var(--primary-light); 99 + padding: 2rem; 100 + border-radius: 8px; 101 + margin-top: 1rem; 102 + } 103 + 104 + .empty-banner-content h4 { 105 + font-size: 18px; 106 + font-weight: 600; 107 + color: var(--text-primary); 108 + margin-bottom: 0.5rem; 109 + } 110 + 111 + .empty-banner-content p { 112 + color: var(--text-secondary); 113 + margin-bottom: 1.5rem; 114 + line-height: 1.6; 115 + } 116 + 117 + .btn-action { 118 + padding: 0.75rem 1.5rem; 119 + background: var(--primary-color); 120 + color: white; 121 + border: none; 122 + border-radius: 6px; 123 + font-size: 14px; 124 + font-weight: 600; 125 + cursor: pointer; 126 + transition: all 0.2s; 127 + } 128 + 129 + .btn-action:hover { 130 + background: var(--primary-hover); 131 + } 132 + 133 + .episodes-list { 134 + display: flex; 135 + flex-direction: column; 136 + gap: 1rem; 137 + margin-top: 1rem; 138 + } 139 + 140 + .loading { 141 + text-align: center; 142 + color: var(--text-light); 143 + padding: 3rem; 144 + } 145 + 146 + @media (max-width: 768px) { 147 + .metrics-grid { 148 + grid-template-columns: repeat(2, 1fr); 149 + } 150 + }
+115
client/src/pages/Dashboard.jsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import { useNavigate } from 'react-router-dom'; 3 + import { api } from '../services/api'; 4 + import EpisodeCard from '../components/EpisodeCard'; 5 + import './Dashboard.css'; 6 + 7 + const Dashboard = () => { 8 + const [episodes, setEpisodes] = useState([]); 9 + const [loading, setLoading] = useState(true); 10 + const navigate = useNavigate(); 11 + 12 + useEffect(() => { 13 + loadEpisodes(); 14 + }, []); 15 + 16 + const loadEpisodes = async () => { 17 + try { 18 + const data = await api.getEpisodes(); 19 + setEpisodes(data.episodes || []); 20 + } catch (error) { 21 + console.error('Error loading episodes:', error); 22 + } finally { 23 + setLoading(false); 24 + } 25 + }; 26 + 27 + const latestEpisodes = episodes.slice(0, 3); 28 + const hasEpisodes = episodes.length > 0; 29 + 30 + return ( 31 + <div className="view-content"> 32 + <h2 className="section-title">Dashboard</h2> 33 + 34 + {/* Analytics Summary */} 35 + <section className="analytics-summary"> 36 + <div className="section-header"> 37 + <h3 className="section-subtitle"> 38 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 39 + <line x1="12" y1="20" x2="12" y2="10"></line> 40 + <line x1="18" y1="20" x2="18" y2="4"></line> 41 + <line x1="6" y1="20" x2="6" y2="16"></line> 42 + </svg> 43 + Analytics Summary 44 + </h3> 45 + </div> 46 + 47 + <div className="metrics-label">Downloads</div> 48 + <div className="metrics-grid"> 49 + <div className="metric-card"> 50 + <div className="metric-label">Today</div> 51 + <div className="metric-value">0</div> 52 + </div> 53 + <div className="metric-card"> 54 + <div className="metric-label">Yesterday</div> 55 + <div className="metric-value">0</div> 56 + </div> 57 + <div className="metric-card"> 58 + <div className="metric-label">Last 7 days</div> 59 + <div className="metric-value">0</div> 60 + </div> 61 + <div className="metric-card"> 62 + <div className="metric-label">Last 28 days</div> 63 + <div className="metric-value">0</div> 64 + </div> 65 + <div className="metric-card"> 66 + <div className="metric-label">Last 90 days</div> 67 + <div className="metric-value">0</div> 68 + </div> 69 + <div className="metric-card"> 70 + <div className="metric-label">All-time</div> 71 + <div className="metric-value">0</div> 72 + </div> 73 + </div> 74 + 75 + <div className="analytics-footer"> 76 + <span className="update-time">Last updated: Just now</span> 77 + <a href="#" className="analytics-link">Go to Full Analytics →</a> 78 + </div> 79 + </section> 80 + 81 + {/* Latest and Upcoming */} 82 + <section className="latest-section"> 83 + <h3 className="section-subtitle"> 84 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 85 + <circle cx="12" cy="12" r="10"></circle> 86 + <polyline points="12 6 12 12 16 14"></polyline> 87 + </svg> 88 + Latest and Upcoming 89 + </h3> 90 + 91 + {loading ? ( 92 + <p className="loading">Loading episodes...</p> 93 + ) : !hasEpisodes ? ( 94 + <div className="empty-state-banner"> 95 + <div className="empty-banner-content"> 96 + <h4>You haven't published an episode yet</h4> 97 + <p>You need to publish an episode on AT Protocol before you can start tracking the analytics, it's super easy too!</p> 98 + <button className="btn-action" onClick={() => navigate('/episodes')}> 99 + Create first episode 100 + </button> 101 + </div> 102 + </div> 103 + ) : ( 104 + <div className="episodes-list"> 105 + {latestEpisodes.map((episode, index) => ( 106 + <EpisodeCard key={index} episode={episode} /> 107 + ))} 108 + </div> 109 + )} 110 + </section> 111 + </div> 112 + ); 113 + }; 114 + 115 + export default Dashboard;
+301
client/src/pages/Episodes.css
··· 1 + .view-header { 2 + display: flex; 3 + align-items: center; 4 + justify-content: space-between; 5 + margin-bottom: 2rem; 6 + } 7 + 8 + /* Search and Filter Bar */ 9 + .search-filter-bar { 10 + display: flex; 11 + gap: 1rem; 12 + margin-bottom: 1.5rem; 13 + } 14 + 15 + .search-bar { 16 + position: relative; 17 + flex: 1; 18 + } 19 + 20 + .search-icon { 21 + position: absolute; 22 + left: 1rem; 23 + top: 50%; 24 + transform: translateY(-50%); 25 + width: 18px; 26 + height: 18px; 27 + color: var(--text-light); 28 + pointer-events: none; 29 + } 30 + 31 + .search-input { 32 + width: 100%; 33 + padding: 0.75rem 1rem 0.75rem 3rem; 34 + border: 1px solid #E5E5E5; 35 + border-radius: 6px; 36 + font-size: 0.9375rem; 37 + background: white; 38 + transition: all 0.2s; 39 + } 40 + 41 + .search-input:focus { 42 + outline: none; 43 + border-color: var(--primary-color); 44 + box-shadow: 0 0 0 3px rgba(17, 133, 254, 0.1); 45 + } 46 + 47 + .btn-filter { 48 + display: flex; 49 + align-items: center; 50 + gap: 0.5rem; 51 + padding: 0.75rem 1.25rem; 52 + border: 1px solid #E5E5E5; 53 + border-radius: 6px; 54 + background: white; 55 + color: var(--text-dark); 56 + font-size: 0.9375rem; 57 + font-weight: 500; 58 + cursor: pointer; 59 + transition: all 0.2s; 60 + } 61 + 62 + .btn-filter:hover { 63 + border-color: var(--primary-color); 64 + color: var(--primary-color); 65 + } 66 + 67 + .dropdown-icon { 68 + width: 16px; 69 + height: 16px; 70 + } 71 + 72 + /* Episodes Table */ 73 + .episodes-table { 74 + background: white; 75 + border-radius: 8px; 76 + overflow: hidden; 77 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 78 + } 79 + 80 + .table-header { 81 + display: grid; 82 + grid-template-columns: 40px 60px 1fr 150px 120px 180px; 83 + gap: 1rem; 84 + padding: 1rem 1.5rem; 85 + background: #FAFAFA; 86 + border-bottom: 1px solid #E5E5E5; 87 + font-size: 0.8125rem; 88 + font-weight: 600; 89 + color: var(--text-light); 90 + text-transform: uppercase; 91 + letter-spacing: 0.5px; 92 + } 93 + 94 + .episode-row { 95 + display: grid; 96 + grid-template-columns: 40px 60px 1fr 150px 120px 180px; 97 + gap: 1rem; 98 + padding: 1.25rem 1.5rem; 99 + border-bottom: 1px solid #F0F0F0; 100 + align-items: center; 101 + transition: all 0.2s; 102 + cursor: pointer; 103 + } 104 + 105 + .episode-row:hover { 106 + background: #FAFAFA; 107 + } 108 + 109 + .episode-row:last-child { 110 + border-bottom: none; 111 + } 112 + 113 + /* Column Styles */ 114 + .col-drag { 115 + display: flex; 116 + align-items: center; 117 + justify-content: center; 118 + } 119 + 120 + .drag-handle { 121 + background: none; 122 + border: none; 123 + color: #CCCCCC; 124 + cursor: grab; 125 + padding: 0.25rem; 126 + transition: color 0.2s; 127 + } 128 + 129 + .drag-handle:hover { 130 + color: var(--text-light); 131 + } 132 + 133 + .drag-handle:active { 134 + cursor: grabbing; 135 + } 136 + 137 + .col-number { 138 + display: flex; 139 + align-items: center; 140 + } 141 + 142 + .episode-number { 143 + font-size: 0.875rem; 144 + color: var(--text-light); 145 + font-weight: 500; 146 + } 147 + 148 + .col-episode { 149 + display: flex; 150 + align-items: center; 151 + } 152 + 153 + .episode-title { 154 + color: var(--text-dark); 155 + font-weight: 500; 156 + font-size: 0.9375rem; 157 + text-decoration: none; 158 + transition: color 0.2s; 159 + } 160 + 161 + .episode-title:hover { 162 + color: var(--primary-color); 163 + } 164 + 165 + .col-date { 166 + font-size: 0.875rem; 167 + color: var(--text-light); 168 + } 169 + 170 + .col-status { 171 + display: flex; 172 + align-items: center; 173 + } 174 + 175 + .status-badge { 176 + display: inline-flex; 177 + align-items: center; 178 + gap: 0.375rem; 179 + padding: 0.375rem 0.75rem; 180 + border-radius: 4px; 181 + font-size: 0.8125rem; 182 + font-weight: 500; 183 + } 184 + 185 + .status-badge.published { 186 + background: #E8F5E9; 187 + color: #2E7D32; 188 + } 189 + 190 + .status-icon { 191 + width: 14px; 192 + height: 14px; 193 + } 194 + 195 + .col-actions { 196 + display: flex; 197 + align-items: center; 198 + justify-content: flex-end; 199 + } 200 + 201 + .action-buttons { 202 + display: flex; 203 + gap: 0.5rem; 204 + } 205 + 206 + .action-btn { 207 + display: flex; 208 + align-items: center; 209 + justify-content: center; 210 + width: 36px; 211 + height: 36px; 212 + border: 1px solid #E5E5E5; 213 + border-radius: 6px; 214 + background: white; 215 + color: var(--text-light); 216 + cursor: pointer; 217 + transition: all 0.2s; 218 + } 219 + 220 + .action-btn:hover { 221 + border-color: var(--primary-color); 222 + color: var(--primary-color); 223 + background: var(--primary-light); 224 + } 225 + 226 + .action-btn.delete:hover { 227 + border-color: #DC3545; 228 + color: #DC3545; 229 + background: #FFEBEE; 230 + } 231 + 232 + .action-btn svg { 233 + width: 18px; 234 + height: 18px; 235 + } 236 + 237 + /* Empty State */ 238 + .empty-state { 239 + text-align: center; 240 + padding: 4rem 2rem; 241 + color: var(--text-light); 242 + background: white; 243 + border-radius: 8px; 244 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 245 + } 246 + 247 + .empty-state-icon { 248 + font-size: 4rem; 249 + margin-bottom: 1rem; 250 + opacity: 0.5; 251 + } 252 + 253 + /* Responsive Design */ 254 + @media (max-width: 1024px) { 255 + .table-header, 256 + .episode-row { 257 + grid-template-columns: 40px 50px 1fr 120px 100px 140px; 258 + gap: 0.75rem; 259 + padding: 1rem; 260 + } 261 + } 262 + 263 + @media (max-width: 768px) { 264 + .search-filter-bar { 265 + flex-direction: column; 266 + } 267 + 268 + .btn-filter { 269 + width: 100%; 270 + justify-content: center; 271 + } 272 + 273 + .table-header { 274 + display: none; 275 + } 276 + 277 + .episode-row { 278 + grid-template-columns: 1fr; 279 + gap: 0.75rem; 280 + padding: 1.25rem; 281 + } 282 + 283 + .col-drag, 284 + .col-number { 285 + display: none; 286 + } 287 + 288 + .col-episode { 289 + margin-bottom: 0.5rem; 290 + } 291 + 292 + .col-date, 293 + .col-status { 294 + font-size: 0.8125rem; 295 + } 296 + 297 + .col-actions { 298 + justify-content: flex-start; 299 + margin-top: 0.75rem; 300 + } 301 + }
+165
client/src/pages/Episodes.jsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import { api, formatDate } from '../services/api'; 3 + import UploadModal from '../components/UploadModal'; 4 + import './Episodes.css'; 5 + 6 + const Episodes = () => { 7 + const [episodes, setEpisodes] = useState([]); 8 + const [loading, setLoading] = useState(true); 9 + const [showUploadModal, setShowUploadModal] = useState(false); 10 + const [searchQuery, setSearchQuery] = useState(''); 11 + 12 + useEffect(() => { 13 + loadEpisodes(); 14 + }, []); 15 + 16 + const loadEpisodes = async () => { 17 + try { 18 + const data = await api.getEpisodes(); 19 + setEpisodes(data.episodes || []); 20 + } catch (error) { 21 + console.error('Error loading episodes:', error); 22 + } finally { 23 + setLoading(false); 24 + } 25 + }; 26 + 27 + const handleUploadSuccess = () => { 28 + setShowUploadModal(false); 29 + loadEpisodes(); 30 + }; 31 + 32 + const filteredEpisodes = episodes.filter(episode => 33 + episode.title.toLowerCase().includes(searchQuery.toLowerCase()) 34 + ); 35 + 36 + return ( 37 + <div className="view-content"> 38 + <h2 className="section-title">Episodes</h2> 39 + 40 + {loading ? ( 41 + <p className="loading">Loading episodes...</p> 42 + ) : ( 43 + <div className="episodes-section"> 44 + <div className="episodes-controls"> 45 + <div className="search-bar"> 46 + <svg className="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 47 + <circle cx="11" cy="11" r="8"></circle> 48 + <path d="m21 21-4.35-4.35"></path> 49 + </svg> 50 + <input 51 + type="text" 52 + className="search-input" 53 + placeholder="Search your episodes..." 54 + value={searchQuery} 55 + onChange={(e) => setSearchQuery(e.target.value)} 56 + /> 57 + </div> 58 + <button className="btn-filter"> 59 + Filter 60 + <svg width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="currentColor" strokeWidth="2"> 61 + <path d="M1 1.5L6 6.5L11 1.5"></path> 62 + </svg> 63 + </button> 64 + </div> 65 + 66 + {filteredEpisodes.length === 0 ? ( 67 + <div className="empty-state"> 68 + <div className="empty-state-icon">🎙️</div> 69 + <p>No episodes yet. Upload your first episode to get started!</p> 70 + </div> 71 + ) : ( 72 + <> 73 + <div className="episodes-table"> 74 + <div className="table-header"> 75 + <div className="col-drag"></div> 76 + <div className="col-number">#</div> 77 + <div className="col-episode">Episode</div> 78 + <div className="col-date">Date</div> 79 + <div className="col-status">Publish Status</div> 80 + <div className="col-actions"></div> 81 + </div> 82 + 83 + {filteredEpisodes.map((episode, index) => ( 84 + <div key={index} className="episode-row"> 85 + <div className="col-drag"> 86 + <button className="drag-handle"> 87 + <svg width="12" height="20" viewBox="0 0 12 20" fill="currentColor"> 88 + <circle cx="3" cy="4" r="1.5"></circle> 89 + <circle cx="9" cy="4" r="1.5"></circle> 90 + <circle cx="3" cy="10" r="1.5"></circle> 91 + <circle cx="9" cy="10" r="1.5"></circle> 92 + <circle cx="3" cy="16" r="1.5"></circle> 93 + <circle cx="9" cy="16" r="1.5"></circle> 94 + </svg> 95 + </button> 96 + </div> 97 + <div className="col-number"> 98 + <span className="episode-number">{episodes.length - index}</span> 99 + </div> 100 + <div className="col-episode"> 101 + <a href="#" className="episode-title">{episode.title}</a> 102 + </div> 103 + <div className="col-date"> 104 + <span className="episode-date">{formatDate(episode.pubDate)}</span> 105 + </div> 106 + <div className="col-status"> 107 + <span className="status-badge published"> 108 + <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2"> 109 + <circle cx="6" cy="6" r="5"></circle> 110 + <path d="M3 6l2 2 4-4"></path> 111 + </svg> 112 + Published 113 + </span> 114 + </div> 115 + <div className="col-actions"> 116 + <div className="action-buttons"> 117 + <button className="action-btn" title="Share"> 118 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 119 + <circle cx="18" cy="5" r="3"></circle> 120 + <circle cx="6" cy="12" r="3"></circle> 121 + <circle cx="18" cy="19" r="3"></circle> 122 + <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line> 123 + <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line> 124 + </svg> 125 + </button> 126 + <button className="action-btn" title="Statistics"> 127 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 128 + <line x1="12" y1="20" x2="12" y2="10"></line> 129 + <line x1="18" y1="20" x2="18" y2="4"></line> 130 + <line x1="6" y1="20" x2="6" y2="16"></line> 131 + </svg> 132 + </button> 133 + <button className="action-btn" title="Edit"> 134 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 135 + <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 136 + <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 137 + </svg> 138 + </button> 139 + <button className="action-btn delete" title="Delete"> 140 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 141 + <polyline points="3 6 5 6 21 6"></polyline> 142 + <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> 143 + </svg> 144 + </button> 145 + </div> 146 + </div> 147 + </div> 148 + ))} 149 + </div> 150 + </> 151 + )} 152 + </div> 153 + )} 154 + 155 + {showUploadModal && ( 156 + <UploadModal 157 + onClose={() => setShowUploadModal(false)} 158 + onSuccess={handleUploadSuccess} 159 + /> 160 + )} 161 + </div> 162 + ); 163 + }; 164 + 165 + export default Episodes;
+262
client/src/pages/MyPodcasts.css
··· 1 + .podcasts-header { 2 + display: flex; 3 + align-items: center; 4 + justify-content: space-between; 5 + margin-bottom: 2rem; 6 + } 7 + 8 + .btn-add-podcast { 9 + display: flex; 10 + align-items: center; 11 + gap: 0.5rem; 12 + padding: 0.75rem 1.5rem; 13 + background: var(--primary-color); 14 + color: white; 15 + border: none; 16 + border-radius: 6px; 17 + font-size: 14px; 18 + font-weight: 600; 19 + cursor: pointer; 20 + transition: all 0.2s; 21 + } 22 + 23 + .btn-add-podcast:hover { 24 + background: var(--primary-hover); 25 + transform: translateY(-1px); 26 + } 27 + 28 + .podcasts-section { 29 + background: white; 30 + border-radius: 8px; 31 + padding: 1.5rem; 32 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 33 + } 34 + 35 + .podcasts-controls { 36 + display: flex; 37 + justify-content: flex-end; 38 + margin-bottom: 1.5rem; 39 + } 40 + 41 + .sort-control { 42 + display: flex; 43 + align-items: center; 44 + gap: 0.75rem; 45 + } 46 + 47 + .sort-control label { 48 + font-size: 14px; 49 + font-weight: 500; 50 + color: var(--text-secondary); 51 + } 52 + 53 + .sort-select { 54 + padding: 0.5rem 2rem 0.5rem 0.75rem; 55 + border: 1px solid var(--border-color); 56 + border-radius: 6px; 57 + font-size: 14px; 58 + background: white; 59 + cursor: pointer; 60 + appearance: none; 61 + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%236B6B6B' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); 62 + background-repeat: no-repeat; 63 + background-position: right 0.75rem center; 64 + } 65 + 66 + .sort-select:focus { 67 + outline: none; 68 + border-color: var(--primary-color); 69 + } 70 + 71 + .podcasts-table { 72 + margin-bottom: 1.5rem; 73 + } 74 + 75 + .table-header { 76 + display: grid; 77 + grid-template-columns: 100px 1fr 200px; 78 + gap: 1.5rem; 79 + padding: 1rem 1.5rem; 80 + background: #F8F9FA; 81 + border-radius: 6px; 82 + font-size: 13px; 83 + font-weight: 600; 84 + color: var(--text-secondary); 85 + margin-bottom: 0.5rem; 86 + } 87 + 88 + .podcast-row { 89 + display: grid; 90 + grid-template-columns: 100px 1fr 200px; 91 + gap: 1.5rem; 92 + padding: 1.5rem; 93 + border: 1px solid var(--border-color); 94 + border-radius: 6px; 95 + align-items: center; 96 + transition: all 0.2s; 97 + cursor: pointer; 98 + } 99 + 100 + .podcast-row:hover { 101 + border-color: var(--primary-color); 102 + box-shadow: 0 2px 8px rgba(17, 133, 254, 0.1); 103 + } 104 + 105 + .col-cover { 106 + display: flex; 107 + align-items: center; 108 + } 109 + 110 + .podcast-cover { 111 + width: 64px; 112 + height: 64px; 113 + border-radius: 8px; 114 + overflow: hidden; 115 + background: var(--primary-light); 116 + } 117 + 118 + .podcast-cover img { 119 + width: 100%; 120 + height: 100%; 121 + object-fit: cover; 122 + } 123 + 124 + .col-name { 125 + display: flex; 126 + align-items: center; 127 + } 128 + 129 + .podcast-info { 130 + width: 100%; 131 + } 132 + 133 + .podcast-name { 134 + display: flex; 135 + align-items: center; 136 + gap: 0.5rem; 137 + font-size: 16px; 138 + font-weight: 600; 139 + color: var(--text-primary); 140 + margin-bottom: 0.25rem; 141 + } 142 + 143 + .private-icon { 144 + color: var(--text-light); 145 + flex-shrink: 0; 146 + } 147 + 148 + .podcast-meta { 149 + font-size: 13px; 150 + color: var(--text-secondary); 151 + } 152 + 153 + .col-actions { 154 + display: flex; 155 + justify-content: flex-end; 156 + } 157 + 158 + .btn-select { 159 + padding: 0.625rem 1.25rem; 160 + border: 2px solid var(--primary-color); 161 + background: white; 162 + color: var(--primary-color); 163 + border-radius: 6px; 164 + font-size: 14px; 165 + font-weight: 600; 166 + cursor: pointer; 167 + transition: all 0.2s; 168 + } 169 + 170 + .btn-select:hover { 171 + background: var(--primary-color); 172 + color: white; 173 + } 174 + 175 + .btn-select.selected { 176 + background: var(--primary-light); 177 + border-color: var(--primary-light); 178 + color: var(--primary-color); 179 + cursor: default; 180 + } 181 + 182 + .table-footer { 183 + display: flex; 184 + align-items: center; 185 + justify-content: space-between; 186 + padding: 1rem 1.5rem; 187 + border-top: 1px solid var(--border-color); 188 + } 189 + 190 + .showing-text { 191 + font-size: 13px; 192 + color: var(--text-secondary); 193 + } 194 + 195 + .pagination { 196 + display: flex; 197 + gap: 0.5rem; 198 + } 199 + 200 + .page-btn { 201 + width: 32px; 202 + height: 32px; 203 + display: flex; 204 + align-items: center; 205 + justify-content: center; 206 + border: 1px solid var(--border-color); 207 + background: white; 208 + border-radius: 4px; 209 + font-size: 14px; 210 + font-weight: 500; 211 + color: var(--text-secondary); 212 + cursor: pointer; 213 + transition: all 0.2s; 214 + } 215 + 216 + .page-btn:hover { 217 + border-color: var(--primary-color); 218 + color: var(--primary-color); 219 + } 220 + 221 + .page-btn.active { 222 + background: var(--primary-color); 223 + border-color: var(--primary-color); 224 + color: white; 225 + } 226 + 227 + @media (max-width: 768px) { 228 + .podcasts-header { 229 + flex-direction: column; 230 + align-items: stretch; 231 + gap: 1rem; 232 + } 233 + 234 + .btn-add-podcast { 235 + width: 100%; 236 + justify-content: center; 237 + } 238 + 239 + .table-header { 240 + display: none; 241 + } 242 + 243 + .podcast-row { 244 + grid-template-columns: 64px 1fr; 245 + gap: 1rem; 246 + } 247 + 248 + .col-actions { 249 + grid-column: 1 / -1; 250 + justify-content: stretch; 251 + margin-top: 1rem; 252 + } 253 + 254 + .btn-select { 255 + width: 100%; 256 + } 257 + 258 + .table-footer { 259 + flex-direction: column; 260 + gap: 1rem; 261 + } 262 + }
+100
client/src/pages/MyPodcasts.jsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import { api } from '../services/api'; 3 + import './MyPodcasts.css'; 4 + 5 + const MyPodcasts = () => { 6 + const [podcast, setPodcast] = useState(null); 7 + const [loading, setLoading] = useState(true); 8 + 9 + useEffect(() => { 10 + loadPodcast(); 11 + }, []); 12 + 13 + const loadPodcast = async () => { 14 + try { 15 + const data = await api.getFeedMetadata(); 16 + setPodcast(data); 17 + } catch (error) { 18 + console.error('Error loading podcast:', error); 19 + } finally { 20 + setLoading(false); 21 + } 22 + }; 23 + 24 + return ( 25 + <div className="view-content"> 26 + <div className="podcasts-header"> 27 + <h2 className="section-title">My Podcasts</h2> 28 + <button className="btn-add-podcast"> 29 + <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"> 30 + <path d="M8 2v12M2 8h12"></path> 31 + </svg> 32 + Add New Podcast 33 + </button> 34 + </div> 35 + 36 + {loading ? ( 37 + <p className="loading">Loading podcasts...</p> 38 + ) : ( 39 + <div className="podcasts-section"> 40 + <div className="podcasts-controls"> 41 + <div className="sort-control"> 42 + <label>Sort By:</label> 43 + <select className="sort-select"> 44 + <option>Podcast Creation Date</option> 45 + <option>Podcast Name</option> 46 + <option>Last Updated</option> 47 + </select> 48 + </div> 49 + </div> 50 + 51 + <div className="podcasts-table"> 52 + <div className="table-header"> 53 + <div className="col-cover">Cover</div> 54 + <div className="col-name">Podcast name</div> 55 + <div className="col-actions"></div> 56 + </div> 57 + 58 + <div className="podcast-row"> 59 + <div className="col-cover"> 60 + <div className="podcast-cover"> 61 + <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> 62 + <rect width="64" height="64" rx="8" fill="#1185FE" opacity="0.2"/> 63 + <circle cx="32" cy="32" r="20" fill="#1185FE"/> 64 + <path d="M32 20v24M24 28l8-8 8 8" stroke="white" strokeWidth="3" strokeLinecap="round"/> 65 + </svg> 66 + </div> 67 + </div> 68 + <div className="col-name"> 69 + <div className="podcast-info"> 70 + <div className="podcast-name"> 71 + {podcast?.title || 'My Podcast'} 72 + <svg className="private-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"> 73 + <rect x="3" y="7" width="10" height="7" rx="1"></rect> 74 + <path d="M5 7V5a3 3 0 0 1 6 0v2"></path> 75 + </svg> 76 + </div> 77 + <div className="podcast-meta"> 78 + Created On: {new Date().toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' })} 79 + </div> 80 + </div> 81 + </div> 82 + <div className="col-actions"> 83 + <button className="btn-select selected">Already Selected</button> 84 + </div> 85 + </div> 86 + </div> 87 + 88 + <div className="table-footer"> 89 + <span className="showing-text">Showing 1 to 1 of 1</span> 90 + <div className="pagination"> 91 + <button className="page-btn active">1</button> 92 + </div> 93 + </div> 94 + </div> 95 + )} 96 + </div> 97 + ); 98 + }; 99 + 100 + export default MyPodcasts;
+217
client/src/pages/Settings.css
··· 1 + .settings-card { 2 + background: white; 3 + padding: 2rem; 4 + border-radius: 8px; 5 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 6 + margin-bottom: 2rem; 7 + } 8 + 9 + .settings-card h3 { 10 + font-size: 18px; 11 + font-weight: 600; 12 + color: var(--text-primary); 13 + margin-bottom: 1.5rem; 14 + } 15 + 16 + .settings-card h4 { 17 + font-size: 16px; 18 + font-weight: 600; 19 + color: var(--text-primary); 20 + margin-bottom: 1rem; 21 + } 22 + 23 + .form-row { 24 + display: grid; 25 + grid-template-columns: 1fr 1fr; 26 + gap: 1rem; 27 + } 28 + 29 + .rss-section { 30 + margin-top: 2rem; 31 + padding-top: 2rem; 32 + border-top: 1px solid var(--border-color); 33 + } 34 + 35 + .rss-header { 36 + display: flex; 37 + align-items: center; 38 + justify-content: space-between; 39 + margin-bottom: 1.5rem; 40 + } 41 + 42 + .rss-header h4 { 43 + margin-bottom: 0; 44 + } 45 + 46 + .btn-publish { 47 + padding: 0.75rem 1.5rem; 48 + background: var(--primary-color); 49 + color: white; 50 + border: none; 51 + border-radius: 6px; 52 + font-size: 14px; 53 + font-weight: 600; 54 + cursor: pointer; 55 + transition: all 0.2s; 56 + } 57 + 58 + .btn-publish:hover { 59 + background: var(--primary-hover); 60 + } 61 + 62 + .btn-publish:disabled { 63 + opacity: 0.6; 64 + cursor: not-allowed; 65 + } 66 + 67 + .feed-status-banner { 68 + display: flex; 69 + align-items: center; 70 + gap: 0.75rem; 71 + padding: 1rem 1.5rem; 72 + border-radius: 6px; 73 + margin-bottom: 1.5rem; 74 + font-size: 14px; 75 + } 76 + 77 + .feed-status-banner.success { 78 + background: #D4EDDA; 79 + color: #155724; 80 + border: 1px solid #C3E6CB; 81 + } 82 + 83 + .feed-status-banner.info { 84 + background: #D1ECF1; 85 + color: #0C5460; 86 + border: 1px solid #BEE5EB; 87 + } 88 + 89 + .feed-status-banner svg { 90 + flex-shrink: 0; 91 + } 92 + 93 + .uri-section { 94 + margin-bottom: 1.5rem; 95 + } 96 + 97 + .uri-section label { 98 + display: block; 99 + font-size: 14px; 100 + font-weight: 600; 101 + color: var(--text-primary); 102 + margin-bottom: 0.5rem; 103 + } 104 + 105 + .uri-description { 106 + display: block; 107 + margin-top: 0.5rem; 108 + color: var(--text-secondary); 109 + font-size: 13px; 110 + line-height: 1.5; 111 + } 112 + 113 + .rss-link { 114 + display: flex; 115 + align-items: center; 116 + gap: 1rem; 117 + padding: 1rem; 118 + background: #F8F9FA; 119 + border-radius: 6px; 120 + } 121 + 122 + .rss-link code { 123 + flex: 1; 124 + background: white; 125 + padding: 0.5rem 0.75rem; 126 + border-radius: 4px; 127 + border: 1px solid var(--border-color); 128 + font-size: 13px; 129 + overflow-x: auto; 130 + color: var(--text-secondary); 131 + font-family: 'Monaco', 'Menlo', 'Courier New', monospace; 132 + } 133 + 134 + .rss-link code.at-uri { 135 + color: var(--primary-color); 136 + font-weight: 500; 137 + } 138 + 139 + .btn-copy { 140 + padding: 0.5rem 1rem; 141 + background: var(--primary-color); 142 + color: white; 143 + border: none; 144 + border-radius: 4px; 145 + cursor: pointer; 146 + font-size: 13px; 147 + font-weight: 600; 148 + white-space: nowrap; 149 + transition: all 0.2s; 150 + } 151 + 152 + .btn-copy:hover { 153 + background: var(--primary-hover); 154 + } 155 + 156 + .did-section { 157 + margin-top: 1.5rem; 158 + padding-top: 1.5rem; 159 + border-top: 1px solid var(--border-color); 160 + } 161 + 162 + .did-section label { 163 + display: block; 164 + font-size: 13px; 165 + font-weight: 600; 166 + color: var(--text-secondary); 167 + margin-bottom: 0.5rem; 168 + } 169 + 170 + .did-display { 171 + display: flex; 172 + align-items: center; 173 + gap: 0.75rem; 174 + } 175 + 176 + .did-display code { 177 + background: #F8F9FA; 178 + padding: 0.5rem 0.75rem; 179 + border-radius: 4px; 180 + border: 1px solid var(--border-color); 181 + font-size: 12px; 182 + color: var(--text-light); 183 + font-family: 'Monaco', 'Menlo', 'Courier New', monospace; 184 + } 185 + 186 + .btn-copy-small { 187 + padding: 0.375rem 0.75rem; 188 + background: transparent; 189 + color: var(--primary-color); 190 + border: 1px solid var(--primary-color); 191 + border-radius: 4px; 192 + cursor: pointer; 193 + font-size: 12px; 194 + font-weight: 600; 195 + transition: all 0.2s; 196 + } 197 + 198 + .btn-copy-small:hover { 199 + background: var(--primary-color); 200 + color: white; 201 + } 202 + 203 + @media (max-width: 768px) { 204 + .form-row { 205 + grid-template-columns: 1fr; 206 + } 207 + 208 + .rss-header { 209 + flex-direction: column; 210 + align-items: stretch; 211 + gap: 1rem; 212 + } 213 + 214 + .btn-publish { 215 + width: 100%; 216 + } 217 + }
+233
client/src/pages/Settings.jsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import { api } from '../services/api'; 3 + import './Settings.css'; 4 + 5 + const Settings = () => { 6 + const [formData, setFormData] = useState({ 7 + title: '', 8 + description: '', 9 + link: '', 10 + language: 'en' 11 + }); 12 + const [loading, setLoading] = useState(false); 13 + const [publishing, setPublishing] = useState(false); 14 + const [message, setMessage] = useState({ type: '', text: '' }); 15 + const [feedUri, setFeedUri] = useState(null); 16 + const rssFeedUrl = api.getRSSFeedUrl(); 17 + 18 + useEffect(() => { 19 + loadMetadata(); 20 + loadFeedUri(); 21 + }, []); 22 + 23 + const loadMetadata = async () => { 24 + try { 25 + const data = await api.getFeedMetadata(); 26 + setFormData({ 27 + title: data.title || '', 28 + description: data.description || '', 29 + link: data.link || '', 30 + language: data.language || 'en' 31 + }); 32 + } catch (error) { 33 + console.error('Error loading metadata:', error); 34 + } 35 + }; 36 + 37 + const loadFeedUri = async () => { 38 + try { 39 + const data = await api.getRSSFeedURI(); 40 + setFeedUri(data); 41 + } catch (error) { 42 + console.error('Error loading feed URI:', error); 43 + } 44 + }; 45 + 46 + const handleSubmit = async (e) => { 47 + e.preventDefault(); 48 + setLoading(true); 49 + setMessage({ type: '', text: '' }); 50 + 51 + try { 52 + await api.updateFeedMetadata(formData); 53 + setMessage({ type: 'success', text: 'Feed settings saved successfully!' }); 54 + } catch (error) { 55 + setMessage({ type: 'error', text: error.message }); 56 + } finally { 57 + setLoading(false); 58 + } 59 + }; 60 + 61 + const handlePublishFeed = async () => { 62 + setPublishing(true); 63 + setMessage({ type: '', text: '' }); 64 + 65 + try { 66 + const result = await api.publishRSSFeed(); 67 + setMessage({ 68 + type: 'success', 69 + text: `RSS feed published to AT Protocol! URI: ${result.uri}` 70 + }); 71 + await loadFeedUri(); 72 + } catch (error) { 73 + setMessage({ type: 'error', text: `Failed to publish: ${error.message}` }); 74 + } finally { 75 + setPublishing(false); 76 + } 77 + }; 78 + 79 + const copyToClipboard = (text, label) => { 80 + navigator.clipboard.writeText(text) 81 + .then(() => { 82 + setMessage({ type: 'success', text: `${label} copied to clipboard!` }); 83 + setTimeout(() => setMessage({ type: '', text: '' }), 3000); 84 + }) 85 + .catch(() => { 86 + setMessage({ type: 'error', text: 'Failed to copy to clipboard' }); 87 + }); 88 + }; 89 + 90 + return ( 91 + <div className="view-content"> 92 + <h2 className="section-title">Podcast Settings</h2> 93 + 94 + <section className="settings-card"> 95 + <h3>Podcast Feed Settings</h3> 96 + <form onSubmit={handleSubmit}> 97 + <div className="form-group"> 98 + <label htmlFor="podcastTitle">Podcast Title</label> 99 + <input 100 + type="text" 101 + id="podcastTitle" 102 + placeholder="My Podcast" 103 + value={formData.title} 104 + onChange={(e) => setFormData({ ...formData, title: e.target.value })} 105 + /> 106 + </div> 107 + 108 + <div className="form-group"> 109 + <label htmlFor="podcastDescription">Podcast Description</label> 110 + <textarea 111 + id="podcastDescription" 112 + rows="3" 113 + placeholder="About my podcast..." 114 + value={formData.description} 115 + onChange={(e) => setFormData({ ...formData, description: e.target.value })} 116 + ></textarea> 117 + </div> 118 + 119 + <div className="form-row"> 120 + <div className="form-group"> 121 + <label htmlFor="podcastLink">Website</label> 122 + <input 123 + type="url" 124 + id="podcastLink" 125 + placeholder="https://example.com" 126 + value={formData.link} 127 + onChange={(e) => setFormData({ ...formData, link: e.target.value })} 128 + /> 129 + </div> 130 + 131 + <div className="form-group"> 132 + <label htmlFor="podcastLanguage">Language</label> 133 + <input 134 + type="text" 135 + id="podcastLanguage" 136 + placeholder="en" 137 + maxLength="5" 138 + value={formData.language} 139 + onChange={(e) => setFormData({ ...formData, language: e.target.value })} 140 + /> 141 + </div> 142 + </div> 143 + 144 + <button type="submit" className="btn-primary" disabled={loading}> 145 + {loading ? 'Saving...' : 'Save Settings'} 146 + </button> 147 + </form> 148 + 149 + {message.text && ( 150 + <div className={`status-message ${message.type}`}> 151 + {message.text} 152 + </div> 153 + )} 154 + 155 + <div className="rss-section"> 156 + <div className="rss-header"> 157 + <h4>RSS Feed Distribution</h4> 158 + <button 159 + className="btn-publish" 160 + onClick={handlePublishFeed} 161 + disabled={publishing} 162 + > 163 + {publishing ? 'Publishing...' : feedUri?.published ? 'Update Feed on AT Protocol' : 'Publish to AT Protocol'} 164 + </button> 165 + </div> 166 + 167 + {feedUri?.published && ( 168 + <div className="feed-status-banner success"> 169 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 170 + <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> 171 + <polyline points="22 4 12 14.01 9 11.01"></polyline> 172 + </svg> 173 + <span>RSS feed is published on AT Protocol</span> 174 + </div> 175 + )} 176 + 177 + {!feedUri?.published && ( 178 + <div className="feed-status-banner info"> 179 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 180 + <circle cx="12" cy="12" r="10"></circle> 181 + <line x1="12" y1="16" x2="12" y2="12"></line> 182 + <line x1="12" y1="8" x2="12.01" y2="8"></line> 183 + </svg> 184 + <span>Feed not yet published to AT Protocol. Click "Publish" to make it available via at:// URI.</span> 185 + </div> 186 + )} 187 + 188 + {feedUri?.atUri && ( 189 + <div className="uri-section"> 190 + <label>AT Protocol URI (Decentralized)</label> 191 + <div className="rss-link"> 192 + <code className="at-uri">{feedUri.atUri}</code> 193 + <button className="btn-copy" onClick={() => copyToClipboard(feedUri.atUri, 'AT URI')}> 194 + Copy 195 + </button> 196 + </div> 197 + <small className="uri-description"> 198 + This is your decentralized RSS feed identifier on AT Protocol. Use this URI to access your feed without relying on a specific server. 199 + </small> 200 + </div> 201 + )} 202 + 203 + <div className="uri-section"> 204 + <label>HTTP Proxy URL (For Podcast Apps)</label> 205 + <div className="rss-link"> 206 + <code>{feedUri?.httpProxy || rssFeedUrl}</code> 207 + <button className="btn-copy" onClick={() => copyToClipboard(feedUri?.httpProxy || rssFeedUrl, 'HTTP URL')}> 208 + Copy 209 + </button> 210 + </div> 211 + <small className="uri-description"> 212 + Use this URL for podcast apps that don't support AT Protocol yet. It fetches the feed from {feedUri?.published ? 'AT Protocol' : 'our server'}. 213 + </small> 214 + </div> 215 + 216 + {feedUri?.did && ( 217 + <div className="did-section"> 218 + <label>Your DID (Decentralized Identifier)</label> 219 + <div className="did-display"> 220 + <code>{feedUri.did}</code> 221 + <button className="btn-copy-small" onClick={() => copyToClipboard(feedUri.did, 'DID')}> 222 + Copy 223 + </button> 224 + </div> 225 + </div> 226 + )} 227 + </div> 228 + </section> 229 + </div> 230 + ); 231 + }; 232 + 233 + export default Settings;
+78
client/src/services/api.js
··· 1 + const API_BASE = window.location.origin; 2 + 3 + export const api = { 4 + // Episodes 5 + async getEpisodes() { 6 + const response = await fetch(`${API_BASE}/api/media/episodes`); 7 + if (!response.ok) throw new Error('Failed to fetch episodes'); 8 + return response.json(); 9 + }, 10 + 11 + async uploadEpisode(formData) { 12 + const response = await fetch(`${API_BASE}/api/upload/episode`, { 13 + method: 'POST', 14 + body: formData 15 + }); 16 + const data = await response.json(); 17 + if (!response.ok) throw new Error(data.error || 'Upload failed'); 18 + return data; 19 + }, 20 + 21 + // Feed metadata 22 + async getFeedMetadata() { 23 + const response = await fetch(`${API_BASE}/api/feed/metadata`); 24 + if (!response.ok) throw new Error('Failed to fetch feed metadata'); 25 + return response.json(); 26 + }, 27 + 28 + async updateFeedMetadata(metadata) { 29 + const response = await fetch(`${API_BASE}/api/feed/metadata`, { 30 + method: 'POST', 31 + headers: { 32 + 'Content-Type': 'application/json' 33 + }, 34 + body: JSON.stringify(metadata) 35 + }); 36 + const data = await response.json(); 37 + if (!response.ok) throw new Error(data.error || 'Update failed'); 38 + return data; 39 + }, 40 + 41 + getRSSFeedUrl() { 42 + return `${API_BASE}/api/feed/rss`; 43 + }, 44 + 45 + // RSS Feed Publishing 46 + async publishRSSFeed() { 47 + const response = await fetch(`${API_BASE}/api/feed/publish`, { 48 + method: 'POST' 49 + }); 50 + const data = await response.json(); 51 + if (!response.ok) throw new Error(data.error || 'Failed to publish RSS feed'); 52 + return data; 53 + }, 54 + 55 + async getRSSFeedURI() { 56 + const response = await fetch(`${API_BASE}/api/feed/uri`); 57 + if (!response.ok) throw new Error('Failed to get RSS feed URI'); 58 + return response.json(); 59 + } 60 + }; 61 + 62 + export const formatFileSize = (bytes) => { 63 + if (bytes === 0) return '0 Bytes'; 64 + 65 + const k = 1024; 66 + const sizes = ['Bytes', 'KB', 'MB', 'GB']; 67 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 68 + 69 + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; 70 + }; 71 + 72 + export const formatDate = (dateString) => { 73 + return new Date(dateString).toLocaleDateString('en-US', { 74 + year: 'numeric', 75 + month: 'long', 76 + day: 'numeric' 77 + }); 78 + };
+16
client/vite.config.js
··· 1 + import { defineConfig } from 'vite' 2 + import react from '@vitejs/plugin-react' 3 + 4 + // https://vite.dev/config/ 5 + export default defineConfig({ 6 + plugins: [react()], 7 + server: { 8 + port: 3000, 9 + proxy: { 10 + '/api': { 11 + target: 'http://localhost:5000', 12 + changeOrigin: true 13 + } 14 + } 15 + } 16 + })
+14
nixpacks.toml
··· 1 + [phases.setup] 2 + nixPkgs = ['nodejs_20'] 3 + 4 + [phases.install] 5 + cmds = [ 6 + 'npm ci', 7 + 'cd client && npm ci' 8 + ] 9 + 10 + [phases.build] 11 + cmds = ['npm run build'] 12 + 13 + [start] 14 + cmd = 'npm start'
+2426
package-lock.json
··· 1 + { 2 + "name": "atproto-podcast", 3 + "version": "1.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "atproto-podcast", 9 + "version": "1.0.0", 10 + "license": "MIT", 11 + "dependencies": { 12 + "@atproto/api": "^0.13.17", 13 + "@atproto/xrpc-server": "^0.7.3", 14 + "cors": "^2.8.5", 15 + "dotenv": "^16.4.7", 16 + "express": "^4.21.2", 17 + "multer": "^1.4.5-lts.1", 18 + "rss": "^1.2.2" 19 + }, 20 + "devDependencies": { 21 + "concurrently": "^9.2.1", 22 + "nodemon": "^3.1.9" 23 + } 24 + }, 25 + "node_modules/@atproto/api": { 26 + "version": "0.13.35", 27 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.35.tgz", 28 + "integrity": "sha512-vsEfBj0C333TLjDppvTdTE0IdKlXuljKSveAeI4PPx/l6eUKNnDTsYxvILtXUVzwUlTDmSRqy5O4Ryh78n1b7g==", 29 + "license": "MIT", 30 + "dependencies": { 31 + "@atproto/common-web": "^0.4.0", 32 + "@atproto/lexicon": "^0.4.6", 33 + "@atproto/syntax": "^0.3.2", 34 + "@atproto/xrpc": "^0.6.8", 35 + "await-lock": "^2.2.2", 36 + "multiformats": "^9.9.0", 37 + "tlds": "^1.234.0", 38 + "zod": "^3.23.8" 39 + } 40 + }, 41 + "node_modules/@atproto/common": { 42 + "version": "0.4.12", 43 + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.4.12.tgz", 44 + "integrity": "sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ==", 45 + "license": "MIT", 46 + "dependencies": { 47 + "@atproto/common-web": "^0.4.3", 48 + "@ipld/dag-cbor": "^7.0.3", 49 + "cbor-x": "^1.5.1", 50 + "iso-datestring-validator": "^2.2.2", 51 + "multiformats": "^9.9.0", 52 + "pino": "^8.21.0" 53 + }, 54 + "engines": { 55 + "node": ">=18.7.0" 56 + } 57 + }, 58 + "node_modules/@atproto/common-web": { 59 + "version": "0.4.3", 60 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", 61 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 62 + "license": "MIT", 63 + "dependencies": { 64 + "graphemer": "^1.4.0", 65 + "multiformats": "^9.9.0", 66 + "uint8arrays": "3.0.0", 67 + "zod": "^3.23.8" 68 + } 69 + }, 70 + "node_modules/@atproto/crypto": { 71 + "version": "0.4.4", 72 + "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.4.tgz", 73 + "integrity": "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA==", 74 + "license": "MIT", 75 + "dependencies": { 76 + "@noble/curves": "^1.7.0", 77 + "@noble/hashes": "^1.6.1", 78 + "uint8arrays": "3.0.0" 79 + }, 80 + "engines": { 81 + "node": ">=18.7.0" 82 + } 83 + }, 84 + "node_modules/@atproto/lexicon": { 85 + "version": "0.4.14", 86 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz", 87 + "integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==", 88 + "license": "MIT", 89 + "dependencies": { 90 + "@atproto/common-web": "^0.4.2", 91 + "@atproto/syntax": "^0.4.0", 92 + "iso-datestring-validator": "^2.2.2", 93 + "multiformats": "^9.9.0", 94 + "zod": "^3.23.8" 95 + } 96 + }, 97 + "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { 98 + "version": "0.4.1", 99 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 100 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 101 + "license": "MIT" 102 + }, 103 + "node_modules/@atproto/syntax": { 104 + "version": "0.3.4", 105 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.4.tgz", 106 + "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 107 + "license": "MIT" 108 + }, 109 + "node_modules/@atproto/xrpc": { 110 + "version": "0.6.12", 111 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.12.tgz", 112 + "integrity": "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==", 113 + "license": "MIT", 114 + "dependencies": { 115 + "@atproto/lexicon": "^0.4.10", 116 + "zod": "^3.23.8" 117 + } 118 + }, 119 + "node_modules/@atproto/xrpc-server": { 120 + "version": "0.7.19", 121 + "resolved": "https://registry.npmjs.org/@atproto/xrpc-server/-/xrpc-server-0.7.19.tgz", 122 + "integrity": "sha512-YSCl/tU2NDykgDYslFSOYCr96esUgDwncFiADKL59/fyIFPLoT0qY8Uq/budpxUh0qPzjow4HHgVWESOaOpUmA==", 123 + "license": "MIT", 124 + "dependencies": { 125 + "@atproto/common": "^0.4.11", 126 + "@atproto/crypto": "^0.4.4", 127 + "@atproto/lexicon": "^0.4.11", 128 + "@atproto/xrpc": "^0.7.0", 129 + "cbor-x": "^1.5.1", 130 + "express": "^4.17.2", 131 + "http-errors": "^2.0.0", 132 + "mime-types": "^2.1.35", 133 + "rate-limiter-flexible": "^2.4.1", 134 + "uint8arrays": "3.0.0", 135 + "ws": "^8.12.0", 136 + "zod": "^3.23.8" 137 + }, 138 + "engines": { 139 + "node": ">=18.7.0" 140 + } 141 + }, 142 + "node_modules/@atproto/xrpc-server/node_modules/@atproto/syntax": { 143 + "version": "0.4.1", 144 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 145 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 146 + "license": "MIT" 147 + }, 148 + "node_modules/@atproto/xrpc-server/node_modules/@atproto/xrpc": { 149 + "version": "0.7.5", 150 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", 151 + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 152 + "license": "MIT", 153 + "dependencies": { 154 + "@atproto/lexicon": "^0.5.1", 155 + "zod": "^3.23.8" 156 + } 157 + }, 158 + "node_modules/@atproto/xrpc-server/node_modules/@atproto/xrpc/node_modules/@atproto/lexicon": { 159 + "version": "0.5.1", 160 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", 161 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 162 + "license": "MIT", 163 + "dependencies": { 164 + "@atproto/common-web": "^0.4.3", 165 + "@atproto/syntax": "^0.4.1", 166 + "iso-datestring-validator": "^2.2.2", 167 + "multiformats": "^9.9.0", 168 + "zod": "^3.23.8" 169 + } 170 + }, 171 + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { 172 + "version": "2.2.0", 173 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", 174 + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", 175 + "cpu": [ 176 + "arm64" 177 + ], 178 + "license": "MIT", 179 + "optional": true, 180 + "os": [ 181 + "darwin" 182 + ] 183 + }, 184 + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { 185 + "version": "2.2.0", 186 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", 187 + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", 188 + "cpu": [ 189 + "x64" 190 + ], 191 + "license": "MIT", 192 + "optional": true, 193 + "os": [ 194 + "darwin" 195 + ] 196 + }, 197 + "node_modules/@cbor-extract/cbor-extract-linux-arm": { 198 + "version": "2.2.0", 199 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", 200 + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", 201 + "cpu": [ 202 + "arm" 203 + ], 204 + "license": "MIT", 205 + "optional": true, 206 + "os": [ 207 + "linux" 208 + ] 209 + }, 210 + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { 211 + "version": "2.2.0", 212 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", 213 + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", 214 + "cpu": [ 215 + "arm64" 216 + ], 217 + "license": "MIT", 218 + "optional": true, 219 + "os": [ 220 + "linux" 221 + ] 222 + }, 223 + "node_modules/@cbor-extract/cbor-extract-linux-x64": { 224 + "version": "2.2.0", 225 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", 226 + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", 227 + "cpu": [ 228 + "x64" 229 + ], 230 + "license": "MIT", 231 + "optional": true, 232 + "os": [ 233 + "linux" 234 + ] 235 + }, 236 + "node_modules/@cbor-extract/cbor-extract-win32-x64": { 237 + "version": "2.2.0", 238 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", 239 + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", 240 + "cpu": [ 241 + "x64" 242 + ], 243 + "license": "MIT", 244 + "optional": true, 245 + "os": [ 246 + "win32" 247 + ] 248 + }, 249 + "node_modules/@ipld/dag-cbor": { 250 + "version": "7.0.3", 251 + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz", 252 + "integrity": "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==", 253 + "license": "(Apache-2.0 AND MIT)", 254 + "dependencies": { 255 + "cborg": "^1.6.0", 256 + "multiformats": "^9.5.4" 257 + } 258 + }, 259 + "node_modules/@noble/curves": { 260 + "version": "1.9.7", 261 + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", 262 + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", 263 + "license": "MIT", 264 + "dependencies": { 265 + "@noble/hashes": "1.8.0" 266 + }, 267 + "engines": { 268 + "node": "^14.21.3 || >=16" 269 + }, 270 + "funding": { 271 + "url": "https://paulmillr.com/funding/" 272 + } 273 + }, 274 + "node_modules/@noble/hashes": { 275 + "version": "1.8.0", 276 + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", 277 + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", 278 + "license": "MIT", 279 + "engines": { 280 + "node": "^14.21.3 || >=16" 281 + }, 282 + "funding": { 283 + "url": "https://paulmillr.com/funding/" 284 + } 285 + }, 286 + "node_modules/abort-controller": { 287 + "version": "3.0.0", 288 + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 289 + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 290 + "license": "MIT", 291 + "dependencies": { 292 + "event-target-shim": "^5.0.0" 293 + }, 294 + "engines": { 295 + "node": ">=6.5" 296 + } 297 + }, 298 + "node_modules/accepts": { 299 + "version": "1.3.8", 300 + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 301 + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 302 + "license": "MIT", 303 + "dependencies": { 304 + "mime-types": "~2.1.34", 305 + "negotiator": "0.6.3" 306 + }, 307 + "engines": { 308 + "node": ">= 0.6" 309 + } 310 + }, 311 + "node_modules/ansi-regex": { 312 + "version": "5.0.1", 313 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 314 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 315 + "dev": true, 316 + "license": "MIT", 317 + "engines": { 318 + "node": ">=8" 319 + } 320 + }, 321 + "node_modules/ansi-styles": { 322 + "version": "4.3.0", 323 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 324 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 325 + "dev": true, 326 + "license": "MIT", 327 + "dependencies": { 328 + "color-convert": "^2.0.1" 329 + }, 330 + "engines": { 331 + "node": ">=8" 332 + }, 333 + "funding": { 334 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 335 + } 336 + }, 337 + "node_modules/anymatch": { 338 + "version": "3.1.3", 339 + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 340 + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 341 + "dev": true, 342 + "license": "ISC", 343 + "dependencies": { 344 + "normalize-path": "^3.0.0", 345 + "picomatch": "^2.0.4" 346 + }, 347 + "engines": { 348 + "node": ">= 8" 349 + } 350 + }, 351 + "node_modules/append-field": { 352 + "version": "1.0.0", 353 + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", 354 + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", 355 + "license": "MIT" 356 + }, 357 + "node_modules/array-flatten": { 358 + "version": "1.1.1", 359 + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 360 + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 361 + "license": "MIT" 362 + }, 363 + "node_modules/atomic-sleep": { 364 + "version": "1.0.0", 365 + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 366 + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 367 + "license": "MIT", 368 + "engines": { 369 + "node": ">=8.0.0" 370 + } 371 + }, 372 + "node_modules/await-lock": { 373 + "version": "2.2.2", 374 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 375 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 376 + "license": "MIT" 377 + }, 378 + "node_modules/balanced-match": { 379 + "version": "1.0.2", 380 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 381 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 382 + "dev": true, 383 + "license": "MIT" 384 + }, 385 + "node_modules/base64-js": { 386 + "version": "1.5.1", 387 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 388 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 389 + "funding": [ 390 + { 391 + "type": "github", 392 + "url": "https://github.com/sponsors/feross" 393 + }, 394 + { 395 + "type": "patreon", 396 + "url": "https://www.patreon.com/feross" 397 + }, 398 + { 399 + "type": "consulting", 400 + "url": "https://feross.org/support" 401 + } 402 + ], 403 + "license": "MIT" 404 + }, 405 + "node_modules/binary-extensions": { 406 + "version": "2.3.0", 407 + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 408 + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 409 + "dev": true, 410 + "license": "MIT", 411 + "engines": { 412 + "node": ">=8" 413 + }, 414 + "funding": { 415 + "url": "https://github.com/sponsors/sindresorhus" 416 + } 417 + }, 418 + "node_modules/body-parser": { 419 + "version": "1.20.3", 420 + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 421 + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 422 + "license": "MIT", 423 + "dependencies": { 424 + "bytes": "3.1.2", 425 + "content-type": "~1.0.5", 426 + "debug": "2.6.9", 427 + "depd": "2.0.0", 428 + "destroy": "1.2.0", 429 + "http-errors": "2.0.0", 430 + "iconv-lite": "0.4.24", 431 + "on-finished": "2.4.1", 432 + "qs": "6.13.0", 433 + "raw-body": "2.5.2", 434 + "type-is": "~1.6.18", 435 + "unpipe": "1.0.0" 436 + }, 437 + "engines": { 438 + "node": ">= 0.8", 439 + "npm": "1.2.8000 || >= 1.4.16" 440 + } 441 + }, 442 + "node_modules/brace-expansion": { 443 + "version": "1.1.12", 444 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 445 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 446 + "dev": true, 447 + "license": "MIT", 448 + "dependencies": { 449 + "balanced-match": "^1.0.0", 450 + "concat-map": "0.0.1" 451 + } 452 + }, 453 + "node_modules/braces": { 454 + "version": "3.0.3", 455 + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 456 + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 457 + "dev": true, 458 + "license": "MIT", 459 + "dependencies": { 460 + "fill-range": "^7.1.1" 461 + }, 462 + "engines": { 463 + "node": ">=8" 464 + } 465 + }, 466 + "node_modules/buffer": { 467 + "version": "6.0.3", 468 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 469 + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 470 + "funding": [ 471 + { 472 + "type": "github", 473 + "url": "https://github.com/sponsors/feross" 474 + }, 475 + { 476 + "type": "patreon", 477 + "url": "https://www.patreon.com/feross" 478 + }, 479 + { 480 + "type": "consulting", 481 + "url": "https://feross.org/support" 482 + } 483 + ], 484 + "license": "MIT", 485 + "dependencies": { 486 + "base64-js": "^1.3.1", 487 + "ieee754": "^1.2.1" 488 + } 489 + }, 490 + "node_modules/buffer-from": { 491 + "version": "1.1.2", 492 + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 493 + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 494 + "license": "MIT" 495 + }, 496 + "node_modules/busboy": { 497 + "version": "1.6.0", 498 + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 499 + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 500 + "dependencies": { 501 + "streamsearch": "^1.1.0" 502 + }, 503 + "engines": { 504 + "node": ">=10.16.0" 505 + } 506 + }, 507 + "node_modules/bytes": { 508 + "version": "3.1.2", 509 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 510 + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 511 + "license": "MIT", 512 + "engines": { 513 + "node": ">= 0.8" 514 + } 515 + }, 516 + "node_modules/call-bind-apply-helpers": { 517 + "version": "1.0.2", 518 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 519 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 520 + "license": "MIT", 521 + "dependencies": { 522 + "es-errors": "^1.3.0", 523 + "function-bind": "^1.1.2" 524 + }, 525 + "engines": { 526 + "node": ">= 0.4" 527 + } 528 + }, 529 + "node_modules/call-bound": { 530 + "version": "1.0.4", 531 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 532 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 533 + "license": "MIT", 534 + "dependencies": { 535 + "call-bind-apply-helpers": "^1.0.2", 536 + "get-intrinsic": "^1.3.0" 537 + }, 538 + "engines": { 539 + "node": ">= 0.4" 540 + }, 541 + "funding": { 542 + "url": "https://github.com/sponsors/ljharb" 543 + } 544 + }, 545 + "node_modules/cbor-extract": { 546 + "version": "2.2.0", 547 + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", 548 + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", 549 + "hasInstallScript": true, 550 + "license": "MIT", 551 + "optional": true, 552 + "dependencies": { 553 + "node-gyp-build-optional-packages": "5.1.1" 554 + }, 555 + "bin": { 556 + "download-cbor-prebuilds": "bin/download-prebuilds.js" 557 + }, 558 + "optionalDependencies": { 559 + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", 560 + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", 561 + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", 562 + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", 563 + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", 564 + "@cbor-extract/cbor-extract-win32-x64": "2.2.0" 565 + } 566 + }, 567 + "node_modules/cbor-x": { 568 + "version": "1.6.0", 569 + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz", 570 + "integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==", 571 + "license": "MIT", 572 + "optionalDependencies": { 573 + "cbor-extract": "^2.2.0" 574 + } 575 + }, 576 + "node_modules/cborg": { 577 + "version": "1.10.2", 578 + "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.2.tgz", 579 + "integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==", 580 + "license": "Apache-2.0", 581 + "bin": { 582 + "cborg": "cli.js" 583 + } 584 + }, 585 + "node_modules/chalk": { 586 + "version": "4.1.2", 587 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 588 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 589 + "dev": true, 590 + "license": "MIT", 591 + "dependencies": { 592 + "ansi-styles": "^4.1.0", 593 + "supports-color": "^7.1.0" 594 + }, 595 + "engines": { 596 + "node": ">=10" 597 + }, 598 + "funding": { 599 + "url": "https://github.com/chalk/chalk?sponsor=1" 600 + } 601 + }, 602 + "node_modules/chalk/node_modules/has-flag": { 603 + "version": "4.0.0", 604 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 605 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 606 + "dev": true, 607 + "license": "MIT", 608 + "engines": { 609 + "node": ">=8" 610 + } 611 + }, 612 + "node_modules/chalk/node_modules/supports-color": { 613 + "version": "7.2.0", 614 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 615 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 616 + "dev": true, 617 + "license": "MIT", 618 + "dependencies": { 619 + "has-flag": "^4.0.0" 620 + }, 621 + "engines": { 622 + "node": ">=8" 623 + } 624 + }, 625 + "node_modules/chokidar": { 626 + "version": "3.6.0", 627 + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 628 + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 629 + "dev": true, 630 + "license": "MIT", 631 + "dependencies": { 632 + "anymatch": "~3.1.2", 633 + "braces": "~3.0.2", 634 + "glob-parent": "~5.1.2", 635 + "is-binary-path": "~2.1.0", 636 + "is-glob": "~4.0.1", 637 + "normalize-path": "~3.0.0", 638 + "readdirp": "~3.6.0" 639 + }, 640 + "engines": { 641 + "node": ">= 8.10.0" 642 + }, 643 + "funding": { 644 + "url": "https://paulmillr.com/funding/" 645 + }, 646 + "optionalDependencies": { 647 + "fsevents": "~2.3.2" 648 + } 649 + }, 650 + "node_modules/cliui": { 651 + "version": "8.0.1", 652 + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 653 + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 654 + "dev": true, 655 + "license": "ISC", 656 + "dependencies": { 657 + "string-width": "^4.2.0", 658 + "strip-ansi": "^6.0.1", 659 + "wrap-ansi": "^7.0.0" 660 + }, 661 + "engines": { 662 + "node": ">=12" 663 + } 664 + }, 665 + "node_modules/color-convert": { 666 + "version": "2.0.1", 667 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 668 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 669 + "dev": true, 670 + "license": "MIT", 671 + "dependencies": { 672 + "color-name": "~1.1.4" 673 + }, 674 + "engines": { 675 + "node": ">=7.0.0" 676 + } 677 + }, 678 + "node_modules/color-name": { 679 + "version": "1.1.4", 680 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 681 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 682 + "dev": true, 683 + "license": "MIT" 684 + }, 685 + "node_modules/concat-map": { 686 + "version": "0.0.1", 687 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 688 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 689 + "dev": true, 690 + "license": "MIT" 691 + }, 692 + "node_modules/concat-stream": { 693 + "version": "1.6.2", 694 + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 695 + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 696 + "engines": [ 697 + "node >= 0.8" 698 + ], 699 + "license": "MIT", 700 + "dependencies": { 701 + "buffer-from": "^1.0.0", 702 + "inherits": "^2.0.3", 703 + "readable-stream": "^2.2.2", 704 + "typedarray": "^0.0.6" 705 + } 706 + }, 707 + "node_modules/concurrently": { 708 + "version": "9.2.1", 709 + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", 710 + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", 711 + "dev": true, 712 + "license": "MIT", 713 + "dependencies": { 714 + "chalk": "4.1.2", 715 + "rxjs": "7.8.2", 716 + "shell-quote": "1.8.3", 717 + "supports-color": "8.1.1", 718 + "tree-kill": "1.2.2", 719 + "yargs": "17.7.2" 720 + }, 721 + "bin": { 722 + "conc": "dist/bin/concurrently.js", 723 + "concurrently": "dist/bin/concurrently.js" 724 + }, 725 + "engines": { 726 + "node": ">=18" 727 + }, 728 + "funding": { 729 + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" 730 + } 731 + }, 732 + "node_modules/concurrently/node_modules/has-flag": { 733 + "version": "4.0.0", 734 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 735 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 736 + "dev": true, 737 + "license": "MIT", 738 + "engines": { 739 + "node": ">=8" 740 + } 741 + }, 742 + "node_modules/concurrently/node_modules/supports-color": { 743 + "version": "8.1.1", 744 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 745 + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 746 + "dev": true, 747 + "license": "MIT", 748 + "dependencies": { 749 + "has-flag": "^4.0.0" 750 + }, 751 + "engines": { 752 + "node": ">=10" 753 + }, 754 + "funding": { 755 + "url": "https://github.com/chalk/supports-color?sponsor=1" 756 + } 757 + }, 758 + "node_modules/content-disposition": { 759 + "version": "0.5.4", 760 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 761 + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 762 + "license": "MIT", 763 + "dependencies": { 764 + "safe-buffer": "5.2.1" 765 + }, 766 + "engines": { 767 + "node": ">= 0.6" 768 + } 769 + }, 770 + "node_modules/content-type": { 771 + "version": "1.0.5", 772 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 773 + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 774 + "license": "MIT", 775 + "engines": { 776 + "node": ">= 0.6" 777 + } 778 + }, 779 + "node_modules/cookie": { 780 + "version": "0.7.1", 781 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 782 + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 783 + "license": "MIT", 784 + "engines": { 785 + "node": ">= 0.6" 786 + } 787 + }, 788 + "node_modules/cookie-signature": { 789 + "version": "1.0.6", 790 + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 791 + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 792 + "license": "MIT" 793 + }, 794 + "node_modules/core-util-is": { 795 + "version": "1.0.3", 796 + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 797 + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", 798 + "license": "MIT" 799 + }, 800 + "node_modules/cors": { 801 + "version": "2.8.5", 802 + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 803 + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 804 + "license": "MIT", 805 + "dependencies": { 806 + "object-assign": "^4", 807 + "vary": "^1" 808 + }, 809 + "engines": { 810 + "node": ">= 0.10" 811 + } 812 + }, 813 + "node_modules/debug": { 814 + "version": "2.6.9", 815 + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 816 + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 817 + "license": "MIT", 818 + "dependencies": { 819 + "ms": "2.0.0" 820 + } 821 + }, 822 + "node_modules/depd": { 823 + "version": "2.0.0", 824 + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 825 + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 826 + "license": "MIT", 827 + "engines": { 828 + "node": ">= 0.8" 829 + } 830 + }, 831 + "node_modules/destroy": { 832 + "version": "1.2.0", 833 + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 834 + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 835 + "license": "MIT", 836 + "engines": { 837 + "node": ">= 0.8", 838 + "npm": "1.2.8000 || >= 1.4.16" 839 + } 840 + }, 841 + "node_modules/detect-libc": { 842 + "version": "2.1.2", 843 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 844 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 845 + "license": "Apache-2.0", 846 + "optional": true, 847 + "engines": { 848 + "node": ">=8" 849 + } 850 + }, 851 + "node_modules/dotenv": { 852 + "version": "16.6.1", 853 + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 854 + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 855 + "license": "BSD-2-Clause", 856 + "engines": { 857 + "node": ">=12" 858 + }, 859 + "funding": { 860 + "url": "https://dotenvx.com" 861 + } 862 + }, 863 + "node_modules/dunder-proto": { 864 + "version": "1.0.1", 865 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 866 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 867 + "license": "MIT", 868 + "dependencies": { 869 + "call-bind-apply-helpers": "^1.0.1", 870 + "es-errors": "^1.3.0", 871 + "gopd": "^1.2.0" 872 + }, 873 + "engines": { 874 + "node": ">= 0.4" 875 + } 876 + }, 877 + "node_modules/ee-first": { 878 + "version": "1.1.1", 879 + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 880 + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 881 + "license": "MIT" 882 + }, 883 + "node_modules/emoji-regex": { 884 + "version": "8.0.0", 885 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 886 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 887 + "dev": true, 888 + "license": "MIT" 889 + }, 890 + "node_modules/encodeurl": { 891 + "version": "2.0.0", 892 + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 893 + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 894 + "license": "MIT", 895 + "engines": { 896 + "node": ">= 0.8" 897 + } 898 + }, 899 + "node_modules/es-define-property": { 900 + "version": "1.0.1", 901 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 902 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 903 + "license": "MIT", 904 + "engines": { 905 + "node": ">= 0.4" 906 + } 907 + }, 908 + "node_modules/es-errors": { 909 + "version": "1.3.0", 910 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 911 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 912 + "license": "MIT", 913 + "engines": { 914 + "node": ">= 0.4" 915 + } 916 + }, 917 + "node_modules/es-object-atoms": { 918 + "version": "1.1.1", 919 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 920 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 921 + "license": "MIT", 922 + "dependencies": { 923 + "es-errors": "^1.3.0" 924 + }, 925 + "engines": { 926 + "node": ">= 0.4" 927 + } 928 + }, 929 + "node_modules/escalade": { 930 + "version": "3.2.0", 931 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 932 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 933 + "dev": true, 934 + "license": "MIT", 935 + "engines": { 936 + "node": ">=6" 937 + } 938 + }, 939 + "node_modules/escape-html": { 940 + "version": "1.0.3", 941 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 942 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 943 + "license": "MIT" 944 + }, 945 + "node_modules/etag": { 946 + "version": "1.8.1", 947 + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 948 + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 949 + "license": "MIT", 950 + "engines": { 951 + "node": ">= 0.6" 952 + } 953 + }, 954 + "node_modules/event-target-shim": { 955 + "version": "5.0.1", 956 + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 957 + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 958 + "license": "MIT", 959 + "engines": { 960 + "node": ">=6" 961 + } 962 + }, 963 + "node_modules/events": { 964 + "version": "3.3.0", 965 + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 966 + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 967 + "license": "MIT", 968 + "engines": { 969 + "node": ">=0.8.x" 970 + } 971 + }, 972 + "node_modules/express": { 973 + "version": "4.21.2", 974 + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 975 + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 976 + "license": "MIT", 977 + "dependencies": { 978 + "accepts": "~1.3.8", 979 + "array-flatten": "1.1.1", 980 + "body-parser": "1.20.3", 981 + "content-disposition": "0.5.4", 982 + "content-type": "~1.0.4", 983 + "cookie": "0.7.1", 984 + "cookie-signature": "1.0.6", 985 + "debug": "2.6.9", 986 + "depd": "2.0.0", 987 + "encodeurl": "~2.0.0", 988 + "escape-html": "~1.0.3", 989 + "etag": "~1.8.1", 990 + "finalhandler": "1.3.1", 991 + "fresh": "0.5.2", 992 + "http-errors": "2.0.0", 993 + "merge-descriptors": "1.0.3", 994 + "methods": "~1.1.2", 995 + "on-finished": "2.4.1", 996 + "parseurl": "~1.3.3", 997 + "path-to-regexp": "0.1.12", 998 + "proxy-addr": "~2.0.7", 999 + "qs": "6.13.0", 1000 + "range-parser": "~1.2.1", 1001 + "safe-buffer": "5.2.1", 1002 + "send": "0.19.0", 1003 + "serve-static": "1.16.2", 1004 + "setprototypeof": "1.2.0", 1005 + "statuses": "2.0.1", 1006 + "type-is": "~1.6.18", 1007 + "utils-merge": "1.0.1", 1008 + "vary": "~1.1.2" 1009 + }, 1010 + "engines": { 1011 + "node": ">= 0.10.0" 1012 + }, 1013 + "funding": { 1014 + "type": "opencollective", 1015 + "url": "https://opencollective.com/express" 1016 + } 1017 + }, 1018 + "node_modules/fast-redact": { 1019 + "version": "3.5.0", 1020 + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", 1021 + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", 1022 + "license": "MIT", 1023 + "engines": { 1024 + "node": ">=6" 1025 + } 1026 + }, 1027 + "node_modules/fill-range": { 1028 + "version": "7.1.1", 1029 + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 1030 + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 1031 + "dev": true, 1032 + "license": "MIT", 1033 + "dependencies": { 1034 + "to-regex-range": "^5.0.1" 1035 + }, 1036 + "engines": { 1037 + "node": ">=8" 1038 + } 1039 + }, 1040 + "node_modules/finalhandler": { 1041 + "version": "1.3.1", 1042 + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 1043 + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 1044 + "license": "MIT", 1045 + "dependencies": { 1046 + "debug": "2.6.9", 1047 + "encodeurl": "~2.0.0", 1048 + "escape-html": "~1.0.3", 1049 + "on-finished": "2.4.1", 1050 + "parseurl": "~1.3.3", 1051 + "statuses": "2.0.1", 1052 + "unpipe": "~1.0.0" 1053 + }, 1054 + "engines": { 1055 + "node": ">= 0.8" 1056 + } 1057 + }, 1058 + "node_modules/forwarded": { 1059 + "version": "0.2.0", 1060 + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 1061 + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 1062 + "license": "MIT", 1063 + "engines": { 1064 + "node": ">= 0.6" 1065 + } 1066 + }, 1067 + "node_modules/fresh": { 1068 + "version": "0.5.2", 1069 + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 1070 + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 1071 + "license": "MIT", 1072 + "engines": { 1073 + "node": ">= 0.6" 1074 + } 1075 + }, 1076 + "node_modules/fsevents": { 1077 + "version": "2.3.3", 1078 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1079 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1080 + "dev": true, 1081 + "hasInstallScript": true, 1082 + "license": "MIT", 1083 + "optional": true, 1084 + "os": [ 1085 + "darwin" 1086 + ], 1087 + "engines": { 1088 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1089 + } 1090 + }, 1091 + "node_modules/function-bind": { 1092 + "version": "1.1.2", 1093 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 1094 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 1095 + "license": "MIT", 1096 + "funding": { 1097 + "url": "https://github.com/sponsors/ljharb" 1098 + } 1099 + }, 1100 + "node_modules/get-caller-file": { 1101 + "version": "2.0.5", 1102 + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1103 + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 1104 + "dev": true, 1105 + "license": "ISC", 1106 + "engines": { 1107 + "node": "6.* || 8.* || >= 10.*" 1108 + } 1109 + }, 1110 + "node_modules/get-intrinsic": { 1111 + "version": "1.3.0", 1112 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 1113 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 1114 + "license": "MIT", 1115 + "dependencies": { 1116 + "call-bind-apply-helpers": "^1.0.2", 1117 + "es-define-property": "^1.0.1", 1118 + "es-errors": "^1.3.0", 1119 + "es-object-atoms": "^1.1.1", 1120 + "function-bind": "^1.1.2", 1121 + "get-proto": "^1.0.1", 1122 + "gopd": "^1.2.0", 1123 + "has-symbols": "^1.1.0", 1124 + "hasown": "^2.0.2", 1125 + "math-intrinsics": "^1.1.0" 1126 + }, 1127 + "engines": { 1128 + "node": ">= 0.4" 1129 + }, 1130 + "funding": { 1131 + "url": "https://github.com/sponsors/ljharb" 1132 + } 1133 + }, 1134 + "node_modules/get-proto": { 1135 + "version": "1.0.1", 1136 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 1137 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 1138 + "license": "MIT", 1139 + "dependencies": { 1140 + "dunder-proto": "^1.0.1", 1141 + "es-object-atoms": "^1.0.0" 1142 + }, 1143 + "engines": { 1144 + "node": ">= 0.4" 1145 + } 1146 + }, 1147 + "node_modules/glob-parent": { 1148 + "version": "5.1.2", 1149 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1150 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1151 + "dev": true, 1152 + "license": "ISC", 1153 + "dependencies": { 1154 + "is-glob": "^4.0.1" 1155 + }, 1156 + "engines": { 1157 + "node": ">= 6" 1158 + } 1159 + }, 1160 + "node_modules/gopd": { 1161 + "version": "1.2.0", 1162 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 1163 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 1164 + "license": "MIT", 1165 + "engines": { 1166 + "node": ">= 0.4" 1167 + }, 1168 + "funding": { 1169 + "url": "https://github.com/sponsors/ljharb" 1170 + } 1171 + }, 1172 + "node_modules/graphemer": { 1173 + "version": "1.4.0", 1174 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 1175 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 1176 + "license": "MIT" 1177 + }, 1178 + "node_modules/has-flag": { 1179 + "version": "3.0.0", 1180 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 1181 + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 1182 + "dev": true, 1183 + "license": "MIT", 1184 + "engines": { 1185 + "node": ">=4" 1186 + } 1187 + }, 1188 + "node_modules/has-symbols": { 1189 + "version": "1.1.0", 1190 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 1191 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 1192 + "license": "MIT", 1193 + "engines": { 1194 + "node": ">= 0.4" 1195 + }, 1196 + "funding": { 1197 + "url": "https://github.com/sponsors/ljharb" 1198 + } 1199 + }, 1200 + "node_modules/hasown": { 1201 + "version": "2.0.2", 1202 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 1203 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1204 + "license": "MIT", 1205 + "dependencies": { 1206 + "function-bind": "^1.1.2" 1207 + }, 1208 + "engines": { 1209 + "node": ">= 0.4" 1210 + } 1211 + }, 1212 + "node_modules/http-errors": { 1213 + "version": "2.0.0", 1214 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 1215 + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1216 + "license": "MIT", 1217 + "dependencies": { 1218 + "depd": "2.0.0", 1219 + "inherits": "2.0.4", 1220 + "setprototypeof": "1.2.0", 1221 + "statuses": "2.0.1", 1222 + "toidentifier": "1.0.1" 1223 + }, 1224 + "engines": { 1225 + "node": ">= 0.8" 1226 + } 1227 + }, 1228 + "node_modules/iconv-lite": { 1229 + "version": "0.4.24", 1230 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1231 + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1232 + "license": "MIT", 1233 + "dependencies": { 1234 + "safer-buffer": ">= 2.1.2 < 3" 1235 + }, 1236 + "engines": { 1237 + "node": ">=0.10.0" 1238 + } 1239 + }, 1240 + "node_modules/ieee754": { 1241 + "version": "1.2.1", 1242 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1243 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1244 + "funding": [ 1245 + { 1246 + "type": "github", 1247 + "url": "https://github.com/sponsors/feross" 1248 + }, 1249 + { 1250 + "type": "patreon", 1251 + "url": "https://www.patreon.com/feross" 1252 + }, 1253 + { 1254 + "type": "consulting", 1255 + "url": "https://feross.org/support" 1256 + } 1257 + ], 1258 + "license": "BSD-3-Clause" 1259 + }, 1260 + "node_modules/ignore-by-default": { 1261 + "version": "1.0.1", 1262 + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 1263 + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 1264 + "dev": true, 1265 + "license": "ISC" 1266 + }, 1267 + "node_modules/inherits": { 1268 + "version": "2.0.4", 1269 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1270 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1271 + "license": "ISC" 1272 + }, 1273 + "node_modules/ipaddr.js": { 1274 + "version": "1.9.1", 1275 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1276 + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 1277 + "license": "MIT", 1278 + "engines": { 1279 + "node": ">= 0.10" 1280 + } 1281 + }, 1282 + "node_modules/is-binary-path": { 1283 + "version": "2.1.0", 1284 + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1285 + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1286 + "dev": true, 1287 + "license": "MIT", 1288 + "dependencies": { 1289 + "binary-extensions": "^2.0.0" 1290 + }, 1291 + "engines": { 1292 + "node": ">=8" 1293 + } 1294 + }, 1295 + "node_modules/is-extglob": { 1296 + "version": "2.1.1", 1297 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1298 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1299 + "dev": true, 1300 + "license": "MIT", 1301 + "engines": { 1302 + "node": ">=0.10.0" 1303 + } 1304 + }, 1305 + "node_modules/is-fullwidth-code-point": { 1306 + "version": "3.0.0", 1307 + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1308 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1309 + "dev": true, 1310 + "license": "MIT", 1311 + "engines": { 1312 + "node": ">=8" 1313 + } 1314 + }, 1315 + "node_modules/is-glob": { 1316 + "version": "4.0.3", 1317 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1318 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1319 + "dev": true, 1320 + "license": "MIT", 1321 + "dependencies": { 1322 + "is-extglob": "^2.1.1" 1323 + }, 1324 + "engines": { 1325 + "node": ">=0.10.0" 1326 + } 1327 + }, 1328 + "node_modules/is-number": { 1329 + "version": "7.0.0", 1330 + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1331 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1332 + "dev": true, 1333 + "license": "MIT", 1334 + "engines": { 1335 + "node": ">=0.12.0" 1336 + } 1337 + }, 1338 + "node_modules/isarray": { 1339 + "version": "1.0.0", 1340 + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1341 + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", 1342 + "license": "MIT" 1343 + }, 1344 + "node_modules/iso-datestring-validator": { 1345 + "version": "2.2.2", 1346 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 1347 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 1348 + "license": "MIT" 1349 + }, 1350 + "node_modules/math-intrinsics": { 1351 + "version": "1.1.0", 1352 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 1353 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 1354 + "license": "MIT", 1355 + "engines": { 1356 + "node": ">= 0.4" 1357 + } 1358 + }, 1359 + "node_modules/media-typer": { 1360 + "version": "0.3.0", 1361 + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1362 + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 1363 + "license": "MIT", 1364 + "engines": { 1365 + "node": ">= 0.6" 1366 + } 1367 + }, 1368 + "node_modules/merge-descriptors": { 1369 + "version": "1.0.3", 1370 + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 1371 + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 1372 + "license": "MIT", 1373 + "funding": { 1374 + "url": "https://github.com/sponsors/sindresorhus" 1375 + } 1376 + }, 1377 + "node_modules/methods": { 1378 + "version": "1.1.2", 1379 + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1380 + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 1381 + "license": "MIT", 1382 + "engines": { 1383 + "node": ">= 0.6" 1384 + } 1385 + }, 1386 + "node_modules/mime": { 1387 + "version": "1.6.0", 1388 + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1389 + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 1390 + "license": "MIT", 1391 + "bin": { 1392 + "mime": "cli.js" 1393 + }, 1394 + "engines": { 1395 + "node": ">=4" 1396 + } 1397 + }, 1398 + "node_modules/mime-db": { 1399 + "version": "1.52.0", 1400 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1401 + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1402 + "license": "MIT", 1403 + "engines": { 1404 + "node": ">= 0.6" 1405 + } 1406 + }, 1407 + "node_modules/mime-types": { 1408 + "version": "2.1.35", 1409 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1410 + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1411 + "license": "MIT", 1412 + "dependencies": { 1413 + "mime-db": "1.52.0" 1414 + }, 1415 + "engines": { 1416 + "node": ">= 0.6" 1417 + } 1418 + }, 1419 + "node_modules/minimatch": { 1420 + "version": "3.1.2", 1421 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1422 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1423 + "dev": true, 1424 + "license": "ISC", 1425 + "dependencies": { 1426 + "brace-expansion": "^1.1.7" 1427 + }, 1428 + "engines": { 1429 + "node": "*" 1430 + } 1431 + }, 1432 + "node_modules/minimist": { 1433 + "version": "1.2.8", 1434 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 1435 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 1436 + "license": "MIT", 1437 + "funding": { 1438 + "url": "https://github.com/sponsors/ljharb" 1439 + } 1440 + }, 1441 + "node_modules/mkdirp": { 1442 + "version": "0.5.6", 1443 + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 1444 + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 1445 + "license": "MIT", 1446 + "dependencies": { 1447 + "minimist": "^1.2.6" 1448 + }, 1449 + "bin": { 1450 + "mkdirp": "bin/cmd.js" 1451 + } 1452 + }, 1453 + "node_modules/ms": { 1454 + "version": "2.0.0", 1455 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1456 + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 1457 + "license": "MIT" 1458 + }, 1459 + "node_modules/multer": { 1460 + "version": "1.4.5-lts.2", 1461 + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", 1462 + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", 1463 + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", 1464 + "license": "MIT", 1465 + "dependencies": { 1466 + "append-field": "^1.0.0", 1467 + "busboy": "^1.0.0", 1468 + "concat-stream": "^1.5.2", 1469 + "mkdirp": "^0.5.4", 1470 + "object-assign": "^4.1.1", 1471 + "type-is": "^1.6.4", 1472 + "xtend": "^4.0.0" 1473 + }, 1474 + "engines": { 1475 + "node": ">= 6.0.0" 1476 + } 1477 + }, 1478 + "node_modules/multiformats": { 1479 + "version": "9.9.0", 1480 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 1481 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 1482 + "license": "(Apache-2.0 AND MIT)" 1483 + }, 1484 + "node_modules/negotiator": { 1485 + "version": "0.6.3", 1486 + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1487 + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1488 + "license": "MIT", 1489 + "engines": { 1490 + "node": ">= 0.6" 1491 + } 1492 + }, 1493 + "node_modules/node-gyp-build-optional-packages": { 1494 + "version": "5.1.1", 1495 + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", 1496 + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", 1497 + "license": "MIT", 1498 + "optional": true, 1499 + "dependencies": { 1500 + "detect-libc": "^2.0.1" 1501 + }, 1502 + "bin": { 1503 + "node-gyp-build-optional-packages": "bin.js", 1504 + "node-gyp-build-optional-packages-optional": "optional.js", 1505 + "node-gyp-build-optional-packages-test": "build-test.js" 1506 + } 1507 + }, 1508 + "node_modules/nodemon": { 1509 + "version": "3.1.10", 1510 + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", 1511 + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", 1512 + "dev": true, 1513 + "license": "MIT", 1514 + "dependencies": { 1515 + "chokidar": "^3.5.2", 1516 + "debug": "^4", 1517 + "ignore-by-default": "^1.0.1", 1518 + "minimatch": "^3.1.2", 1519 + "pstree.remy": "^1.1.8", 1520 + "semver": "^7.5.3", 1521 + "simple-update-notifier": "^2.0.0", 1522 + "supports-color": "^5.5.0", 1523 + "touch": "^3.1.0", 1524 + "undefsafe": "^2.0.5" 1525 + }, 1526 + "bin": { 1527 + "nodemon": "bin/nodemon.js" 1528 + }, 1529 + "engines": { 1530 + "node": ">=10" 1531 + }, 1532 + "funding": { 1533 + "type": "opencollective", 1534 + "url": "https://opencollective.com/nodemon" 1535 + } 1536 + }, 1537 + "node_modules/nodemon/node_modules/debug": { 1538 + "version": "4.4.3", 1539 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1540 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1541 + "dev": true, 1542 + "license": "MIT", 1543 + "dependencies": { 1544 + "ms": "^2.1.3" 1545 + }, 1546 + "engines": { 1547 + "node": ">=6.0" 1548 + }, 1549 + "peerDependenciesMeta": { 1550 + "supports-color": { 1551 + "optional": true 1552 + } 1553 + } 1554 + }, 1555 + "node_modules/nodemon/node_modules/ms": { 1556 + "version": "2.1.3", 1557 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1558 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1559 + "dev": true, 1560 + "license": "MIT" 1561 + }, 1562 + "node_modules/normalize-path": { 1563 + "version": "3.0.0", 1564 + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1565 + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1566 + "dev": true, 1567 + "license": "MIT", 1568 + "engines": { 1569 + "node": ">=0.10.0" 1570 + } 1571 + }, 1572 + "node_modules/object-assign": { 1573 + "version": "4.1.1", 1574 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1575 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1576 + "license": "MIT", 1577 + "engines": { 1578 + "node": ">=0.10.0" 1579 + } 1580 + }, 1581 + "node_modules/object-inspect": { 1582 + "version": "1.13.4", 1583 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 1584 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 1585 + "license": "MIT", 1586 + "engines": { 1587 + "node": ">= 0.4" 1588 + }, 1589 + "funding": { 1590 + "url": "https://github.com/sponsors/ljharb" 1591 + } 1592 + }, 1593 + "node_modules/on-exit-leak-free": { 1594 + "version": "2.1.2", 1595 + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", 1596 + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", 1597 + "license": "MIT", 1598 + "engines": { 1599 + "node": ">=14.0.0" 1600 + } 1601 + }, 1602 + "node_modules/on-finished": { 1603 + "version": "2.4.1", 1604 + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1605 + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1606 + "license": "MIT", 1607 + "dependencies": { 1608 + "ee-first": "1.1.1" 1609 + }, 1610 + "engines": { 1611 + "node": ">= 0.8" 1612 + } 1613 + }, 1614 + "node_modules/parseurl": { 1615 + "version": "1.3.3", 1616 + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1617 + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1618 + "license": "MIT", 1619 + "engines": { 1620 + "node": ">= 0.8" 1621 + } 1622 + }, 1623 + "node_modules/path-to-regexp": { 1624 + "version": "0.1.12", 1625 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 1626 + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 1627 + "license": "MIT" 1628 + }, 1629 + "node_modules/picomatch": { 1630 + "version": "2.3.1", 1631 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1632 + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1633 + "dev": true, 1634 + "license": "MIT", 1635 + "engines": { 1636 + "node": ">=8.6" 1637 + }, 1638 + "funding": { 1639 + "url": "https://github.com/sponsors/jonschlinkert" 1640 + } 1641 + }, 1642 + "node_modules/pino": { 1643 + "version": "8.21.0", 1644 + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", 1645 + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", 1646 + "license": "MIT", 1647 + "dependencies": { 1648 + "atomic-sleep": "^1.0.0", 1649 + "fast-redact": "^3.1.1", 1650 + "on-exit-leak-free": "^2.1.0", 1651 + "pino-abstract-transport": "^1.2.0", 1652 + "pino-std-serializers": "^6.0.0", 1653 + "process-warning": "^3.0.0", 1654 + "quick-format-unescaped": "^4.0.3", 1655 + "real-require": "^0.2.0", 1656 + "safe-stable-stringify": "^2.3.1", 1657 + "sonic-boom": "^3.7.0", 1658 + "thread-stream": "^2.6.0" 1659 + }, 1660 + "bin": { 1661 + "pino": "bin.js" 1662 + } 1663 + }, 1664 + "node_modules/pino-abstract-transport": { 1665 + "version": "1.2.0", 1666 + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", 1667 + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", 1668 + "license": "MIT", 1669 + "dependencies": { 1670 + "readable-stream": "^4.0.0", 1671 + "split2": "^4.0.0" 1672 + } 1673 + }, 1674 + "node_modules/pino-abstract-transport/node_modules/readable-stream": { 1675 + "version": "4.7.0", 1676 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", 1677 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", 1678 + "license": "MIT", 1679 + "dependencies": { 1680 + "abort-controller": "^3.0.0", 1681 + "buffer": "^6.0.3", 1682 + "events": "^3.3.0", 1683 + "process": "^0.11.10", 1684 + "string_decoder": "^1.3.0" 1685 + }, 1686 + "engines": { 1687 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1688 + } 1689 + }, 1690 + "node_modules/pino-abstract-transport/node_modules/string_decoder": { 1691 + "version": "1.3.0", 1692 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1693 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1694 + "license": "MIT", 1695 + "dependencies": { 1696 + "safe-buffer": "~5.2.0" 1697 + } 1698 + }, 1699 + "node_modules/pino-std-serializers": { 1700 + "version": "6.2.2", 1701 + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", 1702 + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", 1703 + "license": "MIT" 1704 + }, 1705 + "node_modules/process": { 1706 + "version": "0.11.10", 1707 + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", 1708 + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", 1709 + "license": "MIT", 1710 + "engines": { 1711 + "node": ">= 0.6.0" 1712 + } 1713 + }, 1714 + "node_modules/process-nextick-args": { 1715 + "version": "2.0.1", 1716 + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1717 + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 1718 + "license": "MIT" 1719 + }, 1720 + "node_modules/process-warning": { 1721 + "version": "3.0.0", 1722 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", 1723 + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", 1724 + "license": "MIT" 1725 + }, 1726 + "node_modules/proxy-addr": { 1727 + "version": "2.0.7", 1728 + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1729 + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1730 + "license": "MIT", 1731 + "dependencies": { 1732 + "forwarded": "0.2.0", 1733 + "ipaddr.js": "1.9.1" 1734 + }, 1735 + "engines": { 1736 + "node": ">= 0.10" 1737 + } 1738 + }, 1739 + "node_modules/pstree.remy": { 1740 + "version": "1.1.8", 1741 + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 1742 + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 1743 + "dev": true, 1744 + "license": "MIT" 1745 + }, 1746 + "node_modules/qs": { 1747 + "version": "6.13.0", 1748 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 1749 + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 1750 + "license": "BSD-3-Clause", 1751 + "dependencies": { 1752 + "side-channel": "^1.0.6" 1753 + }, 1754 + "engines": { 1755 + "node": ">=0.6" 1756 + }, 1757 + "funding": { 1758 + "url": "https://github.com/sponsors/ljharb" 1759 + } 1760 + }, 1761 + "node_modules/quick-format-unescaped": { 1762 + "version": "4.0.4", 1763 + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 1764 + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", 1765 + "license": "MIT" 1766 + }, 1767 + "node_modules/range-parser": { 1768 + "version": "1.2.1", 1769 + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1770 + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1771 + "license": "MIT", 1772 + "engines": { 1773 + "node": ">= 0.6" 1774 + } 1775 + }, 1776 + "node_modules/rate-limiter-flexible": { 1777 + "version": "2.4.2", 1778 + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.4.2.tgz", 1779 + "integrity": "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw==", 1780 + "license": "ISC" 1781 + }, 1782 + "node_modules/raw-body": { 1783 + "version": "2.5.2", 1784 + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1785 + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1786 + "license": "MIT", 1787 + "dependencies": { 1788 + "bytes": "3.1.2", 1789 + "http-errors": "2.0.0", 1790 + "iconv-lite": "0.4.24", 1791 + "unpipe": "1.0.0" 1792 + }, 1793 + "engines": { 1794 + "node": ">= 0.8" 1795 + } 1796 + }, 1797 + "node_modules/readable-stream": { 1798 + "version": "2.3.8", 1799 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 1800 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 1801 + "license": "MIT", 1802 + "dependencies": { 1803 + "core-util-is": "~1.0.0", 1804 + "inherits": "~2.0.3", 1805 + "isarray": "~1.0.0", 1806 + "process-nextick-args": "~2.0.0", 1807 + "safe-buffer": "~5.1.1", 1808 + "string_decoder": "~1.1.1", 1809 + "util-deprecate": "~1.0.1" 1810 + } 1811 + }, 1812 + "node_modules/readable-stream/node_modules/safe-buffer": { 1813 + "version": "5.1.2", 1814 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1815 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 1816 + "license": "MIT" 1817 + }, 1818 + "node_modules/readdirp": { 1819 + "version": "3.6.0", 1820 + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1821 + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1822 + "dev": true, 1823 + "license": "MIT", 1824 + "dependencies": { 1825 + "picomatch": "^2.2.1" 1826 + }, 1827 + "engines": { 1828 + "node": ">=8.10.0" 1829 + } 1830 + }, 1831 + "node_modules/real-require": { 1832 + "version": "0.2.0", 1833 + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 1834 + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 1835 + "license": "MIT", 1836 + "engines": { 1837 + "node": ">= 12.13.0" 1838 + } 1839 + }, 1840 + "node_modules/require-directory": { 1841 + "version": "2.1.1", 1842 + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1843 + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1844 + "dev": true, 1845 + "license": "MIT", 1846 + "engines": { 1847 + "node": ">=0.10.0" 1848 + } 1849 + }, 1850 + "node_modules/rss": { 1851 + "version": "1.2.2", 1852 + "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", 1853 + "integrity": "sha512-xUhRTgslHeCBeHAqaWSbOYTydN2f0tAzNXvzh3stjz7QDhQMzdgHf3pfgNIngeytQflrFPfy6axHilTETr6gDg==", 1854 + "license": "MIT", 1855 + "dependencies": { 1856 + "mime-types": "2.1.13", 1857 + "xml": "1.0.1" 1858 + } 1859 + }, 1860 + "node_modules/rss/node_modules/mime-db": { 1861 + "version": "1.25.0", 1862 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", 1863 + "integrity": "sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w==", 1864 + "license": "MIT", 1865 + "engines": { 1866 + "node": ">= 0.6" 1867 + } 1868 + }, 1869 + "node_modules/rss/node_modules/mime-types": { 1870 + "version": "2.1.13", 1871 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", 1872 + "integrity": "sha512-ryBDp1Z/6X90UvjUK3RksH0IBPM137T7cmg4OgD5wQBojlAiUwuok0QeELkim/72EtcYuNlmbkrcGuxj3Kl0YQ==", 1873 + "license": "MIT", 1874 + "dependencies": { 1875 + "mime-db": "~1.25.0" 1876 + }, 1877 + "engines": { 1878 + "node": ">= 0.6" 1879 + } 1880 + }, 1881 + "node_modules/rxjs": { 1882 + "version": "7.8.2", 1883 + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", 1884 + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", 1885 + "dev": true, 1886 + "license": "Apache-2.0", 1887 + "dependencies": { 1888 + "tslib": "^2.1.0" 1889 + } 1890 + }, 1891 + "node_modules/safe-buffer": { 1892 + "version": "5.2.1", 1893 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1894 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1895 + "funding": [ 1896 + { 1897 + "type": "github", 1898 + "url": "https://github.com/sponsors/feross" 1899 + }, 1900 + { 1901 + "type": "patreon", 1902 + "url": "https://www.patreon.com/feross" 1903 + }, 1904 + { 1905 + "type": "consulting", 1906 + "url": "https://feross.org/support" 1907 + } 1908 + ], 1909 + "license": "MIT" 1910 + }, 1911 + "node_modules/safe-stable-stringify": { 1912 + "version": "2.5.0", 1913 + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", 1914 + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", 1915 + "license": "MIT", 1916 + "engines": { 1917 + "node": ">=10" 1918 + } 1919 + }, 1920 + "node_modules/safer-buffer": { 1921 + "version": "2.1.2", 1922 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1923 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1924 + "license": "MIT" 1925 + }, 1926 + "node_modules/semver": { 1927 + "version": "7.7.3", 1928 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 1929 + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 1930 + "dev": true, 1931 + "license": "ISC", 1932 + "bin": { 1933 + "semver": "bin/semver.js" 1934 + }, 1935 + "engines": { 1936 + "node": ">=10" 1937 + } 1938 + }, 1939 + "node_modules/send": { 1940 + "version": "0.19.0", 1941 + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 1942 + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1943 + "license": "MIT", 1944 + "dependencies": { 1945 + "debug": "2.6.9", 1946 + "depd": "2.0.0", 1947 + "destroy": "1.2.0", 1948 + "encodeurl": "~1.0.2", 1949 + "escape-html": "~1.0.3", 1950 + "etag": "~1.8.1", 1951 + "fresh": "0.5.2", 1952 + "http-errors": "2.0.0", 1953 + "mime": "1.6.0", 1954 + "ms": "2.1.3", 1955 + "on-finished": "2.4.1", 1956 + "range-parser": "~1.2.1", 1957 + "statuses": "2.0.1" 1958 + }, 1959 + "engines": { 1960 + "node": ">= 0.8.0" 1961 + } 1962 + }, 1963 + "node_modules/send/node_modules/encodeurl": { 1964 + "version": "1.0.2", 1965 + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1966 + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 1967 + "license": "MIT", 1968 + "engines": { 1969 + "node": ">= 0.8" 1970 + } 1971 + }, 1972 + "node_modules/send/node_modules/ms": { 1973 + "version": "2.1.3", 1974 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1975 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1976 + "license": "MIT" 1977 + }, 1978 + "node_modules/serve-static": { 1979 + "version": "1.16.2", 1980 + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 1981 + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 1982 + "license": "MIT", 1983 + "dependencies": { 1984 + "encodeurl": "~2.0.0", 1985 + "escape-html": "~1.0.3", 1986 + "parseurl": "~1.3.3", 1987 + "send": "0.19.0" 1988 + }, 1989 + "engines": { 1990 + "node": ">= 0.8.0" 1991 + } 1992 + }, 1993 + "node_modules/setprototypeof": { 1994 + "version": "1.2.0", 1995 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1996 + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 1997 + "license": "ISC" 1998 + }, 1999 + "node_modules/shell-quote": { 2000 + "version": "1.8.3", 2001 + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", 2002 + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", 2003 + "dev": true, 2004 + "license": "MIT", 2005 + "engines": { 2006 + "node": ">= 0.4" 2007 + }, 2008 + "funding": { 2009 + "url": "https://github.com/sponsors/ljharb" 2010 + } 2011 + }, 2012 + "node_modules/side-channel": { 2013 + "version": "1.1.0", 2014 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 2015 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 2016 + "license": "MIT", 2017 + "dependencies": { 2018 + "es-errors": "^1.3.0", 2019 + "object-inspect": "^1.13.3", 2020 + "side-channel-list": "^1.0.0", 2021 + "side-channel-map": "^1.0.1", 2022 + "side-channel-weakmap": "^1.0.2" 2023 + }, 2024 + "engines": { 2025 + "node": ">= 0.4" 2026 + }, 2027 + "funding": { 2028 + "url": "https://github.com/sponsors/ljharb" 2029 + } 2030 + }, 2031 + "node_modules/side-channel-list": { 2032 + "version": "1.0.0", 2033 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 2034 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 2035 + "license": "MIT", 2036 + "dependencies": { 2037 + "es-errors": "^1.3.0", 2038 + "object-inspect": "^1.13.3" 2039 + }, 2040 + "engines": { 2041 + "node": ">= 0.4" 2042 + }, 2043 + "funding": { 2044 + "url": "https://github.com/sponsors/ljharb" 2045 + } 2046 + }, 2047 + "node_modules/side-channel-map": { 2048 + "version": "1.0.1", 2049 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 2050 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 2051 + "license": "MIT", 2052 + "dependencies": { 2053 + "call-bound": "^1.0.2", 2054 + "es-errors": "^1.3.0", 2055 + "get-intrinsic": "^1.2.5", 2056 + "object-inspect": "^1.13.3" 2057 + }, 2058 + "engines": { 2059 + "node": ">= 0.4" 2060 + }, 2061 + "funding": { 2062 + "url": "https://github.com/sponsors/ljharb" 2063 + } 2064 + }, 2065 + "node_modules/side-channel-weakmap": { 2066 + "version": "1.0.2", 2067 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 2068 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 2069 + "license": "MIT", 2070 + "dependencies": { 2071 + "call-bound": "^1.0.2", 2072 + "es-errors": "^1.3.0", 2073 + "get-intrinsic": "^1.2.5", 2074 + "object-inspect": "^1.13.3", 2075 + "side-channel-map": "^1.0.1" 2076 + }, 2077 + "engines": { 2078 + "node": ">= 0.4" 2079 + }, 2080 + "funding": { 2081 + "url": "https://github.com/sponsors/ljharb" 2082 + } 2083 + }, 2084 + "node_modules/simple-update-notifier": { 2085 + "version": "2.0.0", 2086 + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 2087 + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 2088 + "dev": true, 2089 + "license": "MIT", 2090 + "dependencies": { 2091 + "semver": "^7.5.3" 2092 + }, 2093 + "engines": { 2094 + "node": ">=10" 2095 + } 2096 + }, 2097 + "node_modules/sonic-boom": { 2098 + "version": "3.8.1", 2099 + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", 2100 + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", 2101 + "license": "MIT", 2102 + "dependencies": { 2103 + "atomic-sleep": "^1.0.0" 2104 + } 2105 + }, 2106 + "node_modules/split2": { 2107 + "version": "4.2.0", 2108 + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 2109 + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 2110 + "license": "ISC", 2111 + "engines": { 2112 + "node": ">= 10.x" 2113 + } 2114 + }, 2115 + "node_modules/statuses": { 2116 + "version": "2.0.1", 2117 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 2118 + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 2119 + "license": "MIT", 2120 + "engines": { 2121 + "node": ">= 0.8" 2122 + } 2123 + }, 2124 + "node_modules/streamsearch": { 2125 + "version": "1.1.0", 2126 + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 2127 + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 2128 + "engines": { 2129 + "node": ">=10.0.0" 2130 + } 2131 + }, 2132 + "node_modules/string_decoder": { 2133 + "version": "1.1.1", 2134 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 2135 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 2136 + "license": "MIT", 2137 + "dependencies": { 2138 + "safe-buffer": "~5.1.0" 2139 + } 2140 + }, 2141 + "node_modules/string_decoder/node_modules/safe-buffer": { 2142 + "version": "5.1.2", 2143 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 2144 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 2145 + "license": "MIT" 2146 + }, 2147 + "node_modules/string-width": { 2148 + "version": "4.2.3", 2149 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 2150 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 2151 + "dev": true, 2152 + "license": "MIT", 2153 + "dependencies": { 2154 + "emoji-regex": "^8.0.0", 2155 + "is-fullwidth-code-point": "^3.0.0", 2156 + "strip-ansi": "^6.0.1" 2157 + }, 2158 + "engines": { 2159 + "node": ">=8" 2160 + } 2161 + }, 2162 + "node_modules/strip-ansi": { 2163 + "version": "6.0.1", 2164 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 2165 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 2166 + "dev": true, 2167 + "license": "MIT", 2168 + "dependencies": { 2169 + "ansi-regex": "^5.0.1" 2170 + }, 2171 + "engines": { 2172 + "node": ">=8" 2173 + } 2174 + }, 2175 + "node_modules/supports-color": { 2176 + "version": "5.5.0", 2177 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 2178 + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 2179 + "dev": true, 2180 + "license": "MIT", 2181 + "dependencies": { 2182 + "has-flag": "^3.0.0" 2183 + }, 2184 + "engines": { 2185 + "node": ">=4" 2186 + } 2187 + }, 2188 + "node_modules/thread-stream": { 2189 + "version": "2.7.0", 2190 + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", 2191 + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", 2192 + "license": "MIT", 2193 + "dependencies": { 2194 + "real-require": "^0.2.0" 2195 + } 2196 + }, 2197 + "node_modules/tlds": { 2198 + "version": "1.260.0", 2199 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.260.0.tgz", 2200 + "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", 2201 + "license": "MIT", 2202 + "bin": { 2203 + "tlds": "bin.js" 2204 + } 2205 + }, 2206 + "node_modules/to-regex-range": { 2207 + "version": "5.0.1", 2208 + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 2209 + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 2210 + "dev": true, 2211 + "license": "MIT", 2212 + "dependencies": { 2213 + "is-number": "^7.0.0" 2214 + }, 2215 + "engines": { 2216 + "node": ">=8.0" 2217 + } 2218 + }, 2219 + "node_modules/toidentifier": { 2220 + "version": "1.0.1", 2221 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 2222 + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 2223 + "license": "MIT", 2224 + "engines": { 2225 + "node": ">=0.6" 2226 + } 2227 + }, 2228 + "node_modules/touch": { 2229 + "version": "3.1.1", 2230 + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", 2231 + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", 2232 + "dev": true, 2233 + "license": "ISC", 2234 + "bin": { 2235 + "nodetouch": "bin/nodetouch.js" 2236 + } 2237 + }, 2238 + "node_modules/tree-kill": { 2239 + "version": "1.2.2", 2240 + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 2241 + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 2242 + "dev": true, 2243 + "license": "MIT", 2244 + "bin": { 2245 + "tree-kill": "cli.js" 2246 + } 2247 + }, 2248 + "node_modules/tslib": { 2249 + "version": "2.8.1", 2250 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2251 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2252 + "dev": true, 2253 + "license": "0BSD" 2254 + }, 2255 + "node_modules/type-is": { 2256 + "version": "1.6.18", 2257 + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 2258 + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 2259 + "license": "MIT", 2260 + "dependencies": { 2261 + "media-typer": "0.3.0", 2262 + "mime-types": "~2.1.24" 2263 + }, 2264 + "engines": { 2265 + "node": ">= 0.6" 2266 + } 2267 + }, 2268 + "node_modules/typedarray": { 2269 + "version": "0.0.6", 2270 + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 2271 + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", 2272 + "license": "MIT" 2273 + }, 2274 + "node_modules/uint8arrays": { 2275 + "version": "3.0.0", 2276 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 2277 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 2278 + "license": "MIT", 2279 + "dependencies": { 2280 + "multiformats": "^9.4.2" 2281 + } 2282 + }, 2283 + "node_modules/undefsafe": { 2284 + "version": "2.0.5", 2285 + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 2286 + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 2287 + "dev": true, 2288 + "license": "MIT" 2289 + }, 2290 + "node_modules/unpipe": { 2291 + "version": "1.0.0", 2292 + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2293 + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 2294 + "license": "MIT", 2295 + "engines": { 2296 + "node": ">= 0.8" 2297 + } 2298 + }, 2299 + "node_modules/util-deprecate": { 2300 + "version": "1.0.2", 2301 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 2302 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 2303 + "license": "MIT" 2304 + }, 2305 + "node_modules/utils-merge": { 2306 + "version": "1.0.1", 2307 + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 2308 + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 2309 + "license": "MIT", 2310 + "engines": { 2311 + "node": ">= 0.4.0" 2312 + } 2313 + }, 2314 + "node_modules/vary": { 2315 + "version": "1.1.2", 2316 + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2317 + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 2318 + "license": "MIT", 2319 + "engines": { 2320 + "node": ">= 0.8" 2321 + } 2322 + }, 2323 + "node_modules/wrap-ansi": { 2324 + "version": "7.0.0", 2325 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 2326 + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 2327 + "dev": true, 2328 + "license": "MIT", 2329 + "dependencies": { 2330 + "ansi-styles": "^4.0.0", 2331 + "string-width": "^4.1.0", 2332 + "strip-ansi": "^6.0.0" 2333 + }, 2334 + "engines": { 2335 + "node": ">=10" 2336 + }, 2337 + "funding": { 2338 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 2339 + } 2340 + }, 2341 + "node_modules/ws": { 2342 + "version": "8.18.3", 2343 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", 2344 + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", 2345 + "license": "MIT", 2346 + "engines": { 2347 + "node": ">=10.0.0" 2348 + }, 2349 + "peerDependencies": { 2350 + "bufferutil": "^4.0.1", 2351 + "utf-8-validate": ">=5.0.2" 2352 + }, 2353 + "peerDependenciesMeta": { 2354 + "bufferutil": { 2355 + "optional": true 2356 + }, 2357 + "utf-8-validate": { 2358 + "optional": true 2359 + } 2360 + } 2361 + }, 2362 + "node_modules/xml": { 2363 + "version": "1.0.1", 2364 + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", 2365 + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", 2366 + "license": "MIT" 2367 + }, 2368 + "node_modules/xtend": { 2369 + "version": "4.0.2", 2370 + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 2371 + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 2372 + "license": "MIT", 2373 + "engines": { 2374 + "node": ">=0.4" 2375 + } 2376 + }, 2377 + "node_modules/y18n": { 2378 + "version": "5.0.8", 2379 + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 2380 + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 2381 + "dev": true, 2382 + "license": "ISC", 2383 + "engines": { 2384 + "node": ">=10" 2385 + } 2386 + }, 2387 + "node_modules/yargs": { 2388 + "version": "17.7.2", 2389 + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 2390 + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 2391 + "dev": true, 2392 + "license": "MIT", 2393 + "dependencies": { 2394 + "cliui": "^8.0.1", 2395 + "escalade": "^3.1.1", 2396 + "get-caller-file": "^2.0.5", 2397 + "require-directory": "^2.1.1", 2398 + "string-width": "^4.2.3", 2399 + "y18n": "^5.0.5", 2400 + "yargs-parser": "^21.1.1" 2401 + }, 2402 + "engines": { 2403 + "node": ">=12" 2404 + } 2405 + }, 2406 + "node_modules/yargs-parser": { 2407 + "version": "21.1.1", 2408 + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 2409 + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 2410 + "dev": true, 2411 + "license": "ISC", 2412 + "engines": { 2413 + "node": ">=12" 2414 + } 2415 + }, 2416 + "node_modules/zod": { 2417 + "version": "3.25.76", 2418 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 2419 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 2420 + "license": "MIT", 2421 + "funding": { 2422 + "url": "https://github.com/sponsors/colinhacks" 2423 + } 2424 + } 2425 + } 2426 + }
+40
package.json
··· 1 + { 2 + "name": "atproto-podcast", 3 + "version": "1.0.0", 4 + "description": "AT Protocol podcast hosting and media player with blob storage", 5 + "main": "src/index.js", 6 + "type": "module", 7 + "scripts": { 8 + "start": "node src/index.js", 9 + "dev": "node --watch src/index.js", 10 + "client": "cd client && npm run dev", 11 + "client:build": "cd client && npm run build", 12 + "build": "npm run client:build", 13 + "dev:all": "concurrently \"npm run dev\" \"npm run client\"", 14 + "postinstall": "cd client && npm install", 15 + "railway:build": "npm install && npm run build", 16 + "railway:start": "npm start" 17 + }, 18 + "keywords": [ 19 + "atproto", 20 + "podcast", 21 + "rss", 22 + "bluesky", 23 + "media" 24 + ], 25 + "author": "", 26 + "license": "MIT", 27 + "dependencies": { 28 + "@atproto/api": "^0.13.17", 29 + "@atproto/xrpc-server": "^0.7.3", 30 + "cors": "^2.8.5", 31 + "dotenv": "^16.4.7", 32 + "express": "^4.21.2", 33 + "multer": "^1.4.5-lts.1", 34 + "rss": "^1.2.2" 35 + }, 36 + "devDependencies": { 37 + "concurrently": "^9.2.1", 38 + "nodemon": "^3.1.9" 39 + } 40 + }
+321
public/app.js
··· 1 + const API_BASE = window.location.origin; 2 + 3 + // Initialize on page load 4 + document.addEventListener('DOMContentLoaded', () => { 5 + loadFeedMetadata(); 6 + loadEpisodes(); 7 + setupEventListeners(); 8 + updateRSSFeedUrl(); 9 + }); 10 + 11 + function setupEventListeners() { 12 + // Navigation 13 + document.querySelectorAll('.nav-item').forEach(item => { 14 + item.addEventListener('click', handleNavigation); 15 + }); 16 + 17 + // Upload form 18 + document.getElementById('uploadForm').addEventListener('submit', handleUpload); 19 + 20 + // Feed metadata form 21 + document.getElementById('feedForm').addEventListener('submit', handleFeedUpdate); 22 + 23 + // Create New button 24 + document.getElementById('createNewBtn').addEventListener('click', () => { 25 + openUploadModal(); 26 + }); 27 + 28 + // Upload Episode button 29 + document.getElementById('uploadEpisodeBtn').addEventListener('click', () => { 30 + openUploadModal(); 31 + }); 32 + 33 + // Create first episode button 34 + document.getElementById('createFirstEpisodeBtn').addEventListener('click', () => { 35 + openUploadModal(); 36 + }); 37 + } 38 + 39 + // Navigation 40 + function handleNavigation(e) { 41 + e.preventDefault(); 42 + 43 + // Remove active class from all nav items 44 + document.querySelectorAll('.nav-item').forEach(item => { 45 + item.classList.remove('active'); 46 + }); 47 + 48 + // Add active class to clicked item 49 + e.currentTarget.classList.add('active'); 50 + 51 + // Hide all views 52 + document.getElementById('dashboardView').style.display = 'none'; 53 + document.getElementById('episodesView').style.display = 'none'; 54 + document.getElementById('settingsView').style.display = 'none'; 55 + 56 + // Show selected view 57 + const navId = e.currentTarget.id; 58 + if (navId === 'navEpisodes') { 59 + document.getElementById('episodesView').style.display = 'block'; 60 + } else if (navId === 'navSettings') { 61 + document.getElementById('settingsView').style.display = 'block'; 62 + } else { 63 + // Default to dashboard 64 + document.getElementById('dashboardView').style.display = 'block'; 65 + } 66 + } 67 + 68 + // Modal functions 69 + function openUploadModal() { 70 + document.getElementById('uploadModal').style.display = 'flex'; 71 + } 72 + 73 + function closeUploadModal() { 74 + document.getElementById('uploadModal').style.display = 'none'; 75 + document.getElementById('uploadForm').reset(); 76 + hideStatus('uploadStatus'); 77 + } 78 + 79 + // Upload episode 80 + async function handleUpload(e) { 81 + e.preventDefault(); 82 + 83 + const fileInput = document.getElementById('audioFile'); 84 + const title = document.getElementById('title').value; 85 + const description = document.getElementById('description').value; 86 + 87 + if (!fileInput.files[0]) { 88 + showStatus('uploadStatus', 'Please select an audio file', 'error'); 89 + return; 90 + } 91 + 92 + const formData = new FormData(); 93 + formData.append('audio', fileInput.files[0]); 94 + formData.append('title', title); 95 + formData.append('description', description); 96 + 97 + // Show loading state 98 + setUploadLoading(true); 99 + hideStatus('uploadStatus'); 100 + 101 + try { 102 + const response = await fetch(`${API_BASE}/api/upload/episode`, { 103 + method: 'POST', 104 + body: formData 105 + }); 106 + 107 + const data = await response.json(); 108 + 109 + if (response.ok) { 110 + showStatus('uploadStatus', `Episode "${title}" uploaded successfully!`, 'success'); 111 + 112 + // Reset form 113 + document.getElementById('uploadForm').reset(); 114 + 115 + // Reload episodes list 116 + await loadEpisodes(); 117 + 118 + // Close modal after a brief delay 119 + setTimeout(() => { 120 + closeUploadModal(); 121 + }, 1500); 122 + } else { 123 + throw new Error(data.error || 'Upload failed'); 124 + } 125 + } catch (error) { 126 + console.error('Upload error:', error); 127 + showStatus('uploadStatus', `Upload failed: ${error.message}`, 'error'); 128 + } finally { 129 + setUploadLoading(false); 130 + } 131 + } 132 + 133 + function setUploadLoading(loading) { 134 + const btn = document.querySelector('#uploadForm button[type="submit"]'); 135 + const btnText = document.getElementById('uploadBtnText'); 136 + const spinner = document.getElementById('uploadSpinner'); 137 + 138 + btn.disabled = loading; 139 + btnText.style.display = loading ? 'none' : 'inline'; 140 + spinner.style.display = loading ? 'inline-block' : 'none'; 141 + } 142 + 143 + // Load and display episodes 144 + async function loadEpisodes() { 145 + const episodesListContainer = document.getElementById('episodesList'); 146 + const dashboardEpisodesContainer = document.getElementById('dashboardEpisodesList'); 147 + const emptyBanner = document.getElementById('episodesEmptyBanner'); 148 + 149 + try { 150 + const response = await fetch(`${API_BASE}/api/media/episodes`); 151 + const data = await response.json(); 152 + 153 + if (data.episodes && data.episodes.length > 0) { 154 + // Render full episodes list 155 + const episodesHTML = data.episodes.map(episode => createEpisodeCard(episode)).join(''); 156 + episodesListContainer.innerHTML = episodesHTML; 157 + 158 + // Render dashboard preview (latest 3 episodes) 159 + const latestEpisodes = data.episodes.slice(0, 3); 160 + dashboardEpisodesContainer.innerHTML = latestEpisodes.map(episode => createEpisodeCard(episode, true)).join(''); 161 + 162 + // Hide empty state banner 163 + emptyBanner.style.display = 'none'; 164 + } else { 165 + // Show empty states 166 + episodesListContainer.innerHTML = ` 167 + <div class="empty-state"> 168 + <div class="empty-state-icon">🎙️</div> 169 + <p>No episodes yet. Upload your first episode to get started!</p> 170 + </div> 171 + `; 172 + 173 + dashboardEpisodesContainer.innerHTML = ''; 174 + emptyBanner.style.display = 'block'; 175 + } 176 + } catch (error) { 177 + console.error('Error loading episodes:', error); 178 + episodesListContainer.innerHTML = `<p class="loading">Failed to load episodes: ${error.message}</p>`; 179 + } 180 + } 181 + 182 + function createEpisodeCard(episode, compact = false) { 183 + const date = new Date(episode.pubDate).toLocaleDateString('en-US', { 184 + year: 'numeric', 185 + month: 'long', 186 + day: 'numeric' 187 + }); 188 + 189 + const size = formatFileSize(episode.blob.size); 190 + 191 + return ` 192 + <div class="episode-item"> 193 + <div class="episode-header"> 194 + <div> 195 + <h3 class="episode-title">${escapeHtml(episode.title)}</h3> 196 + <div class="episode-date">${date}</div> 197 + </div> 198 + </div> 199 + ${episode.description ? `<p class="episode-description">${escapeHtml(episode.description)}</p>` : ''} 200 + <div class="episode-meta"> 201 + <span>📦 ${size}</span> 202 + <span>🎵 ${episode.blob.mimeType}</span> 203 + </div> 204 + <audio class="audio-player" controls preload="metadata"> 205 + <source src="${API_BASE}${episode.streamUrl}" type="${episode.blob.mimeType}"> 206 + Your browser does not support the audio element. 207 + </audio> 208 + </div> 209 + `; 210 + } 211 + 212 + // Feed metadata management 213 + async function loadFeedMetadata() { 214 + try { 215 + const response = await fetch(`${API_BASE}/api/feed/metadata`); 216 + const data = await response.json(); 217 + 218 + document.getElementById('podcastTitle').value = data.title || ''; 219 + document.getElementById('podcastDescription').value = data.description || ''; 220 + document.getElementById('podcastLink').value = data.link || ''; 221 + document.getElementById('podcastLanguage').value = data.language || 'en'; 222 + 223 + // Update the main podcast title in the header 224 + if (data.title) { 225 + document.getElementById('mainPodcastTitle').textContent = data.title; 226 + } 227 + } catch (error) { 228 + console.error('Error loading feed metadata:', error); 229 + } 230 + } 231 + 232 + async function handleFeedUpdate(e) { 233 + e.preventDefault(); 234 + 235 + const feedData = { 236 + title: document.getElementById('podcastTitle').value, 237 + description: document.getElementById('podcastDescription').value, 238 + link: document.getElementById('podcastLink').value, 239 + language: document.getElementById('podcastLanguage').value 240 + }; 241 + 242 + try { 243 + const response = await fetch(`${API_BASE}/api/feed/metadata`, { 244 + method: 'POST', 245 + headers: { 246 + 'Content-Type': 'application/json' 247 + }, 248 + body: JSON.stringify(feedData) 249 + }); 250 + 251 + const data = await response.json(); 252 + 253 + if (response.ok) { 254 + showStatus('feedStatus', 'Feed settings saved successfully!', 'success'); 255 + 256 + // Update the main podcast title in the header 257 + if (feedData.title) { 258 + document.getElementById('mainPodcastTitle').textContent = feedData.title; 259 + } 260 + } else { 261 + throw new Error(data.error || 'Update failed'); 262 + } 263 + } catch (error) { 264 + console.error('Feed update error:', error); 265 + showStatus('feedStatus', `Update failed: ${error.message}`, 'error'); 266 + } 267 + } 268 + 269 + // RSS Feed URL 270 + function updateRSSFeedUrl() { 271 + const url = `${API_BASE}/api/feed/rss`; 272 + document.getElementById('rssFeedUrl').textContent = url; 273 + } 274 + 275 + function copyRSSUrl(event) { 276 + const url = document.getElementById('rssFeedUrl').textContent; 277 + navigator.clipboard.writeText(url).then(() => { 278 + const btn = event.target; 279 + const originalText = btn.textContent; 280 + btn.textContent = 'Copied!'; 281 + setTimeout(() => { 282 + btn.textContent = originalText; 283 + }, 2000); 284 + }).catch(err => { 285 + console.error('Failed to copy:', err); 286 + alert('Failed to copy URL to clipboard'); 287 + }); 288 + } 289 + 290 + // Utility functions 291 + function showStatus(elementId, message, type) { 292 + const element = document.getElementById(elementId); 293 + element.textContent = message; 294 + element.className = `status-message ${type}`; 295 + 296 + // Auto-hide success messages after 5 seconds 297 + if (type === 'success') { 298 + setTimeout(() => hideStatus(elementId), 5000); 299 + } 300 + } 301 + 302 + function hideStatus(elementId) { 303 + const element = document.getElementById(elementId); 304 + element.className = 'status-message'; 305 + } 306 + 307 + function formatFileSize(bytes) { 308 + if (bytes === 0) return '0 Bytes'; 309 + 310 + const k = 1024; 311 + const sizes = ['Bytes', 'KB', 'MB', 'GB']; 312 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 313 + 314 + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; 315 + } 316 + 317 + function escapeHtml(text) { 318 + const div = document.createElement('div'); 319 + div.textContent = text; 320 + return div.innerHTML; 321 + }
+289
public/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>AT Protocol Podcast Host</title> 7 + <link rel="stylesheet" href="styles.css"> 8 + </head> 9 + <body> 10 + <!-- Top Header --> 11 + <header class="top-header"> 12 + <div class="header-left"> 13 + <div class="logo"> 14 + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 15 + <circle cx="12" cy="12" r="10" fill="#C75B9B"/> 16 + <path d="M12 6v12M8 10l4-4 4 4" stroke="white" stroke-width="2" stroke-linecap="round"/> 17 + </svg> 18 + <span class="logo-text">AT Protocol</span> 19 + </div> 20 + </div> 21 + <div class="header-right"> 22 + <div class="search-container"> 23 + <input type="text" class="search-input" placeholder="Search for anything..."> 24 + <span class="search-shortcut">⌘K</span> 25 + </div> 26 + <button class="header-btn"> 27 + <span class="icon">🎙️</span> 28 + My Podcasts 29 + </button> 30 + <button class="user-avatar">A</button> 31 + </div> 32 + </header> 33 + 34 + <div class="app-container"> 35 + <!-- Sidebar Navigation --> 36 + <aside class="sidebar"> 37 + <nav> 38 + <div class="nav-section"> 39 + <div class="nav-section-title">My Captivate</div> 40 + <a href="#" class="nav-item active"> 41 + <svg class="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 42 + <rect x="3" y="3" width="7" height="7"></rect> 43 + <rect x="14" y="3" width="7" height="7"></rect> 44 + <rect x="14" y="14" width="7" height="7"></rect> 45 + <rect x="3" y="14" width="7" height="7"></rect> 46 + </svg> 47 + <span>Dashboard</span> 48 + </a> 49 + <a href="#" class="nav-item"> 50 + <svg class="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 51 + <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path> 52 + </svg> 53 + <span>My Podcasts</span> 54 + </a> 55 + </div> 56 + 57 + <div class="nav-section"> 58 + <div class="nav-section-title">Content Management</div> 59 + <a href="#" class="nav-item" id="navEpisodes"> 60 + <svg class="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 61 + <polygon points="5 3 19 12 5 21 5 3"></polygon> 62 + </svg> 63 + <span>Episodes</span> 64 + </a> 65 + <a href="#" class="nav-item"> 66 + <svg class="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 67 + <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> 68 + <circle cx="9" cy="7" r="4"></circle> 69 + <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> 70 + <path d="M16 3.13a4 4 0 0 1 0 7.75"></path> 71 + </svg> 72 + <span>Subscribers</span> 73 + </a> 74 + </div> 75 + 76 + <div class="nav-section"> 77 + <div class="nav-section-title">Settings</div> 78 + <a href="#" class="nav-item" id="navSettings"> 79 + <svg class="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 80 + <circle cx="12" cy="12" r="3"></circle> 81 + <path d="M12 1v6m0 6v6M1 12h6m6 0h6"></path> 82 + </svg> 83 + <span>Podcast Settings</span> 84 + </a> 85 + </div> 86 + </nav> 87 + </aside> 88 + 89 + <!-- Main Content Area --> 90 + <main class="main-content"> 91 + <!-- Podcast Header --> 92 + <div class="podcast-header"> 93 + <div class="podcast-info"> 94 + <div class="podcast-avatar"> 95 + <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> 96 + <rect width="48" height="48" rx="8" fill="#C75B9B" opacity="0.2"/> 97 + <circle cx="24" cy="24" r="12" fill="#C75B9B"/> 98 + </svg> 99 + </div> 100 + <div> 101 + <h1 class="podcast-title" id="mainPodcastTitle">My Podcast</h1> 102 + <div class="podcast-meta"> 103 + <a href="#" class="podcast-link">Podcast Settings</a> 104 + <span class="podcast-badge">Private Podcast</span> 105 + </div> 106 + </div> 107 + </div> 108 + <button class="btn-create" id="createNewBtn"> 109 + <span>Create New</span> 110 + <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"> 111 + <path d="M4 6l4 4 4-4"></path> 112 + </svg> 113 + </button> 114 + </div> 115 + 116 + <!-- Dashboard View --> 117 + <div id="dashboardView" class="view-content"> 118 + <h2 class="section-title">Dashboard</h2> 119 + 120 + <!-- Analytics Summary --> 121 + <section class="analytics-summary"> 122 + <div class="section-header"> 123 + <h3 class="section-subtitle"> 124 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 125 + <line x1="12" y1="20" x2="12" y2="10"></line> 126 + <line x1="18" y1="20" x2="18" y2="4"></line> 127 + <line x1="6" y1="20" x2="6" y2="16"></line> 128 + </svg> 129 + Analytics Summary 130 + </h3> 131 + </div> 132 + 133 + <div class="metrics-label">Downloads</div> 134 + <div class="metrics-grid"> 135 + <div class="metric-card"> 136 + <div class="metric-label">Today</div> 137 + <div class="metric-value">0</div> 138 + </div> 139 + <div class="metric-card"> 140 + <div class="metric-label">Yesterday</div> 141 + <div class="metric-value">0</div> 142 + </div> 143 + <div class="metric-card"> 144 + <div class="metric-label">Last 7 days</div> 145 + <div class="metric-value">0</div> 146 + </div> 147 + <div class="metric-card"> 148 + <div class="metric-label">Last 28 days</div> 149 + <div class="metric-value">0</div> 150 + </div> 151 + <div class="metric-card"> 152 + <div class="metric-label">Last 90 days</div> 153 + <div class="metric-value">0</div> 154 + </div> 155 + <div class="metric-card"> 156 + <div class="metric-label">All-time</div> 157 + <div class="metric-value">0</div> 158 + </div> 159 + </div> 160 + 161 + <div class="analytics-footer"> 162 + <span class="update-time">Last updated: Just now</span> 163 + <a href="#" class="analytics-link">Go to Full Analytics →</a> 164 + </div> 165 + </section> 166 + 167 + <!-- Latest and Upcoming --> 168 + <section class="latest-section"> 169 + <h3 class="section-subtitle"> 170 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 171 + <circle cx="12" cy="12" r="10"></circle> 172 + <polyline points="12 6 12 12 16 14"></polyline> 173 + </svg> 174 + Latest and Upcoming 175 + </h3> 176 + 177 + <div class="empty-state-banner" id="episodesEmptyBanner"> 178 + <div class="empty-banner-content"> 179 + <h4>You haven't published an episode yet</h4> 180 + <p>You need to publish an episode on AT Protocol before you can start tracking the analytics, it's super easy too!</p> 181 + <button class="btn-action" id="createFirstEpisodeBtn">Create first episode</button> 182 + </div> 183 + </div> 184 + 185 + <div id="dashboardEpisodesList"></div> 186 + </section> 187 + </div> 188 + 189 + <!-- Episodes View --> 190 + <div id="episodesView" class="view-content" style="display: none;"> 191 + <div class="view-header"> 192 + <h2 class="section-title">Episodes</h2> 193 + <button class="btn-create" id="uploadEpisodeBtn"> 194 + <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"> 195 + <path d="M8 2v12M2 8h12"></path> 196 + </svg> 197 + <span>Upload Episode</span> 198 + </button> 199 + </div> 200 + 201 + <div id="episodesList"> 202 + <p class="loading">Loading episodes...</p> 203 + </div> 204 + </div> 205 + 206 + <!-- Settings View --> 207 + <div id="settingsView" class="view-content" style="display: none;"> 208 + <h2 class="section-title">Podcast Settings</h2> 209 + 210 + <section class="settings-card"> 211 + <h3>Podcast Feed Settings</h3> 212 + <form id="feedForm"> 213 + <div class="form-group"> 214 + <label for="podcastTitle">Podcast Title</label> 215 + <input type="text" id="podcastTitle" placeholder="My Podcast"> 216 + </div> 217 + 218 + <div class="form-group"> 219 + <label for="podcastDescription">Podcast Description</label> 220 + <textarea id="podcastDescription" rows="3" placeholder="About my podcast..."></textarea> 221 + </div> 222 + 223 + <div class="form-row"> 224 + <div class="form-group"> 225 + <label for="podcastLink">Website</label> 226 + <input type="url" id="podcastLink" placeholder="https://example.com"> 227 + </div> 228 + 229 + <div class="form-group"> 230 + <label for="podcastLanguage">Language</label> 231 + <input type="text" id="podcastLanguage" placeholder="en" maxlength="5"> 232 + </div> 233 + </div> 234 + 235 + <button type="submit" class="btn-primary">Save Settings</button> 236 + </form> 237 + <div id="feedStatus" class="status-message"></div> 238 + 239 + <div class="rss-section"> 240 + <h4>RSS Feed URL</h4> 241 + <div class="rss-link"> 242 + <code id="rssFeedUrl">Loading...</code> 243 + <button class="btn-copy" onclick="copyRSSUrl()">Copy</button> 244 + </div> 245 + </div> 246 + </section> 247 + </div> 248 + </main> 249 + </div> 250 + 251 + <!-- Upload Modal --> 252 + <div id="uploadModal" class="modal" style="display: none;"> 253 + <div class="modal-content"> 254 + <div class="modal-header"> 255 + <h2>Upload Episode</h2> 256 + <button class="modal-close" onclick="closeUploadModal()">&times;</button> 257 + </div> 258 + <form id="uploadForm"> 259 + <div class="form-group"> 260 + <label for="audioFile">Audio File *</label> 261 + <input type="file" id="audioFile" accept="audio/*" required> 262 + <small>Supported formats: MP3, M4A, WAV, etc.</small> 263 + </div> 264 + 265 + <div class="form-group"> 266 + <label for="title">Episode Title *</label> 267 + <input type="text" id="title" placeholder="My Awesome Episode" required> 268 + </div> 269 + 270 + <div class="form-group"> 271 + <label for="description">Description</label> 272 + <textarea id="description" rows="4" placeholder="Episode description..."></textarea> 273 + </div> 274 + 275 + <div class="modal-footer"> 276 + <button type="button" class="btn-secondary" onclick="closeUploadModal()">Cancel</button> 277 + <button type="submit" class="btn-primary"> 278 + <span id="uploadBtnText">Upload Episode</span> 279 + <span id="uploadSpinner" class="spinner" style="display: none;"></span> 280 + </button> 281 + </div> 282 + </form> 283 + <div id="uploadStatus" class="status-message"></div> 284 + </div> 285 + </div> 286 + 287 + <script src="app.js"></script> 288 + </body> 289 + </html>
+832
public/styles.css
··· 1 + * { 2 + margin: 0; 3 + padding: 0; 4 + box-sizing: border-box; 5 + } 6 + 7 + :root { 8 + --primary-color: #C75B9B; 9 + --primary-hover: #B04A87; 10 + --primary-light: #F4E8F0; 11 + --sidebar-bg: #FAFAFA; 12 + --text-primary: #2D2D2D; 13 + --text-secondary: #6B6B6B; 14 + --text-light: #9B9B9B; 15 + --border-color: #E8E8E8; 16 + --white: #FFFFFF; 17 + --hover-bg: #F5F5F5; 18 + } 19 + 20 + body { 21 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 22 + background: #F7F7F7; 23 + color: var(--text-primary); 24 + font-size: 14px; 25 + line-height: 1.5; 26 + } 27 + 28 + /* Top Header */ 29 + .top-header { 30 + background: #1E2240; 31 + color: white; 32 + padding: 0 1.5rem; 33 + height: 56px; 34 + display: flex; 35 + align-items: center; 36 + justify-content: space-between; 37 + position: fixed; 38 + top: 0; 39 + left: 0; 40 + right: 0; 41 + z-index: 100; 42 + border-bottom: 1px solid rgba(255, 255, 255, 0.1); 43 + } 44 + 45 + .header-left { 46 + display: flex; 47 + align-items: center; 48 + gap: 2rem; 49 + } 50 + 51 + .logo { 52 + display: flex; 53 + align-items: center; 54 + gap: 0.75rem; 55 + font-weight: 600; 56 + font-size: 16px; 57 + } 58 + 59 + .logo-text { 60 + color: white; 61 + } 62 + 63 + .header-right { 64 + display: flex; 65 + align-items: center; 66 + gap: 1rem; 67 + } 68 + 69 + .search-container { 70 + position: relative; 71 + width: 300px; 72 + } 73 + 74 + .search-input { 75 + width: 100%; 76 + padding: 0.5rem 2.5rem 0.5rem 1rem; 77 + border: 1px solid rgba(255, 255, 255, 0.2); 78 + border-radius: 6px; 79 + background: rgba(255, 255, 255, 0.1); 80 + color: white; 81 + font-size: 14px; 82 + transition: all 0.2s; 83 + } 84 + 85 + .search-input::placeholder { 86 + color: rgba(255, 255, 255, 0.5); 87 + } 88 + 89 + .search-input:focus { 90 + outline: none; 91 + background: rgba(255, 255, 255, 0.15); 92 + border-color: rgba(255, 255, 255, 0.3); 93 + } 94 + 95 + .search-shortcut { 96 + position: absolute; 97 + right: 0.75rem; 98 + top: 50%; 99 + transform: translateY(-50%); 100 + color: rgba(255, 255, 255, 0.5); 101 + font-size: 12px; 102 + } 103 + 104 + .header-btn { 105 + display: flex; 106 + align-items: center; 107 + gap: 0.5rem; 108 + padding: 0.5rem 1rem; 109 + background: transparent; 110 + border: 1px solid rgba(255, 255, 255, 0.2); 111 + border-radius: 6px; 112 + color: white; 113 + cursor: pointer; 114 + font-size: 14px; 115 + transition: all 0.2s; 116 + } 117 + 118 + .header-btn:hover { 119 + background: rgba(255, 255, 255, 0.1); 120 + } 121 + 122 + .user-avatar { 123 + width: 32px; 124 + height: 32px; 125 + border-radius: 50%; 126 + background: var(--primary-color); 127 + color: white; 128 + border: none; 129 + cursor: pointer; 130 + font-weight: 600; 131 + display: flex; 132 + align-items: center; 133 + justify-content: center; 134 + } 135 + 136 + /* App Container */ 137 + .app-container { 138 + display: flex; 139 + margin-top: 56px; 140 + min-height: calc(100vh - 56px); 141 + } 142 + 143 + /* Sidebar */ 144 + .sidebar { 145 + width: 240px; 146 + background: var(--sidebar-bg); 147 + border-right: 1px solid var(--border-color); 148 + padding: 1.5rem 0; 149 + position: fixed; 150 + left: 0; 151 + top: 56px; 152 + bottom: 0; 153 + overflow-y: auto; 154 + } 155 + 156 + .nav-section { 157 + margin-bottom: 2rem; 158 + } 159 + 160 + .nav-section-title { 161 + padding: 0 1.5rem; 162 + margin-bottom: 0.5rem; 163 + font-size: 11px; 164 + font-weight: 600; 165 + text-transform: uppercase; 166 + color: var(--text-light); 167 + letter-spacing: 0.5px; 168 + } 169 + 170 + .nav-item { 171 + display: flex; 172 + align-items: center; 173 + gap: 0.75rem; 174 + padding: 0.625rem 1.5rem; 175 + color: var(--text-secondary); 176 + text-decoration: none; 177 + font-size: 14px; 178 + transition: all 0.2s; 179 + cursor: pointer; 180 + } 181 + 182 + .nav-item:hover { 183 + background: var(--hover-bg); 184 + color: var(--text-primary); 185 + } 186 + 187 + .nav-item.active { 188 + background: var(--primary-light); 189 + color: var(--primary-color); 190 + font-weight: 600; 191 + } 192 + 193 + .nav-icon { 194 + width: 18px; 195 + height: 18px; 196 + flex-shrink: 0; 197 + } 198 + 199 + /* Main Content */ 200 + .main-content { 201 + flex: 1; 202 + margin-left: 240px; 203 + padding: 2rem; 204 + background: #F7F7F7; 205 + } 206 + 207 + /* Podcast Header */ 208 + .podcast-header { 209 + background: white; 210 + padding: 1.5rem 2rem; 211 + border-radius: 8px; 212 + margin-bottom: 2rem; 213 + display: flex; 214 + align-items: center; 215 + justify-content: space-between; 216 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 217 + } 218 + 219 + .podcast-info { 220 + display: flex; 221 + align-items: center; 222 + gap: 1rem; 223 + } 224 + 225 + .podcast-avatar { 226 + width: 56px; 227 + height: 56px; 228 + border-radius: 8px; 229 + background: var(--primary-light); 230 + display: flex; 231 + align-items: center; 232 + justify-content: center; 233 + } 234 + 235 + .podcast-title { 236 + font-size: 24px; 237 + font-weight: 600; 238 + color: var(--text-primary); 239 + margin-bottom: 0.25rem; 240 + } 241 + 242 + .podcast-meta { 243 + display: flex; 244 + align-items: center; 245 + gap: 1rem; 246 + font-size: 13px; 247 + } 248 + 249 + .podcast-link { 250 + color: #5B8DEE; 251 + text-decoration: none; 252 + } 253 + 254 + .podcast-link:hover { 255 + text-decoration: underline; 256 + } 257 + 258 + .podcast-badge { 259 + padding: 0.25rem 0.5rem; 260 + background: #F0F0F0; 261 + border-radius: 4px; 262 + color: var(--text-secondary); 263 + font-size: 12px; 264 + } 265 + 266 + .btn-create { 267 + display: flex; 268 + align-items: center; 269 + gap: 0.5rem; 270 + padding: 0.75rem 1.5rem; 271 + background: var(--primary-color); 272 + color: white; 273 + border: none; 274 + border-radius: 6px; 275 + font-size: 14px; 276 + font-weight: 600; 277 + cursor: pointer; 278 + transition: all 0.2s; 279 + } 280 + 281 + .btn-create:hover { 282 + background: var(--primary-hover); 283 + transform: translateY(-1px); 284 + } 285 + 286 + /* View Content */ 287 + .view-content { 288 + animation: fadeIn 0.3s ease-in; 289 + } 290 + 291 + @keyframes fadeIn { 292 + from { 293 + opacity: 0; 294 + transform: translateY(10px); 295 + } 296 + to { 297 + opacity: 1; 298 + transform: translateY(0); 299 + } 300 + } 301 + 302 + .section-title { 303 + font-size: 28px; 304 + font-weight: 600; 305 + color: var(--text-primary); 306 + margin-bottom: 2rem; 307 + } 308 + 309 + .section-subtitle { 310 + display: flex; 311 + align-items: center; 312 + gap: 0.5rem; 313 + font-size: 16px; 314 + font-weight: 600; 315 + color: var(--text-primary); 316 + margin-bottom: 1rem; 317 + } 318 + 319 + /* Analytics Summary */ 320 + .analytics-summary { 321 + background: white; 322 + padding: 2rem; 323 + border-radius: 8px; 324 + margin-bottom: 2rem; 325 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 326 + } 327 + 328 + .metrics-label { 329 + font-size: 14px; 330 + font-weight: 600; 331 + color: var(--text-primary); 332 + margin-bottom: 1rem; 333 + } 334 + 335 + .metrics-grid { 336 + display: grid; 337 + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 338 + gap: 1rem; 339 + margin-bottom: 1.5rem; 340 + } 341 + 342 + .metric-card { 343 + padding: 1rem; 344 + border: 1px solid var(--border-color); 345 + border-radius: 6px; 346 + background: white; 347 + } 348 + 349 + .metric-label { 350 + font-size: 13px; 351 + color: var(--text-secondary); 352 + margin-bottom: 0.5rem; 353 + } 354 + 355 + .metric-value { 356 + font-size: 28px; 357 + font-weight: 600; 358 + color: var(--primary-color); 359 + } 360 + 361 + .analytics-footer { 362 + display: flex; 363 + align-items: center; 364 + justify-content: space-between; 365 + padding-top: 1rem; 366 + border-top: 1px solid var(--border-color); 367 + } 368 + 369 + .update-time { 370 + font-size: 13px; 371 + color: var(--text-light); 372 + } 373 + 374 + .analytics-link { 375 + font-size: 14px; 376 + color: #5B8DEE; 377 + text-decoration: none; 378 + font-weight: 500; 379 + } 380 + 381 + .analytics-link:hover { 382 + text-decoration: underline; 383 + } 384 + 385 + /* Latest Section */ 386 + .latest-section { 387 + background: white; 388 + padding: 2rem; 389 + border-radius: 8px; 390 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 391 + } 392 + 393 + .empty-state-banner { 394 + background: var(--primary-light); 395 + padding: 2rem; 396 + border-radius: 8px; 397 + margin-top: 1rem; 398 + } 399 + 400 + .empty-banner-content h4 { 401 + font-size: 18px; 402 + font-weight: 600; 403 + color: var(--text-primary); 404 + margin-bottom: 0.5rem; 405 + } 406 + 407 + .empty-banner-content p { 408 + color: var(--text-secondary); 409 + margin-bottom: 1.5rem; 410 + line-height: 1.6; 411 + } 412 + 413 + .btn-action { 414 + padding: 0.75rem 1.5rem; 415 + background: var(--primary-color); 416 + color: white; 417 + border: none; 418 + border-radius: 6px; 419 + font-size: 14px; 420 + font-weight: 600; 421 + cursor: pointer; 422 + transition: all 0.2s; 423 + } 424 + 425 + .btn-action:hover { 426 + background: var(--primary-hover); 427 + } 428 + 429 + /* Episodes View */ 430 + .view-header { 431 + display: flex; 432 + align-items: center; 433 + justify-content: space-between; 434 + margin-bottom: 2rem; 435 + } 436 + 437 + #episodesList { 438 + display: flex; 439 + flex-direction: column; 440 + gap: 1rem; 441 + } 442 + 443 + .episode-item { 444 + background: white; 445 + padding: 1.5rem; 446 + border-radius: 8px; 447 + border: 1px solid var(--border-color); 448 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 449 + transition: all 0.2s; 450 + } 451 + 452 + .episode-item:hover { 453 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 454 + } 455 + 456 + .episode-header { 457 + display: flex; 458 + justify-content: space-between; 459 + align-items: start; 460 + margin-bottom: 0.75rem; 461 + } 462 + 463 + .episode-title { 464 + font-size: 18px; 465 + font-weight: 600; 466 + color: var(--text-primary); 467 + margin-bottom: 0.25rem; 468 + } 469 + 470 + .episode-date { 471 + color: var(--text-light); 472 + font-size: 13px; 473 + } 474 + 475 + .episode-description { 476 + color: var(--text-secondary); 477 + margin-bottom: 1rem; 478 + line-height: 1.6; 479 + } 480 + 481 + .episode-meta { 482 + display: flex; 483 + gap: 1.5rem; 484 + font-size: 13px; 485 + color: var(--text-light); 486 + margin-bottom: 1rem; 487 + } 488 + 489 + .audio-player { 490 + width: 100%; 491 + margin-top: 1rem; 492 + border-radius: 6px; 493 + } 494 + 495 + .loading { 496 + text-align: center; 497 + color: var(--text-light); 498 + padding: 3rem; 499 + } 500 + 501 + .empty-state { 502 + text-align: center; 503 + padding: 4rem 2rem; 504 + color: var(--text-light); 505 + } 506 + 507 + .empty-state-icon { 508 + font-size: 4rem; 509 + margin-bottom: 1rem; 510 + opacity: 0.5; 511 + } 512 + 513 + /* Settings Card */ 514 + .settings-card { 515 + background: white; 516 + padding: 2rem; 517 + border-radius: 8px; 518 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 519 + margin-bottom: 2rem; 520 + } 521 + 522 + .settings-card h3 { 523 + font-size: 18px; 524 + font-weight: 600; 525 + color: var(--text-primary); 526 + margin-bottom: 1.5rem; 527 + } 528 + 529 + .settings-card h4 { 530 + font-size: 16px; 531 + font-weight: 600; 532 + color: var(--text-primary); 533 + margin-bottom: 1rem; 534 + } 535 + 536 + /* Forms */ 537 + .form-group { 538 + margin-bottom: 1.5rem; 539 + } 540 + 541 + .form-group label { 542 + display: block; 543 + margin-bottom: 0.5rem; 544 + font-weight: 500; 545 + color: var(--text-primary); 546 + font-size: 14px; 547 + } 548 + 549 + .form-group input[type="text"], 550 + .form-group input[type="url"], 551 + .form-group input[type="file"], 552 + .form-group textarea { 553 + width: 100%; 554 + padding: 0.75rem; 555 + border: 1px solid var(--border-color); 556 + border-radius: 6px; 557 + font-size: 14px; 558 + font-family: inherit; 559 + transition: all 0.2s; 560 + background: white; 561 + } 562 + 563 + .form-group input:focus, 564 + .form-group textarea:focus { 565 + outline: none; 566 + border-color: var(--primary-color); 567 + box-shadow: 0 0 0 3px rgba(199, 91, 155, 0.1); 568 + } 569 + 570 + .form-group small { 571 + display: block; 572 + margin-top: 0.5rem; 573 + color: var(--text-light); 574 + font-size: 13px; 575 + } 576 + 577 + .form-row { 578 + display: grid; 579 + grid-template-columns: 1fr 1fr; 580 + gap: 1rem; 581 + } 582 + 583 + .btn-primary { 584 + padding: 0.75rem 1.5rem; 585 + background: var(--primary-color); 586 + color: white; 587 + border: none; 588 + border-radius: 6px; 589 + font-size: 14px; 590 + font-weight: 600; 591 + cursor: pointer; 592 + transition: all 0.2s; 593 + display: inline-flex; 594 + align-items: center; 595 + gap: 0.5rem; 596 + } 597 + 598 + .btn-primary:hover { 599 + background: var(--primary-hover); 600 + } 601 + 602 + .btn-primary:disabled { 603 + opacity: 0.6; 604 + cursor: not-allowed; 605 + } 606 + 607 + .btn-secondary { 608 + padding: 0.75rem 1.5rem; 609 + background: #F0F0F0; 610 + color: var(--text-primary); 611 + border: none; 612 + border-radius: 6px; 613 + font-size: 14px; 614 + font-weight: 600; 615 + cursor: pointer; 616 + transition: all 0.2s; 617 + } 618 + 619 + .btn-secondary:hover { 620 + background: #E0E0E0; 621 + } 622 + 623 + .spinner { 624 + display: inline-block; 625 + width: 16px; 626 + height: 16px; 627 + border: 2px solid rgba(255, 255, 255, 0.3); 628 + border-radius: 50%; 629 + border-top-color: white; 630 + animation: spin 0.8s linear infinite; 631 + } 632 + 633 + @keyframes spin { 634 + to { transform: rotate(360deg); } 635 + } 636 + 637 + .status-message { 638 + margin-top: 1rem; 639 + padding: 1rem; 640 + border-radius: 6px; 641 + font-size: 14px; 642 + display: none; 643 + } 644 + 645 + .status-message.success { 646 + background: #D4EDDA; 647 + color: #155724; 648 + border: 1px solid #C3E6CB; 649 + display: block; 650 + } 651 + 652 + .status-message.error { 653 + background: #F8D7DA; 654 + color: #721C24; 655 + border: 1px solid #F5C6CB; 656 + display: block; 657 + } 658 + 659 + /* RSS Section */ 660 + .rss-section { 661 + margin-top: 2rem; 662 + padding-top: 2rem; 663 + border-top: 1px solid var(--border-color); 664 + } 665 + 666 + .rss-link { 667 + display: flex; 668 + align-items: center; 669 + gap: 1rem; 670 + padding: 1rem; 671 + background: #F8F9FA; 672 + border-radius: 6px; 673 + margin-top: 0.5rem; 674 + } 675 + 676 + .rss-link code { 677 + flex: 1; 678 + background: white; 679 + padding: 0.5rem 0.75rem; 680 + border-radius: 4px; 681 + border: 1px solid var(--border-color); 682 + font-size: 13px; 683 + overflow-x: auto; 684 + color: var(--text-secondary); 685 + } 686 + 687 + .btn-copy { 688 + padding: 0.5rem 1rem; 689 + background: var(--primary-color); 690 + color: white; 691 + border: none; 692 + border-radius: 4px; 693 + cursor: pointer; 694 + font-size: 13px; 695 + font-weight: 600; 696 + white-space: nowrap; 697 + transition: all 0.2s; 698 + } 699 + 700 + .btn-copy:hover { 701 + background: var(--primary-hover); 702 + } 703 + 704 + /* Modal */ 705 + .modal { 706 + position: fixed; 707 + top: 0; 708 + left: 0; 709 + right: 0; 710 + bottom: 0; 711 + background: rgba(0, 0, 0, 0.5); 712 + display: flex; 713 + align-items: center; 714 + justify-content: center; 715 + z-index: 1000; 716 + animation: fadeIn 0.2s ease-in; 717 + } 718 + 719 + .modal-content { 720 + background: white; 721 + border-radius: 8px; 722 + width: 90%; 723 + max-width: 600px; 724 + max-height: 90vh; 725 + overflow-y: auto; 726 + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); 727 + } 728 + 729 + .modal-header { 730 + display: flex; 731 + align-items: center; 732 + justify-content: space-between; 733 + padding: 1.5rem 2rem; 734 + border-bottom: 1px solid var(--border-color); 735 + } 736 + 737 + .modal-header h2 { 738 + font-size: 20px; 739 + font-weight: 600; 740 + color: var(--text-primary); 741 + } 742 + 743 + .modal-close { 744 + background: transparent; 745 + border: none; 746 + font-size: 28px; 747 + color: var(--text-light); 748 + cursor: pointer; 749 + width: 32px; 750 + height: 32px; 751 + display: flex; 752 + align-items: center; 753 + justify-content: center; 754 + border-radius: 4px; 755 + transition: all 0.2s; 756 + } 757 + 758 + .modal-close:hover { 759 + background: var(--hover-bg); 760 + color: var(--text-primary); 761 + } 762 + 763 + .modal-content form { 764 + padding: 2rem; 765 + } 766 + 767 + .modal-footer { 768 + display: flex; 769 + align-items: center; 770 + justify-content: flex-end; 771 + gap: 1rem; 772 + padding-top: 1.5rem; 773 + border-top: 1px solid var(--border-color); 774 + } 775 + 776 + /* Responsive */ 777 + @media (max-width: 1024px) { 778 + .sidebar { 779 + width: 200px; 780 + } 781 + 782 + .main-content { 783 + margin-left: 200px; 784 + } 785 + 786 + .search-container { 787 + width: 200px; 788 + } 789 + } 790 + 791 + @media (max-width: 768px) { 792 + .sidebar { 793 + transform: translateX(-100%); 794 + transition: transform 0.3s; 795 + } 796 + 797 + .sidebar.open { 798 + transform: translateX(0); 799 + } 800 + 801 + .main-content { 802 + margin-left: 0; 803 + padding: 1rem; 804 + } 805 + 806 + .podcast-header { 807 + flex-direction: column; 808 + align-items: flex-start; 809 + gap: 1rem; 810 + } 811 + 812 + .metrics-grid { 813 + grid-template-columns: repeat(2, 1fr); 814 + } 815 + 816 + .form-row { 817 + grid-template-columns: 1fr; 818 + } 819 + 820 + .search-container { 821 + display: none; 822 + } 823 + 824 + .header-btn { 825 + font-size: 0; 826 + padding: 0.5rem; 827 + } 828 + 829 + .header-btn .icon { 830 + font-size: 18px; 831 + } 832 + }
public/uploads/.gitkeep

This is a binary file and will not be displayed.

+12
railway.json
··· 1 + { 2 + "$schema": "https://railway.app/railway.schema.json", 3 + "build": { 4 + "builder": "NIXPACKS", 5 + "buildCommand": "npm run build" 6 + }, 7 + "deploy": { 8 + "startCommand": "npm start", 9 + "restartPolicyType": "ON_FAILURE", 10 + "restartPolicyMaxRetries": 10 11 + } 12 + }
+67
src/atproto-client.js
··· 1 + import { BskyAgent } from '@atproto/api'; 2 + import dotenv from 'dotenv'; 3 + 4 + dotenv.config(); 5 + 6 + const agent = new BskyAgent({ 7 + service: process.env.ATPROTO_SERVICE || 'https://bsky.social' 8 + }); 9 + 10 + let isAuthenticated = false; 11 + 12 + export async function authenticate() { 13 + if (isAuthenticated) return agent; 14 + 15 + try { 16 + await agent.login({ 17 + identifier: process.env.ATPROTO_IDENTIFIER, 18 + password: process.env.ATPROTO_PASSWORD 19 + }); 20 + isAuthenticated = true; 21 + console.log('✅ Authenticated with AT Protocol'); 22 + return agent; 23 + } catch (error) { 24 + console.error('❌ Authentication failed:', error); 25 + throw error; 26 + } 27 + } 28 + 29 + export const atprotoClient = { 30 + getAgent: async () => { 31 + if (!isAuthenticated) { 32 + await authenticate(); 33 + } 34 + return agent; 35 + }, 36 + 37 + uploadBlob: async (data, mimeType) => { 38 + const authenticatedAgent = await atprotoClient.getAgent(); 39 + const response = await authenticatedAgent.uploadBlob(data, { 40 + encoding: mimeType 41 + }); 42 + return response.data.blob; 43 + }, 44 + 45 + getBlob: async (did, cid) => { 46 + const authenticatedAgent = await atprotoClient.getAgent(); 47 + // Use the public PDS blob endpoint (works for blobs referenced in records) 48 + const serviceUrl = (authenticatedAgent.service?.toString() || process.env.ATPROTO_SERVICE || 'https://bsky.social').replace(/\/$/, ''); 49 + const url = `${serviceUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`; 50 + console.log('Fetching blob from:', url); 51 + 52 + const response = await fetch(url); 53 + console.log('Blob response:', response.status, response.statusText); 54 + 55 + // If public endpoint fails, try with authentication 56 + if (!response.ok && authenticatedAgent.session?.accessJwt) { 57 + console.log('Public blob fetch failed, trying authenticated...'); 58 + const authResponse = await fetch(url, { 59 + headers: { 'Authorization': `Bearer ${authenticatedAgent.session.accessJwt}` } 60 + }); 61 + console.log('Authenticated blob response:', authResponse.status); 62 + return authResponse; 63 + } 64 + 65 + return response; 66 + } 67 + };
+72
src/index.js
··· 1 + import express from 'express'; 2 + import cors from 'cors'; 3 + import dotenv from 'dotenv'; 4 + import path from 'path'; 5 + import { fileURLToPath } from 'url'; 6 + import { existsSync } from 'fs'; 7 + import { atprotoClient } from './atproto-client.js'; 8 + import { uploadRouter } from './routes/upload.js'; 9 + import { feedRouter } from './routes/feed.js'; 10 + import { mediaRouter } from './routes/media.js'; 11 + 12 + dotenv.config(); 13 + 14 + const __filename = fileURLToPath(import.meta.url); 15 + const __dirname = path.dirname(__filename); 16 + 17 + const app = express(); 18 + const PORT = process.env.PORT || 5000; 19 + 20 + app.use(cors()); 21 + app.use(express.json()); 22 + 23 + // API Routes 24 + app.use('/api/upload', uploadRouter); 25 + app.use('/api/feed', feedRouter); 26 + app.use('/api/media', mediaRouter); 27 + 28 + // Serve React build in production, or fallback to public folder 29 + const reactBuildPath = path.join(__dirname, '../client/dist'); 30 + const publicPath = path.join(__dirname, '../public'); 31 + 32 + if (existsSync(reactBuildPath)) { 33 + // Production: serve React build 34 + console.log('Serving React build from:', reactBuildPath); 35 + app.use(express.static(reactBuildPath)); 36 + 37 + app.get('*', (req, res) => { 38 + res.sendFile(path.join(reactBuildPath, 'index.html')); 39 + }); 40 + } else { 41 + // Development: serve vanilla HTML or show API info 42 + console.log('React build not found. Serving from public folder or API-only mode.'); 43 + app.use(express.static(publicPath)); 44 + 45 + app.get('/', (req, res) => { 46 + const indexPath = path.join(publicPath, 'index.html'); 47 + if (existsSync(indexPath)) { 48 + res.sendFile(indexPath); 49 + } else { 50 + res.json({ 51 + name: 'AT Protocol Podcast Host', 52 + version: '1.0.0', 53 + message: 'React app not built yet. Run: npm run build', 54 + endpoints: { 55 + upload: '/api/upload', 56 + feed: '/api/feed', 57 + media: '/api/media' 58 + } 59 + }); 60 + } 61 + }); 62 + } 63 + 64 + app.listen(PORT, '0.0.0.0', () => { 65 + console.log(`🎙️ AT Protocol Podcast Server running on port ${PORT}`); 66 + console.log(` API available at http://0.0.0.0:${PORT}/api`); 67 + if (existsSync(reactBuildPath)) { 68 + console.log(` React app at http://0.0.0.0:${PORT}`); 69 + } else { 70 + console.log(` For development with React, run: npm run client (on port 3000)`); 71 + } 72 + });
+239
src/routes/feed.js
··· 1 + import express from 'express'; 2 + import RSS from 'rss'; 3 + import { getAllEpisodes, getFeedMetadata, storeFeedMetadata } from '../storage/metadata.js'; 4 + import { atprotoClient } from '../atproto-client.js'; 5 + 6 + const router = express.Router(); 7 + 8 + /** 9 + * Generate RSS XML content 10 + */ 11 + async function generateRSSXML(baseUrl) { 12 + const feedMeta = await getFeedMetadata(); 13 + const episodes = await getAllEpisodes(); 14 + const agent = await atprotoClient.getAgent(); 15 + 16 + const feed = new RSS({ 17 + title: feedMeta.title, 18 + description: feedMeta.description, 19 + feed_url: `at://${agent.session.did}/app.podcast.feed/self`, 20 + site_url: feedMeta.link, 21 + language: feedMeta.language, 22 + pubDate: new Date(), 23 + ttl: 60, 24 + custom_namespaces: { 25 + 'atproto': 'https://atproto.com/ns/1.0' 26 + } 27 + }); 28 + 29 + for (const episode of episodes) { 30 + const mediaUrl = `${baseUrl}/api/media/stream/${agent.session.did}/${episode.blob.cid}`; 31 + const atUri = `at://${agent.session.did}/app.podcast.episode/${episode.blob.cid}`; 32 + 33 + feed.item({ 34 + title: episode.title, 35 + description: episode.description, 36 + url: mediaUrl, 37 + date: episode.pubDate, 38 + enclosure: { 39 + url: mediaUrl, 40 + type: episode.blob.mimeType, 41 + size: episode.blob.size 42 + }, 43 + custom_elements: [ 44 + { 'itunes:duration': Math.floor(episode.blob.size / 16000) }, // Rough estimate 45 + { 'atproto:cid': episode.blob.cid }, 46 + { 'atproto:uri': atUri }, 47 + { 'atproto:did': agent.session.did } 48 + ] 49 + }); 50 + } 51 + 52 + return feed.xml({ indent: true }); 53 + } 54 + 55 + /** 56 + * Publish RSS feed as AT Protocol record 57 + * POST /api/feed/publish 58 + */ 59 + router.post('/publish', async (req, res) => { 60 + try { 61 + const agent = await atprotoClient.getAgent(); 62 + const baseUrl = `${req.protocol}://${req.get('host')}`; 63 + const rssXml = await generateRSSXML(baseUrl); 64 + 65 + // Create a record with the RSS XML content 66 + const record = { 67 + $type: 'app.podcast.feed', 68 + content: rssXml, 69 + createdAt: new Date().toISOString(), 70 + metadata: await getFeedMetadata() 71 + }; 72 + 73 + // Use putRecord to create/update the feed record with a fixed rkey 74 + const result = await agent.com.atproto.repo.putRecord({ 75 + repo: agent.session.did, 76 + collection: 'app.podcast.feed', 77 + rkey: 'self', // Fixed key so there's only one feed record 78 + record: record 79 + }); 80 + 81 + const atUri = `at://${agent.session.did}/app.podcast.feed/self`; 82 + 83 + res.json({ 84 + success: true, 85 + uri: atUri, 86 + cid: result.data.cid, 87 + message: 'RSS feed published to AT Protocol' 88 + }); 89 + } catch (error) { 90 + console.error('RSS publish error:', error); 91 + res.status(500).json({ 92 + error: 'Failed to publish RSS feed', 93 + details: error.message 94 + }); 95 + } 96 + }); 97 + 98 + /** 99 + * Get RSS feed as XML (legacy HTTP endpoint) 100 + * GET /api/feed/rss 101 + */ 102 + router.get('/rss', async (req, res) => { 103 + try { 104 + const baseUrl = `${req.protocol}://${req.get('host')}`; 105 + const rssXml = await generateRSSXML(baseUrl); 106 + 107 + res.set('Content-Type', 'application/rss+xml'); 108 + res.send(rssXml); 109 + } catch (error) { 110 + console.error('RSS generation error:', error); 111 + res.status(500).json({ error: 'Failed to generate RSS feed', details: error.message }); 112 + } 113 + }); 114 + 115 + /** 116 + * Get RSS feed from AT Protocol record 117 + * GET /api/feed/at-rss 118 + */ 119 + router.get('/at-rss', async (req, res) => { 120 + try { 121 + const agent = await atprotoClient.getAgent(); 122 + 123 + // Fetch the RSS feed record from AT Protocol 124 + const result = await agent.com.atproto.repo.getRecord({ 125 + repo: agent.session.did, 126 + collection: 'app.podcast.feed', 127 + rkey: 'self' 128 + }); 129 + 130 + if (!result.data.value || !result.data.value.content) { 131 + return res.status(404).json({ 132 + error: 'RSS feed not found', 133 + message: 'Please publish the feed first using POST /api/feed/publish' 134 + }); 135 + } 136 + 137 + res.set('Content-Type', 'application/rss+xml'); 138 + res.send(result.data.value.content); 139 + } catch (error) { 140 + if (error.message.includes('Could not locate record')) { 141 + return res.status(404).json({ 142 + error: 'RSS feed not found', 143 + message: 'Please publish the feed first using POST /api/feed/publish' 144 + }); 145 + } 146 + console.error('AT RSS fetch error:', error); 147 + res.status(500).json({ 148 + error: 'Failed to fetch RSS feed from AT Protocol', 149 + details: error.message 150 + }); 151 + } 152 + }); 153 + 154 + /** 155 + * Get AT URI for the published RSS feed 156 + * GET /api/feed/uri 157 + */ 158 + router.get('/uri', async (req, res) => { 159 + try { 160 + const agent = await atprotoClient.getAgent(); 161 + 162 + // Check if feed exists 163 + try { 164 + await agent.com.atproto.repo.getRecord({ 165 + repo: agent.session.did, 166 + collection: 'app.podcast.feed', 167 + rkey: 'self' 168 + }); 169 + 170 + const atUri = `at://${agent.session.did}/app.podcast.feed/self`; 171 + const httpProxy = `${req.protocol}://${req.get('host')}/api/feed/at-rss`; 172 + 173 + res.json({ 174 + atUri, 175 + httpProxy, 176 + did: agent.session.did, 177 + published: true 178 + }); 179 + } catch (error) { 180 + if (error.message.includes('Could not locate record')) { 181 + res.json({ 182 + atUri: null, 183 + httpProxy: `${req.protocol}://${req.get('host')}/api/feed/rss`, 184 + did: agent.session.did, 185 + published: false, 186 + message: 'Feed not published yet. Use POST /api/feed/publish to publish.' 187 + }); 188 + } else { 189 + throw error; 190 + } 191 + } 192 + } catch (error) { 193 + console.error('Feed URI error:', error); 194 + res.status(500).json({ 195 + error: 'Failed to get feed URI', 196 + details: error.message 197 + }); 198 + } 199 + }); 200 + 201 + /** 202 + * Update feed metadata 203 + * POST /api/feed/metadata 204 + */ 205 + router.post('/metadata', async (req, res) => { 206 + try { 207 + const { title, description, link, language } = req.body; 208 + 209 + const feedData = { 210 + title: title || 'My AT Protocol Podcast', 211 + description: description || 'A podcast hosted on AT Protocol', 212 + link: link || 'https://example.com', 213 + language: language || 'en' 214 + }; 215 + 216 + await storeFeedMetadata(feedData); 217 + 218 + res.json({ success: true, feed: feedData }); 219 + } catch (error) { 220 + console.error('Feed metadata error:', error); 221 + res.status(500).json({ error: 'Failed to update feed metadata', details: error.message }); 222 + } 223 + }); 224 + 225 + /** 226 + * Get feed metadata 227 + * GET /api/feed/metadata 228 + */ 229 + router.get('/metadata', async (req, res) => { 230 + try { 231 + const feedMeta = await getFeedMetadata(); 232 + res.json(feedMeta); 233 + } catch (error) { 234 + console.error('Feed metadata error:', error); 235 + res.status(500).json({ error: 'Failed to get feed metadata', details: error.message }); 236 + } 237 + }); 238 + 239 + export { router as feedRouter };
+91
src/routes/media.js
··· 1 + import express from 'express'; 2 + import { atprotoClient } from '../atproto-client.js'; 3 + import { getAllEpisodes, getEpisodeById } from '../storage/metadata.js'; 4 + 5 + const router = express.Router(); 6 + 7 + /** 8 + * Stream media from AT Protocol blob storage 9 + * GET /api/media/stream/:did/:cid 10 + */ 11 + router.get('/stream/:did/:cid', async (req, res) => { 12 + try { 13 + const { did, cid } = req.params; 14 + 15 + console.log('Stream request:', { did, cid }); 16 + 17 + // Fetch blob from AT Protocol 18 + const blobResponse = await atprotoClient.getBlob(did, cid); 19 + 20 + if (!blobResponse.ok) { 21 + console.error('Blob not found:', { did, cid, status: blobResponse.status }); 22 + return res.status(404).json({ error: 'Media not found' }); 23 + } 24 + 25 + // Get content type from response headers 26 + const contentType = blobResponse.headers.get('content-type') || 'audio/mpeg'; 27 + const contentLength = blobResponse.headers.get('content-length'); 28 + 29 + res.setHeader('Content-Type', contentType); 30 + if (contentLength) { 31 + res.setHeader('Content-Length', contentLength); 32 + } 33 + res.setHeader('Accept-Ranges', 'bytes'); 34 + 35 + // Stream the blob data to the response 36 + const buffer = await blobResponse.arrayBuffer(); 37 + res.send(Buffer.from(buffer)); 38 + } catch (error) { 39 + console.error('Media streaming error:', error); 40 + res.status(500).json({ error: 'Failed to stream media', details: error.message }); 41 + } 42 + }); 43 + 44 + /** 45 + * Get all episodes 46 + * GET /api/media/episodes 47 + */ 48 + router.get('/episodes', async (req, res) => { 49 + try { 50 + const episodes = await getAllEpisodes(); 51 + const agent = await atprotoClient.getAgent(); 52 + 53 + const episodesWithUrls = episodes.map(ep => ({ 54 + ...ep, 55 + streamUrl: `/api/media/stream/${agent.session.did}/${ep.blob.cid}`, 56 + atUri: `at://${agent.session.did}/${ep.blob.cid}` 57 + })); 58 + 59 + res.json({ episodes: episodesWithUrls }); 60 + } catch (error) { 61 + console.error('Episodes fetch error:', error); 62 + res.status(500).json({ error: 'Failed to fetch episodes', details: error.message }); 63 + } 64 + }); 65 + 66 + /** 67 + * Get episode by ID 68 + * GET /api/media/episodes/:id 69 + */ 70 + router.get('/episodes/:id', async (req, res) => { 71 + try { 72 + const episode = await getEpisodeById(req.params.id); 73 + 74 + if (!episode) { 75 + return res.status(404).json({ error: 'Episode not found' }); 76 + } 77 + 78 + const agent = await atprotoClient.getAgent(); 79 + 80 + res.json({ 81 + ...episode, 82 + streamUrl: `/api/media/stream/${agent.session.did}/${episode.blob.cid}`, 83 + atUri: `at://${agent.session.did}/${episode.blob.cid}` 84 + }); 85 + } catch (error) { 86 + console.error('Episode fetch error:', error); 87 + res.status(500).json({ error: 'Failed to fetch episode', details: error.message }); 88 + } 89 + }); 90 + 91 + export { router as mediaRouter };
+92
src/routes/upload.js
··· 1 + import express from 'express'; 2 + import multer from 'multer'; 3 + import fs from 'fs/promises'; 4 + import { atprotoClient } from '../atproto-client.js'; 5 + import { storePodcastMetadata } from '../storage/metadata.js'; 6 + 7 + const router = express.Router(); 8 + const upload = multer({ dest: 'public/uploads/' }); 9 + 10 + /** 11 + * Upload a podcast episode 12 + * POST /api/upload/episode 13 + * Body: multipart/form-data with audio file and metadata 14 + */ 15 + router.post('/episode', upload.single('audio'), async (req, res) => { 16 + try { 17 + if (!req.file) { 18 + return res.status(400).json({ error: 'No audio file provided' }); 19 + } 20 + 21 + const { title, description, pubDate } = req.body; 22 + 23 + if (!title) { 24 + return res.status(400).json({ error: 'Episode title is required' }); 25 + } 26 + 27 + // Read the uploaded file 28 + const audioData = await fs.readFile(req.file.path); 29 + 30 + // Upload to AT Protocol as a blob 31 + const blob = await atprotoClient.uploadBlob(audioData, req.file.mimetype); 32 + 33 + // Extract CID from blob reference 34 + const cid = blob.ref.$link || blob.ref.toString(); 35 + 36 + console.log('Blob uploaded:', { cid, mimeType: blob.mimeType, size: blob.size }); 37 + 38 + // Store metadata 39 + const episode = { 40 + title, 41 + description: description || '', 42 + pubDate: pubDate || new Date().toISOString(), 43 + blob: { 44 + cid: cid, 45 + mimeType: blob.mimeType, 46 + size: blob.size 47 + }, 48 + originalFilename: req.file.originalname 49 + }; 50 + 51 + // Create a public record to make the blob accessible 52 + const agent = await atprotoClient.getAgent(); 53 + 54 + // Use a simple app.podcast.episode collection (custom namespace) 55 + const rkey = `${Date.now()}`; 56 + const record = await agent.com.atproto.repo.createRecord({ 57 + repo: agent.session.did, 58 + collection: 'app.podcast.episode', 59 + rkey: rkey, 60 + record: { 61 + $type: 'app.podcast.episode', 62 + title: title, 63 + description: description || '', 64 + audio: blob, // Reference the blob in the record 65 + publishedAt: episode.pubDate, 66 + createdAt: episode.pubDate 67 + } 68 + }); 69 + 70 + episode.recordUri = record.uri; 71 + episode.recordCid = record.cid; 72 + 73 + await storePodcastMetadata(episode); 74 + 75 + // Clean up temporary file 76 + await fs.unlink(req.file.path); 77 + 78 + res.json({ 79 + success: true, 80 + episode: { 81 + ...episode, 82 + atUri: `at://${agent.session.did}/app.podcast.episode/${rkey}`, 83 + recordUri: record.uri 84 + } 85 + }); 86 + } catch (error) { 87 + console.error('Upload error:', error); 88 + res.status(500).json({ error: 'Failed to upload episode', details: error.message }); 89 + } 90 + }); 91 + 92 + export { router as uploadRouter };
+60
src/storage/metadata.js
··· 1 + import fs from 'fs/promises'; 2 + import path from 'path'; 3 + 4 + const METADATA_FILE = 'podcast-metadata.json'; 5 + 6 + async function ensureMetadataFile() { 7 + try { 8 + await fs.access(METADATA_FILE); 9 + } catch { 10 + await fs.writeFile(METADATA_FILE, JSON.stringify({ episodes: [] }, null, 2)); 11 + } 12 + } 13 + 14 + export async function storePodcastMetadata(episode) { 15 + await ensureMetadataFile(); 16 + 17 + const data = JSON.parse(await fs.readFile(METADATA_FILE, 'utf-8')); 18 + 19 + episode.id = Date.now().toString(); 20 + data.episodes.push(episode); 21 + 22 + await fs.writeFile(METADATA_FILE, JSON.stringify(data, null, 2)); 23 + 24 + return episode; 25 + } 26 + 27 + export async function getAllEpisodes() { 28 + await ensureMetadataFile(); 29 + 30 + const data = JSON.parse(await fs.readFile(METADATA_FILE, 'utf-8')); 31 + return data.episodes.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)); 32 + } 33 + 34 + export async function getEpisodeById(id) { 35 + const episodes = await getAllEpisodes(); 36 + return episodes.find(ep => ep.id === id); 37 + } 38 + 39 + export async function storeFeedMetadata(feedData) { 40 + await ensureMetadataFile(); 41 + 42 + const data = JSON.parse(await fs.readFile(METADATA_FILE, 'utf-8')); 43 + data.feed = feedData; 44 + 45 + await fs.writeFile(METADATA_FILE, JSON.stringify(data, null, 2)); 46 + 47 + return feedData; 48 + } 49 + 50 + export async function getFeedMetadata() { 51 + await ensureMetadataFile(); 52 + 53 + const data = JSON.parse(await fs.readFile(METADATA_FILE, 'utf-8')); 54 + return data.feed || { 55 + title: 'My AT Protocol Podcast', 56 + description: 'A podcast hosted on AT Protocol', 57 + link: 'https://example.com', 58 + language: 'en' 59 + }; 60 + }